From 487673dc0f55869470ee2ed0a7ed0a58a67e969c Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:33:26 -0500 Subject: [PATCH] =?UTF-8?q?Historial=20y=20bandeja:=20mostrar=20qu=C3=A9?= =?UTF-8?q?=20pas=C3=B3=20con=20cada=20turno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Para saber quién retiró un formulario de un turno había que consultar la base a mano. La información existía, pero repartida en cinco tablas y sin ninguna pantalla que la juntara. get_traza_turno.php arma una sola línea de tiempo con el recorrido del turno (con quién atendió en cada punto), los documentos enviados y firmados, los comentarios del personal —donde caen las ausencias con su motivo—, las muestras recibidas y rechazadas, y las acciones administrativas auditadas. Se muestra en el detalle desplegable de Historial y en el panel de Bandeja. Del chat de WhatsApp solo se indica cuántos mensajes hubo ese día, con enlace a la conversación. Volcar los mensajes sería esparcir datos personales del paciente por una pantalla de consulta. Y la parte que faltaba: cinco acciones no dejaban ningún rastro. Ahora se auditan cambiar_estado, resetear_consentimiento, resetear_toma, vincular_paciente y cancelar_toma_pendiente — las que borran una firma, descartan datos de un protocolo o mandan la atención a otra historia clínica. La auditoría va en try/catch: si falla, no tumba la operación. La respuesta declara en `sinRastro` lo que el sistema todavía no registra, para que la ausencia de un evento no se lea como prueba de que no ocurrió. Co-Authored-By: Claude Opus 5 --- modules/turnero/api/cambiar_estado.php | 22 ++ .../turnero/api/cancelar_toma_pendiente.php | 32 +++ modules/turnero/api/get_traza_turno.php | 205 ++++++++++++++++++ .../turnero/api/resetear_consentimiento.php | 28 ++- modules/turnero/api/resetear_toma.php | 27 ++- modules/turnero/api/vincular_paciente.php | 19 ++ modules/turnero/views/bandeja.php | 48 +++- modules/turnero/views/historial.php | 80 +++++++ 8 files changed, 456 insertions(+), 5 deletions(-) create mode 100644 modules/turnero/api/get_traza_turno.php diff --git a/modules/turnero/api/cambiar_estado.php b/modules/turnero/api/cambiar_estado.php index e6dd699..c30d9f4 100644 --- a/modules/turnero/api/cambiar_estado.php +++ b/modules/turnero/api/cambiar_estado.php @@ -22,6 +22,7 @@ */ require_once __DIR__ . '/_helpers.php'; +require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php'; requireMethod('POST'); requireTurnero(); @@ -233,6 +234,27 @@ try { notificarSSE((int) $turnoActualizado['sesion_id']); + // Queda constancia de quién cambió el estado a mano. Sin esto, en el + // historial aparecía un turno cancelado o ausente sin ninguna huella + // de quién lo hizo ni desde qué estado. + try { + (new ActividadAdmin())->registrar( + adminId(), + 'turnero', + 'cambiar_estado', + $turnoId, + [ + 'turno_id' => $turnoId, + 'turno_codigo' => $turnoActualizado['codigo'] ?? null, + 'estado_previo' => $estadoActual, + 'estado_nuevo' => $nuevoEstado, + ] + ); + } catch (\Throwable $e) { + // La auditoría nunca debe tumbar la operación + error_log('[cambiar_estado] auditoría: ' . $e->getMessage()); + } + jsonOk(['turno' => $turnoActualizado], "Estado actualizado a '{$nuevoEstado}'"); } catch (\Throwable $e) { diff --git a/modules/turnero/api/cancelar_toma_pendiente.php b/modules/turnero/api/cancelar_toma_pendiente.php index 9fecc0c..0c6fe17 100644 --- a/modules/turnero/api/cancelar_toma_pendiente.php +++ b/modules/turnero/api/cancelar_toma_pendiente.php @@ -5,6 +5,7 @@ * Body: { consentimiento_id } */ require_once __DIR__ . '/_helpers.php'; +require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php'; requireMethod('POST'); requireTurnero(); @@ -30,4 +31,35 @@ $stmt->execute([$turnoId, $consentId]); if ($stmt->rowCount() === 0) jsonError('Toma no encontrada o el paciente no coincide.', 404); +// Cancelar una toma pendiente cierra un protocolo que quedó a medias en otra +// visita: conviene saber quién lo dio por terminado. +try { + $inf = $pdo->prepare( + "SELECT tc.turno_id, t.codigo, f.nombre AS formulario + FROM turnero_consentimientos tc + JOIN turnero_turnos t ON t.id = tc.turno_id + JOIN lab_formularios f ON f.id = tc.formulario_id + WHERE tc.id = ? LIMIT 1" + ); + $inf->execute([$consentId]); + $d = $inf->fetch(PDO::FETCH_ASSOC) ?: []; + + (new ActividadAdmin())->registrar( + adminId(), + 'turnero', + 'cancelar_toma_pendiente', + $consentId, + [ + // Se registra bajo el turno donde se hizo la cancelación, que es + // donde alguien la va a buscar en el historial. + 'turno_id' => $turnoId, + 'turno_origen_id' => isset($d['turno_id']) ? (int)$d['turno_id'] : null, + 'turno_origen' => $d['codigo'] ?? null, + 'formulario_nombre' => $d['formulario'] ?? null, + ] + ); +} catch (\Throwable $e) { + error_log('[cancelar_toma_pendiente] auditoría: ' . $e->getMessage()); +} + jsonOk(['cancelado' => true]); diff --git a/modules/turnero/api/get_traza_turno.php b/modules/turnero/api/get_traza_turno.php new file mode 100644 index 0000000..97ce3a3 --- /dev/null +++ b/modules/turnero/api/get_traza_turno.php @@ -0,0 +1,205 @@ +prepare( + "SELECT t.*, l.nombre AS lugar_nombre, d.nombre AS desk_nombre, + ur.full_name AS recepcionista, ul.full_name AS profesional + FROM turnero_turnos t + LEFT JOIN turnero_lugares l ON l.id = t.lugar_destino_id + LEFT JOIN turnero_lugares d ON d.id = t.recepcion_desk_id + LEFT JOIN admin_users ur ON ur.id = t.atendido_recepcion_por + LEFT JOIN admin_users ul ON ul.id = t.atendido_lugar_por + WHERE t.id = ? LIMIT 1" +); +$t->execute([$turnoId]); +$turno = $t->fetch(PDO::FETCH_ASSOC); +if (!$turno) jsonError('Turno no encontrado', 404); + +$ev = []; +$add = function ($cuando, $tipo, $texto, $quien = null, $extra = null) use (&$ev) { + if (!$cuando) return; + $ev[] = [ + 'cuando' => $cuando, + 'tipo' => $tipo, // la vista lo usa para el ícono y el color + 'texto' => $texto, + 'quien' => $quien ?: null, + 'extra' => $extra ?: null, + ]; +}; + +// ── 1. Recorrido del turno ──────────────────────────────────────────────── +$add($turno['creado_at'], 'turno', 'Turno creado'); +$add($turno['llamado_recepcion_at'], 'llamado', 'Llamado a ' . ($turno['desk_nombre'] ?: 'recepción')); +$add($turno['inicio_recepcion_at'], 'atencion', 'Inicia atención en recepción', $turno['recepcionista']); +$add($turno['fin_recepcion_at'], 'atencion', 'Termina recepción'); +$add($turno['llamado_lugar_at'], 'llamado', 'Llamado a ' . ($turno['lugar_nombre'] ?: 'toma de muestras')); +$add($turno['inicio_lugar_at'], 'atencion', 'Inicia toma de muestras', $turno['profesional']); +$add($turno['muestra_espera_at'], 'muestra', 'Pasa a espera de muestra'); +$add($turno['fin_lugar_at'], 'fin', 'Finaliza la atención'); + +// ── 2. Documentos ───────────────────────────────────────────────────────── +$q = $pdo->prepare( + "SELECT tc.enviado_at, tc.firmado_at, tc.firmado_profesional_at, tc.estado, + f.nombre AS formulario + FROM turnero_consentimientos tc + JOIN lab_formularios f ON f.id = tc.formulario_id + WHERE tc.turno_id = ?" +); +$q->execute([$turnoId]); +foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $c) { + $add($c['enviado_at'], 'doc', 'Enviado: ' . $c['formulario']); + $add($c['firmado_at'], 'doc_ok', 'Firmado por el paciente: ' . $c['formulario']); + $add($c['firmado_profesional_at'], 'doc_ok', 'Firmado por el profesional: ' . $c['formulario']); +} + +// ── 3. Comentarios del personal ─────────────────────────────────────────── +// Aquí caen también las ausencias, que se guardan como comentario con su motivo. +$q = $pdo->prepare( + "SELECT creado_at, usuario_nombre, comentario, tipo + FROM turnero_comentarios WHERE turno_id = ? ORDER BY creado_at" +); +$q->execute([$turnoId]); +foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $c) { + $add($c['creado_at'], 'comentario', $c['comentario'], $c['usuario_nombre'], $c['tipo']); +} + +// ── 4. Muestras ─────────────────────────────────────────────────────────── +$q = $pdo->prepare( + "SELECT m.recibida_at, m.tipo_muestra, m.estado, m.motivo_rechazo, + u.full_name AS quien + FROM turnero_muestras m + LEFT JOIN admin_users u ON u.id = m.recibida_por + WHERE m.recibida_en_turno_id = ?" +); +$q->execute([$turnoId]); +foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $m) { + $texto = $m['estado'] === 'rechazada' + ? 'Muestra RECHAZADA (' . $m['tipo_muestra'] . '): ' . ($m['motivo_rechazo'] ?: 'sin motivo') + : 'Muestra recibida (' . $m['tipo_muestra'] . ')'; + $add($m['recibida_at'], $m['estado'] === 'rechazada' ? 'alerta' : 'muestra', $texto, $m['quien']); +} + +// ── 5. Acciones administrativas auditadas ───────────────────────────────── +// El detalle es JSON; se filtra por turno en PHP para no depender de LIKE. +$q = $pdo->prepare( + "SELECT created_at, admin_nombre, accion, detalle + FROM lab_actividad_admin + WHERE modulo IN ('turnero','turnero_config') AND detalle LIKE ?" +); +$q->execute(['%"turno_id":' . $turnoId . '%']); +foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $a) { + $d = json_decode($a['detalle'], true) ?: []; + if ((int)($d['turno_id'] ?? 0) !== $turnoId) continue; // evita coincidencias por prefijo + + // Cada acción se traduce a algo que se entienda leyéndolo, no al nombre + // técnico del endpoint. + switch ($a['accion']) { + case 'quitar_consentimiento': + $add($a['created_at'], 'alerta', + 'Formulario RETIRADO: ' . ($d['formulario_nombre'] ?? '?'), + $a['admin_nombre'], + 'Motivo: ' . ($d['motivo'] ?? 'sin motivo') + . ' · estaba en «' . ($d['estado_previo'] ?? '?') . '»'); + break; + + case 'cambiar_estado': + $add($a['created_at'], 'admin', + 'Estado cambiado a mano: ' . ($d['estado_previo'] ?? '?') + . ' → ' . ($d['estado_nuevo'] ?? '?'), + $a['admin_nombre']); + break; + + case 'resetear_consentimiento': + $add($a['created_at'], 'alerta', + 'Formulario REINICIADO: ' . ($d['formulario_nombre'] ?? '?'), + $a['admin_nombre'], + !empty($d['tenia_firma']) + ? 'Se descartó una firma ya hecha' + : 'No tenía firma todavía'); + break; + + case 'resetear_toma': + $n = is_array($d['campos_olvidados'] ?? null) ? count($d['campos_olvidados']) : 0; + $add($a['created_at'], 'alerta', + 'Toma olvidada en ' . ($d['formulario_nombre'] ?? 'protocolo'), + $a['admin_nombre'], + $n ? "Se descartaron $n dato(s) de la toma" : null); + break; + + case 'cancelar_toma_pendiente': + $add($a['created_at'], 'alerta', + 'Cancelada la toma pendiente de ' . ($d['turno_origen'] ?? 'otra visita'), + $a['admin_nombre'], + $d['formulario_nombre'] ?? null); + break; + + case 'vincular_paciente': + $add($a['created_at'], 'admin', + 'Paciente vinculado: ' . ($d['paciente_nombre'] ?? '#' . ($d['paciente_id'] ?? '?')), + $a['admin_nombre']); + break; + + default: + $add($a['created_at'], 'admin', $a['accion'], $a['admin_nombre']); + } +} + +usort($ev, fn($a, $b) => strcmp($a['cuando'], $b['cuando'])); + +// ── 6. Conversación de WhatsApp ─────────────────────────────────────────── +// Solo el conteo del día y el enlace al chat: volcar los mensajes aquí sería +// esparcir datos personales del paciente por una pantalla de consulta. +$chat = null; +if (!empty($turno['paciente_id'])) { + $q = $pdo->prepare( + "SELECT u.id AS user_id, + (SELECT COUNT(*) FROM conversations c + WHERE c.user_id = u.id AND DATE(c.created_at) = DATE(?)) AS mensajes + FROM lab_pacientes p JOIN users u ON u.id = p.user_id + WHERE p.id = ? LIMIT 1" + ); + $q->execute([$turno['creado_at'], (int)$turno['paciente_id']]); + $r = $q->fetch(PDO::FETCH_ASSOC); + if ($r && (int)$r['mensajes'] > 0) { + $chat = ['user_id' => (int)$r['user_id'], 'mensajes' => (int)$r['mensajes']]; + } +} + +jsonOk([ + 'turno' => [ + 'id' => (int)$turno['id'], + 'codigo' => $turno['codigo'], + 'estado' => $turno['estado'], + 'paciente' => $turno['paciente_nombre'], + ], + 'eventos' => $ev, + 'chat' => $chat, + // Se declara lo que el sistema NO registra, para que nadie lea la ausencia + // de un evento como prueba de que no ocurrió. + // Lo que el sistema sigue sin registrar. Se declara para que la ausencia de + // un evento no se lea como prueba de que no ocurrió. + 'sinRastro' => [ + 'rellamados', + 'quién guardó cada toma de un protocolo', + ], +]); diff --git a/modules/turnero/api/resetear_consentimiento.php b/modules/turnero/api/resetear_consentimiento.php index fd1e223..4fbcd59 100644 --- a/modules/turnero/api/resetear_consentimiento.php +++ b/modules/turnero/api/resetear_consentimiento.php @@ -5,6 +5,7 @@ * Body JSON: { token: string } */ require_once __DIR__ . '/_helpers.php'; +require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php'; requireMethod('POST'); requireTurnero(); @@ -14,9 +15,11 @@ if (!$token) jsonError('token requerido.'); $pdo = db(); $stmt = $pdo->prepare( - "SELECT tc.id, t.sesion_id + "SELECT tc.id, tc.turno_id, tc.estado, tc.firmado_at, t.sesion_id, t.codigo, + f.nombre AS formulario FROM turnero_consentimientos tc - JOIN turnero_turnos t ON t.id = tc.turno_id + JOIN turnero_turnos t ON t.id = tc.turno_id + JOIN lab_formularios f ON f.id = tc.formulario_id WHERE tc.token = ? LIMIT 1" ); $stmt->execute([$token]); @@ -34,4 +37,25 @@ $pdo->prepare( )->execute([$token]); notificarSSE((int)$tc['sesion_id']); + +// Esta acción descarta una firma ya hecha, así que tiene que dejar constancia: +// se anota qué documento era, en qué estado estaba y si tenía firma. +try { + (new ActividadAdmin())->registrar( + adminId(), + 'turnero', + 'resetear_consentimiento', + (int)$tc['id'], + [ + 'turno_id' => (int)$tc['turno_id'], + 'turno_codigo' => $tc['codigo'], + 'formulario_nombre' => $tc['formulario'], + 'estado_previo' => $tc['estado'], + 'tenia_firma' => !empty($tc['firmado_at']), + ] + ); +} catch (\Throwable $e) { + error_log('[resetear_consentimiento] auditoría: ' . $e->getMessage()); +} + jsonOk([], 'Consentimiento reiniciado correctamente.'); diff --git a/modules/turnero/api/resetear_toma.php b/modules/turnero/api/resetear_toma.php index 41603de..99a5d71 100644 --- a/modules/turnero/api/resetear_toma.php +++ b/modules/turnero/api/resetear_toma.php @@ -5,6 +5,7 @@ * Body JSON: { token: string, campos: string[] } */ require_once __DIR__ . '/_helpers.php'; +require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php'; requireMethod('POST'); requireTurnero(); @@ -17,9 +18,11 @@ if (!is_array($campos) || empty($campos)) jsonError('campos requerido.'); $pdo = db(); $stmt = $pdo->prepare( - "SELECT tc.id, tc.datos_respuestas, tc.estado, t.sesion_id + "SELECT tc.id, tc.turno_id, tc.datos_respuestas, tc.estado, t.sesion_id, t.codigo, + f.nombre AS formulario FROM turnero_consentimientos tc - JOIN turnero_turnos t ON t.id = tc.turno_id + JOIN turnero_turnos t ON t.id = tc.turno_id + JOIN lab_formularios f ON f.id = tc.formulario_id WHERE tc.token = ? LIMIT 1" ); $stmt->execute([$token]); @@ -40,4 +43,24 @@ $pdo->prepare( )->execute([json_encode($dr, JSON_UNESCAPED_UNICODE), $token]); notificarSSE((int)$tc['sesion_id']); + +// Olvidar una toma borra datos de un protocolo prolongado en curso: se registra +// qué campos se descartaron, porque después no hay forma de saberlo. +try { + (new ActividadAdmin())->registrar( + adminId(), + 'turnero', + 'resetear_toma', + (int)$tc['id'], + [ + 'turno_id' => (int)$tc['turno_id'], + 'turno_codigo' => $tc['codigo'], + 'formulario_nombre' => $tc['formulario'], + 'campos_olvidados' => $campos, + ] + ); +} catch (\Throwable $e) { + error_log('[resetear_toma] auditoría: ' . $e->getMessage()); +} + jsonOk([], 'Toma olvidada.'); diff --git a/modules/turnero/api/vincular_paciente.php b/modules/turnero/api/vincular_paciente.php index 32c1886..612cb14 100644 --- a/modules/turnero/api/vincular_paciente.php +++ b/modules/turnero/api/vincular_paciente.php @@ -4,6 +4,7 @@ * Vincula un paciente al turno para que los consentimientos puedan crearse. */ require_once __DIR__ . '/_helpers.php'; +require_once __DIR__ . '/../../../classes/lab/ActividadAdmin.php'; requireMethod('POST'); requireTurnero(); @@ -27,4 +28,22 @@ $stmt->execute([$pacienteId, $nombreCompleto, $turnoId]); if (!$stmt->rowCount()) jsonError('Turno no encontrado.', 404); +// Vincular a la persona equivocada manda la atención a otra historia clínica, +// así que queda registrado quién hizo la vinculación. +try { + (new ActividadAdmin())->registrar( + adminId(), + 'turnero', + 'vincular_paciente', + $turnoId, + [ + 'turno_id' => $turnoId, + 'paciente_id' => $pacienteId, + 'paciente_nombre' => $nombreCompleto, + ] + ); +} catch (\Throwable $e) { + error_log('[vincular_paciente] auditoría: ' . $e->getMessage()); +} + jsonOk(['turno_id' => $turnoId, 'paciente_id' => $pacienteId]); diff --git a/modules/turnero/views/bandeja.php b/modules/turnero/views/bandeja.php index c879631..f4b026a 100644 --- a/modules/turnero/views/bandeja.php +++ b/modules/turnero/views/bandeja.php @@ -251,6 +251,9 @@ Layout::open('Bandeja del día', 'fas fa-layer-group'); .bnd-com-user { font-weight: 600; color: #1e293b; } .bnd-com-date { font-size: 0.7rem; color: #94a3b8; margin-left: auto; } .bnd-com-text { color: #334155; } +.bnd-traza { border-left:2px solid #e2e8f0; margin-left:4px; padding-left:10px; } +.bnd-traza-fila { display:flex; align-items:flex-start; gap:8px; padding:3px 0; font-size:.8rem; color:#334155; } +.bnd-traza-hora { color:#94a3b8; font-variant-numeric:tabular-nums; min-width:42px; } /* Agregar comentario */ .bnd-add-com { display: flex; gap: 8px; align-items: flex-end; } .bnd-add-com textarea { @@ -702,7 +705,50 @@ function _renderDetalle(d) { `; - document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secRel + secC + secCom; + const secTraza = `
+
Qué pasó con este turno
+
+ Cargando…
+
`; + + document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secRel + secC + secCom + secTraza; + _bndCargarTraza(t.id); +} + +// ── Bitácora del turno ──────────────────────────────────────── +// Los comentarios ya salen arriba; aquí va el recorrido completo, incluidas las +// acciones administrativas (por ejemplo el retiro de un formulario, con motivo). +const BND_TRAZA_ICO = { + turno:'fa-plus-circle', llamado:'fa-bullhorn', atencion:'fa-user-check', + muestra:'fa-vial', fin:'fa-flag-checkered', doc:'fa-paper-plane', + doc_ok:'fa-file-signature', comentario:'fa-comment', + alerta:'fa-triangle-exclamation', admin:'fa-user-shield', +}; +const BND_TRAZA_COLOR = { alerta:'#dc2626', doc_ok:'#16a34a', comentario:'#2563eb', fin:'#0f766e', admin:'#7c3aed' }; + +async function _bndCargarTraza(turnoId) { + const cont = document.getElementById('bnd-traza'); + if (!cont) return; + const esc = x => String(x ?? '').replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c])); + try { + const j = await fetch(`modules/turnero/api/get_traza_turno.php?turno_id=${turnoId}`).then(r=>r.json()); + if (!j.ok || !j.eventos?.length) { + cont.innerHTML = 'Sin registros'; return; + } + const hora = c => c ? new Date(c.replace(' ','T')).toLocaleTimeString('es-CO',{hour:'2-digit',minute:'2-digit'}) : ''; + cont.innerHTML = '
' + j.eventos.map(e => ` +
+ ${hora(e.cuando)} + +
${esc(e.texto)} + ${e.quien ? ` ${esc(e.quien)}` : ''} + ${e.extra ? `
${esc(e.extra)}
` : ''} +
+
`).join('') + '
'; + } catch (_) { + cont.innerHTML = 'Error al cargar'; + } } // ── Marcar visto ────────────────────────────────────────────── diff --git a/modules/turnero/views/historial.php b/modules/turnero/views/historial.php index a08f601..3eaf7c9 100644 --- a/modules/turnero/views/historial.php +++ b/modules/turnero/views/historial.php @@ -110,6 +110,14 @@ Layout::open('Historial de Turnos', 'fas fa-history'); /* ── Documentos firmados ── */ .docs-section { margin-top:10px; padding-top:10px; border-top:1px solid #e2e8f0; } + .traza { border-left:2px solid #e2e8f0; margin:2px 0 0 6px; padding-left:10px; } + .traza-fila { display:flex; align-items:flex-start; gap:8px; padding:3px 0; font-size:.78rem; } + .traza-hora { color:#94a3b8; font-variant-numeric:tabular-nums; min-width:42px; } + .traza-ico { width:14px; text-align:center; margin-top:2px; font-size:.72rem; } + .traza-txt { flex:1; color:#334155; } + .traza-quien { color:#64748b; font-style:italic; } + .traza-extra { color:#b45309; font-size:.74rem; margin-top:1px; } + .traza-nota { color:#94a3b8; font-size:.7rem; margin-top:6px; font-style:italic; } .docs-lbl { font-size:.65rem; font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:#94a3b8; margin-bottom:5px; } .doc-pill { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:99px; font-size:.73rem; font-weight:600; margin:2px 2px; text-decoration:none; transition:opacity .15s; } @@ -594,6 +602,10 @@ function renderDetalle(t) { `).join('')} ${relHtml}${examHtml}${comHtml} +
+
Qué pasó con este turno
+ Cargando… +
Documentos
Cargando… @@ -614,11 +626,79 @@ function toggleDetalle(rowId, btn, turnoId) { docsEl.dataset.loaded = '1'; cargarDocumentosTurno(turnoId, docsEl); } + const trazaEl = document.getElementById(`traza-${turnoId}`); + if (trazaEl && !trazaEl.dataset.loaded) { + trazaEl.dataset.loaded = '1'; + cargarTrazaTurno(turnoId, trazaEl); + } } } const BASE_URL_ROOT = ''; +// ── Bitácora del turno ──────────────────────────────────────── +// Junta en una sola línea de tiempo lo que antes estaba repartido: el recorrido +// del turno, los documentos, los comentarios del personal, las muestras y las +// acciones administrativas auditadas. +const TRAZA_ICONO = { + turno:'fa-plus-circle', llamado:'fa-bullhorn', atencion:'fa-user-check', + muestra:'fa-vial', fin:'fa-flag-checkered', doc:'fa-paper-plane', + doc_ok:'fa-file-signature', comentario:'fa-comment', alerta:'fa-triangle-exclamation', + admin:'fa-user-shield', +}; +const TRAZA_COLOR = { + alerta:'#dc2626', doc_ok:'#16a34a', comentario:'#2563eb', + fin:'#0f766e', admin:'#7c3aed', +}; + +async function cargarTrazaTurno(turnoId, container) { + const titulo = '
Qué pasó con este turno
'; + try { + const res = await fetch(`${API}get_traza_turno.php?turno_id=${turnoId}`); + const json = await res.json(); + if (!json.ok) { container.innerHTML = titulo + 'No se pudo cargar'; return; } + + const ev = json.eventos || []; + if (!ev.length) { container.innerHTML = titulo + 'Sin registros'; return; } + + const hora = c => c ? new Date(c.replace(' ','T')).toLocaleTimeString('es-CO',{hour:'2-digit',minute:'2-digit'}) : ''; + const filas = ev.map(e => { + const color = TRAZA_COLOR[e.tipo] || '#94a3b8'; + const ico = TRAZA_ICONO[e.tipo] || 'fa-circle'; + const quien = e.quien ? `${esc(e.quien)}` : ''; + const extra = e.extra ? `
${esc(e.extra)}
` : ''; + return `
+ ${hora(e.cuando)} + +
${esc(e.texto)} ${quien}${extra}
+
`; + }).join(''); + + // El chat no se vuelca aquí: son datos personales del paciente y esta es + // una pantalla de consulta. Se indica que existe y se enlaza. + const chat = json.chat + ? `` : ''; + + // Se dice qué NO quedó registrado, para que la ausencia de un evento no + // se lea como prueba de que no ocurrió. + const nota = (json.sinRastro && json.sinRastro.length) + ? `
El sistema aún no deja rastro de: ${json.sinRastro.map(esc).join(' · ')}.
` + : ''; + + container.innerHTML = titulo + `
${filas}${chat}
` + nota; + } catch (_) { + container.innerHTML = titulo + 'Error al cargar'; + } +} + async function cargarDocumentosTurno(turnoId, container) { try { const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);