Autosave: 20260518-164428
BIN
assets/uploads/vouchers/6a0b2f023dfb0-704.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 183 KiB |
BIN
assets/uploads/vouchers/6a0b35f5208d1-1215.png
Normal file
|
After Width: | Height: | Size: 426 KiB |
BIN
assets/uploads/vouchers/6a0b36b8e4044-607.png
Normal file
|
After Width: | Height: | Size: 493 KiB |
BIN
assets/uploads/vouchers/6a0b39d6978af-771.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
assets/uploads/vouchers/6a0b3b844e619-205.png
Normal file
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 339 KiB |
BIN
assets/uploads/vouchers/6a0b3e0ad66aa-0255.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
assets/uploads/vouchers/6a0b3eee68f77-018.png
Normal file
|
After Width: | Height: | Size: 324 KiB |
@ -21,39 +21,47 @@ $date_condition = "";
|
|||||||
$label_period = "";
|
$label_period = "";
|
||||||
|
|
||||||
if ($period === 'custom' && !empty($start_date) && !empty($end_date)) {
|
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));
|
$label_period = "Desde " . date('d/m/Y', strtotime($start_date)) . " hasta " . date('d/m/Y', strtotime($end_date));
|
||||||
} else {
|
} else {
|
||||||
switch ($period) {
|
switch ($period) {
|
||||||
case 'today':
|
case 'today':
|
||||||
$date_condition = "DATE(created_at) = CURDATE()";
|
$date_condition = "DATE(p.created_at) = CURDATE()";
|
||||||
$label_period = "Hoy";
|
$label_period = "Hoy";
|
||||||
break;
|
break;
|
||||||
|
case 'yesterday':
|
||||||
|
$date_condition = "DATE(p.created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)";
|
||||||
|
$label_period = "Ayer";
|
||||||
|
break;
|
||||||
case '7':
|
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";
|
$label_period = "Últimos 7 días";
|
||||||
break;
|
break;
|
||||||
|
case '15':
|
||||||
|
$date_condition = "DATE(p.created_at) >= DATE_SUB(CURDATE(), INTERVAL 15 DAY)";
|
||||||
|
$label_period = "Últimos 15 días";
|
||||||
|
break;
|
||||||
case '30':
|
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";
|
$label_period = "Últimos 30 días";
|
||||||
break;
|
break;
|
||||||
case 'month':
|
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";
|
$label_period = "Este Mes";
|
||||||
break;
|
break;
|
||||||
case 'year':
|
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";
|
$label_period = "Último Año";
|
||||||
break;
|
break;
|
||||||
default:
|
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";
|
$label_period = "Últimos 7 días";
|
||||||
$period = '7';
|
$period = '7';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Estadísticas del periodo seleccionado
|
// 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);
|
$statsPeriodo = $stmtStats->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 2. Pedidos Pendientes (Global, no depende del filtro de fecha usualmente, pero lo mantendremos así)
|
// 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)
|
// 5. Datos para Gráfico de Ventas (Ajustado al periodo)
|
||||||
$ventasTendencia = [];
|
$ventasTendencia = [];
|
||||||
if ($period === 'today') {
|
if ($period === 'today' || $period === 'yesterday') {
|
||||||
// Si es hoy, mostrar por horas
|
// Si es hoy o ayer, 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");
|
$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);
|
$res = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
foreach ($res as $r) {
|
foreach ($res as $r) {
|
||||||
$ventasTendencia[] = ['fecha' => $r['hora'] . ':00', 'cantidad' => $r['cant'], 'monto' => $r['monto']];
|
$ventasTendencia[] = ['fecha' => $r['hora'] . ':00', 'cantidad' => $r['cant'], 'monto' => $r['monto']];
|
||||||
}
|
}
|
||||||
} elseif ($period === 'year') {
|
} elseif ($period === 'year') {
|
||||||
// Si es un año, mostrar por meses
|
// 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);
|
$res = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
foreach ($res as $r) {
|
foreach ($res as $r) {
|
||||||
$ventasTendencia[] = ['fecha' => date('M Y', strtotime($r['mes'] . '-01')), 'cantidad' => $r['cant'], 'monto' => $r['monto']];
|
$ventasTendencia[] = ['fecha' => date('M Y', strtotime($r['mes'] . '-01')), 'cantidad' => $r['cant'], 'monto' => $r['monto']];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Mostrar por días
|
// 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);
|
$res = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
foreach ($res as $r) {
|
foreach ($res as $r) {
|
||||||
$ventasTendencia[] = ['fecha' => date('d/m', strtotime($r['fecha'])), 'cantidad' => $r['cant'], 'monto' => $r['monto']];
|
$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)
|
// 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);
|
$estadosData = $stmtEstados->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 7. Top Productos (Ajustado al periodo)
|
// 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);
|
$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)
|
// 8. Ventas por Asesor (Ajustado al periodo)
|
||||||
$stmtAsesores = $db->query("SELECT u.nombre_asesor,
|
$stmtAsesores = $db->query("SELECT u.nombre_asesor,
|
||||||
COUNT(p.id) as total_pedidos,
|
COUNT(p.id) as total_pedidos,
|
||||||
@ -117,56 +129,72 @@ $ventasAsesores = $stmtAsesores->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
// 9. Comparativa Mensual (Se mantiene global para contexto)
|
// 9. Comparativa Mensual (Se mantiene global para contexto)
|
||||||
$mesActual = date('Y-m');
|
$mesActual = date('Y-m');
|
||||||
$mesPasado = date('Y-m', strtotime('first day of last month'));
|
$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]);
|
$stmtMesActual->execute([$mesActual]);
|
||||||
$montoMesActual = $stmtMesActual->fetchColumn() ?: 0;
|
$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]);
|
$stmtMesPasado->execute([$mesPasado]);
|
||||||
$montoMesPasado = $stmtMesPasado->fetchColumn() ?: 0;
|
$montoMesPasado = $stmtMesPasado->fetchColumn() ?: 0;
|
||||||
$crecimientoMensual = $montoMesPasado > 0 ? (($montoMesActual - $montoMesPasado) / $montoMesPasado) * 100 : 0;
|
$crecimientoMensual = $montoMesPasado > 0 ? (($montoMesActual - $montoMesPasado) / $montoMesPasado) * 100 : 0;
|
||||||
|
|
||||||
// 10. Eficiencia Logística (Ajustado al periodo)
|
// 10. Eficiencia Logística (Ajustado al periodo)
|
||||||
$stmtTiempo = $db->query("SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, fecha_completado)) / 24 as promedio_dias
|
$stmtTiempo = $db->query("SELECT AVG(TIMESTAMPDIFF(HOUR, p.created_at, fecha_completado)) / 24 as promedio_dias
|
||||||
FROM pedidos
|
FROM pedidos p
|
||||||
WHERE $date_condition AND estado = 'COMPLETADO ✅' AND fecha_completado IS NOT NULL");
|
WHERE $date_condition AND estado = 'COMPLETADO ✅' AND fecha_completado IS NOT NULL");
|
||||||
$tiempoPromedio = $stmtTiempo->fetchColumn() ?: 0;
|
$tiempoPromedio = $stmtTiempo->fetchColumn() ?: 0;
|
||||||
|
|
||||||
$stmtRetorno = $db->query("SELECT (COUNT(CASE WHEN estado = 'RETORNADO' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0)) as tasa_retorno
|
$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;
|
$tasaRetorno = $stmtRetorno->fetchColumn() ?: 0;
|
||||||
|
|
||||||
// 11. Utilidad Total Estimada (Ajustado al periodo)
|
// 11. Utilidad Total Estimada (Ajustado al periodo)
|
||||||
$stmtUtilidad = $db->query("
|
$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
|
FROM pedidos p
|
||||||
LEFT JOIN products pr ON p.producto = pr.nombre
|
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;
|
$utilidadTotal = $stmtUtilidad->fetchColumn() ?: 0;
|
||||||
|
|
||||||
// 12. Datos Detallados por Canal (Provincia vs Contraentrega)
|
// 12. Datos Detallados por Canal (Provincia vs Contraentrega)
|
||||||
$stmtDetalleCanal = $db->query("SELECT
|
$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,
|
estado,
|
||||||
COUNT(*) as total,
|
COUNT(*) as total,
|
||||||
SUM(monto_total) as monto
|
SUM(monto_total) as monto
|
||||||
FROM pedidos
|
FROM pedidos p
|
||||||
WHERE $date_condition
|
WHERE $date_condition
|
||||||
GROUP BY canal, estado");
|
GROUP BY canal, estado");
|
||||||
$detalleCanalRaw = $stmtDetalleCanal->fetchAll(PDO::FETCH_ASSOC);
|
$detalleCanalRaw = $stmtDetalleCanal->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 13. Top Productos Detallado (Para la tabla)
|
// 13. Top Productos Detallado (Para la tabla)
|
||||||
$stmtTopDetalle = $db->query("
|
$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
|
FROM pedidos p
|
||||||
LEFT JOIN products pr ON p.producto = pr.nombre
|
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
|
GROUP BY p.producto
|
||||||
ORDER BY total_cantidad DESC
|
ORDER BY total_cantidad DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
");
|
");
|
||||||
$topProductosDetalle = $stmtTopDetalle->fetchAll(PDO::FETCH_ASSOC);
|
$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 = [
|
$canalesResumen = [
|
||||||
'Provincia' => ['pedidos' => 0, 'monto' => 0, 'estados' => []],
|
'Provincia' => ['pedidos' => 0, 'monto' => 0, 'estados' => []],
|
||||||
'Contraentrega' => ['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">
|
<form action="" method="GET" class="d-inline-block">
|
||||||
<div class="btn-group me-2" role="group">
|
<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=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=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=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=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>
|
<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>
|
||||||
</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 -->
|
<!-- Comparativa de Canales: Provincia vs Contraentrega -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<!-- Sección Provincia -->
|
<!-- Sección Provincia -->
|
||||||
@ -513,8 +584,65 @@ include 'layout_header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Ventas por Asesor -->
|
|
||||||
|
<!-- Top Productos Contraentrega -->
|
||||||
<div class="col-md-6 mb-4">
|
<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 shadow">
|
||||||
<div class="card-header bg-white">
|
<div class="card-header bg-white">
|
||||||
<h5 class="mb-0">Rendimiento por Asesor (Monto Total)</h5>
|
<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 src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<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
|
// Gráfico de Ventas
|
||||||
const ctxVentas = document.getElementById('ventasChart').getContext('2d');
|
const ctxVentas = document.getElementById('ventasChart').getContext('2d');
|
||||||
new Chart(ctxVentas, {
|
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
|
// Gráfico de Asesores
|
||||||
const ctxAsesores = document.getElementById('asesoresChart').getContext('2d');
|
const ctxAsesores = document.getElementById('asesoresChart').getContext('2d');
|
||||||
new Chart(ctxAsesores, {
|
new Chart(ctxAsesores, {
|
||||||
|
|||||||
@ -72,6 +72,40 @@ if ($user_role === 'Administrador' || $user_role === 'Logistica') {
|
|||||||
$stmt_products = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
|
$stmt_products = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
|
||||||
$products = $stmt_products->fetchAll();
|
$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
|
// Fetch Shalom branches
|
||||||
$stmt_sedes = $pdo->query("SELECT nombre_sede FROM sedes_shalom ORDER BY nombre_sede ASC");
|
$stmt_sedes = $pdo->query("SELECT nombre_sede FROM sedes_shalom ORDER BY nombre_sede ASC");
|
||||||
$sedes_shalom = $stmt_sedes->fetchAll(PDO::FETCH_COLUMN);
|
$sedes_shalom = $stmt_sedes->fetchAll(PDO::FETCH_COLUMN);
|
||||||
@ -214,26 +248,28 @@ include 'layout_header.php';
|
|||||||
<hr>
|
<hr>
|
||||||
<h5>Productos</h5>
|
<h5>Productos</h5>
|
||||||
<div id="productos-container">
|
<div id="productos-container">
|
||||||
<div class="row producto-row mb-3">
|
<?php foreach ($display_products as $index => $dp): ?>
|
||||||
<div class="col-md-6">
|
<div class="row producto-row mb-3">
|
||||||
<label for="producto" class="form-label">Producto</label>
|
<div class="col-md-6">
|
||||||
<select class="form-select" name="productos[0][nombre]" required>
|
<label for="producto" class="form-label">Producto</label>
|
||||||
<option value="">Seleccione un producto</option>
|
<select class="form-select" name="productos[<?php echo $index; ?>][nombre]" required>
|
||||||
<?php foreach ($products as $product): ?>
|
<option value="">Seleccione un producto</option>
|
||||||
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($pedido['producto'] == $product['nombre']) ? 'selected' : ''; ?>>
|
<?php foreach ($products as $product): ?>
|
||||||
<?php echo htmlspecialchars($product['nombre']); ?>
|
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($dp['nombre'] == $product['nombre']) ? 'selected' : ''; ?>>
|
||||||
</option>
|
<?php echo htmlspecialchars($product['nombre']); ?>
|
||||||
<?php endforeach; ?>
|
</option>
|
||||||
</select>
|
<?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>
|
||||||
<div class="col-md-3">
|
<?php endforeach; ?>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="add-producto-btn" class="btn btn-success btn-sm mb-3">Agregar producto adicional</button>
|
<button type="button" id="add-producto-btn" class="btn btn-success btn-sm mb-3">Agregar producto adicional</button>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@ -67,6 +67,40 @@ if ($user_role === 'Administrador') {
|
|||||||
$stmt_products = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
|
$stmt_products = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre ASC");
|
||||||
$products = $stmt_products->fetchAll();
|
$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
|
<?php
|
||||||
$pageTitle = 'Agregar Pedidos Contraentrega';
|
$pageTitle = 'Agregar Pedidos Contraentrega';
|
||||||
@ -173,26 +207,28 @@ include 'layout_header.php';
|
|||||||
<hr>
|
<hr>
|
||||||
<h5>Productos</h5>
|
<h5>Productos</h5>
|
||||||
<div id="productos-container">
|
<div id="productos-container">
|
||||||
<div class="row producto-row mb-3">
|
<?php foreach ($display_products as $index => $dp): ?>
|
||||||
<div class="col-md-6">
|
<div class="row producto-row mb-3">
|
||||||
<label for="producto" class="form-label">Producto</label>
|
<div class="col-md-6">
|
||||||
<select class="form-select" name="productos[0][nombre]" required>
|
<label for="producto" class="form-label">Producto</label>
|
||||||
<option value="">Seleccione un producto</option>
|
<select class="form-select" name="productos[<?php echo $index; ?>][nombre]" required>
|
||||||
<?php foreach ($products as $product): ?>
|
<option value="">Seleccione un producto</option>
|
||||||
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($pedido['producto'] == $product['nombre']) ? 'selected' : ''; ?>>
|
<?php foreach ($products as $product): ?>
|
||||||
<?php echo htmlspecialchars($product['nombre']); ?>
|
<option value="<?php echo htmlspecialchars($product['nombre']); ?>" <?php echo ($dp['nombre'] == $product['nombre']) ? 'selected' : ''; ?>>
|
||||||
</option>
|
<?php echo htmlspecialchars($product['nombre']); ?>
|
||||||
<?php endforeach; ?>
|
</option>
|
||||||
</select>
|
<?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>
|
||||||
<div class="col-md-3">
|
<?php endforeach; ?>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="add-producto-btn" class="btn btn-success btn-sm mb-3">Agregar producto adicional</button>
|
<button type="button" id="add-producto-btn" class="btn btn-success btn-sm mb-3">Agregar producto adicional</button>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@ -266,7 +266,11 @@ include 'layout_header.php';
|
|||||||
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
|
<td><?php echo htmlspecialchars($pedido['monto_total']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
|
<td><?php echo htmlspecialchars($pedido['monto_debe']); ?></td>
|
||||||
<?php endif; ?>
|
<?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>
|
<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; ?>
|
<?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>
|
<td><?php echo htmlspecialchars($pedido['created_at']); ?></td>
|
||||||
@ -300,6 +304,42 @@ function getPaqueteStyleJS(paquete) {
|
|||||||
return 'background-color: #6c757d; color: white;';
|
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() {
|
$(document).ready(function() {
|
||||||
$('#pedidos-table').DataTable({
|
$('#pedidos-table').DataTable({
|
||||||
"language": {
|
"language": {
|
||||||
@ -394,6 +434,79 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
select.addEventListener('blur', save);
|
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')) {
|
if (e.target && e.target.classList.contains('editable')) {
|
||||||
const cell = e.target;
|
const cell = e.target;
|
||||||
if (cell.querySelector('input')) {
|
if (cell.querySelector('input')) {
|
||||||
|
|||||||
43
update_estado.php
Normal 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']);
|
||||||
|
}
|
||||||