Historial turnero: desglose de tomas de muestra y modal para documentos firmados

Muestra el detalle de cada toma (tipo, quien la recibió, tiempo) cuando un
turno tuvo varias, para explicar tiempos de servicio prolongados. Los
documentos firmados ahora se abren en un modal en vez de nueva pestaña,
preservando los filtros de búsqueda activos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-03 11:44:31 -05:00
co-authored by Claude Sonnet 4.6
parent 397dc208a0
commit 0b8418ec76
2 changed files with 79 additions and 4 deletions
+23 -1
View File
@@ -139,11 +139,12 @@ $stmtTurnos = $pdo->prepare($sqlTurnos);
$stmtTurnos->execute($bindsPage);
$turnos = $stmtTurnos->fetchAll(PDO::FETCH_ASSOC);
// ── Enriquecer con exámenes, consentimientos y comentarios (batch) ─
// ── Enriquecer con exámenes, consentimientos, comentarios y tomas (batch) ─
$turnoIds = array_column($turnos, 'id');
$examenes = [];
$consentimientos = [];
$comentarios = [];
$tomas = [];
if ($turnoIds) {
$ph = implode(',', array_fill(0, count($turnoIds), '?'));
@@ -193,6 +194,26 @@ if ($turnoIds) {
$comentarios[$tid][] = $row;
}
}
// Tomas de muestra — batch (una solicitud por turno, varias tomas por solicitud)
$stmtTom = $pdo->prepare(
"SELECT ts.turno_id, tm.tipo_muestra, tm.estado, tm.creado_at, tm.recibida_at,
ar.full_name AS recibida_por_nombre
FROM turnero_muestras tm
JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id
LEFT JOIN admin_users ar ON ar.id = tm.recibida_por
WHERE ts.turno_id IN ($ph)
ORDER BY tm.creado_at ASC"
);
$stmtTom->execute($turnoIds);
foreach ($stmtTom->fetchAll(PDO::FETCH_ASSOC) as $row) {
$tid = (int)$row['turno_id'];
unset($row['turno_id']);
$row['tiempo_min'] = ($row['creado_at'] && $row['recibida_at'])
? round((strtotime($row['recibida_at']) - strtotime($row['creado_at'])) / 60, 1)
: null;
$tomas[$tid][] = $row;
}
}
foreach ($turnos as &$turno) {
@@ -200,6 +221,7 @@ foreach ($turnos as &$turno) {
$turno['examenes'] = $examenes[$tid] ?? [];
$turno['consentimientos'] = $consentimientos[$tid] ?? [];
$turno['comentarios'] = $comentarios[$tid] ?? [];
$turno['tomas'] = $tomas[$tid] ?? [];
}
// ── Resumen por estado (del rango filtrado) ───────────────────
+56 -3
View File
@@ -126,6 +126,18 @@ Layout::open('Historial de Turnos', 'fas fa-history');
.estado-modal select { width:100%; padding:8px 10px; border:1px solid #e2e8f0;
border-radius:8px; font-size:.85rem; margin-bottom:16px; }
.estado-modal .actions { display:flex; gap:8px; justify-content:flex-end; }
/* ── Modal documento firmado ── */
.doc-modal-backdrop {
position:fixed; inset:0; background:rgba(0,0,0,.55); z-index:1060;
display:flex; align-items:center; justify-content:center; padding:20px;
}
.doc-modal { background:#fff; border-radius:12px; width:100%; max-width:820px; height:88vh;
display:flex; flex-direction:column; overflow:hidden; box-shadow:0 20px 60px rgba(0,0,0,.25); }
.doc-modal-head { display:flex; align-items:center; gap:10px; padding:12px 16px; border-bottom:1px solid #e2e8f0; }
.doc-modal-head h5 { font-size:.9rem; font-weight:700; margin:0; flex:1; }
.doc-modal-head button { border:none; background:none; font-size:1.1rem; color:#64748b; cursor:pointer; }
.doc-modal iframe { flex:1; border:none; width:100%; }
</style>
<!-- ── Encabezado ────────────────────────────────────────────── -->
@@ -521,6 +533,25 @@ function renderDetalle(t) {
</div>`;
}
// Tomas de muestra (solo si hay más de una — turno prolongado por varias tomas)
let tomasHtml = '';
if (t.tomas && t.tomas.length > 1) {
tomasHtml = `<div class="detail-extra">
<div class="d-lbl"><i class="fas fa-tint me-1 text-danger"></i>Tomas de muestra (${t.tomas.length})</div>
<table class="table table-sm mb-0" style="font-size:.78rem">
<thead><tr><th>Muestra</th><th>Recibió</th><th>Recibida</th><th>Tiempo</th></tr></thead>
<tbody>
${t.tomas.map(m => `<tr>
<td>${esc(m.tipo_muestra)}</td>
<td>${esc(m.recibida_por_nombre || '—')}</td>
<td>${m.recibida_at ? new Date(m.recibida_at.replace(' ','T')).toLocaleTimeString('es-CO',{hour:'2-digit',minute:'2-digit'}) : '—'}</td>
<td>${m.tiempo_min != null ? m.tiempo_min + ' m' : '—'}</td>
</tr>`).join('')}
</tbody>
</table>
</div>`;
}
// Comentarios
let comHtml = '';
if (t.comentarios && t.comentarios.length) {
@@ -543,7 +574,7 @@ function renderDetalle(t) {
<div class="d-val">${it.val}</div>
</div>`).join('')}
</div>
${examHtml}${comHtml}
${examHtml}${tomasHtml}${comHtml}
<div class="docs-section" id="docs-${t.id}">
<div class="docs-lbl"><i class="fas fa-file-signature me-1"></i>Documentos</div>
<span class="text-muted small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando…</span>
@@ -586,9 +617,8 @@ async function cargarDocumentosTurno(turnoId, container) {
const nombre = esc(c.formulario_nombre || 'Documento');
if (est === 'firmado' && c.token) {
const url = BASE_URL_ROOT + 'ver_formulario_enviado.php?token=' + encodeURIComponent(c.token);
return `<a class="doc-pill firmado" href="${url}" target="_blank">
return `<a class="doc-pill firmado" href="${url}" onclick="return abrirDocModal(event,'${url}','${nombre.replace(/'/g,"\\'")}')">
<i class="fas fa-check-circle"></i>${nombre}
<i class="fas fa-external-link-alt ms-1" style="font-size:.6rem;opacity:.6"></i>
</a>`;
}
const cls = est === 'enviado' || est === 'visto' ? 'enviado' : 'pendiente';
@@ -746,6 +776,29 @@ async function confirmarCambioEstado() {
}
}
// ── Modal documento firmado ────────────────────────────────────
function abrirDocModal(ev, url, nombre) {
ev.preventDefault();
const backdrop = document.createElement('div');
backdrop.className = 'doc-modal-backdrop';
backdrop.id = 'docModalBackdrop';
backdrop.innerHTML = `
<div class="doc-modal" onclick="event.stopPropagation()">
<div class="doc-modal-head">
<h5><i class="fas fa-file-signature me-2 text-success"></i>${esc(nombre)}</h5>
<a href="${url}" target="_blank" title="Abrir en pestaña nueva" class="me-2"><i class="fas fa-external-link-alt"></i></a>
<button onclick="cerrarDocModal()" title="Cerrar"><i class="fas fa-times"></i></button>
</div>
<iframe src="${url}"></iframe>
</div>`;
backdrop.addEventListener('click', cerrarDocModal);
document.body.appendChild(backdrop);
return false;
}
function cerrarDocModal() {
document.getElementById('docModalBackdrop')?.remove();
}
// ── Helpers visuales ──────────────────────────────────────────
function mostrarSpinner() {
document.getElementById('tbody').innerHTML =