Autosave: 20260518-164428

This commit is contained in:
Flatlogic Bot 2026-05-18 16:44:29 +00:00
parent 5336ec4122
commit 4048403247
15 changed files with 496 additions and 66 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

View File

@ -21,39 +21,47 @@ $date_condition = "";
$label_period = "";
if ($period === 'custom' && !empty($start_date) && !empty($end_date)) {
$date_condition = "DATE(created_at) BETWEEN '$start_date' AND '$end_date'";
$date_condition = "DATE(p.created_at) BETWEEN '$start_date' AND '$end_date'";
$label_period = "Desde " . date('d/m/Y', strtotime($start_date)) . " hasta " . date('d/m/Y', strtotime($end_date));
} else {
switch ($period) {
case 'today':
$date_condition = "DATE(created_at) = CURDATE()";
$date_condition = "DATE(p.created_at) = CURDATE()";
$label_period = "Hoy";
break;
case 'yesterday':
$date_condition = "DATE(p.created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)";
$label_period = "Ayer";
break;
case '7':
$date_condition = "DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)";
$date_condition = "DATE(p.created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)";
$label_period = "Últimos 7 días";
break;
case '15':
$date_condition = "DATE(p.created_at) >= DATE_SUB(CURDATE(), INTERVAL 15 DAY)";
$label_period = "Últimos 15 días";
break;
case '30':
$date_condition = "DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)";
$date_condition = "DATE(p.created_at) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)";
$label_period = "Últimos 30 días";
break;
case 'month':
$date_condition = "MONTH(created_at) = MONTH(CURDATE()) AND YEAR(created_at) = YEAR(CURDATE())";
$date_condition = "MONTH(p.created_at) = MONTH(CURDATE()) AND YEAR(p.created_at) = YEAR(CURDATE())";
$label_period = "Este Mes";
break;
case 'year':
$date_condition = "DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 1 YEAR)";
$date_condition = "DATE(p.created_at) >= DATE_SUB(CURDATE(), INTERVAL 1 YEAR)";
$label_period = "Último Año";
break;
default:
$date_condition = "DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)";
$date_condition = "DATE(p.created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)";
$label_period = "Últimos 7 días";
$period = '7';
}
}
// 1. Estadísticas del periodo seleccionado
$stmtStats = $db->query("SELECT COUNT(*) as total_pedidos, SUM(monto_total) as total_dinero FROM pedidos WHERE $date_condition AND estado != 'RETORNADO'");
$stmtStats = $db->query("SELECT COUNT(*) as total_pedidos, SUM(monto_total) as total_dinero FROM pedidos p WHERE $date_condition AND estado != 'RETORNADO'");
$statsPeriodo = $stmtStats->fetch(PDO::FETCH_ASSOC);
// 2. Pedidos Pendientes (Global, no depende del filtro de fecha usualmente, pero lo mantendremos así)
@ -70,23 +78,23 @@ $stockCritico = $stmtStock->fetchColumn() ?: 0;
// 5. Datos para Gráfico de Ventas (Ajustado al periodo)
$ventasTendencia = [];
if ($period === 'today') {
// Si es hoy, mostrar por horas
$stmt = $db->query("SELECT HOUR(created_at) as hora, COUNT(*) as cant, SUM(monto_total) as monto FROM pedidos WHERE $date_condition AND estado != 'RETORNADO' GROUP BY HOUR(created_at) ORDER BY hora ASC");
if ($period === 'today' || $period === 'yesterday') {
// Si es hoy o ayer, mostrar por horas
$stmt = $db->query("SELECT HOUR(p.created_at) as hora, COUNT(*) as cant, SUM(monto_total) as monto FROM pedidos p WHERE $date_condition AND estado != 'RETORNADO' GROUP BY HOUR(p.created_at) ORDER BY hora ASC");
$res = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($res as $r) {
$ventasTendencia[] = ['fecha' => $r['hora'] . ':00', 'cantidad' => $r['cant'], 'monto' => $r['monto']];
}
} elseif ($period === 'year') {
// Si es un año, mostrar por meses
$stmt = $db->query("SELECT DATE_FORMAT(created_at, '%Y-%m') as mes, COUNT(*) as cant, SUM(monto_total) as monto FROM pedidos WHERE $date_condition AND estado != 'RETORNADO' GROUP BY mes ORDER BY mes ASC");
$stmt = $db->query("SELECT DATE_FORMAT(p.created_at, '%Y-%m') as mes, COUNT(*) as cant, SUM(monto_total) as monto FROM pedidos p WHERE $date_condition AND estado != 'RETORNADO' GROUP BY mes ORDER BY mes ASC");
$res = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($res as $r) {
$ventasTendencia[] = ['fecha' => date('M Y', strtotime($r['mes'] . '-01')), 'cantidad' => $r['cant'], 'monto' => $r['monto']];
}
} else {
// Mostrar por días
$stmt = $db->query("SELECT DATE(created_at) as fecha, COUNT(*) as cant, SUM(monto_total) as monto FROM pedidos WHERE $date_condition AND estado != 'RETORNADO' GROUP BY DATE(created_at) ORDER BY fecha ASC");
$stmt = $db->query("SELECT DATE(p.created_at) as fecha, COUNT(*) as cant, SUM(monto_total) as monto FROM pedidos p WHERE $date_condition AND estado != 'RETORNADO' GROUP BY DATE(p.created_at) ORDER BY fecha ASC");
$res = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($res as $r) {
$ventasTendencia[] = ['fecha' => date('d/m', strtotime($r['fecha'])), 'cantidad' => $r['cant'], 'monto' => $r['monto']];
@ -94,13 +102,17 @@ if ($period === 'today') {
}
// 6. Estados de Pedidos (Ajustado al periodo)
$stmtEstados = $db->query("SELECT estado, COUNT(*) as total FROM pedidos WHERE $date_condition GROUP BY estado");
$stmtEstados = $db->query("SELECT estado, COUNT(*) as total FROM pedidos p WHERE $date_condition GROUP BY estado");
$estadosData = $stmtEstados->fetchAll(PDO::FETCH_ASSOC);
// 7. Top Productos (Ajustado al periodo)
$stmtTopProd = $db->query("SELECT producto, SUM(cantidad) as ventas FROM pedidos WHERE $date_condition AND estado != 'RETORNADO' GROUP BY producto ORDER BY ventas DESC LIMIT 5");
$stmtTopProd = $db->query("SELECT producto, SUM(cantidad) as ventas FROM pedidos p WHERE $date_condition AND estado != 'RETORNADO' GROUP BY producto ORDER BY ventas DESC LIMIT 5");
$topProductos = $stmtTopProd->fetchAll(PDO::FETCH_ASSOC);
// 7b. Top Productos Contraentrega (Ajustado al periodo)
$stmtTopProdCE = $db->query("SELECT producto, SUM(cantidad) as ventas FROM pedidos p WHERE $date_condition AND estado IN ('RUTA_CONTRAENTREGA', 'ENTREGA EXITOSA') GROUP BY producto ORDER BY ventas DESC LIMIT 5");
$topProductosCE = $stmtTopProdCE->fetchAll(PDO::FETCH_ASSOC);
// 8. Ventas por Asesor (Ajustado al periodo)
$stmtAsesores = $db->query("SELECT u.nombre_asesor,
COUNT(p.id) as total_pedidos,
@ -117,56 +129,72 @@ $ventasAsesores = $stmtAsesores->fetchAll(PDO::FETCH_ASSOC);
// 9. Comparativa Mensual (Se mantiene global para contexto)
$mesActual = date('Y-m');
$mesPasado = date('Y-m', strtotime('first day of last month'));
$stmtMesActual = $db->prepare("SELECT SUM(monto_total) as monto FROM pedidos WHERE DATE_FORMAT(created_at, '%Y-%m') = ? AND estado != 'RETORNADO'");
$stmtMesActual = $db->prepare("SELECT SUM(monto_total) as monto FROM pedidos p WHERE DATE_FORMAT(p.created_at, '%Y-%m') = ? AND estado != 'RETORNADO'");
$stmtMesActual->execute([$mesActual]);
$montoMesActual = $stmtMesActual->fetchColumn() ?: 0;
$stmtMesPasado = $db->prepare("SELECT SUM(monto_total) as monto FROM pedidos WHERE DATE_FORMAT(created_at, '%Y-%m') = ? AND estado != 'RETORNADO'");
$stmtMesPasado = $db->prepare("SELECT SUM(monto_total) as monto FROM pedidos p WHERE DATE_FORMAT(p.created_at, '%Y-%m') = ? AND estado != 'RETORNADO'");
$stmtMesPasado->execute([$mesPasado]);
$montoMesPasado = $stmtMesPasado->fetchColumn() ?: 0;
$crecimientoMensual = $montoMesPasado > 0 ? (($montoMesActual - $montoMesPasado) / $montoMesPasado) * 100 : 0;
// 10. Eficiencia Logística (Ajustado al periodo)
$stmtTiempo = $db->query("SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, fecha_completado)) / 24 as promedio_dias
FROM pedidos
$stmtTiempo = $db->query("SELECT AVG(TIMESTAMPDIFF(HOUR, p.created_at, fecha_completado)) / 24 as promedio_dias
FROM pedidos p
WHERE $date_condition AND estado = 'COMPLETADO ✅' AND fecha_completado IS NOT NULL");
$tiempoPromedio = $stmtTiempo->fetchColumn() ?: 0;
$stmtRetorno = $db->query("SELECT (COUNT(CASE WHEN estado = 'RETORNADO' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0)) as tasa_retorno
FROM pedidos WHERE $date_condition");
FROM pedidos p WHERE $date_condition");
$tasaRetorno = $stmtRetorno->fetchColumn() ?: 0;
// 11. Utilidad Total Estimada (Ajustado al periodo)
$stmtUtilidad = $db->query("
SELECT SUM(p.monto_total - (COALESCE(pr.costo_unitario, 0) * p.cantidad)) as utilidad_total
SELECT SUM(p.monto_total - (COALESCE(pr.costo, 0) * p.cantidad)) as utilidad_total
FROM pedidos p
LEFT JOIN products pr ON p.producto = pr.nombre
WHERE p.$date_condition AND p.estado != 'RETORNADO'
WHERE $date_condition AND p.estado != 'RETORNADO'
");
$utilidadTotal = $stmtUtilidad->fetchColumn() ?: 0;
// 12. Datos Detallados por Canal (Provincia vs Contraentrega)
$stmtDetalleCanal = $db->query("SELECT
CASE WHEN agencia = 'CONTRAENTREGA' THEN 'Contraentrega' ELSE 'Provincia' END as canal,
CASE
WHEN estado IN ('RUTA_CONTRAENTREGA', 'ENTREGA EXITOSA') THEN 'Contraentrega'
WHEN estado = 'RETORNADO' AND agencia = 'CONTRAENTREGA' THEN 'Contraentrega'
ELSE 'Provincia'
END as canal,
estado,
COUNT(*) as total,
SUM(monto_total) as monto
FROM pedidos
FROM pedidos p
WHERE $date_condition
GROUP BY canal, estado");
$detalleCanalRaw = $stmtDetalleCanal->fetchAll(PDO::FETCH_ASSOC);
// 13. Top Productos Detallado (Para la tabla)
$stmtTopDetalle = $db->query("
SELECT p.producto, SUM(p.cantidad) as total_cantidad, SUM(p.monto_total) as monto_total, AVG(pr.costo_unitario) as costo_promedio
SELECT p.producto, SUM(p.cantidad) as total_cantidad, SUM(p.monto_total) as monto_total, AVG(pr.costo) as costo_promedio
FROM pedidos p
LEFT JOIN products pr ON p.producto = pr.nombre
WHERE p.$date_condition AND p.estado != 'RETORNADO'
WHERE $date_condition AND p.estado != 'RETORNADO'
GROUP BY p.producto
ORDER BY total_cantidad DESC
LIMIT 5
");
$topProductosDetalle = $stmtTopDetalle->fetchAll(PDO::FETCH_ASSOC);
// 13b. Top Productos Detallado Contraentrega
$stmtTopDetalleCE = $db->query("
SELECT p.producto, SUM(p.cantidad) as total_cantidad, SUM(p.monto_total) as monto_total, AVG(pr.costo) as costo_promedio
FROM pedidos p
LEFT JOIN products pr ON p.producto = pr.nombre
WHERE $date_condition AND p.estado IN ('RUTA_CONTRAENTREGA', 'ENTREGA EXITOSA')
GROUP BY p.producto
ORDER BY total_cantidad DESC
LIMIT 5
");
$topProductosDetalleCE = $stmtTopDetalleCE->fetchAll(PDO::FETCH_ASSOC);
$canalesResumen = [
'Provincia' => ['pedidos' => 0, 'monto' => 0, 'estados' => []],
'Contraentrega' => ['pedidos' => 0, 'monto' => 0, 'estados' => []]
@ -204,7 +232,9 @@ include 'layout_header.php';
<form action="" method="GET" class="d-inline-block">
<div class="btn-group me-2" role="group">
<a href="?period=today" class="btn btn-outline-primary btn-sm <?php echo $period == 'today' ? 'active' : ''; ?>">Hoy</a>
<a href="?period=yesterday" class="btn btn-outline-primary btn-sm <?php echo $period == 'yesterday' ? 'active' : ''; ?>">Ayer</a>
<a href="?period=7" class="btn btn-outline-primary btn-sm <?php echo $period == '7' ? 'active' : ''; ?>">7 Días</a>
<a href="?period=15" class="btn btn-outline-primary btn-sm <?php echo $period == '15' ? 'active' : ''; ?>">15 Días</a>
<a href="?period=30" class="btn btn-outline-primary btn-sm <?php echo $period == '30' ? 'active' : ''; ?>">30 Días</a>
<a href="?period=month" class="btn btn-outline-primary btn-sm <?php echo $period == 'month' ? 'active' : ''; ?>">Este Mes</a>
<a href="?period=year" class="btn btn-outline-primary btn-sm <?php echo $period == 'year' ? 'active' : ''; ?>">1 Año</a>
@ -344,6 +374,47 @@ include 'layout_header.php';
</div>
</div>
<!-- Gráficas Circulares por Canal -->
<div class="row mb-4">
<!-- Gráfica Provincia -->
<div class="col-md-6 mb-3">
<div class="card shadow h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0 text-primary"><i class="fas fa-chart-pie me-2"></i> Estados Provincia</h5>
<span class="badge bg-primary text-white">Distribución Interna</span>
</div>
<div class="card-body d-flex flex-column align-items-center justify-content-center" style="min-height: 300px;">
<div style="width: 100%; max-width: 280px;">
<canvas id="provinciaPieChart"></canvas>
</div>
<div class="mt-3 text-center">
<div class="h4 mb-0 fw-bold text-primary"><?php echo $canalesResumen['Provincia']['pedidos']; ?></div>
<div class="small text-muted">Pedidos Totales Provincia</div>
</div>
</div>
</div>
</div>
<!-- Gráfica Contraentrega -->
<div class="col-md-6 mb-3">
<div class="card shadow h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0 text-warning"><i class="fas fa-chart-pie me-2"></i> Estados Contraentrega</h5>
<span class="badge bg-warning text-dark">Distribución Interna</span>
</div>
<div class="card-body d-flex flex-column align-items-center justify-content-center" style="min-height: 300px;">
<div style="width: 100%; max-width: 280px;">
<canvas id="contraentregaPieChart"></canvas>
</div>
<div class="mt-3 text-center">
<div class="h4 mb-0 fw-bold text-warning"><?php echo $canalesResumen['Contraentrega']['pedidos']; ?></div>
<div class="small text-muted">Pedidos Totales Contraentrega</div>
</div>
</div>
</div>
</div>
</div>
<!-- Comparativa de Canales: Provincia vs Contraentrega -->
<div class="row mb-4">
<!-- Sección Provincia -->
@ -513,8 +584,65 @@ include 'layout_header.php';
</div>
</div>
</div>
<!-- Ventas por Asesor -->
<!-- Top Productos Contraentrega -->
<div class="col-md-6 mb-4">
<div class="card shadow border-left-warning">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0 text-warning"><i class="fas fa-motorcycle me-2"></i> Top 5 Contraentrega</h5>
<span class="badge bg-warning text-dark">Solo Contraentrega</span>
</div>
<div class="card-body">
<canvas id="productosCEChart" class="mb-4"></canvas>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Producto</th>
<th class="text-center">Cant.</th>
<th class="text-end">Monto Total</th>
<th class="text-end">Rent. %</th>
</tr>
</thead>
<tbody>
<?php foreach ($topProductosDetalleCE as $prod):
$costoTotal = $prod['costo_promedio'] * $prod['total_cantidad'];
$utilidad = $prod['monto_total'] - $costoTotal;
$rentabilidad = ($prod['monto_total'] > 0) ? ($utilidad / $prod['monto_total']) * 100 : 0;
$rentClass = $rentabilidad > 30 ? 'text-success' : ($rentabilidad > 15 ? 'text-warning' : 'text-danger');
?>
<tr>
<td class="fw-bold text-dark small"><?php echo htmlspecialchars($prod['producto']); ?></td>
<td class="text-center">
<span class="badge bg-light text-warning border border-warning px-2">
<?php echo number_format($prod['total_cantidad']); ?>
</span>
</td>
<td class="text-end fw-bold text-success small">
S/ <?php echo number_format($prod['monto_total'], 2); ?>
</td>
<td class="text-end fw-bold <?php echo $rentClass; ?> small">
<?php echo number_format($rentabilidad, 1); ?>%
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($topProductosDetalleCE)): ?>
<tr>
<td colspan="4" class="text-center text-muted py-3">No hay datos en este periodo</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Ventas por Asesor -->
<div class="col-md-12 mb-4">
<div class="card shadow">
<div class="card-header bg-white">
<h5 class="mb-0">Rendimiento por Asesor (Monto Total)</h5>
@ -529,6 +657,62 @@ include 'layout_header.php';
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Gráfico de Provincia (Doughnut)
const ctxProvincia = document.getElementById('provinciaPieChart').getContext('2d');
new Chart(ctxProvincia, {
type: 'doughnut',
data: {
labels: ['ROTULADO 📦', 'EN TRANSITO 🚛', 'EN DESTINO 🏬', 'COMPLETADO ✅', 'RETORNADO'],
datasets: [{
data: [
<?php echo $canalesResumen['Provincia']['estados']['ROTULADO 📦'] ?? 0; ?>,
<?php echo $canalesResumen['Provincia']['estados']['EN TRANSITO 🚛'] ?? 0; ?>,
<?php echo $canalesResumen['Provincia']['estados']['EN DESTINO 🏬'] ?? 0; ?>,
<?php echo $canalesResumen['Provincia']['estados']['COMPLETADO ✅'] ?? 0; ?>,
<?php echo $canalesResumen['Provincia']['estados']['RETORNADO'] ?? 0; ?>
],
backgroundColor: ['#ffc107', '#0dcaf0', '#6610f2', '#198754', '#dc3545'],
hoverOffset: 10,
borderWidth: 0,
cutout: '70%'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
}
}
});
// Gráfico de Contraentrega (Doughnut)
const ctxContraentrega = document.getElementById('contraentregaPieChart').getContext('2d');
new Chart(ctxContraentrega, {
type: 'doughnut',
data: {
labels: ['RUTA_CONTRAENTREGA', 'ENTREGA EXITOSA', 'RETORNADO'],
datasets: [{
data: [
<?php echo $canalesResumen['Contraentrega']['estados']['RUTA_CONTRAENTREGA'] ?? 0; ?>,
<?php echo $canalesResumen['Contraentrega']['estados']['ENTREGA EXITOSA'] ?? 0; ?>,
<?php echo $canalesResumen['Contraentrega']['estados']['RETORNADO'] ?? 0; ?>
],
backgroundColor: ['#0d6efd', '#198754', '#dc3545'],
hoverOffset: 10,
borderWidth: 0,
cutout: '70%'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
}
}
});
// Gráfico de Ventas
const ctxVentas = document.getElementById('ventasChart').getContext('2d');
new Chart(ctxVentas, {
@ -601,6 +785,24 @@ include 'layout_header.php';
}
});
// Gráfico de Productos Contraentrega
const ctxProductosCE = document.getElementById('productosCEChart').getContext('2d');
new Chart(ctxProductosCE, {
type: 'bar',
data: {
labels: <?php echo json_encode(array_map(function($p) { return strlen($p['producto']) > 20 ? substr($p['producto'], 0, 20) . '...' : $p['producto']; }, $topProductosCE)); ?>,
datasets: [{
label: 'Unidades Vendidas (CE)',
data: <?php echo json_encode(array_column($topProductosCE, 'ventas')); ?>,
backgroundColor: '#ffc107'
}]
},
options: {
indexAxis: 'y',
responsive: true
}
});
// Gráfico de Asesores
const ctxAsesores = document.getElementById('asesoresChart').getContext('2d');
new Chart(ctxAsesores, {

View File

@ -72,6 +72,40 @@ if ($user_role === 'Administrador' || $user_role === 'Logistica') {
$stmt_products = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
$products = $stmt_products->fetchAll();
// Parse products for editing
$display_products = [];
if (!empty($pedido['id'])) {
// Try to parse from notas first as it has quantities
if (preg_match_all('/Detalle de productos: (.*)$/m', $pedido['notas'], $matches)) {
// Take the last match
$last_match = end($matches[1]);
$details = explode(', ', $last_match);
foreach ($details as $detail) {
if (preg_match('/(.*) \(x(\d+)\)/', $detail, $d_matches)) {
$display_products[] = [
'nombre' => trim($d_matches[1]),
'cantidad' => (int)$d_matches[2]
];
}
}
}
// Fallback if parsing failed or no details in notas
if (empty($display_products) && !empty($pedido['producto'])) {
$names = explode(', ', $pedido['producto']);
foreach ($names as $name) {
$display_products[] = [
'nombre' => trim($name),
'cantidad' => count($names) == 1 ? $pedido['cantidad'] : 1
];
}
}
}
if (empty($display_products)) {
$display_products[] = ['nombre' => '', 'cantidad' => 1];
}
// Fetch Shalom branches
$stmt_sedes = $pdo->query("SELECT nombre_sede FROM sedes_shalom ORDER BY nombre_sede ASC");
$sedes_shalom = $stmt_sedes->fetchAll(PDO::FETCH_COLUMN);
@ -214,26 +248,28 @@ include 'layout_header.php';
<hr>
<h5>Productos</h5>
<div id="productos-container">
<div class="row producto-row mb-3">
<div class="col-md-6">
<label for="producto" class="form-label">Producto</label>
<select class="form-select" name="productos[0][nombre]" required>
<option value="">Seleccione un producto</option>
<?php foreach ($products as $product): ?>
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($pedido['producto'] == $product['nombre']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($product['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
<?php foreach ($display_products as $index => $dp): ?>
<div class="row producto-row mb-3">
<div class="col-md-6">
<label for="producto" class="form-label">Producto</label>
<select class="form-select" name="productos[<?php echo $index; ?>][nombre]" required>
<option value="">Seleccione un producto</option>
<?php foreach ($products as $product): ?>
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($dp['nombre'] == $product['nombre']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($product['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" name="productos[<?php echo $index; ?>][cantidad]" value="<?php echo htmlspecialchars($dp['cantidad']); ?>" required>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-danger btn-sm remove-producto-btn" style="<?php echo $index === 0 ? 'display: none;' : ''; ?>">Eliminar</button>
</div>
</div>
<div class="col-md-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" name="productos[0][cantidad]" value="<?php echo htmlspecialchars($pedido['cantidad']); ?>" required>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-danger btn-sm remove-producto-btn" style="display: none;">Eliminar</button>
</div>
</div>
<?php endforeach; ?>
</div>
<button type="button" id="add-producto-btn" class="btn btn-success btn-sm mb-3">Agregar producto adicional</button>
<hr>

View File

@ -67,6 +67,40 @@ if ($user_role === 'Administrador') {
$stmt_products = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
$products = $stmt_products->fetchAll();
// Parse products for editing
$display_products = [];
if (!empty($pedido['id'])) {
// Try to parse from notas first as it has quantities
if (preg_match_all('/Detalle de productos: (.*)$/m', $pedido['notas'], $matches)) {
// Take the last match
$last_match = end($matches[1]);
$details = explode(', ', $last_match);
foreach ($details as $detail) {
if (preg_match('/(.*) \(x(\d+)\)/', $detail, $d_matches)) {
$display_products[] = [
'nombre' => trim($d_matches[1]),
'cantidad' => (int)$d_matches[2]
];
}
}
}
// Fallback if parsing failed or no details in notas
if (empty($display_products) && !empty($pedido['producto'])) {
$names = explode(', ', $pedido['producto']);
foreach ($names as $name) {
$display_products[] = [
'nombre' => trim($name),
'cantidad' => count($names) == 1 ? $pedido['cantidad'] : 1
];
}
}
}
if (empty($display_products)) {
$display_products[] = ['nombre' => '', 'cantidad' => 1];
}
?>
<?php
$pageTitle = 'Agregar Pedidos Contraentrega';
@ -173,26 +207,28 @@ include 'layout_header.php';
<hr>
<h5>Productos</h5>
<div id="productos-container">
<div class="row producto-row mb-3">
<div class="col-md-6">
<label for="producto" class="form-label">Producto</label>
<select class="form-select" name="productos[0][nombre]" required>
<option value="">Seleccione un producto</option>
<?php foreach ($products as $product): ?>
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($pedido['producto'] == $product['nombre']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($product['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
<?php foreach ($display_products as $index => $dp): ?>
<div class="row producto-row mb-3">
<div class="col-md-6">
<label for="producto" class="form-label">Producto</label>
<select class="form-select" name="productos[<?php echo $index; ?>][nombre]" required>
<option value="">Seleccione un producto</option>
<?php foreach ($products as $product): ?>
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($dp['nombre'] == $product['nombre']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($product['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" name="productos[<?php echo $index; ?>][cantidad]" value="<?php echo htmlspecialchars($dp['cantidad']); ?>" required>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-danger btn-sm remove-producto-btn" style="<?php echo $index === 0 ? 'display: none;' : ''; ?>">Eliminar</button>
</div>
</div>
<div class="col-md-3">
<label for="cantidad" class="form-label">Cantidad</label>
<input type="number" class="form-control" name="productos[0][cantidad]" value="<?php echo htmlspecialchars($pedido['cantidad']); ?>" required>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-danger btn-sm remove-producto-btn" style="display: none;">Eliminar</button>
</div>
</div>
<?php endforeach; ?>
</div>
<button type="button" id="add-producto-btn" class="btn btn-success btn-sm mb-3">Agregar producto adicional</button>
<hr>

View File

@ -266,7 +266,11 @@ include 'layout_header.php';
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
<?php endif; ?>
<td><span class="badge" style="<?php echo getStatusStyle($pedido['estado']); ?>"><?php echo htmlspecialchars($pedido['estado']); ?></span></td>
<td class="editable-estado" data-id="<?php echo $pedido['id']; ?>" data-value="<?php echo htmlspecialchars($pedido['estado']); ?>" style="cursor: pointer;">
<span class="badge" style="<?php echo getStatusStyle($pedido['estado']); ?>">
<?php echo htmlspecialchars($pedido['estado']); ?>
</span>
</td>
<td><span class="badge" style="<?php echo getFechaEntregaStyle($pedido['fecha_entrega']); ?>"><?php echo htmlspecialchars(!empty($pedido['fecha_entrega']) && $pedido['fecha_entrega'] != '0000-00-00' ? date('d/m/Y', strtotime($pedido['fecha_entrega'])) : 'N/A'); ?></span></td>
<?php if ($user_role !== 'Asesor'): ?><td><?php echo htmlspecialchars($pedido['asesor_nombre'] ?? 'N/A'); ?></td><?php endif; ?>
<td><?php echo htmlspecialchars($pedido['created_at']); ?></td>
@ -300,6 +304,42 @@ function getPaqueteStyleJS(paquete) {
return 'background-color: #6c757d; color: white;';
}
function getStatusStyleJS(status) {
let bgColor = '#0dcaf0';
let style = 'color: white;';
switch (status.toUpperCase().trim()) {
case 'ROTULADO':
bgColor = '#ffc107';
style = 'color: black;';
break;
case 'EN TRANSITO':
bgColor = '#90EE90';
style = 'color: black;';
break;
case 'EN DESTINO':
bgColor = '#800080';
break;
case 'COMPLETADO':
case 'COMPLETADO ✅':
bgColor = '#198754';
break;
case 'GESTION':
bgColor = '#6c757d';
break;
case 'RUTA_CONTRAENTREGA':
bgColor = '#007bff';
break;
case 'ENTREGA EXITOSA':
bgColor = '#198754';
break;
case 'RETORNADO':
bgColor = '#dc3545';
break;
}
return `background-color: ${bgColor} !important; ${style}`;
}
$(document).ready(function() {
$('#pedidos-table').DataTable({
"language": {
@ -394,6 +434,79 @@ document.addEventListener('DOMContentLoaded', function() {
select.addEventListener('blur', save);
}
// Handle Estado editing
if (e.target && (e.target.classList.contains('editable-estado') || e.target.closest('.editable-estado'))) {
const cell = e.target.classList.contains('editable-estado') ? e.target : e.target.closest('.editable-estado');
if (cell.querySelector('select')) return; // Already editing
const currentVal = cell.dataset.value;
const pedidoId = cell.dataset.id;
const select = document.createElement('select');
select.className = 'form-select form-select-sm';
const options = [
{val: 'RUTA_CONTRAENTREGA', text: 'RUTA_CONTRAENTREGA'},
{val: 'ENTREGA EXITOSA', text: 'ENTREGA EXITOSA'},
{val: 'RETORNADO', text: 'RETORNADO'}
];
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.val;
option.text = opt.text;
if (opt.val === currentVal) option.selected = true;
select.appendChild(option);
});
const originalContent = cell.innerHTML;
cell.innerHTML = '';
cell.appendChild(select);
select.focus();
const save = () => {
const newVal = select.value;
if (newVal === currentVal) {
cell.innerHTML = originalContent;
return;
}
// Optimistic update
cell.dataset.value = newVal;
cell.innerHTML = `<span class="badge" style="${getStatusStyleJS(newVal)}">${newVal}</span>`;
const formData = new URLSearchParams();
formData.append('pedido_id', pedidoId);
formData.append('estado', newVal);
fetch('update_estado.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: formData
})
.then(r => r.json())
.then(data => {
if (!data.success) {
alert('Error: ' + (data.message || 'No se pudo actualizar.'));
cell.innerHTML = originalContent; // Revert
cell.dataset.value = currentVal;
}
})
.catch(err => {
console.error(err);
alert('Error de conexión.');
cell.innerHTML = originalContent; // Revert
cell.dataset.value = currentVal;
});
};
select.addEventListener('change', save);
select.addEventListener('blur', save);
}
if (e.target && e.target.classList.contains('editable')) {
const cell = e.target;
if (cell.querySelector('input')) {

43
update_estado.php Normal file
View File

@ -0,0 +1,43 @@
<?php
session_start();
require_once 'db/config.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'message' => 'No autorizado']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pedido_id = $_POST['pedido_id'] ?? null;
$estado = $_POST['estado'] ?? null;
if (!$pedido_id) {
echo json_encode(['success' => false, 'message' => 'ID de pedido faltante']);
exit;
}
try {
$pdo = db();
$valid_states = ['RUTA_CONTRAENTREGA', 'ENTREGA EXITOSA', 'RETORNADO'];
if (!in_array($estado, $valid_states)) {
echo json_encode(['success' => false, 'message' => 'Estado inválido']);
exit;
}
$stmt = $pdo->prepare("UPDATE pedidos SET estado = ? WHERE id = ?");
$result = $stmt->execute([$estado, $pedido_id]);
if ($result) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Error al actualizar en la base de datos']);
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Error de base de datos: ' . $e->getMessage()]);
}
} else {
echo json_encode(['success' => false, 'message' => 'Método no permitido']);
}