Autosave: 20260428-011435
BIN
assets/uploads/info_images/info_69eb7d3890581.webp
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
assets/uploads/info_images/info_69eb7d4196d9e.webp
Normal file
|
After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 273 KiB |
|
Before Width: | Height: | Size: 44 KiB |
BIN
assets/uploads/vouchers/69e96579c6cc2-Captura.PNG
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
assets/uploads/vouchers/69e96646a13a4-Captura1.PNG
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
assets/uploads/vouchers/69ea449916f98-Screenshot_24.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
assets/uploads/vouchers/69ea4c3a2fe4a-Screenshot_25.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
assets/uploads/vouchers/69ea4d1452fa1-Screenshot_26.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
assets/uploads/vouchers/69ea76e0be320-Screenshot_251.png
Normal file
|
After Width: | Height: | Size: 344 KiB |
BIN
assets/uploads/vouchers/69ea77c4ac611-347.png
Normal file
|
After Width: | Height: | Size: 568 KiB |
BIN
assets/uploads/vouchers/69ea7fa45914d-Screenshot_27.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
assets/uploads/vouchers/69ea8019a49d1-Screenshot_28.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
assets/uploads/vouchers/69ea807ef30af-Screenshot_29.png
Normal file
|
After Width: | Height: | Size: 370 KiB |
BIN
assets/uploads/vouchers/69ea930d4bab5-974.png
Normal file
|
After Width: | Height: | Size: 567 KiB |
BIN
assets/uploads/vouchers/69ea935c9b083-104.png
Normal file
|
After Width: | Height: | Size: 543 KiB |
BIN
assets/uploads/vouchers/69ea939cace67-492.png
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
assets/uploads/vouchers/69ea93f288cc5-29.png
Normal file
|
After Width: | Height: | Size: 534 KiB |
BIN
assets/uploads/vouchers/69ea9466ac126-535.png
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
assets/uploads/vouchers/69ea9771c3997-188.png
Normal file
|
After Width: | Height: | Size: 799 KiB |
BIN
assets/uploads/vouchers/69ea97f1128af-18.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
assets/uploads/vouchers/69ea9b6ce1a57-Captura 4.PNG
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
assets/uploads/vouchers/69ea9cbb80140-Screenshot_252.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
assets/uploads/vouchers/69ea9d16b7ea6-Screenshot_30.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
assets/uploads/vouchers/69ea9d8fbefd2-Screenshot_31.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
assets/uploads/vouchers/69ea9fffe1fc0-Screenshot_32.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
assets/uploads/vouchers/69eaa0856d529-Screenshot_33.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
assets/uploads/vouchers/69eb7abb807d2-Screenshot_253.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
assets/uploads/vouchers/69eb7df68cdbf-Screenshot_253.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
assets/uploads/vouchers/69eba2148ab4d-Screenshot_34.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
assets/uploads/vouchers/69eba51397ecf-Screenshot_36.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
assets/uploads/vouchers/69ebaafbd9330-Screenshot_37.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
assets/uploads/vouchers/69ebf74c269f5-Screenshot_38.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
assets/uploads/vouchers/69ecd12524bd6-Screenshot_255.png
Normal file
|
After Width: | Height: | Size: 400 KiB |
BIN
assets/uploads/vouchers/69ece50d18860-Screenshot_39.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
assets/uploads/vouchers/69ece60b5825f-Screenshot_256.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
assets/uploads/vouchers/69ecf26cbe3d6-Screenshot_257.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
BIN
assets/uploads/vouchers/69ecf8e29bfa9-Screenshot_258.png
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
assets/uploads/vouchers/69ed0c224d8ae-510.png
Normal file
|
After Width: | Height: | Size: 972 KiB |
BIN
assets/uploads/vouchers/69ed15537eceb-Screenshot_259.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
assets/uploads/vouchers/69ed15eb90ad0-Screenshot_260.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
assets/uploads/vouchers/69ed1800b6931-Screenshot_261.png
Normal file
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/uploads/vouchers/69ed41e4f2602-Screenshot_262.png
Normal file
|
After Width: | Height: | Size: 471 KiB |
BIN
assets/uploads/vouchers/69ef74ac91c58-158.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
assets/uploads/vouchers/69ef8b17458dc-Screenshot_40.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
assets/uploads/vouchers/69ef8be0cf1b8-Screenshot_41.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/uploads/vouchers/69ef8c92b2743-Screenshot_42.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
assets/uploads/vouchers/69ef97515bb08-Screenshot_44.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
assets/uploads/vouchers/69ef97b4a7508-Screenshot_45.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
assets/uploads/vouchers/69efab10216f2-Screenshot_46.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
assets/uploads/vouchers/69efbacef3107-Screenshot_263.png
Normal file
|
After Width: | Height: | Size: 418 KiB |
BIN
assets/uploads/vouchers/69efbef4dbd24-Screenshot_47.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
assets/uploads/vouchers/69efe12e3745f-Screenshot_48.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
@ -151,13 +151,14 @@ include 'layout_header.php';
|
||||
<th>Fecha Completado</th>
|
||||
<th>Voucher Restante</th>
|
||||
<th>Verificación de Pago</th>
|
||||
<th>OBSERVACION</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($pedidos)): ?>
|
||||
<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>
|
||||
<?php else: ?>
|
||||
<?php foreach ($pedidos as $pedido): ?>
|
||||
@ -231,6 +232,9 @@ include 'layout_header.php';
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="observacion">
|
||||
<?php echo htmlspecialchars($pedido['observacion'] ?? 'N/A'); ?>
|
||||
</td>
|
||||
<td>
|
||||
<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'): ?>
|
||||
@ -433,7 +437,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
} // 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) {
|
||||
alert('El número de operación debe tener al menos 6 dígitos.');
|
||||
cell.innerHTML = originalText === '' ? 'N/A' : originalText;
|
||||
|
||||
2
db/migrations/054_add_observacion_to_pedidos.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Add observacion column to pedidos table
|
||||
ALTER TABLE pedidos ADD COLUMN observacion TEXT NULL;
|
||||
@ -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;
|
||||
2
db/migrations/064_update_tipo_paquete_enum.sql
Normal 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;
|
||||
6
db/migrations/065_merge_no_contesta_options.sql
Normal 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;
|
||||
@ -99,17 +99,46 @@ try {
|
||||
$producto_chart_labels = json_encode(array_column($stock_por_producto, 'nombre'));
|
||||
$producto_chart_data = json_encode(array_column($stock_por_producto, 'total_stock'));
|
||||
|
||||
// 4. Datos para el historial de movimientos (últimos 50)
|
||||
$movements_stmt = $pdo->query("
|
||||
SELECT sm.movement_date, p.nombre as product_name, s.nombre as sede_name, sm.quantity, sm.type, sm.metodo_registro
|
||||
// 4. Datos para el historial de movimientos
|
||||
$fecha_filtro = $_GET['fecha'] ?? '';
|
||||
$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
|
||||
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 50
|
||||
");
|
||||
LIMIT $limit
|
||||
";
|
||||
|
||||
$movements_stmt = $pdo->prepare($query);
|
||||
$movements_stmt->execute($params);
|
||||
$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) {
|
||||
echo "<div class='alert alert-danger'>Error al conectar o consultar la base de datos: " . $e->getMessage() . "</div>";
|
||||
require_once 'layout_footer.php';
|
||||
@ -217,9 +246,45 @@ try {
|
||||
</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 -->
|
||||
<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="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
@ -230,12 +295,13 @@ try {
|
||||
<th>Sede</th>
|
||||
<th class="text-center">Cantidad</th>
|
||||
<th class="text-center">Tipo</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 movimientos registrados.</td></tr>
|
||||
<tr><td colspan="7" class="text-center">No hay movimientos registrados.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($movements as $mov): ?>
|
||||
<tr>
|
||||
@ -250,6 +316,9 @@ try {
|
||||
<span class="badge bg-danger">Salida</span>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
|
||||
@ -267,6 +267,11 @@ include 'layout_header.php';
|
||||
<textarea class="form-control" id="notas" name="notas" rows="3"><?php echo htmlspecialchars($pedido['notas']); ?></textarea>
|
||||
</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>
|
||||
<a href="<?php echo htmlspecialchars($_SERVER['HTTP_REFERER'] ?? 'pedidos.php'); ?>" class="btn btn-secondary">Cancelar</a>
|
||||
</form>
|
||||
|
||||
@ -56,13 +56,14 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
|
||||
// 4. Insertar en el historial de movimientos
|
||||
$history_stmt = $pdo->prepare(
|
||||
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date, borrador_id, metodo_registro)
|
||||
VALUES (:product_id, :sede_id, :quantity, 'entrada', :movement_date, :borrador_id, 'Código de Barras')"
|
||||
"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', :codigo_unico, :movement_date, :borrador_id, 'Código de Barras')"
|
||||
);
|
||||
$history_stmt->execute([
|
||||
'product_id' => $product_id,
|
||||
'sede_id' => $sede_id,
|
||||
'quantity' => $quantity,
|
||||
'codigo_unico' => $codigo_unico,
|
||||
'movement_date' => $movement_date,
|
||||
'borrador_id' => $borrador_id
|
||||
]);
|
||||
|
||||
@ -62,13 +62,14 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
}
|
||||
|
||||
$history_stmt = $pdo->prepare(
|
||||
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, movement_date, metodo_registro)
|
||||
VALUES (:product_id, :sede_id, :quantity, 'salida', :movement_date, 'Código de Barras')"
|
||||
"INSERT INTO stock_movements (product_id, sede_id, quantity, type, codigo_unico, movement_date, metodo_registro)
|
||||
VALUES (:product_id, :sede_id, :quantity, 'salida', :codigo_unico, :movement_date, 'Código de Barras')"
|
||||
);
|
||||
$history_stmt->execute([
|
||||
'product_id' => $product_id,
|
||||
'sede_id' => $sede_id,
|
||||
'quantity' => $quantity,
|
||||
'codigo_unico' => $codigo_unico,
|
||||
'movement_date' => $movement_date
|
||||
]);
|
||||
|
||||
|
||||
@ -25,6 +25,46 @@ try {
|
||||
$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) {
|
||||
$error_page_load = "Error al cargar datos iniciales: " . $e->getMessage();
|
||||
}
|
||||
@ -147,6 +187,91 @@ try {
|
||||
</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>
|
||||
|
||||
<!-- Modal para la cámara -->
|
||||
@ -239,6 +364,28 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
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 ---
|
||||
|
||||
// ** Lógica de Cámara **
|
||||
@ -313,6 +460,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, true);
|
||||
refreshHistoryAndSummary();
|
||||
} else {
|
||||
throw new Error(data.message || 'Error desconocido.');
|
||||
}
|
||||
@ -354,6 +502,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
showNotification(data.message, true);
|
||||
playBeep(true);
|
||||
manualForm.reset();
|
||||
refreshHistoryAndSummary();
|
||||
} else {
|
||||
throw new Error(data.message || 'Error desconocido al registrar la entrada.');
|
||||
}
|
||||
|
||||
@ -26,6 +26,46 @@ try {
|
||||
$products_stmt = $pdo->query("SELECT id, nombre, sku FROM products ORDER BY nombre ASC");
|
||||
$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) {
|
||||
$error_page_load = "Error al cargar datos iniciales: " . $e->getMessage();
|
||||
}
|
||||
@ -131,6 +171,91 @@ try {
|
||||
</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>
|
||||
|
||||
<!-- Modal para la cámara -->
|
||||
@ -218,6 +343,28 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
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) ---
|
||||
if (isMobile && almacenPrincipalId) {
|
||||
if(sedeContainer) sedeContainer.style.display = 'none';
|
||||
@ -281,6 +428,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, true);
|
||||
refreshHistoryAndSummary();
|
||||
} else {
|
||||
throw new Error(data.message || 'Error desconocido.');
|
||||
}
|
||||
@ -327,6 +475,7 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
||||
showNotification(data.message, true);
|
||||
playBeep(true);
|
||||
manualExitForm.reset();
|
||||
refreshHistoryAndSummary();
|
||||
} else {
|
||||
throw new Error(data.message || 'Error desconocido.');
|
||||
}
|
||||
|
||||
@ -46,6 +46,8 @@ function getStatusStyle($status) {
|
||||
function getPaqueteStyle($paquete) {
|
||||
if ($paquete === 'RUTA') return 'background-color: #0dcaf0; color: black;'; // Info cyan
|
||||
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
|
||||
}
|
||||
|
||||
@ -291,6 +293,8 @@ include 'layout_header.php';
|
||||
function getPaqueteStyleJS(paquete) {
|
||||
if (paquete === 'RUTA') return 'background-color: #0dcaf0; 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;';
|
||||
}
|
||||
|
||||
@ -329,7 +333,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const options = [
|
||||
{val: '', text: 'Seleccionar'},
|
||||
{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 => {
|
||||
|
||||
@ -81,6 +81,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$banco = trim($_POST['banco'] ?? '');
|
||||
$estado = $_POST['estado'];
|
||||
$notas = trim($_POST['notas']);
|
||||
$observacion = trim($_POST['observacion'] ?? '');
|
||||
|
||||
if (!empty($productos_detalle)) {
|
||||
$notas .= "\n\n" . $notas_adicionales;
|
||||
@ -149,6 +150,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'monto_debe' => $monto_debe,
|
||||
'estado' => $estado,
|
||||
'notas' => $notas,
|
||||
'observacion' => $observacion,
|
||||
'voucher_adelanto_path' => $voucher_adelanto_path,
|
||||
'voucher_restante_path' => $voucher_restante_path
|
||||
];
|
||||
@ -176,6 +178,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
"monto_debe = :monto_debe",
|
||||
"estado = :estado",
|
||||
"notas = :notas",
|
||||
"observacion = :observacion",
|
||||
"voucher_adelanto_path = :voucher_adelanto_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.
|
||||
$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";
|
||||
$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";
|
||||
$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, :observacion, :voucher_adelanto_path, :voucher_restante_path";
|
||||
|
||||
$completed_states = ['Completado', 'COMPLETADO ✅'];
|
||||
if (in_array($estado, $completed_states)) {
|
||||
|
||||
@ -26,7 +26,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt = $pdo->prepare("UPDATE pedidos SET tipo_paquete = NULL WHERE id = ?");
|
||||
$result = $stmt->execute([$pedido_id]);
|
||||
} else {
|
||||
$valid_types = ['RUTA', 'CONTRAENTREGA'];
|
||||
$valid_types = ['RUTA', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'PENDIENTE A RETORNO'];
|
||||
if (!in_array($tipo_paquete, $valid_types)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Tipo inválido']);
|
||||
exit;
|
||||
|
||||
@ -21,7 +21,7 @@ $field = $data['field'];
|
||||
$value = $data['value'];
|
||||
|
||||
// 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)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Campo no permitido.']);
|
||||
|
||||