Autosave: 20260212-193734
@ -86,6 +86,7 @@ body {
|
||||
@media (max-width: 992px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
z-index: 1020; /* Ensure sidebar is on top */
|
||||
}
|
||||
|
||||
.sidebar.active {
|
||||
@ -95,6 +96,7 @@ body {
|
||||
.content {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
transition: filter 0.3s ease; /* Smooth transition for the filter */
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
@ -102,18 +104,21 @@ body {
|
||||
}
|
||||
|
||||
body.sidebar-active .content {
|
||||
margin-left: 260px;
|
||||
/* Don't push content, but apply a visual effect */
|
||||
filter: blur(3px) brightness(0.6);
|
||||
pointer-events: none; /* Prevent interaction with content when sidebar is open */
|
||||
}
|
||||
|
||||
.sidebar .nav {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
|
||||
}
|
||||
|
||||
.sidebar .nav-item {
|
||||
flex-basis: auto; /* Allow items to take their natural width */
|
||||
/* The body itself gets an overlay to darken it */
|
||||
body.sidebar-active::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.4);
|
||||
z-index: 1010; /* Below sidebar, above content */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 36 KiB |
BIN
assets/uploads/vouchers/698e14248ef39-Screenshot_1.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
assets/uploads/vouchers/698e1e9c38f85-249.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
assets/uploads/vouchers/698e1edfbe627-086.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
assets/uploads/vouchers/698e1f028c75e-0125.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
assets/uploads/vouchers/698e1fc9d9fee-862.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
assets/uploads/vouchers/698e1ff94eafc-137.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
assets/uploads/vouchers/698e226b697a6-203.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
assets/uploads/vouchers/698e229c3e93c-366.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
assets/uploads/vouchers/698e22bde0511-963.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
assets/uploads/vouchers/698e24fe5473c-913.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
assets/uploads/vouchers/698e252edb996-811.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
assets/uploads/vouchers/698e258a009ff-854.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
BIN
assets/uploads/vouchers/698e293b2b560-300.png
Normal file
|
After Width: | Height: | Size: 527 KiB |
BIN
assets/uploads/vouchers/698e2966812dd-696.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
assets/uploads/vouchers/698e2993669be-615.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
assets/uploads/vouchers/698e29b2b30c8-754.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 55 KiB |
@ -117,11 +117,22 @@ if (!empty($generated_codes)) {
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h3 class="card-title mb-0">Códigos Generados</h3>
|
||||
<form action="imprimir_etiquetas.php" method="POST" target="_blank" class="m-0">
|
||||
<?php
|
||||
// Get product name to pass to the export script
|
||||
$product_name = '';
|
||||
foreach ($products as $p) {
|
||||
if ($p['id'] == $producto_id) {
|
||||
$product_name = $p['nombre'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<input type="hidden" name="product_name" value="<?php echo htmlspecialchars($product_name); ?>">
|
||||
<?php foreach ($generated_codes as $code): ?>
|
||||
<input type="hidden" name="codes[]" value="<?php echo htmlspecialchars($code); ?>">
|
||||
<?php endforeach; ?>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-print"></i> Imprimir Etiquetas
|
||||
<i class="fas fa-file-excel"></i> Exportar a Excel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -1,99 +1,38 @@
|
||||
<?php
|
||||
|
||||
|
||||
// Receive data from the form
|
||||
$codes = $_POST['codes'] ?? [];
|
||||
$product_name = $_POST['product_name'] ?? 'Producto Desconocido';
|
||||
|
||||
if (empty($codes)) {
|
||||
// For direct access, you can add a fallback or a message.
|
||||
// For now, we'll just show a blank page if no codes are provided.
|
||||
die("No se proporcionaron códigos para exportar.");
|
||||
}
|
||||
|
||||
// 1. Set HTTP headers to trigger file download
|
||||
$filename = "etiquetas_" . str_replace(' ', '_', $product_name) . "_" . date("Y-m-d") . ".csv";
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Imprimir Etiquetas</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
@media print {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
.label {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.label-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr; /* Two columns */
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.label {
|
||||
border: 1px dashed #ccc;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
}
|
||||
.barcode-svg svg {
|
||||
max-width: 100%;
|
||||
height: 50px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.code-text {
|
||||
font-size: 0.7rem;
|
||||
word-break: break-all;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.print-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
// 2. Create a file pointer connected to the output stream
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="my-3 text-center no-print">
|
||||
<h1 class="h3">Vista Previa de Impresión</h1>
|
||||
<p>Ajusta el tamaño y la escala en el diálogo de impresión de tu navegador si es necesario.</p>
|
||||
<button onclick="window.print();" class="btn btn-primary">
|
||||
<i class="fas fa-print"></i> Imprimir Ahora
|
||||
</button>
|
||||
</div>
|
||||
// 3. Add a UTF-8 BOM to ensure Excel opens it correctly
|
||||
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
|
||||
|
||||
<?php if (!empty($codes)): ?>
|
||||
<div class="label-grid">
|
||||
<?php foreach ($codes as $code): ?>
|
||||
<div class="label">
|
||||
<div class="barcode-svg">
|
||||
<img src="https://barcode.tec-it.com/barcode.ashx?data=<?php echo urlencode($code); ?>&code=Code128" alt="Barcode for <?php echo htmlspecialchars($code); ?>" style="max-height: 50px;">
|
||||
</div>
|
||||
<div class="code-text">
|
||||
<code><?php echo htmlspecialchars($code); ?></code>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning text-center no-print">
|
||||
No se han proporcionado códigos. Por favor, <a href="generar_etiquetas.php">vuelve al generador</a> para crear nuevas etiquetas.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
// 4. Write the header row
|
||||
$header = ['Nombre del producto', 'Código único', 'Código de barra (usar fuente)'];
|
||||
fputcsv($output, $header, ';');
|
||||
|
||||
</body>
|
||||
</html>
|
||||
// 5. Write the data rows
|
||||
foreach ($codes as $code) {
|
||||
$row = [
|
||||
$product_name,
|
||||
$code,
|
||||
$code // The same code, to be formatted as a barcode in Excel
|
||||
];
|
||||
fputcsv($output, $row, ';');
|
||||
}
|
||||
|
||||
// 6. Close the file pointer
|
||||
fclose($output);
|
||||
|
||||
exit;
|
||||
@ -31,13 +31,36 @@
|
||||
const sidebarToggle = document.querySelector('.sidebar-toggle');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const body = document.querySelector('body');
|
||||
const content = document.querySelector('.content');
|
||||
|
||||
function closeSidebar() {
|
||||
if (sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
body.classList.remove('sidebar-active');
|
||||
}
|
||||
}
|
||||
|
||||
if (sidebarToggle && sidebar) {
|
||||
sidebarToggle.addEventListener('click', function() {
|
||||
sidebarToggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // Prevent click from bubbling to body
|
||||
sidebar.classList.toggle('active');
|
||||
body.classList.toggle('sidebar-active');
|
||||
});
|
||||
}
|
||||
|
||||
// Close sidebar if user clicks on the content area
|
||||
if (content) {
|
||||
content.addEventListener('click', function() {
|
||||
closeSidebar();
|
||||
});
|
||||
}
|
||||
|
||||
// Close sidebar on body click if the click is outside the sidebar
|
||||
body.addEventListener('click', function(e) {
|
||||
if (body.classList.contains('sidebar-active') && !sidebar.contains(e.target)) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
83
registrar_entrada_api.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = ['success' => false, 'message' => 'Solicitud inválida.'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$codigo_unico = filter_input(INPUT_POST, 'codigo_unico', FILTER_SANITIZE_STRING);
|
||||
$sede_id = filter_input(INPUT_POST, 'sede_id', FILTER_VALIDATE_INT);
|
||||
$movement_date = date('Y-m-d H:i:s');
|
||||
|
||||
if ($codigo_unico && $sede_id) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Buscar la unidad de inventario
|
||||
$stmt_unidad = $pdo->prepare("SELECT * FROM unidades_inventario WHERE codigo_unico = :codigo_unico");
|
||||
$stmt_unidad->execute(['codigo_unico' => $codigo_unico]);
|
||||
$unidad = $stmt_unidad->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$unidad) {
|
||||
throw new Exception("El código de unidad '$codigo_unico' no existe.");
|
||||
}
|
||||
|
||||
if ($unidad['estado'] === 'En Almacén') {
|
||||
throw new Exception("Esta unidad ya se encuentra en el almacén.");
|
||||
}
|
||||
|
||||
if ($unidad['estado'] === 'Vendido') {
|
||||
throw new Exception("Esta unidad ya fue vendida y no puede ser ingresada nuevamente.");
|
||||
}
|
||||
|
||||
// 2. Actualizar el estado de la unidad
|
||||
$update_unidad_stmt = $pdo->prepare("UPDATE unidades_inventario SET estado = 'En Almacén', fecha_ingreso = :fecha_ingreso WHERE id = :id");
|
||||
$update_unidad_stmt->execute(['fecha_ingreso' => $movement_date, 'id' => $unidad['id']]);
|
||||
|
||||
$product_id = $unidad['producto_id'];
|
||||
$quantity = 1;
|
||||
|
||||
// 3. Actualizar o insertar en stock_sedes
|
||||
$stmt_stock = $pdo->prepare("SELECT * FROM stock_sedes WHERE product_id = :product_id AND sede_id = :sede_id");
|
||||
$stmt_stock->execute(['product_id' => $product_id, 'sede_id' => $sede_id]);
|
||||
$existing_stock = $stmt_stock->fetch();
|
||||
|
||||
if ($existing_stock) {
|
||||
$new_quantity = $existing_stock['quantity'] + $quantity;
|
||||
$update_stock_stmt = $pdo->prepare("UPDATE stock_sedes SET quantity = :quantity WHERE id = :id");
|
||||
$update_stock_stmt->execute(['quantity' => $new_quantity, 'id' => $existing_stock['id']]);
|
||||
} else {
|
||||
$insert_stock_stmt = $pdo->prepare("INSERT INTO stock_sedes (product_id, sede_id, quantity) VALUES (:product_id, :sede_id, :quantity)");
|
||||
$insert_stock_stmt->execute(['product_id' => $product_id, 'sede_id' => $sede_id, 'quantity' => $quantity]);
|
||||
}
|
||||
|
||||
// 4. Insertar en el historial de movimientos
|
||||
$history_stmt = $pdo->prepare(
|
||||
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date)
|
||||
VALUES (:product_id, :sede_id, :quantity, 'entrada', :movement_date)"
|
||||
);
|
||||
$history_stmt->execute([
|
||||
'product_id' => $product_id,
|
||||
'sede_id' => $sede_id,
|
||||
'quantity' => $quantity,
|
||||
'movement_date' => $movement_date
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
$response = ['success' => true, 'message' => "Unidad '$codigo_unico' registrada correctamente."];
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
$response = ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
} else {
|
||||
$response['message'] = 'Por favor, proporcione un código de unidad y una sede.';
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
?>
|
||||
@ -3,81 +3,7 @@ $pageTitle = "Registro de Entrada por Unidad";
|
||||
require_once 'layout_header.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
$message = '';
|
||||
$error = '';
|
||||
|
||||
// Lógica para manejar el envío del formulario
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$codigo_unico = filter_input(INPUT_POST, 'codigo_unico', FILTER_SANITIZE_STRING);
|
||||
$sede_id = filter_input(INPUT_POST, 'sede_id', FILTER_VALIDATE_INT);
|
||||
$movement_date = date('Y-m-d H:i:s');
|
||||
|
||||
if ($codigo_unico && $sede_id) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Buscar la unidad de inventario
|
||||
$stmt_unidad = $pdo->prepare("SELECT * FROM unidades_inventario WHERE codigo_unico = :codigo_unico");
|
||||
$stmt_unidad->execute(['codigo_unico' => $codigo_unico]);
|
||||
$unidad = $stmt_unidad->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$unidad) {
|
||||
throw new Exception("El código de unidad '$codigo_unico' no existe.");
|
||||
}
|
||||
|
||||
if ($unidad['estado'] === 'En Almacén') {
|
||||
throw new Exception("Esta unidad ya se encuentra en el almacén.");
|
||||
}
|
||||
|
||||
if ($unidad['estado'] === 'Vendido') {
|
||||
throw new Exception("Esta unidad ya fue vendida y no puede ser ingresada nuevamente.");
|
||||
}
|
||||
|
||||
// 2. Actualizar el estado de la unidad
|
||||
$update_unidad_stmt = $pdo->prepare("UPDATE unidades_inventario SET estado = 'En Almacén', fecha_ingreso = :fecha_ingreso WHERE id = :id");
|
||||
$update_unidad_stmt->execute(['fecha_ingreso' => $movement_date, 'id' => $unidad['id']]);
|
||||
|
||||
$product_id = $unidad['producto_id'];
|
||||
$quantity = 1; // Siempre es 1 para el inventario serializado
|
||||
|
||||
// 3. Actualizar o insertar en stock_sedes (reutilizando lógica anterior)
|
||||
$stmt_stock = $pdo->prepare("SELECT * FROM stock_sedes WHERE product_id = :product_id AND sede_id = :sede_id");
|
||||
$stmt_stock->execute(['product_id' => $product_id, 'sede_id' => $sede_id]);
|
||||
$existing_stock = $stmt_stock->fetch();
|
||||
|
||||
if ($existing_stock) {
|
||||
$new_quantity = $existing_stock['quantity'] + $quantity;
|
||||
$update_stock_stmt = $pdo->prepare("UPDATE stock_sedes SET quantity = :quantity WHERE id = :id");
|
||||
$update_stock_stmt->execute(['quantity' => $new_quantity, 'id' => $existing_stock['id']]);
|
||||
} else {
|
||||
$insert_stock_stmt = $pdo->prepare("INSERT INTO stock_sedes (product_id, sede_id, quantity) VALUES (:product_id, :sede_id, :quantity)");
|
||||
$insert_stock_stmt->execute(['product_id' => $product_id, 'sede_id' => $sede_id, 'quantity' => $quantity]);
|
||||
}
|
||||
|
||||
// 4. Insertar en el historial de movimientos
|
||||
$history_stmt = $pdo->prepare(
|
||||
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date)
|
||||
VALUES (:product_id, :sede_id, :quantity, 'entrada', :movement_date)"
|
||||
);
|
||||
$history_stmt->execute([
|
||||
'product_id' => $product_id,
|
||||
'sede_id' => $sede_id,
|
||||
'quantity' => $quantity,
|
||||
'movement_date' => $movement_date
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
$message = "Unidad '$codigo_unico' registrada en el inventario correctamente.";
|
||||
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
$error = "Error: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = "Por favor, escanee un código y seleccione una sede.";
|
||||
}
|
||||
}
|
||||
$error_page_load = '';
|
||||
|
||||
// Obtener sedes para el dropdown
|
||||
$sedes = [];
|
||||
@ -86,7 +12,7 @@ try {
|
||||
$sedes_stmt = $pdo->query("SELECT id, nombre FROM sedes ORDER BY nombre ASC");
|
||||
$sedes = $sedes_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
$error = "Error al cargar las sedes: " . $e->getMessage();
|
||||
$error_page_load = "Error al cargar las sedes: " . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
|
||||
@ -94,11 +20,13 @@ try {
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mx-auto">
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success" role="alert" id="form-message"><?php echo htmlspecialchars($message); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger" role="alert" id="form-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<!-- Contenedor para notificaciones (toasts) -->
|
||||
<div id="notification-container" class="position-fixed top-0 end-0 p-3" style="z-index: 1100"></div>
|
||||
|
||||
<?php if (!empty($error_page_load)): ?>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<?php echo htmlspecialchars($error_page_load); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
@ -106,17 +34,7 @@ try {
|
||||
<i class="fa fa-barcode"></i> Registro de Entrada por Unidad
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="registro_entrada.php" method="post" id="entrada-form">
|
||||
<div class="mb-3">
|
||||
<label for="codigo_unico" class="form-label">Código de Unidad</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="codigo_unico" name="codigo_unico" required autofocus>
|
||||
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#scannerModal">
|
||||
<i class="fa fa-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="scan-form" onsubmit="return false;">
|
||||
<div class="mb-3">
|
||||
<label for="sede" class="form-label">Sede de Destino</label>
|
||||
<select class="form-select" id="sede" name="sede_id" required>
|
||||
@ -128,7 +46,23 @@ try {
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100"> <i class="fa fa-plus-circle"></i> Registrar Entrada</button>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label for="barcode-input" class="form-label">Esperando código de barras...</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-lg" id="barcode-input" placeholder="Escanee el producto aquí" autofocus>
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#camera-modal">
|
||||
<i class="fa fa-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scanned-product-info" class="mt-3" style="display: none;">
|
||||
<p><strong>Producto escaneado:</strong> <span id="scanned-code"></span></p>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" id="accept-btn" class="btn btn-success"> <i class="fa fa-check-circle"></i> Aceptar y Registrar Entrada</button>
|
||||
<button type="button" id="cancel-btn" class="btn btn-danger"> <i class="fa fa-times-circle"></i> Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -136,77 +70,194 @@ try {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para el Escáner -->
|
||||
<div class="modal fade" id="scannerModal" tabindex="-1" aria-labelledby="scannerModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="scannerModalLabel">Escanear Código de Unidad</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="reader" style="width: 100%;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
</div>
|
||||
<!-- Modal para la cámara -->
|
||||
<div class="modal fade" id="camera-modal" tabindex="-1" aria-labelledby="camera-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="camera-modal-label">Escanear Código de Barras</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="reader" width="100%"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
|
||||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const codigoUnicoInput = document.getElementById('codigo_unico');
|
||||
const form = document.getElementById('entrada-form');
|
||||
|
||||
if(codigoUnicoInput) {
|
||||
codigoUnicoInput.focus();
|
||||
}
|
||||
|
||||
if (form) {
|
||||
codigoUnicoInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof bootstrap === 'undefined' || typeof Html5Qrcode === 'undefined') {
|
||||
console.error('Bootstrap o Html5Qrcode no están cargados.');
|
||||
// --- CONFIGURACIÓN INICIAL ---
|
||||
if (typeof bootstrap === 'undefined') {
|
||||
console.error('Bootstrap no está cargado.');
|
||||
return;
|
||||
}
|
||||
|
||||
const scannerModalElement = document.getElementById('scannerModal');
|
||||
if (!scannerModalElement) return;
|
||||
const barcodeInput = document.getElementById('barcode-input');
|
||||
const sedeSelect = document.getElementById('sede');
|
||||
const scannedProductInfo = document.getElementById('scanned-product-info');
|
||||
const scannedCodeSpan = document.getElementById('scanned-code');
|
||||
const acceptBtn = document.getElementById('accept-btn');
|
||||
const cancelBtn = document.getElementById('cancel-btn');
|
||||
|
||||
const scannerModal = new bootstrap.Modal(scannerModalElement);
|
||||
const html5QrCode = new Html5Qrcode("reader");
|
||||
let processing = false;
|
||||
let lastScannedCode = null;
|
||||
|
||||
const qrCodeSuccessCallback = (decodedText, decodedResult) => {
|
||||
html5QrCode.stop().then(ignore => {}).catch(err => console.log("Failed to stop scanner"));
|
||||
scannerModal.hide();
|
||||
|
||||
if(codigoUnicoInput) {
|
||||
codigoUnicoInput.value = decodedText;
|
||||
codigoUnicoInput.focus();
|
||||
// --- LÓGICA DE SONIDO (WEB AUDIO API) ---
|
||||
let audioCtx;
|
||||
function wakeUpAudio() {
|
||||
if (!audioCtx) {
|
||||
try {
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
} catch (e) { console.error("Web Audio API no es soportada.", e); return; }
|
||||
}
|
||||
};
|
||||
if (audioCtx.state === 'suspended') {
|
||||
audioCtx.resume();
|
||||
}
|
||||
}
|
||||
document.body.addEventListener('click', wakeUpAudio, { once: true });
|
||||
document.body.addEventListener('touchstart', wakeUpAudio, { once: true });
|
||||
|
||||
function playBeep() {
|
||||
if (!audioCtx || audioCtx.state !== 'running') {
|
||||
wakeUpAudio();
|
||||
if (!audioCtx || audioCtx.state !== 'running') return;
|
||||
}
|
||||
const oscillator = audioCtx.createOscillator();
|
||||
const gainNode = audioCtx.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioCtx.destination);
|
||||
gainNode.gain.value = 0.1;
|
||||
oscillator.frequency.value = 880;
|
||||
oscillator.type = 'sine';
|
||||
oscillator.start();
|
||||
setTimeout(() => oscillator.stop(), 150);
|
||||
}
|
||||
|
||||
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||
// --- INICIALIZACIÓN DE CÁMARA ---
|
||||
const cameraModal = document.getElementById('camera-modal');
|
||||
let html5QrCode;
|
||||
|
||||
scannerModalElement.addEventListener('shown.bs.modal', function () {
|
||||
html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
|
||||
.catch(err => {
|
||||
alert("Error al iniciar la cámara. Asegúrese de dar permisos.");
|
||||
});
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
const modal = bootstrap.Modal.getInstance(cameraModal);
|
||||
if(modal) modal.hide();
|
||||
|
||||
barcodeInput.value = decodedText;
|
||||
const changeEvent = new Event('change');
|
||||
barcodeInput.dispatchEvent(changeEvent);
|
||||
}
|
||||
|
||||
cameraModal.addEventListener('shown.bs.modal', function () {
|
||||
wakeUpAudio();
|
||||
html5QrCode = new Html5Qrcode("reader");
|
||||
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||
html5QrCode.start({ facingMode: "environment" }, config, onScanSuccess, (e)=>{})
|
||||
.catch(err => alert("Error al iniciar la cámara. Asegúrate de dar permisos."));
|
||||
});
|
||||
|
||||
scannerModalElement.addEventListener('hidden.bs.modal', function () {
|
||||
html5QrCode.stop().catch(err => {});
|
||||
cameraModal.addEventListener('hidden.bs.modal', function () {
|
||||
if (html5QrCode && html5QrCode.isScanning) {
|
||||
html5QrCode.stop().catch(err => {});
|
||||
}
|
||||
});
|
||||
|
||||
// --- FUNCIONES DE AYUDA (Notificaciones) ---
|
||||
function showNotification(message, isSuccess) {
|
||||
const container = document.getElementById('notification-container');
|
||||
if (!container) return;
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHTML = `
|
||||
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header ${isSuccess ? 'bg-success text-white' : 'bg-danger text-white'}">
|
||||
<strong class="me-auto">${isSuccess ? 'Éxito' : 'Error'}</strong>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">${message}</div>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML('beforeend', toastHTML);
|
||||
const toast = new bootstrap.Toast(document.getElementById(toastId), { delay: 5000 });
|
||||
toast.show();
|
||||
document.getElementById(toastId).addEventListener('hidden.bs.toast', e => e.target.remove());
|
||||
}
|
||||
|
||||
function resetScanner() {
|
||||
lastScannedCode = null;
|
||||
barcodeInput.value = '';
|
||||
barcodeInput.disabled = false;
|
||||
scannedProductInfo.style.display = 'none';
|
||||
processing = false;
|
||||
barcodeInput.focus();
|
||||
}
|
||||
|
||||
// --- LÓGICA PRINCIPAL DEL ESCÁNER ---
|
||||
barcodeInput.addEventListener('change', function() {
|
||||
const barcodeValue = this.value.trim();
|
||||
if (barcodeValue === '') return;
|
||||
|
||||
playBeep();
|
||||
|
||||
lastScannedCode = barcodeValue;
|
||||
scannedCodeSpan.textContent = barcodeValue;
|
||||
scannedProductInfo.style.display = 'block';
|
||||
this.disabled = true;
|
||||
acceptBtn.focus(); // Mover foco al botón de aceptar
|
||||
});
|
||||
|
||||
// --- LÓGICA DE BOTONES ACEPTAR/CANCELAR ---
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
resetScanner();
|
||||
});
|
||||
|
||||
acceptBtn.addEventListener('click', function() {
|
||||
if (processing || !lastScannedCode) return;
|
||||
|
||||
processing = true;
|
||||
|
||||
const sedeId = sedeSelect.value;
|
||||
if (!sedeId) {
|
||||
showNotification("Por favor, seleccione una sede de destino.", false);
|
||||
processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('codigo_unico', lastScannedCode);
|
||||
formData.append('sede_id', sedeId);
|
||||
|
||||
fetch('registrar_entrada_api.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, true);
|
||||
} else {
|
||||
throw new Error(data.message || 'Error desconocido al registrar la entrada.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification(error.message, false);
|
||||
})
|
||||
.finally(() => {
|
||||
resetScanner();
|
||||
});
|
||||
});
|
||||
|
||||
// Mantener el foco en el input principal cuando no se interactúa con otros elementos
|
||||
barcodeInput.focus();
|
||||
document.body.addEventListener('click', (e) => {
|
||||
// No re-enfocar si se hace clic en inputs, botones, selects, o dentro de un modal
|
||||
if (!e.target.closest('input, button, select, .modal')) {
|
||||
barcodeInput.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'layout_footer.php'; ?>
|
||||
<?php require_once 'layout_footer.php'; ?>
|
||||
|
||||