Facturación del día: finalizados vs en proceso, en vez de extrapolación lineal
La proyección estimaba el cierre del día multiplicando el ritmo promedio por
las horas que faltaban hasta un cierre fijo a las 17:00. Eso sobreestimaba
(el laboratorio concentra las tomas en la mañana por el ayuno), dependía de
una hora de cierre escrita a mano, y en fechas pasadas usaba date('G') —la
hora actual real— así que proyectaba facturación futura sobre días ya
cerrados.
Ahora son dos cifras concretas, sin estimar: Facturado = turnos finalizados;
En proceso = turnos aún activos, cuyo total_cobrado ya está registrado pero
no han cerrado. Ausentes y cancelados quedan fuera de ambos. La gráfica por
hora usa la misma separación.
Hoy: $22.779.145 en 92 finalizados + $667.000 en 2 en proceso (antes
proyectaba $28.740.490).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d448ccc707
commit
d79d33db36
@@ -1,7 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* GET get_proyeccion.php
|
||||
* Facturación real por hora + proyección lineal para el resto del día.
|
||||
* Facturación del día por hora, separando lo ya cerrado de lo que sigue en curso.
|
||||
*
|
||||
* Facturado → turnos finalizados (plata efectivamente cobrada)
|
||||
* En proceso → turnos aún activos (espera / recepción / servicio); su
|
||||
* total_cobrado ya está registrado pero el turno no ha cerrado.
|
||||
*
|
||||
* Los turnos ausentes o cancelados quedan fuera de ambos: no se van a cobrar.
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireTurnero();
|
||||
@@ -16,50 +22,66 @@ $ses->execute([$hoy]);
|
||||
$sesion = $ses->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$sesion) {
|
||||
jsonOk(['labels'=>[],'real'=>[],'proyectado'=>[],'total_real'=>0,'total_proyectado'=>0]);
|
||||
jsonOk(['labels'=>[],'real'=>[],'proyectado'=>[],'total_real'=>0,'total_proyectado'=>0,
|
||||
'turnos_finalizados'=>0,'turnos_en_proceso'=>0,'total_en_proceso'=>0]);
|
||||
}
|
||||
$sesId = (int)$sesion['id'];
|
||||
|
||||
// Facturación por hora real
|
||||
const ESTADOS_EN_PROCESO = ['espera', 'en_recepcion', 'en_espera_lugar', 'en_servicio'];
|
||||
|
||||
// Facturación por hora, separada por estado del turno
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT HOUR(t.creado_at) AS hora,
|
||||
t.estado,
|
||||
COALESCE(SUM(ts.total_cobrado), 0) AS total,
|
||||
COUNT(DISTINCT t.id) AS turnos
|
||||
FROM turnero_turnos t
|
||||
JOIN turnero_solicitudes ts ON ts.turno_id = t.id
|
||||
WHERE t.sesion_id = ? AND ts.total_cobrado > 0
|
||||
GROUP BY hora ORDER BY hora"
|
||||
GROUP BY hora, t.estado ORDER BY hora"
|
||||
);
|
||||
$stmt->execute([$sesId]);
|
||||
$porHora = [];
|
||||
|
||||
$finPorHora = $procPorHora = [];
|
||||
$totalReal = $totalProceso = 0.0;
|
||||
$nFin = $nProc = 0;
|
||||
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$porHora[(int)$r['hora']] = ['total' => (float)$r['total'], 'turnos' => (int)$r['turnos']];
|
||||
$h = (int)$r['hora'];
|
||||
$monto = (float)$r['total'];
|
||||
$turnos = (int)$r['turnos'];
|
||||
|
||||
if ($r['estado'] === 'finalizado') {
|
||||
$finPorHora[$h] = ($finPorHora[$h] ?? 0) + $monto;
|
||||
$totalReal += $monto;
|
||||
$nFin += $turnos;
|
||||
} elseif (in_array($r['estado'], ESTADOS_EN_PROCESO, true)) {
|
||||
$procPorHora[$h] = ($procPorHora[$h] ?? 0) + $monto;
|
||||
$totalProceso += $monto;
|
||||
$nProc += $turnos;
|
||||
}
|
||||
// ausente / cancelado: se ignoran, no representan ingreso
|
||||
}
|
||||
|
||||
$inicioH = $sesion['inicio_at'] ? (int)date('G', strtotime($sesion['inicio_at'])) : 6;
|
||||
$horaAct = (int)date('G');
|
||||
$finDia = 17; // 5pm cierre estimado
|
||||
|
||||
// Calcular tasa por hora basada en horas con datos
|
||||
$totalReal = array_sum(array_column($porHora, 'total'));
|
||||
$horasConDatos = max(1, $horaAct - $inicioH);
|
||||
$tasaPorHora = $totalReal / $horasConDatos;
|
||||
// Rango de horas a graficar: desde que abrió la sesión hasta la última con datos
|
||||
$inicioH = $sesion['inicio_at'] ? (int)date('G', strtotime($sesion['inicio_at'])) : 6;
|
||||
$horas = array_merge(array_keys($finPorHora), array_keys($procPorHora));
|
||||
$finH = $horas ? max($horas) : $inicioH;
|
||||
|
||||
$labels = $real = $proyectado = [];
|
||||
for ($h = $inicioH; $h <= max($finDia, $horaAct); $h++) {
|
||||
for ($h = $inicioH; $h <= $finH; $h++) {
|
||||
$labels[] = sprintf('%02d:00', $h);
|
||||
$real[] = isset($porHora[$h]) ? round($porHora[$h]['total']) : 0;
|
||||
$proyectado[] = ($h > $horaAct && $h <= $finDia) ? round($tasaPorHora) : null;
|
||||
$real[] = round($finPorHora[$h] ?? 0);
|
||||
$proyectado[] = round($procPorHora[$h] ?? 0);
|
||||
}
|
||||
|
||||
$horasRestantes = max(0, $finDia - $horaAct);
|
||||
$totalProyectado = round($totalReal + ($tasaPorHora * $horasRestantes));
|
||||
|
||||
jsonOk([
|
||||
'labels' => $labels,
|
||||
'real' => $real,
|
||||
'proyectado' => $proyectado,
|
||||
'total_real' => $totalReal,
|
||||
'total_proyectado' => $totalProyectado,
|
||||
'tasa_por_hora' => round($tasaPorHora),
|
||||
'labels' => $labels,
|
||||
'real' => $real,
|
||||
'proyectado' => $proyectado,
|
||||
'total_real' => round($totalReal),
|
||||
'total_en_proceso' => round($totalProceso),
|
||||
'total_proyectado' => round($totalReal + $totalProceso),
|
||||
'turnos_finalizados' => $nFin,
|
||||
'turnos_en_proceso' => $nProc,
|
||||
]);
|
||||
|
||||
@@ -256,7 +256,7 @@ try {
|
||||
<!-- Proyección de ventas -->
|
||||
<div class="section-card" id="sectionProyeccion" style="display:none">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="section-title mb-0"><i class="fas fa-chart-line me-1"></i>Facturación y proyección del día</div>
|
||||
<div class="section-title mb-0"><i class="fas fa-chart-line me-1"></i>Facturación del día</div>
|
||||
<span id="proyeccionResumen" class="small text-muted"></span>
|
||||
</div>
|
||||
<canvas id="chartProyeccion" height="90"></canvas>
|
||||
@@ -939,7 +939,9 @@ async function cargarProyeccion(fecha) {
|
||||
sec.style.display = '';
|
||||
const fmt = v => v == null ? null : '$' + Intl.NumberFormat('es-CO').format(v);
|
||||
document.getElementById('proyeccionResumen').textContent =
|
||||
`Real: ${fmt(d.total_real)} · Proyectado: ${fmt(d.total_proyectado)}`;
|
||||
`Facturado: ${fmt(d.total_real)} (${d.turnos_finalizados} finalizados)`
|
||||
+ ` · En proceso: ${fmt(d.total_en_proceso)} (${d.turnos_en_proceso})`
|
||||
+ ` · Total estimado: ${fmt(d.total_proyectado)}`;
|
||||
const ctx = document.getElementById('chartProyeccion').getContext('2d');
|
||||
if (_chartProy) _chartProy.destroy();
|
||||
_chartProy = new Chart(ctx, {
|
||||
@@ -954,11 +956,10 @@ async function cargarProyeccion(fecha) {
|
||||
borderRadius: 6,
|
||||
},
|
||||
{
|
||||
label: 'Proyectado',
|
||||
label: 'En proceso',
|
||||
data: d.proyectado,
|
||||
backgroundColor: 'rgba(2,136,209,.3)',
|
||||
backgroundColor: 'rgba(245,158,11,.55)',
|
||||
borderRadius: 6,
|
||||
borderDash: [4,4],
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user