Autosave: 20260217-031338
This commit is contained in:
parent
520d1acb1b
commit
1bf602b5b0
@ -16,11 +16,11 @@ body {
|
||||
left: 0;
|
||||
background: linear-gradient(145deg, #2c3e50, #34495e);
|
||||
color: #ecf0f1;
|
||||
padding-top: 20px;
|
||||
box-shadow: 2px 0 15px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
overflow-y: auto; /* Permite el scroll vertical si el contenido es muy largo */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar .navbar-brand {
|
||||
@ -29,6 +29,12 @@ body {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0; /* Prevent brand from shrinking */
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
flex-grow: 1; /* Takes up all available space */
|
||||
overflow-y: auto; /* Allows scrolling for menu items */
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
@ -53,10 +59,13 @@ body {
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
.sidebar .nav-item.mt-auto {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
.logout-wrapper {
|
||||
flex-shrink: 0; /* Prevents the logout area from shrinking */
|
||||
border-top: 1px solid #4a627a; /* Separator line */
|
||||
}
|
||||
|
||||
.logout-wrapper .nav-link {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Content Area Styles */
|
||||
|
||||
@ -216,7 +216,7 @@ $navItems = [
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo isset($pageTitle) ? htmlspecialchars($pageTitle) : 'FLOOWER CRM'; ?></title>
|
||||
<title><?php echo isset($pageTitle) ? htmlspecialchars($pageTitle) : 'FLOOWER ERP'; ?></title>
|
||||
<meta name="description" content="CRM de seguimiento de pedidos para Floower Store.">
|
||||
<meta name="keywords" content="dashboard, call center, payment validation, order tracking, crm">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
@ -230,53 +230,55 @@ $navItems = [
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="sidebar">
|
||||
<a href="pedidos.php" class="navbar-brand"><h3>FLOOWER CRM</h3></a>
|
||||
<ul class="nav flex-column">
|
||||
<?php
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
<a href="pedidos.php" class="navbar-brand"><h3>FLOOWER ERP</h3></a>
|
||||
<div class="menu-wrapper">
|
||||
<ul class="nav flex-column">
|
||||
<?php
|
||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
foreach ($navItems as $key => $item):
|
||||
if (in_array($userRole, $item['roles'])):
|
||||
if (isset($item['submenu'])):
|
||||
$isSubmenuActive = false;
|
||||
foreach ($item['submenu'] as $sub_item) {
|
||||
if ($currentPage == $sub_item['url']) {
|
||||
$isSubmenuActive = true;
|
||||
break;
|
||||
foreach ($navItems as $key => $item):
|
||||
if (in_array($userRole, $item['roles'])):
|
||||
if (isset($item['submenu'])):
|
||||
$isSubmenuActive = false;
|
||||
foreach ($item['submenu'] as $sub_item) {
|
||||
if ($currentPage == $sub_item['url']) {
|
||||
$isSubmenuActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<li class="nav-item has-submenu <?php echo $isSubmenuActive ? 'active' : ''; ?>">
|
||||
<a href="#" class="nav-link"><i class="fas <?php echo $item['icon']; ?>"></i> <?php echo $item['text']; ?></a>
|
||||
<ul class="submenu <?php echo $isSubmenuActive ? 'show' : ''; ?>">
|
||||
<?php foreach ($item['submenu'] as $sub_item):
|
||||
if (in_array($userRole, $sub_item['roles'])):
|
||||
$isActive = $currentPage == $sub_item['url'] ? 'active' : '';
|
||||
?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo $sub_item['url']; ?>" class="nav-link <?php echo $isActive; ?>"><i class="fas <?php echo $sub_item['icon']; ?>"></i> <?php echo $sub_item['text']; ?></a>
|
||||
</li>
|
||||
<?php
|
||||
endif;
|
||||
endforeach; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php else:
|
||||
$isActive = $currentPage == $item['url'] ? 'active' : '';
|
||||
?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo $item['url']; ?>" class="nav-link <?php echo $isActive; ?>"><i class="fas <?php echo $item['icon']; ?>"></i> <?php echo $item['text']; ?></a>
|
||||
</li>
|
||||
<?php endif;
|
||||
endif;
|
||||
endforeach;
|
||||
?>
|
||||
<li class="nav-item mt-auto">
|
||||
<a href="logout.php" class="nav-link"><i class="fas fa-sign-out-alt"></i> Cerrar Sesión</a>
|
||||
</li>
|
||||
</ul>
|
||||
?>
|
||||
<li class="nav-item has-submenu <?php echo $isSubmenuActive ? 'active' : ''; ?>">
|
||||
<a href="#" class="nav-link"><i class="fas <?php echo $item['icon']; ?>"></i> <?php echo $item['text']; ?></a>
|
||||
<ul class="submenu <?php echo $isSubmenuActive ? 'show' : ''; ?>">
|
||||
<?php foreach ($item['submenu'] as $sub_item):
|
||||
if (in_array($userRole, $sub_item['roles'])):
|
||||
$isActive = $currentPage == $sub_item['url'] ? 'active' : '';
|
||||
?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo $sub_item['url']; ?>" class="nav-link <?php echo $isActive; ?>"><i class="fas <?php echo $sub_item['icon']; ?>"></i> <?php echo $sub_item['text']; ?></a>
|
||||
</li>
|
||||
<?php
|
||||
endif;
|
||||
endforeach; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php else:
|
||||
$isActive = $currentPage == $item['url'] ? 'active' : '';
|
||||
?>
|
||||
<li class="nav-item">
|
||||
<a href="<?php echo $item['url']; ?>" class="nav-link <?php echo $isActive; ?>"><i class="fas <?php echo $item['icon']; ?>"></i> <?php echo $item['text']; ?></a>
|
||||
</li>
|
||||
<?php endif;
|
||||
endif;
|
||||
endforeach;
|
||||
?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="logout-wrapper">
|
||||
<a href="logout.php" class="nav-link"><i class="fas fa-sign-out-alt"></i> Cerrar Sesión</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4"><?php echo isset($pageTitle) ? htmlspecialchars($pageTitle) : 'Bienvenido'; ?></h1>
|
||||
<h1 class="mt-4"><?php echo isset($pageTitle) ? htmlspecialchars($pageTitle) : 'Bienvenido'; ?></h1>
|
||||
|
||||
@ -155,6 +155,7 @@ include 'layout_header.php';
|
||||
<th>Celular</th>
|
||||
<th>Producto</th>
|
||||
<th>Sede de Envío</th>
|
||||
<th>Cantidad</th>
|
||||
<th>Monto Total</th>
|
||||
<th>Monto Debe</th>
|
||||
<th>Nº De Orden</th>
|
||||
@ -176,6 +177,7 @@ include 'layout_header.php';
|
||||
<td><?php echo htmlspecialchars($pedido['celular']); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['producto']); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['sede_envio'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['cantidad'] ?? '1'); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
|
||||
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="codigo_rastreo"><?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?></td>
|
||||
|
||||
62
registrar_entrada_manual_api.php
Normal file
62
registrar_entrada_manual_api.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once 'db/config.php';
|
||||
|
||||
$response = ['success' => false, 'message' => 'Petición inválida.'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$sede_id = isset($_POST['sede_id']) ? filter_var($_POST['sede_id'], FILTER_VALIDATE_INT) : null;
|
||||
$product_id = isset($_POST['product_id']) ? filter_var($_POST['product_id'], FILTER_VALIDATE_INT) : null;
|
||||
$cantidad = isset($_POST['cantidad']) ? filter_var($_POST['cantidad'], FILTER_VALIDATE_INT) : null;
|
||||
|
||||
if (!$sede_id || !$product_id || !$cantidad || $cantidad <= 0) {
|
||||
$response['message'] = 'Datos inválidos. Por favor, complete todos los campos correctamente.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Actualizar o insertar en stock_sedes
|
||||
$sql_stock = "INSERT INTO stock_sedes (id_producto, id_sede, cantidad)
|
||||
VALUES (:product_id, :sede_id, :cantidad)
|
||||
ON DUPLICATE KEY UPDATE cantidad = cantidad + :cantidad";
|
||||
|
||||
$stmt_stock = $pdo->prepare($sql_stock);
|
||||
$stmt_stock->bindParam(':product_id', $product_id, PDO::PARAM_INT);
|
||||
$stmt_stock->bindParam(':sede_id', $sede_id, PDO::PARAM_INT);
|
||||
$stmt_stock->bindParam(':cantidad', $cantidad, PDO::PARAM_INT);
|
||||
$stmt_stock->execute();
|
||||
|
||||
// 2. Registrar el movimiento
|
||||
$sql_movement = "INSERT INTO stock_movements (product_id, sede_id, tipo_movimiento, cantidad, origen)
|
||||
VALUES (:product_id, :sede_id, 'entrada', :cantidad, 'manual')";
|
||||
|
||||
$stmt_movement = $pdo->prepare($sql_movement);
|
||||
$stmt_movement->bindParam(':product_id', $product_id, PDO::PARAM_INT);
|
||||
$stmt_movement->bindParam(':sede_id', $sede_id, PDO::PARAM_INT);
|
||||
$stmt_movement->bindParam(':cantidad', $cantidad, PDO::PARAM_INT);
|
||||
$stmt_movement->execute();
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$response['success'] = true;
|
||||
$response['message'] = "Se han añadido {$cantidad} unidades al stock correctamente.";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
// Log del error para depuración, no mostrar al usuario final
|
||||
error_log("Error en registrar_entrada_manual_api.php: " . $e->getMessage());
|
||||
$response['message'] = 'Error en la base de datos. No se pudo registrar la entrada.';
|
||||
}
|
||||
|
||||
} else {
|
||||
$response['message'] = 'Método no permitido.';
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
?>
|
||||
@ -5,72 +5,70 @@ require_once 'db/config.php';
|
||||
$response = ['success' => false, 'message' => 'Petición inválida.'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$sede_id = isset($_POST['sede_id']) ? trim($_POST['sede_id']) : '';
|
||||
$product_id_raw = isset($_POST['product_id']) ? trim($_POST['product_id']) : '';
|
||||
$quantity_to_remove = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 0;
|
||||
$sede_id = isset($_POST['sede_id']) ? filter_var($_POST['sede_id'], FILTER_VALIDATE_INT) : null;
|
||||
$product_id = isset($_POST['product_id']) ? filter_var($_POST['product_id'], FILTER_VALIDATE_INT) : null;
|
||||
$quantity = isset($_POST['quantity']) ? filter_var($_POST['quantity'], FILTER_VALIDATE_INT) : null;
|
||||
|
||||
// Extraer el ID numérico del producto
|
||||
$product_id_parts = explode('-', $product_id_raw);
|
||||
$product_id = end($product_id_parts);
|
||||
|
||||
if (empty($sede_id) || !is_numeric($product_id) || $quantity_to_remove <= 0) {
|
||||
$response['message'] = 'Por favor, complete todos los campos correctamente.';
|
||||
if (!$sede_id || !$product_id || !$quantity || $quantity <= 0) {
|
||||
$response['message'] = 'Datos inválidos. Por favor, complete todos los campos correctamente.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
// 1. Obtener el stock actual y bloquear la fila para evitar concurrencia
|
||||
$stmt = $pdo->prepare("SELECT quantity FROM stock_sedes WHERE sede_id = :sede_id AND product_id = :product_id FOR UPDATE");
|
||||
$stmt->execute(['sede_id' => $sede_id, 'product_id' => $product_id]);
|
||||
$stock = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$current_quantity = $stock ? (int)$stock['quantity'] : 0;
|
||||
// 1. Verificar stock actual
|
||||
$sql_check = "SELECT cantidad FROM stock_sedes WHERE id_producto = :product_id AND id_sede = :sede_id FOR UPDATE";
|
||||
$stmt_check = $pdo->prepare($sql_check);
|
||||
$stmt_check->bindParam(':product_id', $product_id, PDO::PARAM_INT);
|
||||
$stmt_check->bindParam(':sede_id', $sede_id, PDO::PARAM_INT);
|
||||
$stmt_check->execute();
|
||||
$current_stock_row = $stmt_check->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($current_quantity < $quantity_to_remove) {
|
||||
$response['message'] = "No se puede retirar más stock del disponible. Stock actual: {$current_quantity}.";
|
||||
$current_stock = $current_stock_row ? (int)$current_stock_row['cantidad'] : 0;
|
||||
|
||||
if ($current_stock < $quantity) {
|
||||
$response['message'] = "Stock insuficiente. Stock actual: {$current_stock} unidades.";
|
||||
$pdo->rollBack();
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Calcular la nueva cantidad
|
||||
$new_quantity = $current_quantity - $quantity_to_remove;
|
||||
// 2. Actualizar stock_sedes
|
||||
$sql_update = "UPDATE stock_sedes SET cantidad = cantidad - :quantity WHERE id_producto = :product_id AND id_sede = :sede_id";
|
||||
$stmt_update = $pdo->prepare($sql_update);
|
||||
$stmt_update->bindParam(':quantity', $quantity, PDO::PARAM_INT);
|
||||
$stmt_update->bindParam(':product_id', $product_id, PDO::PARAM_INT);
|
||||
$stmt_update->bindParam(':sede_id', $sede_id, PDO::PARAM_INT);
|
||||
$stmt_update->execute();
|
||||
|
||||
// 3. Actualizar la tabla de stock
|
||||
$update_stmt = $pdo->prepare(
|
||||
"UPDATE stock_sedes SET quantity = :new_quantity WHERE sede_id = :sede_id AND product_id = :product_id"
|
||||
);
|
||||
$update_stmt->execute([
|
||||
'new_quantity' => $new_quantity,
|
||||
'sede_id' => $sede_id,
|
||||
'product_id' => $product_id
|
||||
]);
|
||||
|
||||
// 4. (Opcional pero recomendado) Registrar el movimiento
|
||||
$movement_stmt = $pdo->prepare(
|
||||
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date) VALUES (:product_id, :sede_id, :quantity, 'salida', NOW())"
|
||||
);
|
||||
$movement_stmt->execute([
|
||||
'product_id' => $product_id,
|
||||
'sede_id' => $sede_id,
|
||||
'quantity' => $quantity_to_remove
|
||||
]);
|
||||
// 3. Registrar el movimiento
|
||||
$sql_movement = "INSERT INTO stock_movements (product_id, sede_id, tipo_movimiento, cantidad, origen)
|
||||
VALUES (:product_id, :sede_id, 'salida', :quantity, 'manual')";
|
||||
$stmt_movement = $pdo->prepare($sql_movement);
|
||||
$stmt_movement->bindParam(':product_id', $product_id, PDO::PARAM_INT);
|
||||
$stmt_movement->bindParam(':sede_id', $sede_id, PDO::PARAM_INT);
|
||||
$stmt_movement->bindParam(':quantity', $quantity, PDO::PARAM_INT);
|
||||
$stmt_movement->execute();
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$response['success'] = true;
|
||||
$response['message'] = "Salida manual registrada con éxito. Stock actualizado a {$new_quantity} unidades.";
|
||||
$response['message'] = "Se han retirado {$quantity} unidades del stock correctamente.";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
// En un entorno de producción, no deberías exponer el mensaje de error detallado.
|
||||
// Considera registrar el error en un archivo de logs.
|
||||
$response['message'] = 'Error en la base de datos al procesar la solicitud.'; // Mensaje genérico para el usuario
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log("Error en registrar_salida_manual_api.php: " . $e->getMessage());
|
||||
$response['message'] = 'Error en la base de datos. No se pudo registrar la salida.';
|
||||
}
|
||||
|
||||
} else {
|
||||
$response['message'] = 'Método no permitido.';
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
?>
|
||||
?>
|
||||
|
||||
@ -1,24 +1,30 @@
|
||||
<?php
|
||||
$pageTitle = "Registro de Entrada por Unidad";
|
||||
$pageTitle = "Registro de Entrada";
|
||||
require_once 'layout_header.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
$error_page_load = '';
|
||||
|
||||
// Obtener sedes para el dropdown
|
||||
$sedes = [];
|
||||
$products = [];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
// Obtener sedes
|
||||
$sedes_stmt = $pdo->query("SELECT id, nombre FROM sedes ORDER BY nombre ASC");
|
||||
$sedes = $sedes_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Obtener productos
|
||||
$products_stmt = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
|
||||
$products = $products_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$error_page_load = "Error al cargar las sedes: " . $e->getMessage();
|
||||
$error_page_load = "Error al cargar datos iniciales: " . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
|
||||
<!-- Contenedor para notificaciones (toasts) -->
|
||||
<div id="notification-container" class="position-fixed top-0 end-0 p-3" style="z-index: 1100"></div>
|
||||
@ -31,39 +37,88 @@ try {
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fa fa-barcode"></i> Registro de Entrada por Unidad
|
||||
<i class="fa fa-sign-in"></i> <?php echo $pageTitle; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<option value="">Seleccione una sede</option>
|
||||
<?php foreach ($sedes as $sede): ?>
|
||||
<option value="<?php echo htmlspecialchars($sede['id']); ?>">
|
||||
<?php echo htmlspecialchars($sede['nombre']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<!-- Pestañas de Navegación -->
|
||||
<ul class="nav nav-tabs" id="entryTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="barcode-tab" data-bs-toggle="tab" data-bs-target="#barcode-entry" type="button" role="tab" aria-controls="barcode-entry" aria-selected="true">
|
||||
<i class="fa fa-barcode"></i> Por Código de Barras
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="manual-tab" data-bs-toggle="tab" data-bs-target="#manual-entry" type="button" role="tab" aria-controls="manual-entry" aria-selected="false">
|
||||
<i class="fa fa-edit"></i> Manual por Cantidad
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Contenido de las Pestañas -->
|
||||
<div class="tab-content" id="entryTabsContent">
|
||||
<!-- Pestaña 1: Entrada por Código de Barras -->
|
||||
<div class="tab-pane fade show active" id="barcode-entry" role="tabpanel" aria-labelledby="barcode-tab">
|
||||
<form id="scan-form" onsubmit="return false;" class="mt-3">
|
||||
<div class="mb-3">
|
||||
<label for="sede-barcode" class="form-label">Sede de Destino</label>
|
||||
<select class="form-select" id="sede-barcode" name="sede_id" required>
|
||||
<option value="">Seleccione una sede</option>
|
||||
<?php foreach ($sedes as $sede): ?>
|
||||
<option value="<?php echo htmlspecialchars($sede['id']); ?>"><?php echo htmlspecialchars($sede['nombre']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<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</button>
|
||||
<button type="button" id="cancel-btn" class="btn btn-danger"> <i class="fa fa-times-circle"></i> Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Pestaña 2: Entrada Manual por Cantidad -->
|
||||
<div class="tab-pane fade" id="manual-entry" role="tabpanel" aria-labelledby="manual-tab">
|
||||
<form id="manual-entry-form" class="mt-3">
|
||||
<div class="mb-3">
|
||||
<label for="sede-manual" class="form-label">Sede de Destino</label>
|
||||
<select class="form-select" id="sede-manual" name="sede_id" required>
|
||||
<option value="">Seleccione una sede</option>
|
||||
<?php foreach ($sedes as $sede): ?>
|
||||
<option value="<?php echo htmlspecialchars($sede['id']); ?>"><?php echo htmlspecialchars($sede['nombre']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="product-manual" class="form-label">Producto</label>
|
||||
<select class="form-select" id="product-manual" name="product_id" required>
|
||||
<option value="">Seleccione un producto</option>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<option value="<?php echo htmlspecialchars($product['id']); ?>"><?php echo htmlspecialchars($product['nombre']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="quantity-manual" class="form-label">Cantidad a Ingresar</label>
|
||||
<input type="number" class="form-control" id="quantity-manual" name="cantidad" min="1" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-plus-circle"></i> Registrar Entrada Manual</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,18 +147,20 @@ try {
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// --- CONFIGURACIÓN INICIAL ---
|
||||
// --- CONFIGURACIÓN GENERAL ---
|
||||
if (typeof bootstrap === 'undefined') {
|
||||
console.error('Bootstrap no está cargado.');
|
||||
return;
|
||||
}
|
||||
|
||||
const barcodeInput = document.getElementById('barcode-input');
|
||||
const sedeSelect = document.getElementById('sede');
|
||||
const sedeBarcodeSelect = document.getElementById('sede-barcode');
|
||||
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 manualForm = document.getElementById('manual-entry-form');
|
||||
|
||||
let processing = false;
|
||||
let lastScannedCode = null;
|
||||
@ -123,7 +180,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
document.body.addEventListener('click', wakeUpAudio, { once: true });
|
||||
document.body.addEventListener('touchstart', wakeUpAudio, { once: true });
|
||||
|
||||
function playBeep() {
|
||||
function playBeep(success = true) {
|
||||
if (!audioCtx || audioCtx.state !== 'running') {
|
||||
wakeUpAudio();
|
||||
if (!audioCtx || audioCtx.state !== 'running') return;
|
||||
@ -133,20 +190,40 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioCtx.destination);
|
||||
gainNode.gain.value = 0.1;
|
||||
oscillator.frequency.value = 880;
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.value = success ? 880 : 440;
|
||||
oscillator.type = success ? 'sine' : 'square';
|
||||
oscillator.start();
|
||||
setTimeout(() => oscillator.stop(), 150);
|
||||
}
|
||||
|
||||
// --- INICIALIZACIÓN DE CÁMARA ---
|
||||
// --- 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());
|
||||
}
|
||||
|
||||
// --- PESTAÑA: CÓDIGO DE BARRAS ---
|
||||
|
||||
// ** Lógica de Cámara **
|
||||
const cameraModal = document.getElementById('camera-modal');
|
||||
let html5QrCode;
|
||||
|
||||
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);
|
||||
@ -166,25 +243,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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());
|
||||
}
|
||||
|
||||
// ** Lógica del Escáner **
|
||||
function resetScanner() {
|
||||
lastScannedCode = null;
|
||||
barcodeInput.value = '';
|
||||
@ -194,33 +253,27 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
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
|
||||
acceptBtn.focus();
|
||||
});
|
||||
|
||||
// --- LÓGICA DE BOTONES ACEPTAR/CANCELAR ---
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
resetScanner();
|
||||
});
|
||||
cancelBtn.addEventListener('click', resetScanner);
|
||||
|
||||
acceptBtn.addEventListener('click', function() {
|
||||
if (processing || !lastScannedCode) return;
|
||||
|
||||
processing = true;
|
||||
|
||||
const sedeId = sedeSelect.value;
|
||||
const sedeId = sedeBarcodeSelect.value;
|
||||
if (!sedeId) {
|
||||
showNotification("Por favor, seleccione una sede de destino.", false);
|
||||
playBeep(false);
|
||||
processing = false;
|
||||
return;
|
||||
}
|
||||
@ -229,7 +282,43 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
formData.append('codigo_unico', lastScannedCode);
|
||||
formData.append('sede_id', sedeId);
|
||||
|
||||
fetch('registrar_entrada_api.php', {
|
||||
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.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification(error.message, false);
|
||||
playBeep(false);
|
||||
})
|
||||
.finally(() => {
|
||||
resetScanner();
|
||||
});
|
||||
});
|
||||
|
||||
// --- PESTAÑA: ENTRADA MANUAL ---
|
||||
manualForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (processing) return;
|
||||
processing = true;
|
||||
|
||||
const formData = new FormData(this);
|
||||
const sedeId = formData.get('sede_id');
|
||||
const productId = formData.get('product_id');
|
||||
const cantidad = formData.get('cantidad');
|
||||
|
||||
if (!sedeId || !productId || !cantidad || cantidad < 1) {
|
||||
showNotification("Todos los campos son obligatorios y la cantidad debe ser mayor a cero.", false);
|
||||
playBeep(false);
|
||||
processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('registrar_entrada_manual_api.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
@ -237,27 +326,44 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, true);
|
||||
playBeep(true);
|
||||
manualForm.reset();
|
||||
} else {
|
||||
throw new Error(data.message || 'Error desconocido al registrar la entrada.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification(error.message, false);
|
||||
playBeep(false);
|
||||
})
|
||||
.finally(() => {
|
||||
resetScanner();
|
||||
processing = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Mantener el foco en el input principal cuando no se interactúa con otros elementos
|
||||
barcodeInput.focus();
|
||||
|
||||
// --- LÓGICA DE PESTAÑAS ---
|
||||
const tabs = new bootstrap.Tab(document.getElementById('barcode-tab'));
|
||||
tabs.show();
|
||||
|
||||
// Auto-focus en el input correcto al cambiar de pestaña
|
||||
document.getElementById('barcode-tab').addEventListener('shown.bs.tab', function () {
|
||||
barcodeInput.focus();
|
||||
});
|
||||
document.getElementById('manual-tab').addEventListener('shown.bs.tab', function () {
|
||||
document.getElementById('sede-manual').focus();
|
||||
});
|
||||
|
||||
// Mantener el foco en el input principal de la pestaña de código de barras
|
||||
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();
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (activeTab && activeTab.id === 'barcode-entry') {
|
||||
if (!e.target.closest('input, button, select, .modal')) {
|
||||
barcodeInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'layout_footer.php'; ?>
|
||||
<?php require_once 'layout_footer.php'; ?>
|
||||
@ -1,19 +1,20 @@
|
||||
<?php
|
||||
$pageTitle = "Registro de Salida de Producto";
|
||||
$pageTitle = "Registro de Salida";
|
||||
require_once 'layout_header.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
$error_page_load = '';
|
||||
$sedes = [];
|
||||
$products = [];
|
||||
$almacen_principal_id = null;
|
||||
|
||||
// Obtener sedes para el dropdown
|
||||
$sedes = [];
|
||||
try {
|
||||
$pdo = db();
|
||||
// Obtener sedes
|
||||
$sedes_stmt = $pdo->query("SELECT id, nombre FROM sedes ORDER BY nombre ASC");
|
||||
$sedes = $sedes_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Encontrar el ID de "ALMACEN PRINCIPAL" para usarlo en el script
|
||||
|
||||
// Encontrar el ID de "ALMACEN PRINCIPAL" para la lógica móvil
|
||||
foreach ($sedes as $sede) {
|
||||
if (trim(strtolower($sede['nombre'])) === 'almacen principal') {
|
||||
$almacen_principal_id = $sede['id'];
|
||||
@ -21,23 +22,18 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$error_page_load = "Error al cargar datos: " . $e->getMessage();
|
||||
}
|
||||
|
||||
// Obtener productos para el dropdown manual
|
||||
$products = [];
|
||||
try {
|
||||
// Obtener productos
|
||||
$products_stmt = $pdo->query("SELECT id, nombre, sku FROM products ORDER BY nombre ASC");
|
||||
$products = $products_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$error_page_load .= " Error al cargar productos: " . $e->getMessage();
|
||||
$error_page_load = "Error al cargar datos iniciales: " . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
|
||||
<!-- Contenedor para notificaciones (toasts) -->
|
||||
<div id="notification-container" class="position-fixed top-0 end-0 p-3" style="z-index: 1100"></div>
|
||||
@ -50,75 +46,89 @@ try {
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fa fa-barcode"></i> Escanear Código de Barras
|
||||
<i class="fa fa-sign-out"></i> <?php echo $pageTitle; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="scan-form" onsubmit="return false;">
|
||||
<div class="mb-3">
|
||||
<label for="movement_date" class="form-label">Fecha de Salida</label>
|
||||
<input type="date" class="form-control" id="movement_date" name="movement_date" value="<?php echo date('Y-m-d'); ?>" required>
|
||||
<!-- Pestañas de Navegación -->
|
||||
<ul class="nav nav-tabs" id="exitTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="barcode-exit-tab" data-bs-toggle="tab" data-bs-target="#barcode-exit" type="button" role="tab" aria-controls="barcode-exit" aria-selected="true">
|
||||
<i class="fa fa-barcode"></i> Por Código de Barras
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="manual-exit-tab" data-bs-toggle="tab" data-bs-target="#manual-exit" type="button" role="tab" aria-controls="manual-exit" aria-selected="false">
|
||||
<i class="fa fa-edit"></i> Manual por Cantidad
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Contenido de las Pestañas -->
|
||||
<div class="tab-content" id="exitTabsContent">
|
||||
<!-- Pestaña 1: Salida por Código de Barras -->
|
||||
<div class="tab-pane fade show active" id="barcode-exit" role="tabpanel" aria-labelledby="barcode-exit-tab">
|
||||
<form id="scan-form" onsubmit="return false;" class="mt-3">
|
||||
<div class="mb-3">
|
||||
<label for="movement_date" class="form-label">Fecha de Salida</label>
|
||||
<input type="date" class="form-control" id="movement_date" name="movement_date" value="<?php echo date('Y-m-d'); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3" id="sede-container">
|
||||
<label for="sede-barcode" class="form-label">Sede de Origen</label>
|
||||
<select class="form-select" id="sede-barcode" name="sede_id" required>
|
||||
<option value="">Seleccione una sede</option>
|
||||
<?php foreach ($sedes as $sede): ?>
|
||||
<option value="<?php echo htmlspecialchars($sede['id']); ?>"><?php echo htmlspecialchars($sede['nombre']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- El contenedor de la sede ahora tiene un ID para poder ocultarlo -->
|
||||
<div class="mb-3" id="sede-container">
|
||||
<label for="sede" class="form-label">Sede de Origen</label>
|
||||
<select class="form-select" id="sede" name="sede_id" required>
|
||||
<option value="">Seleccione una sede</option>
|
||||
<?php foreach ($sedes as $sede): ?>
|
||||
<option value="<?php echo htmlspecialchars($sede['id']); ?>"><?php echo htmlspecialchars($sede['nombre']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<!-- Pestaña 2: Salida Manual por Cantidad -->
|
||||
<div class="tab-pane fade" id="manual-exit" role="tabpanel" aria-labelledby="manual-exit-tab">
|
||||
<form id="manual-exit-form" onsubmit="return false;" class="mt-3">
|
||||
<div class="mb-3">
|
||||
<label for="manual_sede" class="form-label">Sede de Origen</label>
|
||||
<select class="form-select" id="manual_sede" name="sede_id" required>
|
||||
<option value="">Seleccione una sede</option>
|
||||
<?php foreach ($sedes as $sede): ?>
|
||||
<option value="<?php echo htmlspecialchars($sede['id']); ?>"><?php echo htmlspecialchars($sede['nombre']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="manual_product" class="form-label">Producto</label>
|
||||
<select class="form-select" id="manual_product" name="product_id" required>
|
||||
<option value="">Seleccione un producto</option>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<option value="<?php echo htmlspecialchars($product['id']); ?>">
|
||||
<?php echo htmlspecialchars($product['nombre']) . ' (' . htmlspecialchars($product['sku'] ?: 'Sin SKU') . ')'; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="manual_quantity" class="form-label">Cantidad a Retirar</label>
|
||||
<input type="number" class="form-control" id="manual_quantity" name="quantity" min="1" placeholder="Escriba la cantidad" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-warning"><i class="fa fa-minus-circle"></i> Registrar Salida Manual</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<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" id="scan-camera-btn" data-bs-toggle="modal" data-bs-target="#camera-modal">
|
||||
<i class="fa fa-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="fa fa-edit"></i> Salida Manual de Stock
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Utilice este formulario para ajustar el inventario y poner en cero el stock de productos antiguos o sin SKU.</p>
|
||||
<form id="manual-exit-form" onsubmit="return false;">
|
||||
<div class="mb-3">
|
||||
<label for="manual_sede" class="form-label">Sede de Origen</label>
|
||||
<select class="form-select" id="manual_sede" name="sede_id" required>
|
||||
<option value="">Seleccione una sede</option>
|
||||
<?php foreach ($sedes as $sede): ?>
|
||||
<option value="<?php echo htmlspecialchars($sede['id']); ?>"><?php echo htmlspecialchars($sede['nombre']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="manual_product" class="form-label">Producto</label>
|
||||
<select class="form-select" id="manual_product" name="product_id" required>
|
||||
<option value="">Seleccione un producto</option>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<option value="<?php echo htmlspecialchars($product['id']); ?>">
|
||||
<?php echo htmlspecialchars($product['nombre']) . ' (' . htmlspecialchars($product['sku'] ?: 'Sin SKU') . ')'; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="manual_quantity" class="form-label">Cantidad a Retirar</label>
|
||||
<input type="number" class="form-control" id="manual_quantity" name="quantity" min="1" placeholder="Escriba la cantidad exacta a retirar" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning w-100">Registrar Salida Manual y Poner en Cero</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -145,38 +155,35 @@ try {
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// --- CONFIGURACIÓN INICIAL ---
|
||||
// --- CONFIGURACIÓN GENERAL ---
|
||||
if (typeof bootstrap === 'undefined') {
|
||||
console.error('Bootstrap no está cargado.');
|
||||
return;
|
||||
}
|
||||
|
||||
const barcodeInput = document.getElementById('barcode-input');
|
||||
const sedeSelect = document.getElementById('sede');
|
||||
const sedeBarcodeSelect = document.getElementById('sede-barcode');
|
||||
const dateInput = document.getElementById('movement_date');
|
||||
const sedeContainer = document.getElementById('sede-container');
|
||||
|
||||
const manualExitForm = document.getElementById('manual-exit-form');
|
||||
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
const almacenPrincipalId = '<?php echo $almacen_principal_id; ?>';
|
||||
|
||||
let processing = false;
|
||||
|
||||
// --- LÓGICA DE SONIDO (WEB AUDIO API) ---
|
||||
// --- LÓGICA DE SONIDO ---
|
||||
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();
|
||||
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
|
||||
}
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
}
|
||||
document.body.addEventListener('click', wakeUpAudio, { once: true });
|
||||
document.body.addEventListener('touchstart', wakeUpAudio, { once: true });
|
||||
|
||||
function playBeep() {
|
||||
|
||||
function playBeep(success = true) {
|
||||
if (!audioCtx || audioCtx.state !== 'running') {
|
||||
wakeUpAudio();
|
||||
if (!audioCtx || audioCtx.state !== 'running') return;
|
||||
@ -185,25 +192,46 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const gainNode = audioCtx.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioCtx.destination);
|
||||
gainNode.gain.value = 0.1;
|
||||
oscillator.frequency.value = 880;
|
||||
oscillator.type = 'sine';
|
||||
gainNode.gain.value = 0.1;
|
||||
oscillator.frequency.value = success ? 880 : 440;
|
||||
oscillator.type = success ? 'sine' : 'square';
|
||||
oscillator.start();
|
||||
setTimeout(() => oscillator.stop(), 150);
|
||||
}
|
||||
|
||||
// --- LÓGICA PARA MÓVIL ---
|
||||
if (isMobile) {
|
||||
if(sedeContainer) sedeContainer.style.display = 'none';
|
||||
if(sedeSelect && almacenPrincipalId) sedeSelect.value = almacenPrincipalId;
|
||||
// --- 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());
|
||||
}
|
||||
|
||||
// --- INICIALIZACIÓN DE CÁMARA ---
|
||||
// --- LÓGICA PARA MÓVIL (SOLO PESTAÑA CÓDIGO DE BARRAS) ---
|
||||
if (isMobile && almacenPrincipalId) {
|
||||
if(sedeContainer) sedeContainer.style.display = 'none';
|
||||
if(sedeBarcodeSelect) sedeBarcodeSelect.value = almacenPrincipalId;
|
||||
}
|
||||
|
||||
// --- PESTAÑA: CÓDIGO DE BARRAS ---
|
||||
|
||||
// ** Lógica de Cámara **
|
||||
const cameraModal = document.getElementById('camera-modal');
|
||||
let html5QrCode;
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
playBeep();
|
||||
playBeep();
|
||||
barcodeInput.value = decodedText;
|
||||
const changeEvent = new Event('change');
|
||||
barcodeInput.dispatchEvent(changeEvent);
|
||||
@ -225,26 +253,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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());
|
||||
}
|
||||
|
||||
// --- LÓGICA PRINCIPAL DEL ESCÁNER ---
|
||||
// ** Lógica del Escáner **
|
||||
barcodeInput.addEventListener('change', function() {
|
||||
const barcodeValue = this.value.trim();
|
||||
if (barcodeValue === '' || processing) return;
|
||||
@ -252,11 +261,10 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
processing = true;
|
||||
this.disabled = true;
|
||||
|
||||
const sedeId = sedeSelect.value;
|
||||
const movementDate = dateInput.value; // Although not used by new API, good to have if needed later
|
||||
|
||||
const sedeId = sedeBarcodeSelect.value;
|
||||
if (!sedeId) {
|
||||
showNotification("Por favor, seleccione una sede de origen.", false);
|
||||
playBeep(false);
|
||||
this.value = '';
|
||||
this.disabled = false;
|
||||
processing = false;
|
||||
@ -268,20 +276,18 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
formData.append('codigo_unico', barcodeValue);
|
||||
formData.append('sede_id', sedeId);
|
||||
|
||||
fetch('registrar_salida_unidad_api.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
fetch('registrar_salida_unidad_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 salida.');
|
||||
throw new Error(data.message || 'Error desconocido.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification(error.message, false);
|
||||
playBeep(false);
|
||||
})
|
||||
.finally(() => {
|
||||
this.value = '';
|
||||
@ -291,62 +297,70 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- LÓGICA DEL FORMULARIO MANUAL ---
|
||||
const manualExitForm = document.getElementById('manual-exit-form');
|
||||
// --- PESTAÑA: SALIDA MANUAL ---
|
||||
if (manualExitForm) {
|
||||
manualExitForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (processing) return;
|
||||
|
||||
const sedeId = document.getElementById('manual_sede').value;
|
||||
const productId = document.getElementById('manual_product').value;
|
||||
const quantity = document.getElementById('manual_quantity').value;
|
||||
const formData = new FormData(this);
|
||||
const sedeId = formData.get('sede_id');
|
||||
const productId = formData.get('product_id');
|
||||
const quantity = formData.get('quantity');
|
||||
|
||||
if (!sedeId || !productId || !quantity) {
|
||||
showNotification("Por favor, complete todos los campos del formulario manual.", false);
|
||||
if (!sedeId || !productId || !quantity || parseInt(quantity, 10) <= 0) {
|
||||
showNotification("Todos los campos son obligatorios y la cantidad debe ser positiva.", false);
|
||||
playBeep(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parseInt(quantity, 10) <= 0) {
|
||||
showNotification("La cantidad debe ser un número positivo.", false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`¿Está seguro de que desea retirar ${quantity} unidad(es) de este producto? Esta acción ajustará el inventario.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
processing = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('sede_id', sedeId);
|
||||
formData.append('product_id', productId);
|
||||
formData.append('quantity', quantity);
|
||||
|
||||
fetch('registrar_salida_manual_api.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
fetch('registrar_salida_manual_api.php', { method: 'POST', body: formData })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, true);
|
||||
playBeep(true);
|
||||
manualExitForm.reset();
|
||||
} else {
|
||||
throw new Error(data.message || 'Error desconocido al registrar la salida manual.');
|
||||
throw new Error(data.message || 'Error desconocido.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification(error.message, false);
|
||||
playBeep(false);
|
||||
})
|
||||
.finally(() => {
|
||||
processing = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Mantener el foco en el input
|
||||
barcodeInput.focus();
|
||||
// --- LÓGICA DE PESTAÑAS ---
|
||||
const tabs = new bootstrap.Tab(document.getElementById('barcode-exit-tab'));
|
||||
tabs.show();
|
||||
|
||||
document.getElementById('barcode-exit-tab').addEventListener('shown.bs.tab', function () {
|
||||
barcodeInput.focus();
|
||||
});
|
||||
document.getElementById('manual-exit-tab').addEventListener('shown.bs.tab', function () {
|
||||
document.getElementById('manual_sede').focus();
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', (e) => {
|
||||
if (!['INPUT', 'SELECT', 'BUTTON', 'A'].includes(e.target.tagName) && !e.target.closest('button, a, .modal')) {
|
||||
barcodeInput.focus();
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (activeTab && activeTab.id === 'barcode-exit') {
|
||||
if (!e.target.closest('input, button, select, .modal')) {
|
||||
barcodeInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'layout_footer.php'; ?>
|
||||
<?php require_once 'layout_footer.php'; ?>
|
||||
Loading…
x
Reference in New Issue
Block a user