feat(tiempos): agregar tiempo total puerta a puerta en historial y dashboard

- APIs: total_min = creado_at → fin_lugar_at (solo finalizados)
- Dashboard: KPI card 'Total prom. puerta a puerta' + columna Total en tabla
- Historial: columna Total en tabla

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-30 18:14:13 -05:00
co-authored by Claude Sonnet 4.6
parent c357f18b61
commit 44b737c253
4 changed files with 24 additions and 5 deletions
+11 -2
View File
@@ -73,7 +73,15 @@ $stmtRes = $pdo->prepare(
THEN TIMESTAMPDIFF(SECOND, inicio_lugar_at, fin_lugar_at) / 60.0
END
), 1
) AS tiempo_servicio_promedio_min
) AS tiempo_servicio_promedio_min,
ROUND(
AVG(
CASE
WHEN fin_lugar_at IS NOT NULL AND creado_at IS NOT NULL
THEN TIMESTAMPDIFF(SECOND, creado_at, fin_lugar_at) / 60.0
END
), 1
) AS tiempo_total_promedio_min
FROM turnero_turnos
WHERE sesion_id = ?"
);
@@ -133,7 +141,8 @@ $stmtTurnos = $pdo->prepare(
ul.full_name AS atendido_lugar_nombre,
ts.observaciones AS notas,
ROUND(TIMESTAMPDIFF(SECOND, t.creado_at, COALESCE(t.inicio_recepcion_at, NOW())) / 60.0, 1) AS espera_min,
ROUND(TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, COALESCE(t.fin_lugar_at, NOW())) / 60.0, 1) AS servicio_min
ROUND(TIMESTAMPDIFF(SECOND, t.inicio_lugar_at, COALESCE(t.fin_lugar_at, NOW())) / 60.0, 1) AS servicio_min,
ROUND(TIMESTAMPDIFF(SECOND, t.creado_at, t.fin_lugar_at) / 60.0, 1) AS total_min
FROM turnero_turnos t
JOIN turnero_prioridades p ON p.id = t.prioridad_id
LEFT JOIN turnero_lugares l ON l.id = t.lugar_destino_id
+3 -1
View File
@@ -116,7 +116,9 @@ $sqlTurnos = "
AS espera_min,
ROUND(TIMESTAMPDIFF(SECOND, t.inicio_lugar_at,
COALESCE(t.fin_lugar_at, NOW())) / 60.0, 1)
AS servicio_min
AS servicio_min,
ROUND(TIMESTAMPDIFF(SECOND, t.creado_at, t.fin_lugar_at) / 60.0, 1)
AS total_min
FROM turnero_turnos t
JOIN turnero_sesiones s ON s.id = t.sesion_id
JOIN turnero_prioridades p ON p.id = t.prioridad_id
+6 -1
View File
@@ -240,6 +240,7 @@ try {
<th>Estado</th>
<th>Espera</th>
<th>Serv.</th>
<th>Total</th>
<th></th>
</tr>
</thead>
@@ -344,6 +345,7 @@ function renderDashboard(json) {
const cancelados= parseInt(resumen.cancelados || 0);
const tEspera = resumen.tiempo_espera_promedio_min;
const tServicio = resumen.tiempo_servicio_promedio_min;
const tTotal = resumen.tiempo_total_promedio_min;
const pct = total > 0 ? Math.round(atendidos / total * 100) : 0;
document.getElementById('kpiGrid').innerHTML = `
@@ -352,8 +354,9 @@ function renderDashboard(json) {
${kpiCard(enEspera, '⏳ En espera', '')}
${kpiCard(ausentes, '🚫 Ausentes', '')}
${kpiCard(cancelados,'❌ Cancelados', '')}
${kpiCard(tEspera !== null ? tEspera + ' min' : '—', '⏱ Espera prom.', 'hasta recepción')}
${kpiCard(tEspera !== null ? tEspera + ' min' : '—', '⏱ Espera prom.', 'hasta recepción')}
${kpiCard(tServicio !== null ? tServicio + ' min' : '—', '⏱ Servicio prom.', 'en lugar')}
${kpiCard(tTotal !== null ? tTotal + ' min' : '—', '⏱ Total prom.', 'puerta a puerta')}
`;
// ── Consentimientos ────────────────────────────────────
@@ -451,6 +454,7 @@ function renderTabla(turnos) {
const estadoLbl = labelEstado[t.estado] || t.estado;
const espMin = t.espera_min !== null ? t.espera_min + ' m' : '—';
const srvMin = t.servicio_min !== null ? t.servicio_min + ' m' : '—';
const totMin = t.total_min !== null ? t.total_min + ' m' : '—';
const rowId = `dash-det-${t.id}`;
// Recepción
@@ -478,6 +482,7 @@ function renderTabla(turnos) {
<td><span class="estado-badge ${estadoCls}">${estadoLbl}</span></td>
<td>${espMin}</td>
<td>${srvMin}</td>
<td>${totMin}</td>
<td><i class="fas fa-chevron-right" style="font-size:.65rem;color:#94a3b8;transition:transform .2s" id="ico-${t.id}"></i></td>
</tr>
<tr id="${rowId}" class="det-row" style="display:none">
+4 -1
View File
@@ -214,6 +214,7 @@ Layout::open('Historial de Turnos', 'fas fa-history');
<th>Estado</th>
<th>Espera</th>
<th>Servicio</th>
<th>Total</th>
<th></th>
</tr>
</thead>
@@ -407,7 +408,7 @@ const ESTADO_LBL = {
function renderTabla(turnos) {
const tbody = document.getElementById('tbody');
if (!turnos.length) {
tbody.innerHTML = `<tr><td colspan="11">
tbody.innerHTML = `<tr><td colspan="12">
<div class="empty-state"><i class="fas fa-search"></i>Sin resultados para los filtros aplicados</div>
</td></tr>`;
return;
@@ -425,6 +426,7 @@ function renderTabla(turnos) {
: '—';
const espMin = t.espera_min != null ? t.espera_min + ' m' : '—';
const srvMin = t.servicio_min != null ? t.servicio_min + ' m' : '—';
const totMin = t.total_min != null ? t.total_min + ' m' : '—';
const tel = esc(t.paciente_cel || '—');
const priCls = esc(t.prioridad_color || '#888');
const cls = ESTADO_CLS[t.estado] || 'eb-finalizado';
@@ -467,6 +469,7 @@ function renderTabla(turnos) {
<td><span class="eb ${cls}">${lbl}</span></td>
<td class="text-nowrap">${espMin}</td>
<td class="text-nowrap">${srvMin}</td>
<td class="text-nowrap">${totMin}</td>
<td class="text-nowrap">
<button class="btn btn-sm btn-outline-secondary py-0 px-2 me-1"
onclick="toggleDetalle('${rowId}', this, ${t.id})"