Autosave: 20260428-011435

This commit is contained in:
Flatlogic Bot 2026-04-28 01:14:38 +00:00
parent f8fadfdc8a
commit 870bef9157
90 changed files with 419 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@ -151,13 +151,14 @@ include 'layout_header.php';
<th>Fecha Completado</th> <th>Fecha Completado</th>
<th>Voucher Restante</th> <th>Voucher Restante</th>
<th>Verificación de Pago</th> <th>Verificación de Pago</th>
<th>OBSERVACION</th>
<th>Acciones</th> <th>Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (empty($pedidos)): ?> <?php if (empty($pedidos)): ?>
<tr> <tr>
<td colspan="17" class="text-center">No hay pedidos completados que coincidan con el filtro.</td> <td colspan="18" class="text-center">No hay pedidos completados que coincidan con el filtro.</td>
</tr> </tr>
<?php else: ?> <?php else: ?>
<?php foreach ($pedidos as $pedido): ?> <?php foreach ($pedidos as $pedido): ?>
@ -231,6 +232,9 @@ include 'layout_header.php';
<?php endif; ?> <?php endif; ?>
</span> </span>
</td> </td>
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="observacion">
<?php echo htmlspecialchars($pedido['observacion'] ?? 'N/A'); ?>
</td>
<td> <td>
<a href="pedido_form.php?id=<?php echo $pedido['id']; ?>" class="btn btn-sm btn-warning">Editar</a> <a href="pedido_form.php?id=<?php echo $pedido['id']; ?>" class="btn btn-sm btn-warning">Editar</a>
<?php if ($user_role === 'Administrador' || $user_role === 'Logistica'): ?> <?php if ($user_role === 'Administrador' || $user_role === 'Logistica'): ?>
@ -433,7 +437,7 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} // Cierre de if (cell.classList.contains('editable-recojo')) } // Cierre de if (cell.classList.contains('editable-recojo'))
if (field === 'clave' || field === 'numero_operacion') { if (field === 'clave' || field === 'numero_operacion' || field === 'observacion') {
if (field === 'numero_operacion' && newValue !== '' && newValue.length < 6) { if (field === 'numero_operacion' && newValue !== '' && newValue.length < 6) {
alert('El número de operación debe tener al menos 6 dígitos.'); alert('El número de operación debe tener al menos 6 dígitos.');
cell.innerHTML = originalText === '' ? 'N/A' : originalText; cell.innerHTML = originalText === '' ? 'N/A' : originalText;

View File

@ -0,0 +1,2 @@
-- Add observacion column to pedidos table
ALTER TABLE pedidos ADD COLUMN observacion TEXT NULL;

View File

@ -0,0 +1,4 @@
-- Migration: Add codigo_unico to stock_movements
-- Description: Adds a column to store the specific barcode/unique code for a movement if applicable.
ALTER TABLE stock_movements ADD COLUMN codigo_unico VARCHAR(255) DEFAULT NULL AFTER type;

View File

@ -0,0 +1,2 @@
-- Migration to add new options to tipo_paquete ENUM in pedidos table
ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM('RUTA', 'CONTRAENTREGA', 'NO CONTESTA', 'VOLVER A LLAMAR', 'PENDIENTE A RETORNO') DEFAULT NULL;

View File

@ -0,0 +1,6 @@
-- Migration to merge 'NO CONTESTA' and 'VOLVER A LLAMAR' into a single option
ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM('RUTA', 'CONTRAENTREGA', 'NO CONTESTA', 'VOLVER A LLAMAR', 'PENDIENTE A RETORNO', 'NO CONTESTA, VOLVER A LLAMAR') DEFAULT NULL;
UPDATE pedidos SET tipo_paquete = 'NO CONTESTA, VOLVER A LLAMAR' WHERE tipo_paquete IN ('NO CONTESTA', 'VOLVER A LLAMAR');
ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM('RUTA', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'PENDIENTE A RETORNO') DEFAULT NULL;

View File

@ -99,17 +99,46 @@ try {
$producto_chart_labels = json_encode(array_column($stock_por_producto, 'nombre')); $producto_chart_labels = json_encode(array_column($stock_por_producto, 'nombre'));
$producto_chart_data = json_encode(array_column($stock_por_producto, 'total_stock')); $producto_chart_data = json_encode(array_column($stock_por_producto, 'total_stock'));
// 4. Datos para el historial de movimientos (últimos 50) // 4. Datos para el historial de movimientos
$movements_stmt = $pdo->query(" $fecha_filtro = $_GET['fecha'] ?? '';
SELECT sm.movement_date, p.nombre as product_name, s.nombre as sede_name, sm.quantity, sm.type, sm.metodo_registro $where_clause = "";
$params = [];
if (!empty($fecha_filtro)) {
$where_clause = " WHERE DATE(sm.movement_date) = :fecha ";
$params[':fecha'] = $fecha_filtro;
$limit = 500; // Aumentar límite si se filtra por fecha
} else {
$limit = 50;
}
$query = "
SELECT sm.movement_date, p.nombre as product_name, s.nombre as sede_name, sm.quantity, sm.type, sm.codigo_unico, sm.metodo_registro
FROM stock_movements sm FROM stock_movements sm
JOIN products p ON sm.product_id = p.id JOIN products p ON sm.product_id = p.id
JOIN sedes s ON sm.sede_id = s.id JOIN sedes s ON sm.sede_id = s.id
$where_clause
ORDER BY sm.created_at DESC ORDER BY sm.created_at DESC
LIMIT 50 LIMIT $limit
"); ";
$movements_stmt = $pdo->prepare($query);
$movements_stmt->execute($params);
$movements = $movements_stmt->fetchAll(PDO::FETCH_ASSOC); $movements = $movements_stmt->fetchAll(PDO::FETCH_ASSOC);
// 5. Resumen de salidas de hoy
$hoy = date('Y-m-d');
$resumen_salidas_stmt = $pdo->prepare("
SELECT p.nombre, SUM(sm.quantity) as total_salida
FROM stock_movements sm
JOIN products p ON sm.product_id = p.id
WHERE sm.type = 'salida' AND DATE(sm.movement_date) = :hoy
GROUP BY p.nombre
ORDER BY total_salida DESC
");
$resumen_salidas_stmt->execute([':hoy' => $hoy]);
$resumen_salidas = $resumen_salidas_stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) { } catch (PDOException $e) {
echo "<div class='alert alert-danger'>Error al conectar o consultar la base de datos: " . $e->getMessage() . "</div>"; echo "<div class='alert alert-danger'>Error al conectar o consultar la base de datos: " . $e->getMessage() . "</div>";
require_once 'layout_footer.php'; require_once 'layout_footer.php';
@ -217,9 +246,45 @@ try {
</div> </div>
</div> </div>
<!-- Resumen de Salidas de Hoy -->
<div class="card mb-4 border-danger shadow-sm">
<div class="card-header bg-danger text-white d-flex justify-content-between align-items-center">
<h5 class="m-0"><i class="fa fa-exclamation-circle"></i> Resumen de Salidas de Hoy (<?php echo date("d/m/Y"); ?>)</h5>
<span class="badge bg-white text-danger"><?php echo count($resumen_salidas); ?> Productos</span>
</div>
<div class="card-body">
<?php if (empty($resumen_salidas)): ?>
<p class="text-muted mb-0 text-center">No se han registrado salidas el día de hoy.</p>
<?php else: ?>
<div class="row">
<?php foreach ($resumen_salidas as $res): ?>
<div class="col-md-3 col-sm-6 mb-2">
<div class="p-2 border rounded bg-light d-flex justify-content-between align-items-center">
<span class="text-truncate me-2" title="<?php echo htmlspecialchars($res['nombre']); ?>">
<?php echo htmlspecialchars($res['nombre']); ?>
</span>
<span class="badge bg-danger fs-6"><?php echo $res['total_salida']; ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Historial de Movimientos --> <!-- Historial de Movimientos -->
<div class="card"> <div class="card">
<div class="card-header"><h5 class="m-0">Historial de Movimientos Recientes</h5></div> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="m-0">Historial de Movimientos <?php echo !empty($fecha_filtro) ? "del " . date("d/m/Y", strtotime($fecha_filtro)) : "Recientes"; ?></h5>
<form action="" method="GET" class="d-flex align-items-center">
<label for="fecha" class="me-2 mb-0 d-none d-md-block">Filtrar por día:</label>
<input type="date" name="fecha" id="fecha" class="form-control form-control-sm me-2" value="<?php echo htmlspecialchars($fecha_filtro); ?>">
<button type="submit" class="btn btn-sm btn-primary">Filtrar</button>
<?php if (!empty($fecha_filtro)): ?>
<a href="panel_inventario.php" class="btn btn-sm btn-secondary ms-2">Limpiar</a>
<?php endif; ?>
</form>
</div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
@ -230,12 +295,13 @@ try {
<th>Sede</th> <th>Sede</th>
<th class="text-center">Cantidad</th> <th class="text-center">Cantidad</th>
<th class="text-center">Tipo</th> <th class="text-center">Tipo</th>
<th class="text-center">Código</th>
<th class="text-center">Método</th> <th class="text-center">Método</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (empty($movements)): ?> <?php if (empty($movements)): ?>
<tr><td colspan="6" class="text-center">No hay movimientos registrados.</td></tr> <tr><td colspan="7" class="text-center">No hay movimientos registrados.</td></tr>
<?php else: ?> <?php else: ?>
<?php foreach ($movements as $mov): ?> <?php foreach ($movements as $mov): ?>
<tr> <tr>
@ -250,6 +316,9 @@ try {
<span class="badge bg-danger">Salida</span> <span class="badge bg-danger">Salida</span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="text-center">
<small class="text-muted"><?php echo htmlspecialchars($mov['codigo_unico'] ?? '-'); ?></small>
</td>
<td class="text-center"> <td class="text-center">
<small class="text-muted"><?php echo htmlspecialchars($mov['metodo_registro'] ?? 'Manual'); ?></small> <small class="text-muted"><?php echo htmlspecialchars($mov['metodo_registro'] ?? 'Manual'); ?></small>
</td> </td>

View File

@ -267,6 +267,11 @@ include 'layout_header.php';
<textarea class="form-control" id="notas" name="notas" rows="3"><?php echo htmlspecialchars($pedido['notas']); ?></textarea> <textarea class="form-control" id="notas" name="notas" rows="3"><?php echo htmlspecialchars($pedido['notas']); ?></textarea>
</div> </div>
<div class="mb-3">
<label for="observacion" class="form-label">Observación (Verificación de Pago)</label>
<textarea class="form-control" id="observacion" name="observacion" rows="2"><?php echo htmlspecialchars($pedido['observacion'] ?? ''); ?></textarea>
</div>
<button type="submit" class="btn btn-primary">Guardar Pedido</button> <button type="submit" class="btn btn-primary">Guardar Pedido</button>
<a href="<?php echo htmlspecialchars($_SERVER['HTTP_REFERER'] ?? 'pedidos.php'); ?>" class="btn btn-secondary">Cancelar</a> <a href="<?php echo htmlspecialchars($_SERVER['HTTP_REFERER'] ?? 'pedidos.php'); ?>" class="btn btn-secondary">Cancelar</a>
</form> </form>

View File

@ -56,13 +56,14 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// 4. Insertar en el historial de movimientos // 4. Insertar en el historial de movimientos
$history_stmt = $pdo->prepare( $history_stmt = $pdo->prepare(
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date, borrador_id, metodo_registro) "INSERT INTO stock_movements (product_id, sede_id, quantity, type, codigo_unico, movement_date, borrador_id, metodo_registro)
VALUES (:product_id, :sede_id, :quantity, 'entrada', :movement_date, :borrador_id, 'Código de Barras')" VALUES (:product_id, :sede_id, :quantity, 'entrada', :codigo_unico, :movement_date, :borrador_id, 'Código de Barras')"
); );
$history_stmt->execute([ $history_stmt->execute([
'product_id' => $product_id, 'product_id' => $product_id,
'sede_id' => $sede_id, 'sede_id' => $sede_id,
'quantity' => $quantity, 'quantity' => $quantity,
'codigo_unico' => $codigo_unico,
'movement_date' => $movement_date, 'movement_date' => $movement_date,
'borrador_id' => $borrador_id 'borrador_id' => $borrador_id
]); ]);

View File

@ -62,13 +62,14 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
} }
$history_stmt = $pdo->prepare( $history_stmt = $pdo->prepare(
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date, metodo_registro) "INSERT INTO stock_movements (product_id, sede_id, quantity, type, codigo_unico, movement_date, metodo_registro)
VALUES (:product_id, :sede_id, :quantity, 'salida', :movement_date, 'Código de Barras')" VALUES (:product_id, :sede_id, :quantity, 'salida', :codigo_unico, :movement_date, 'Código de Barras')"
); );
$history_stmt->execute([ $history_stmt->execute([
'product_id' => $product_id, 'product_id' => $product_id,
'sede_id' => $sede_id, 'sede_id' => $sede_id,
'quantity' => $quantity, 'quantity' => $quantity,
'codigo_unico' => $codigo_unico,
'movement_date' => $movement_date 'movement_date' => $movement_date
]); ]);

View File

@ -25,6 +25,46 @@ try {
$borrador = $stmt_borrador->fetch(PDO::FETCH_ASSOC); $borrador = $stmt_borrador->fetch(PDO::FETCH_ASSOC);
} }
// --- Lógica para el Historial de Entradas ---
$fecha_filtro = $_GET['fecha'] ?? '';
$where_clause = " WHERE sm.type = 'entrada' ";
$params = [];
if (!empty($fecha_filtro)) {
$where_clause .= " AND DATE(sm.movement_date) = :fecha ";
$params[':fecha'] = $fecha_filtro;
$limit = 500;
} else {
$limit = 50;
}
$query = "
SELECT sm.movement_date, p.nombre as product_name, s.nombre as sede_name, sm.quantity, sm.type, sm.codigo_unico, sm.metodo_registro
FROM stock_movements sm
JOIN products p ON sm.product_id = p.id
JOIN sedes s ON sm.sede_id = s.id
$where_clause
ORDER BY sm.created_at DESC
LIMIT $limit
";
$movements_stmt = $pdo->prepare($query);
$movements_stmt->execute($params);
$movements = $movements_stmt->fetchAll(PDO::FETCH_ASSOC);
// --- Resumen de Entradas de Hoy ---
$hoy = date('Y-m-d');
$resumen_entradas_stmt = $pdo->prepare("
SELECT p.nombre, SUM(sm.quantity) as total_entrada
FROM stock_movements sm
JOIN products p ON sm.product_id = p.id
WHERE sm.type = 'entrada' AND DATE(sm.movement_date) = :hoy
GROUP BY p.nombre
ORDER BY total_entrada DESC
");
$resumen_entradas_stmt->execute([':hoy' => $hoy]);
$resumen_entradas = $resumen_entradas_stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) { } catch (PDOException $e) {
$error_page_load = "Error al cargar datos iniciales: " . $e->getMessage(); $error_page_load = "Error al cargar datos iniciales: " . $e->getMessage();
} }
@ -147,6 +187,91 @@ try {
</div> </div>
</div> </div>
</div> </div>
<!-- Resumen de Entradas de Hoy -->
<div class="row mt-4" id="resumen-hoy-container">
<div class="col-lg-10 mx-auto">
<div class="card border-success shadow-sm">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h5 class="m-0"><i class="fa fa-list-alt"></i> Resumen de Entradas de Hoy (<?php echo date("d/m/Y"); ?>)</h5>
<span class="badge bg-white text-success"><?php echo count($resumen_entradas); ?> Productos</span>
</div>
<div class="card-body">
<?php if (empty($resumen_entradas)): ?>
<p class="text-muted mb-0 text-center">No se han registrado entradas el día de hoy.</p>
<?php else: ?>
<div class="row">
<?php foreach ($resumen_entradas as $res): ?>
<div class="col-md-4 col-sm-6 mb-2">
<div class="p-2 border rounded bg-light d-flex justify-content-between align-items-center">
<span class="text-truncate me-2" title="<?php echo htmlspecialchars($res['nombre']); ?>">
<?php echo htmlspecialchars($res['nombre']); ?>
</span>
<span class="badge bg-success fs-6"><?php echo $res['total_entrada']; ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Historial de Entradas -->
<div class="row mt-4" id="historial-container">
<div class="col-lg-10 mx-auto">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="m-0"><i class="fa fa-history"></i> Historial de Entradas <?php echo !empty($fecha_filtro) ? "del " . date("d/m/Y", strtotime($fecha_filtro)) : "Recientes"; ?></h5>
<form action="" method="GET" class="d-flex align-items-center" id="filter-form">
<label for="fecha" class="me-2 mb-0 d-none d-md-block">Filtrar por día:</label>
<input type="date" name="fecha" id="fecha" class="form-control form-control-sm me-2" value="<?php echo htmlspecialchars($fecha_filtro); ?>">
<button type="submit" class="btn btn-sm btn-primary">Filtrar</button>
<?php if (!empty($fecha_filtro)): ?>
<a href="registro_entrada.php" class="btn btn-sm btn-secondary ms-2">Limpiar</a>
<?php endif; ?>
</form>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-light">
<tr>
<th>Fecha</th>
<th>Producto</th>
<th>Sede</th>
<th class="text-center">Cantidad</th>
<th class="text-center">Código</th>
<th class="text-center">Método</th>
</tr>
</thead>
<tbody>
<?php if (empty($movements)): ?>
<tr><td colspan="6" class="text-center">No hay entradas registradas.</td></tr>
<?php else: ?>
<?php foreach ($movements as $mov): ?>
<tr>
<td><?php echo htmlspecialchars(date("d/m/Y", strtotime($mov['movement_date']))); ?></td>
<td><?php echo htmlspecialchars($mov['product_name']); ?></td>
<td><?php echo htmlspecialchars($mov['sede_name']); ?></td>
<td class="text-center"><?php echo htmlspecialchars($mov['quantity']); ?></td>
<td class="text-center">
<small class="text-muted"><?php echo htmlspecialchars($mov['codigo_unico'] ?? '-'); ?></small>
</td>
<td class="text-center">
<small class="text-muted"><?php echo htmlspecialchars($mov['metodo_registro'] ?? 'Manual'); ?></small>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Modal para la cámara --> <!-- Modal para la cámara -->
@ -239,6 +364,28 @@ document.addEventListener('DOMContentLoaded', (event) => {
document.getElementById(toastId).addEventListener('hidden.bs.toast', e => e.target.remove()); document.getElementById(toastId).addEventListener('hidden.bs.toast', e => e.target.remove());
} }
// --- ACTUALIZACIÓN EN TIEMPO REAL ---
function refreshHistoryAndSummary() {
const currentUrl = window.location.href;
fetch(currentUrl)
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newSummary = doc.getElementById('resumen-hoy-container');
const newHistory = doc.getElementById('historial-container');
if (newSummary) {
document.getElementById('resumen-hoy-container').innerHTML = newSummary.innerHTML;
}
if (newHistory) {
document.getElementById('historial-container').innerHTML = newHistory.innerHTML;
}
})
.catch(error => console.error('Error al actualizar el historial:', error));
}
// --- PESTAÑA: CÓDIGO DE BARRAS --- // --- PESTAÑA: CÓDIGO DE BARRAS ---
// ** Lógica de Cámara ** // ** Lógica de Cámara **
@ -313,6 +460,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
.then(data => { .then(data => {
if (data.success) { if (data.success) {
showNotification(data.message, true); showNotification(data.message, true);
refreshHistoryAndSummary();
} else { } else {
throw new Error(data.message || 'Error desconocido.'); throw new Error(data.message || 'Error desconocido.');
} }
@ -354,6 +502,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
showNotification(data.message, true); showNotification(data.message, true);
playBeep(true); playBeep(true);
manualForm.reset(); manualForm.reset();
refreshHistoryAndSummary();
} else { } else {
throw new Error(data.message || 'Error desconocido al registrar la entrada.'); throw new Error(data.message || 'Error desconocido al registrar la entrada.');
} }

View File

@ -26,6 +26,46 @@ try {
$products_stmt = $pdo->query("SELECT id, nombre, sku FROM products ORDER BY nombre ASC"); $products_stmt = $pdo->query("SELECT id, nombre, sku FROM products ORDER BY nombre ASC");
$products = $products_stmt->fetchAll(PDO::FETCH_ASSOC); $products = $products_stmt->fetchAll(PDO::FETCH_ASSOC);
// --- Lógica para el Historial de Salidas ---
$fecha_filtro = $_GET['fecha'] ?? '';
$where_clause = " WHERE sm.type = 'salida' ";
$params = [];
if (!empty($fecha_filtro)) {
$where_clause .= " AND DATE(sm.movement_date) = :fecha ";
$params[':fecha'] = $fecha_filtro;
$limit = 500;
} else {
$limit = 50;
}
$query = "
SELECT sm.movement_date, p.nombre as product_name, s.nombre as sede_name, sm.quantity, sm.type, sm.codigo_unico, sm.metodo_registro
FROM stock_movements sm
JOIN products p ON sm.product_id = p.id
JOIN sedes s ON sm.sede_id = s.id
$where_clause
ORDER BY sm.created_at DESC
LIMIT $limit
";
$movements_stmt = $pdo->prepare($query);
$movements_stmt->execute($params);
$movements = $movements_stmt->fetchAll(PDO::FETCH_ASSOC);
// --- Resumen de Salidas de Hoy ---
$hoy = date('Y-m-d');
$resumen_salidas_stmt = $pdo->prepare("
SELECT p.nombre, SUM(sm.quantity) as total_salida
FROM stock_movements sm
JOIN products p ON sm.product_id = p.id
WHERE sm.type = 'salida' AND DATE(sm.movement_date) = :hoy
GROUP BY p.nombre
ORDER BY total_salida DESC
");
$resumen_salidas_stmt->execute([':hoy' => $hoy]);
$resumen_salidas = $resumen_salidas_stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) { } catch (PDOException $e) {
$error_page_load = "Error al cargar datos iniciales: " . $e->getMessage(); $error_page_load = "Error al cargar datos iniciales: " . $e->getMessage();
} }
@ -131,6 +171,91 @@ try {
</div> </div>
</div> </div>
</div> </div>
<!-- Resumen de Salidas de Hoy -->
<div class="row mt-4" id="resumen-hoy-container">
<div class="col-lg-10 mx-auto">
<div class="card border-danger shadow-sm">
<div class="card-header bg-danger text-white d-flex justify-content-between align-items-center">
<h5 class="m-0"><i class="fa fa-list-alt"></i> Resumen de Salidas de Hoy (<?php echo date("d/m/Y"); ?>)</h5>
<span class="badge bg-white text-danger"><?php echo count($resumen_salidas); ?> Productos</span>
</div>
<div class="card-body">
<?php if (empty($resumen_salidas)): ?>
<p class="text-muted mb-0 text-center">No se han registrado salidas el día de hoy.</p>
<?php else: ?>
<div class="row">
<?php foreach ($resumen_salidas as $res): ?>
<div class="col-md-4 col-sm-6 mb-2">
<div class="p-2 border rounded bg-light d-flex justify-content-between align-items-center">
<span class="text-truncate me-2" title="<?php echo htmlspecialchars($res['nombre']); ?>">
<?php echo htmlspecialchars($res['nombre']); ?>
</span>
<span class="badge bg-danger fs-6"><?php echo $res['total_salida']; ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Historial de Salidas -->
<div class="row mt-4" id="historial-container">
<div class="col-lg-10 mx-auto">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="m-0"><i class="fa fa-history"></i> Historial de Salidas <?php echo !empty($fecha_filtro) ? "del " . date("d/m/Y", strtotime($fecha_filtro)) : "Recientes"; ?></h5>
<form action="" method="GET" class="d-flex align-items-center" id="filter-form">
<label for="fecha" class="me-2 mb-0 d-none d-md-block">Filtrar por día:</label>
<input type="date" name="fecha" id="fecha" class="form-control form-control-sm me-2" value="<?php echo htmlspecialchars($fecha_filtro); ?>">
<button type="submit" class="btn btn-sm btn-primary">Filtrar</button>
<?php if (!empty($fecha_filtro)): ?>
<a href="registro_salida.php" class="btn btn-sm btn-secondary ms-2">Limpiar</a>
<?php endif; ?>
</form>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-light">
<tr>
<th>Fecha</th>
<th>Producto</th>
<th>Sede</th>
<th class="text-center">Cantidad</th>
<th class="text-center">Código</th>
<th class="text-center">Método</th>
</tr>
</thead>
<tbody>
<?php if (empty($movements)): ?>
<tr><td colspan="6" class="text-center">No hay salidas registradas.</td></tr>
<?php else: ?>
<?php foreach ($movements as $mov): ?>
<tr>
<td><?php echo htmlspecialchars(date("d/m/Y", strtotime($mov['movement_date']))); ?></td>
<td><?php echo htmlspecialchars($mov['product_name']); ?></td>
<td><?php echo htmlspecialchars($mov['sede_name']); ?></td>
<td class="text-center"><?php echo htmlspecialchars($mov['quantity']); ?></td>
<td class="text-center">
<small class="text-muted"><?php echo htmlspecialchars($mov['codigo_unico'] ?? '-'); ?></small>
</td>
<td class="text-center">
<small class="text-muted"><?php echo htmlspecialchars($mov['metodo_registro'] ?? 'Manual'); ?></small>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Modal para la cámara --> <!-- Modal para la cámara -->
@ -218,6 +343,28 @@ document.addEventListener('DOMContentLoaded', (event) => {
document.getElementById(toastId).addEventListener('hidden.bs.toast', e => e.target.remove()); document.getElementById(toastId).addEventListener('hidden.bs.toast', e => e.target.remove());
} }
// --- ACTUALIZACIÓN EN TIEMPO REAL ---
function refreshHistoryAndSummary() {
const currentUrl = window.location.href;
fetch(currentUrl)
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newSummary = doc.getElementById('resumen-hoy-container');
const newHistory = doc.getElementById('historial-container');
if (newSummary) {
document.getElementById('resumen-hoy-container').innerHTML = newSummary.innerHTML;
}
if (newHistory) {
document.getElementById('historial-container').innerHTML = newHistory.innerHTML;
}
})
.catch(error => console.error('Error al actualizar el historial:', error));
}
// --- LÓGICA PARA MÓVIL (SOLO PESTAÑA CÓDIGO DE BARRAS) --- // --- LÓGICA PARA MÓVIL (SOLO PESTAÑA CÓDIGO DE BARRAS) ---
if (isMobile && almacenPrincipalId) { if (isMobile && almacenPrincipalId) {
if(sedeContainer) sedeContainer.style.display = 'none'; if(sedeContainer) sedeContainer.style.display = 'none';
@ -281,6 +428,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
.then(data => { .then(data => {
if (data.success) { if (data.success) {
showNotification(data.message, true); showNotification(data.message, true);
refreshHistoryAndSummary();
} else { } else {
throw new Error(data.message || 'Error desconocido.'); throw new Error(data.message || 'Error desconocido.');
} }
@ -327,6 +475,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
showNotification(data.message, true); showNotification(data.message, true);
playBeep(true); playBeep(true);
manualExitForm.reset(); manualExitForm.reset();
refreshHistoryAndSummary();
} else { } else {
throw new Error(data.message || 'Error desconocido.'); throw new Error(data.message || 'Error desconocido.');
} }

View File

@ -46,6 +46,8 @@ function getStatusStyle($status) {
function getPaqueteStyle($paquete) { function getPaqueteStyle($paquete) {
if ($paquete === 'RUTA') return 'background-color: #0dcaf0; color: black;'; // Info cyan if ($paquete === 'RUTA') return 'background-color: #0dcaf0; color: black;'; // Info cyan
if ($paquete === 'CONTRAENTREGA') return 'background-color: #ffc107; color: black;'; // Warning yellow if ($paquete === 'CONTRAENTREGA') return 'background-color: #ffc107; color: black;'; // Warning yellow
if ($paquete === 'NO CONTESTA, VOLVER A LLAMAR') return 'background-color: #fd7e14; color: white;'; // Orange
if ($paquete === 'PENDIENTE A RETORNO') return 'background-color: #dc3545; color: white;'; // Red
return 'background-color: #6c757d; color: white;'; // Secondary grey return 'background-color: #6c757d; color: white;'; // Secondary grey
} }
@ -291,6 +293,8 @@ include 'layout_header.php';
function getPaqueteStyleJS(paquete) { function getPaqueteStyleJS(paquete) {
if (paquete === 'RUTA') return 'background-color: #0dcaf0; color: black;'; if (paquete === 'RUTA') return 'background-color: #0dcaf0; color: black;';
if (paquete === 'CONTRAENTREGA') return 'background-color: #ffc107; color: black;'; if (paquete === 'CONTRAENTREGA') return 'background-color: #ffc107; color: black;';
if (paquete === 'NO CONTESTA, VOLVER A LLAMAR') return 'background-color: #fd7e14; color: white;';
if (paquete === 'PENDIENTE A RETORNO') return 'background-color: #dc3545; color: white;';
return 'background-color: #6c757d; color: white;'; return 'background-color: #6c757d; color: white;';
} }
@ -329,7 +333,9 @@ document.addEventListener('DOMContentLoaded', function() {
const options = [ const options = [
{val: '', text: 'Seleccionar'}, {val: '', text: 'Seleccionar'},
{val: 'RUTA', text: 'RUTA'}, {val: 'RUTA', text: 'RUTA'},
{val: 'CONTRAENTREGA', text: 'CONTRAENTREGA'} {val: 'CONTRAENTREGA', text: 'CONTRAENTREGA'},
{val: 'NO CONTESTA, VOLVER A LLAMAR', text: 'NO CONTESTA, VOLVER A LLAMAR'},
{val: 'PENDIENTE A RETORNO', text: 'PENDIENTE A RETORNO'}
]; ];
options.forEach(opt => { options.forEach(opt => {

View File

@ -81,6 +81,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$banco = trim($_POST['banco'] ?? ''); $banco = trim($_POST['banco'] ?? '');
$estado = $_POST['estado']; $estado = $_POST['estado'];
$notas = trim($_POST['notas']); $notas = trim($_POST['notas']);
$observacion = trim($_POST['observacion'] ?? '');
if (!empty($productos_detalle)) { if (!empty($productos_detalle)) {
$notas .= "\n\n" . $notas_adicionales; $notas .= "\n\n" . $notas_adicionales;
@ -149,6 +150,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'monto_debe' => $monto_debe, 'monto_debe' => $monto_debe,
'estado' => $estado, 'estado' => $estado,
'notas' => $notas, 'notas' => $notas,
'observacion' => $observacion,
'voucher_adelanto_path' => $voucher_adelanto_path, 'voucher_adelanto_path' => $voucher_adelanto_path,
'voucher_restante_path' => $voucher_restante_path 'voucher_restante_path' => $voucher_restante_path
]; ];
@ -176,6 +178,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
"monto_debe = :monto_debe", "monto_debe = :monto_debe",
"estado = :estado", "estado = :estado",
"notas = :notas", "notas = :notas",
"observacion = :observacion",
"voucher_adelanto_path = :voucher_adelanto_path", "voucher_adelanto_path = :voucher_adelanto_path",
"voucher_restante_path = :voucher_restante_path" "voucher_restante_path = :voucher_restante_path"
]; ];
@ -228,8 +231,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// INSERT: The advisor is the user creating the order. // INSERT: The advisor is the user creating the order.
$params['asesor_id'] = $_SESSION['user_id']; $params['asesor_id'] = $_SESSION['user_id'];
$columns_sql = "dni_cliente, nombre_completo, celular, sede_envio, codigo_rastreo, codigo_tracking, clave, pendientes, producto, cantidad, monto_total, monto_adelantado, numero_operacion, banco, monto_debe, estado, asesor_id, notas, voucher_adelanto_path, voucher_restante_path"; $columns_sql = "dni_cliente, nombre_completo, celular, sede_envio, codigo_rastreo, codigo_tracking, clave, pendientes, producto, cantidad, monto_total, monto_adelantado, numero_operacion, banco, monto_debe, estado, asesor_id, notas, observacion, voucher_adelanto_path, voucher_restante_path";
$values_sql = ":dni_cliente, :nombre_completo, :celular, :sede_envio, :codigo_rastreo, :codigo_tracking, :clave, :pendientes, :producto, :cantidad, :monto_total, :monto_adelantado, :numero_operacion, :banco, :monto_debe, :estado, :asesor_id, :notas, :voucher_adelanto_path, :voucher_restante_path"; $values_sql = ":dni_cliente, :nombre_completo, :celular, :sede_envio, :codigo_rastreo, :codigo_tracking, :clave, :pendientes, :producto, :cantidad, :monto_total, :monto_adelantado, :numero_operacion, :banco, :monto_debe, :estado, :asesor_id, :notas, :observacion, :voucher_adelanto_path, :voucher_restante_path";
$completed_states = ['Completado', 'COMPLETADO ✅']; $completed_states = ['Completado', 'COMPLETADO ✅'];
if (in_array($estado, $completed_states)) { if (in_array($estado, $completed_states)) {

View File

@ -26,7 +26,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare("UPDATE pedidos SET tipo_paquete = NULL WHERE id = ?"); $stmt = $pdo->prepare("UPDATE pedidos SET tipo_paquete = NULL WHERE id = ?");
$result = $stmt->execute([$pedido_id]); $result = $stmt->execute([$pedido_id]);
} else { } else {
$valid_types = ['RUTA', 'CONTRAENTREGA']; $valid_types = ['RUTA', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'PENDIENTE A RETORNO'];
if (!in_array($tipo_paquete, $valid_types)) { if (!in_array($tipo_paquete, $valid_types)) {
echo json_encode(['success' => false, 'message' => 'Tipo inválido']); echo json_encode(['success' => false, 'message' => 'Tipo inválido']);
exit; exit;

View File

@ -21,7 +21,7 @@ $field = $data['field'];
$value = $data['value']; $value = $data['value'];
// Whitelist allowed fields for security // Whitelist allowed fields for security
$allowed_fields = ['numero_operacion', 'banco', 'clave', 'fecha_recojo']; $allowed_fields = ['numero_operacion', 'banco', 'clave', 'fecha_recojo', 'observacion'];
if (!in_array($field, $allowed_fields)) { if (!in_array($field, $allowed_fields)) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Campo no permitido.']); echo json_encode(['error' => 'Campo no permitido.']);