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 = "";
|
||||
|
||||
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, {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
@ -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']);
|
||||
}
|
||||