Autosave: 20260212-031011
This commit is contained in:
parent
3cc9a53922
commit
1c6ec71bf9
@ -26,6 +26,10 @@ try {
|
||||
<label class="form-label" for="nombre"><strong>Nombre del Producto</strong></label>
|
||||
<input class="form-control" type="text" id="nombre" name="nombre" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="codigo_barras"><strong>Código de Barras</strong></label>
|
||||
<input class="form-control" type="text" id="codigo_barras" name="codigo_barras" placeholder="Escanea o escribe el código">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="descripcion"><strong>Descripción</strong></label>
|
||||
<textarea class="form-control" id="descripcion" name="descripcion" rows="4"></textarea>
|
||||
|
||||
@ -306,6 +306,7 @@ a:hover {
|
||||
flex-grow: 1;
|
||||
padding: 2rem;
|
||||
background-color: var(--color-fondo);
|
||||
width: 100%; /* Ensure it takes full width */
|
||||
}
|
||||
|
||||
/* Forzar color de texto en el cuerpo de las tablas */
|
||||
@ -318,4 +319,39 @@ a:hover {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* --- Responsive / Mobile --- */
|
||||
@media (max-width: 768px) {
|
||||
.d-flex {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: auto;
|
||||
display: none; /* Hidden by default on mobile */
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background-color: #00754A;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: none; /* Hidden on desktop */
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
assets/pasted-20260202-195725-d26f89a4.png
Normal file
BIN
assets/pasted-20260202-195725-d26f89a4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -39,6 +39,11 @@ if (!$product) {
|
||||
<input type="text" class="form-control" id="nombre" name="nombre" value="<?php echo htmlspecialchars($product['nombre']); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="codigo_barras" class="form-label">Código de Barras</label>
|
||||
<input type="text" class="form-control" id="codigo_barras" name="codigo_barras" value="<?php echo htmlspecialchars($product['codigo_barras'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="descripcion" class="form-label">Descripción</label>
|
||||
<textarea class="form-control" id="descripcion" name="descripcion" rows="3"><?php echo htmlspecialchars($product['descripcion']); ?></textarea>
|
||||
|
||||
@ -11,6 +11,7 @@ require_once 'db/config.php';
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
// Sanitize and retrieve product data
|
||||
$nombre = filter_input(INPUT_POST, 'nombre', FILTER_SANITIZE_STRING);
|
||||
$codigo_barras = filter_input(INPUT_POST, 'codigo_barras', FILTER_SANITIZE_STRING);
|
||||
$descripcion = filter_input(INPUT_POST, 'descripcion', FILTER_SANITIZE_STRING);
|
||||
$costo = filter_input(INPUT_POST, 'costo', FILTER_VALIDATE_FLOAT);
|
||||
$precio_venta = filter_input(INPUT_POST, 'precio_venta', FILTER_VALIDATE_FLOAT);
|
||||
@ -26,9 +27,9 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
|
||||
// 1. Insert product
|
||||
$sku = 'SKU-' . strtoupper(substr(uniqid(), -8));
|
||||
$sql_producto = "INSERT INTO productos (sku, nombre, descripcion, costo, precio_venta, created_at) VALUES (?, ?, ?, ?, ?, NOW())";
|
||||
$sql_producto = "INSERT INTO productos (sku, nombre, codigo_barras, descripcion, costo, precio_venta, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())";
|
||||
$stmt_producto = $pdo->prepare($sql_producto);
|
||||
$stmt_producto->execute([$sku, $nombre, $descripcion, $costo, $precio_venta]);
|
||||
$stmt_producto->execute([$sku, $nombre, $codigo_barras, $descripcion, $costo, $precio_venta]);
|
||||
|
||||
// Get the ID of the new product
|
||||
$producto_id = $pdo->lastInsertId();
|
||||
|
||||
@ -10,6 +10,7 @@ require_once 'db/config.php';
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$product_id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
$nombre = trim($_POST['nombre']);
|
||||
$codigo_barras = trim($_POST['codigo_barras']);
|
||||
$descripcion = trim($_POST['descripcion']);
|
||||
$costo = filter_input(INPUT_POST, 'costo', FILTER_VALIDATE_FLOAT);
|
||||
$precio_venta = filter_input(INPUT_POST, 'precio_venta', FILTER_VALIDATE_FLOAT);
|
||||
@ -17,10 +18,10 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
if ($product_id > 0 && !empty($nombre) && $costo !== false && $precio_venta !== false) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "UPDATE productos SET nombre = ?, descripcion = ?, costo = ?, precio_venta = ? WHERE id = ?";
|
||||
$sql = "UPDATE productos SET nombre = ?, codigo_barras = ?, descripcion = ?, costo = ?, precio_venta = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
if ($stmt->execute([$nombre, $descripcion, $costo, $precio_venta, $product_id])) {
|
||||
if ($stmt->execute([$nombre, $codigo_barras, $descripcion, $costo, $precio_venta, $product_id])) {
|
||||
$_SESSION['success_message'] = "Producto actualizado correctamente.";
|
||||
} else {
|
||||
$_SESSION['error_message'] = "Error al actualizar el producto.";
|
||||
|
||||
82
imprimir_codigo.php
Normal file
82
imprimir_codigo.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
|
||||
if ($product_id === 0) {
|
||||
die("ID de producto no válido.");
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT * FROM productos WHERE id = ?");
|
||||
$stmt->execute([$product_id]);
|
||||
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$product) {
|
||||
die("Producto no encontrado.");
|
||||
}
|
||||
|
||||
$barcodeValue = $product['codigo_barras'] ? $product['codigo_barras'] : $product['sku'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Imprimir Código de Barras - <?php echo htmlspecialchars($product['nombre']); ?></title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.barcode-container {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
}
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.barcode-container {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="no-print">
|
||||
<button onclick="window.print()">Imprimir</button>
|
||||
<button onclick="window.close()">Cerrar</button>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="barcode-container">
|
||||
<div class="product-name"><?php echo htmlspecialchars($product['nombre']); ?></div>
|
||||
<svg id="barcode"></svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
JsBarcode("#barcode", "<?php echo htmlspecialchars($barcodeValue); ?>", {
|
||||
format: "CODE128",
|
||||
lineColor: "#000",
|
||||
width: 2,
|
||||
height: 40,
|
||||
displayValue: true
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -6,5 +6,25 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Mobile Sidebar Toggle
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toggleBtn = document.getElementById('sidebarToggle');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
|
||||
if (toggleBtn && sidebar) {
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
if (sidebar.style.display === 'flex') {
|
||||
sidebar.style.display = ''; // Reset to CSS rule (none on mobile)
|
||||
sidebar.classList.remove('show');
|
||||
} else {
|
||||
sidebar.style.display = 'flex';
|
||||
sidebar.classList.add('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -29,8 +29,16 @@ if (!is_logged_in()) {
|
||||
<body>
|
||||
|
||||
<div class="d-flex">
|
||||
<!-- Mobile Navigation Bar -->
|
||||
<div class="mobile-nav w-100">
|
||||
<h4 class="m-0">Inventario</h4>
|
||||
<button class="btn btn-outline-light" id="sidebarToggle">
|
||||
<i class="fas fa-bars"></i> Menú
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Menú Lateral (Sidebar) -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<h3 class="sidebar-heading">Panel de control</h3>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
|
||||
@ -110,6 +110,7 @@ $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nombre</th>
|
||||
<th>Código de Barras</th>
|
||||
<th>Descripción</th>
|
||||
<th>Precio</th>
|
||||
<th>Stock por Ciudad</th>
|
||||
@ -122,10 +123,12 @@ $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($product['id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($product['nombre']); ?></td>
|
||||
<td><?php echo htmlspecialchars($product['codigo_barras'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo htmlspecialchars($product['descripcion']); ?></td>
|
||||
<td>$<?php echo htmlspecialchars(number_format($product['precio_venta'], 2)); ?></td>
|
||||
<td><?php echo $product['stock_por_ciudad'] ? $product['stock_por_ciudad'] : 'Sin stock'; ?></td>
|
||||
<td>
|
||||
<a href="imprimir_codigo.php?id=<?php echo $product['id']; ?>" target="_blank" class="btn btn-info btn-sm"><i class="fas fa-print"></i> Código</a>
|
||||
<a href="editar_producto.php?id=<?php echo $product['id']; ?>" class="btn btn-warning btn-sm"><i class="fas fa-edit"></i> Editar</a>
|
||||
<a href="eliminar_producto.php?id=<?php echo $product['id']; ?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Estás seguro de que quieres eliminar este producto?');"><i class="fas fa-trash"></i> Eliminar</a>
|
||||
</td>
|
||||
|
||||
@ -6,7 +6,7 @@ try {
|
||||
$pdo = db();
|
||||
|
||||
// Fetch products
|
||||
$stmt_productos = $pdo->query("SELECT id, nombre, sku FROM productos WHERE activo = 1 ORDER BY nombre ASC");
|
||||
$stmt_productos = $pdo->query("SELECT id, nombre, sku, codigo_barras FROM productos WHERE activo = 1 ORDER BY nombre ASC");
|
||||
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Fetch cities
|
||||
@ -21,66 +21,232 @@ try {
|
||||
|
||||
?>
|
||||
|
||||
<div class="container mt-5">
|
||||
<h2>Registrar Entrada de Stock</h2>
|
||||
<p>Utiliza este formulario para añadir nuevas unidades de un producto a una ciudad específica.</p>
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="h3 mb-3 text-center">Registrar Entrada</h2>
|
||||
<p class="text-muted text-center mb-4">Añade nuevas unidades al inventario.</p>
|
||||
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_SESSION['success'])): ?>
|
||||
<div class="alert alert-success" role="alert">
|
||||
<?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_SESSION['success'])): ?>
|
||||
<div class="alert alert-success" role="alert">
|
||||
<?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="handle_entrada.php" method="POST" class="mt-4">
|
||||
<div class="mb-3">
|
||||
<label for="producto_id" class="form-label">Producto</label>
|
||||
<select class="form-select" id="producto_id" name="producto_id" required>
|
||||
<option value="">Selecciona un producto</option>
|
||||
<?php foreach ($productos as $producto): ?>
|
||||
<option value="<?php echo htmlspecialchars($producto['id']); ?>">
|
||||
<?php echo htmlspecialchars($producto['nombre']) . ' (SKU: ' . htmlspecialchars($producto['sku']) . ')'; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<form action="handle_entrada.php" method="POST">
|
||||
<div class="mb-4">
|
||||
<label for="scan_barcode" class="form-label"><strong>Escanear Código</strong></label>
|
||||
<div class="d-grid gap-2">
|
||||
<input type="text" class="form-control" id="scan_barcode" placeholder="Haz clic y escanea..." autofocus>
|
||||
<button class="btn btn-outline-success" type="button" id="btn-scan-camera">
|
||||
<i class="fas fa-camera"></i> Escanear con Cámara
|
||||
</button>
|
||||
</div>
|
||||
<div id="reader" style="width: 100%; display:none; margin-top: 10px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ciudad_id" class="form-label">Ciudad de Destino</label>
|
||||
<select class="form-select" id="ciudad_id" name="ciudad_id" required>
|
||||
<option value="">Selecciona una ciudad</option>
|
||||
<?php foreach ($ciudades as $ciudad): ?>
|
||||
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>">
|
||||
<?php echo htmlspecialchars($ciudad['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="producto_id" class="form-label">Producto</label>
|
||||
<select class="form-select" id="producto_id" name="producto_id" required>
|
||||
<option value="">Selecciona un producto</option>
|
||||
<?php foreach ($productos as $producto): ?>
|
||||
<option value="<?php echo htmlspecialchars($producto['id']); ?>" data-barcode="<?php echo htmlspecialchars($producto['codigo_barras'] ?? ''); ?>" data-sku="<?php echo htmlspecialchars($producto['sku']); ?>">
|
||||
<?php echo htmlspecialchars($producto['nombre']) . ' (SKU: ' . htmlspecialchars($producto['sku']) . ')'; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="cantidad" class="form-label">Cantidad</label>
|
||||
<input type="number" class="form-control" id="cantidad" name="cantidad" min="1" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="ciudad_id" class="form-label">Ciudad de Destino</label>
|
||||
<select class="form-select" id="ciudad_id" name="ciudad_id" required>
|
||||
<option value="">Selecciona una ciudad</option>
|
||||
<?php foreach ($ciudades as $ciudad): ?>
|
||||
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>">
|
||||
<?php echo htmlspecialchars($ciudad['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fecha" class="form-label">Fecha de Entrada</label>
|
||||
<input type="date" class="form-control" id="fecha" name="fecha" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="observacion" class="form-label">Observación (Opcional)</label>
|
||||
<textarea class="form-control" id="observacion" name="observacion" rows="3" placeholder="Ej: Compra a proveedor, ajuste de inventario, etc."></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="cantidad" class="form-label">Cantidad</label>
|
||||
<input type="number" class="form-control" id="cantidad" name="cantidad" min="1" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">Registrar Entrada</button>
|
||||
<a href="productos.php" class="btn btn-secondary">Cancelar</a>
|
||||
</form>
|
||||
<div class="mb-4">
|
||||
<label for="fecha" class="form-label">Fecha de Entrada</label>
|
||||
<input type="date" class="form-control" id="fecha" name="fecha" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="observacion" class="form-label">Observación (Opcional)</label>
|
||||
<textarea class="form-control" id="observacion" name="observacion" rows="3" placeholder="Ej: Compra, ajuste..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success btn-lg">Registrar Entrada</button>
|
||||
<a href="productos.php" class="btn btn-outline-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include html5-qrcode library -->
|
||||
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const barcodeInput = document.getElementById('scan_barcode');
|
||||
const productSelect = document.getElementById('producto_id');
|
||||
const quantityInput = document.getElementById('cantidad');
|
||||
const btnScan = document.getElementById('btn-scan-camera');
|
||||
const readerDiv = document.getElementById('reader');
|
||||
let html5QrCode = null;
|
||||
let isScanning = false;
|
||||
let audioCtx = null;
|
||||
|
||||
function initAudio() {
|
||||
if (!audioCtx) {
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
if (audioCtx.state === 'suspended') {
|
||||
audioCtx.resume();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to search and select product
|
||||
function searchProduct(code) {
|
||||
if (code) {
|
||||
let found = false;
|
||||
for (let i = 0; i < productSelect.options.length; i++) {
|
||||
const option = productSelect.options[i];
|
||||
const barcode = option.getAttribute('data-barcode');
|
||||
const sku = option.getAttribute('data-sku');
|
||||
|
||||
if (barcode === code || sku === code) {
|
||||
productSelect.selectedIndex = i;
|
||||
found = true;
|
||||
barcodeInput.value = ''; // Clear input
|
||||
quantityInput.focus(); // Move to quantity
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
alert('Producto no encontrado con el código: ' + code);
|
||||
barcodeInput.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manual input listener
|
||||
barcodeInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Prevent form submission
|
||||
searchProduct(barcodeInput.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
// Camera scan listener
|
||||
btnScan.addEventListener('click', function() {
|
||||
initAudio();
|
||||
if (isScanning) {
|
||||
stopScanning();
|
||||
} else {
|
||||
startScanning();
|
||||
}
|
||||
});
|
||||
|
||||
function startScanning() {
|
||||
readerDiv.style.display = 'block';
|
||||
html5QrCode = new Html5Qrcode("reader");
|
||||
|
||||
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||
|
||||
// Prefer back camera
|
||||
html5QrCode.start({ facingMode: "environment" }, config, onScanSuccess, onScanFailure)
|
||||
.then(() => {
|
||||
isScanning = true;
|
||||
btnScan.innerHTML = '<i class="fas fa-stop"></i> Detener Cámara';
|
||||
btnScan.classList.remove('btn-outline-success');
|
||||
btnScan.classList.add('btn-danger');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error starting scanner", err);
|
||||
alert("No se pudo iniciar la cámara. Por favor, verifica los permisos y que estés usando HTTPS.");
|
||||
readerDiv.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanning() {
|
||||
if (html5QrCode) {
|
||||
html5QrCode.stop().then(() => {
|
||||
readerDiv.style.display = 'none';
|
||||
isScanning = false;
|
||||
btnScan.innerHTML = '<i class="fas fa-camera"></i> Escanear con Cámara';
|
||||
btnScan.classList.remove('btn-danger');
|
||||
btnScan.classList.add('btn-outline-success');
|
||||
html5QrCode.clear();
|
||||
}).catch(err => {
|
||||
console.error("Failed to stop scanner", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Professional scanner sound using Web Audio API
|
||||
function playScanSound() {
|
||||
if (!audioCtx) initAudio();
|
||||
const oscillator = audioCtx.createOscillator();
|
||||
const gainNode = audioCtx.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioCtx.destination);
|
||||
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.setValueAtTime(1200, audioCtx.currentTime); // 1200Hz beep
|
||||
|
||||
// Smooth envelope
|
||||
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
|
||||
gainNode.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + 0.01);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
|
||||
|
||||
oscillator.start();
|
||||
oscillator.stop(audioCtx.currentTime + 0.15);
|
||||
}
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
// Handle the scanned code
|
||||
console.log(`Code matched = ${decodedText}`, decodedResult);
|
||||
|
||||
// Play sound
|
||||
playScanSound();
|
||||
|
||||
// Stop scanning after successful read
|
||||
stopScanning();
|
||||
|
||||
// Fill input and search
|
||||
barcodeInput.value = decodedText;
|
||||
searchProduct(decodedText);
|
||||
}
|
||||
|
||||
function onScanFailure(error) {
|
||||
// handle scan failure, usually better to ignore and keep scanning.
|
||||
// console.warn(`Code scan error = ${error}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once 'includes/footer.php';
|
||||
?>
|
||||
?>
|
||||
@ -16,7 +16,7 @@ try {
|
||||
$pdo = db();
|
||||
|
||||
// Fetch products
|
||||
$stmt_productos = $pdo->query("SELECT id, nombre FROM productos ORDER BY nombre ASC");
|
||||
$stmt_productos = $pdo->query("SELECT id, nombre, sku, codigo_barras FROM productos ORDER BY nombre ASC");
|
||||
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Fetch cities
|
||||
@ -32,81 +32,247 @@ try {
|
||||
|
||||
?>
|
||||
|
||||
<div class="container mt-5">
|
||||
<h1>Registrar Salida de Producto</h1>
|
||||
<p>Selecciona el producto, la ciudad y la cantidad que deseas dar de baja del inventario.</p>
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h3 mb-3 text-center">Registrar Salida</h1>
|
||||
<p class="text-muted text-center mb-4">Escanea o selecciona el producto para dar de baja.</p>
|
||||
|
||||
<?php
|
||||
if (isset($_SESSION['error'])):
|
||||
?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<?php
|
||||
echo htmlspecialchars($_SESSION['error']);
|
||||
unset($_SESSION['error']);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
endif;
|
||||
?>
|
||||
<?php
|
||||
if (isset($_SESSION['success'])):
|
||||
?>
|
||||
<div class="alert alert-success" role="alert">
|
||||
<?php
|
||||
echo htmlspecialchars($_SESSION['success']);
|
||||
unset($_SESSION['success']);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
endif;
|
||||
?>
|
||||
<?php
|
||||
if (isset($_SESSION['error'])):
|
||||
?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<?php
|
||||
echo htmlspecialchars($_SESSION['error']);
|
||||
unset($_SESSION['error']);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
endif;
|
||||
?>
|
||||
<?php
|
||||
if (isset($_SESSION['success'])):
|
||||
?>
|
||||
<div class="alert alert-success" role="alert">
|
||||
<?php
|
||||
echo htmlspecialchars($_SESSION['success']);
|
||||
unset($_SESSION['success']);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
endif;
|
||||
?>
|
||||
|
||||
<form action="handle_salida.php" method="POST" class="mt-4">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="producto_id" class="form-label">Producto</label>
|
||||
<select class="form-select" id="producto_id" name="producto_id" required>
|
||||
<option value="">Selecciona un producto</option>
|
||||
<?php foreach ($productos as $producto): ?>
|
||||
<option value="<?php echo htmlspecialchars($producto['id']); ?>">
|
||||
<?php echo htmlspecialchars($producto['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<form action="handle_salida.php" method="POST">
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="scan_barcode" class="form-label"><strong>Escanear Código</strong></label>
|
||||
<div class="d-grid gap-2">
|
||||
<input type="text" class="form-control" id="scan_barcode" placeholder="Haz clic y escanea..." autofocus>
|
||||
<button class="btn btn-outline-primary" type="button" id="btn-scan-camera">
|
||||
<i class="fas fa-camera"></i> Escanear con Cámara
|
||||
</button>
|
||||
</div>
|
||||
<div id="reader" style="width: 100%; display:none; margin-top: 10px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ciudad_id" class="form-label">Ciudad</label>
|
||||
<select class="form-select" id="ciudad_id" name="ciudad_id" required>
|
||||
<option value="">Selecciona una ciudad</option>
|
||||
<?php foreach ($ciudades as $ciudad): ?>
|
||||
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>">
|
||||
<?php echo htmlspecialchars($ciudad['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="producto_id" class="form-label">Producto</label>
|
||||
<select class="form-select" id="producto_id" name="producto_id" required>
|
||||
<option value="">Selecciona un producto</option>
|
||||
<?php foreach ($productos as $producto): ?>
|
||||
<option value="<?php echo htmlspecialchars($producto['id']); ?>" data-barcode="<?php echo htmlspecialchars($producto['codigo_barras'] ?? ''); ?>" data-sku="<?php echo htmlspecialchars($producto['sku']); ?>">
|
||||
<?php echo htmlspecialchars($producto['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="cantidad" class="form-label">Cantidad</label>
|
||||
<input type="number" class="form-control" id="cantidad" name="cantidad" min="1" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fecha" class="form-label">Fecha de Salida</label>
|
||||
<input type="date" class="form-control" id="fecha" name="fecha" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="descripcion" class="form-label">Descripción (Opcional)</label>
|
||||
<textarea class="form-control" id="descripcion" name="descripcion" rows="3" placeholder="Ej: Venta a cliente, traslado a otra tienda, etc."></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="ciudad_id" class="form-label">Ciudad</label>
|
||||
<select class="form-select" id="ciudad_id" name="ciudad_id" required>
|
||||
<option value="">Selecciona una ciudad</option>
|
||||
<?php foreach ($ciudades as $ciudad): ?>
|
||||
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>">
|
||||
<?php echo htmlspecialchars($ciudad['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Registrar Salida</button>
|
||||
<a href="productos.php" class="btn btn-secondary">Cancelar</a>
|
||||
</form>
|
||||
<div class="mb-4">
|
||||
<label for="cantidad" class="form-label">Cantidad</label>
|
||||
<input type="number" class="form-control" id="cantidad" name="cantidad" min="1" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="fecha" class="form-label">Fecha de Salida</label>
|
||||
<input type="date" class="form-control" id="fecha" name="fecha" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="descripcion" class="form-label">Descripción (Opcional)</label>
|
||||
<textarea class="form-control" id="descripcion" name="descripcion" rows="3" placeholder="Ej: Venta, traslado..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Registrar Salida</button>
|
||||
<a href="productos.php" class="btn btn-outline-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include html5-qrcode library -->
|
||||
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const barcodeInput = document.getElementById('scan_barcode');
|
||||
const productSelect = document.getElementById('producto_id');
|
||||
const quantityInput = document.getElementById('cantidad');
|
||||
const btnScan = document.getElementById('btn-scan-camera');
|
||||
const readerDiv = document.getElementById('reader');
|
||||
let html5QrCode = null;
|
||||
let isScanning = false;
|
||||
let audioCtx = null;
|
||||
|
||||
function initAudio() {
|
||||
if (!audioCtx) {
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
if (audioCtx.state === 'suspended') {
|
||||
audioCtx.resume();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to search and select product
|
||||
function searchProduct(code) {
|
||||
if (code) {
|
||||
let found = false;
|
||||
for (let i = 0; i < productSelect.options.length; i++) {
|
||||
const option = productSelect.options[i];
|
||||
const barcode = option.getAttribute('data-barcode');
|
||||
const sku = option.getAttribute('data-sku');
|
||||
|
||||
if (barcode === code || sku === code) {
|
||||
productSelect.selectedIndex = i;
|
||||
found = true;
|
||||
barcodeInput.value = ''; // Clear input
|
||||
quantityInput.focus(); // Move to quantity
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
alert('Producto no encontrado con el código: ' + code);
|
||||
barcodeInput.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manual input listener
|
||||
barcodeInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Prevent form submission
|
||||
searchProduct(barcodeInput.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
// Camera scan listener
|
||||
btnScan.addEventListener('click', function() {
|
||||
initAudio();
|
||||
if (isScanning) {
|
||||
stopScanning();
|
||||
} else {
|
||||
startScanning();
|
||||
}
|
||||
});
|
||||
|
||||
function startScanning() {
|
||||
readerDiv.style.display = 'block';
|
||||
html5QrCode = new Html5Qrcode("reader");
|
||||
|
||||
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||
|
||||
// Prefer back camera
|
||||
html5QrCode.start({ facingMode: "environment" }, config, onScanSuccess, onScanFailure)
|
||||
.then(() => {
|
||||
isScanning = true;
|
||||
btnScan.innerHTML = '<i class="fas fa-stop"></i> Detener Cámara';
|
||||
btnScan.classList.remove('btn-outline-primary');
|
||||
btnScan.classList.add('btn-danger');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error starting scanner", err);
|
||||
alert("No se pudo iniciar la cámara. Por favor, verifica los permisos y que estés usando HTTPS.");
|
||||
readerDiv.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanning() {
|
||||
if (html5QrCode) {
|
||||
html5QrCode.stop().then(() => {
|
||||
readerDiv.style.display = 'none';
|
||||
isScanning = false;
|
||||
btnScan.innerHTML = '<i class="fas fa-camera"></i> Escanear con Cámara';
|
||||
btnScan.classList.remove('btn-danger');
|
||||
btnScan.classList.add('btn-outline-primary');
|
||||
html5QrCode.clear();
|
||||
}).catch(err => {
|
||||
console.error("Failed to stop scanner", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Professional scanner sound using Web Audio API
|
||||
function playScanSound() {
|
||||
if (!audioCtx) initAudio();
|
||||
const oscillator = audioCtx.createOscillator();
|
||||
const gainNode = audioCtx.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioCtx.destination);
|
||||
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.setValueAtTime(1200, audioCtx.currentTime); // 1200Hz beep
|
||||
|
||||
// Smooth envelope
|
||||
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
|
||||
gainNode.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + 0.01);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
|
||||
|
||||
oscillator.start();
|
||||
oscillator.stop(audioCtx.currentTime + 0.15);
|
||||
}
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
// Handle the scanned code
|
||||
console.log(`Code matched = ${decodedText}`, decodedResult);
|
||||
|
||||
// Play sound
|
||||
playScanSound();
|
||||
|
||||
// Stop scanning after successful read
|
||||
stopScanning();
|
||||
|
||||
// Fill input and search
|
||||
barcodeInput.value = decodedText;
|
||||
searchProduct(decodedText);
|
||||
}
|
||||
|
||||
function onScanFailure(error) {
|
||||
// handle scan failure, usually better to ignore and keep scanning.
|
||||
// console.warn(`Code scan error = ${error}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once 'includes/footer.php';
|
||||
?>
|
||||
Loading…
x
Reference in New Issue
Block a user