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 <noreply@anthropic.com>
206 lines
9.0 KiB
PHP
206 lines
9.0 KiB
PHP
<?php
|
|
/**
|
|
* GET /modules/turnero/api/get_traza_turno.php?turno_id=N
|
|
*
|
|
* Bitácora de un turno: qué pasó, cuándo y quién lo hizo.
|
|
*
|
|
* La información ya existía, pero repartida en cinco tablas y sin ninguna
|
|
* pantalla que la juntara: para saber quién retiró un formulario había que
|
|
* consultar la base a mano. Aquí se unifica en una sola línea de tiempo.
|
|
*
|
|
* No inventa nada: cada evento sale de un dato guardado. Lo que el sistema no
|
|
* registró —ver `sinRastro` más abajo— simplemente no aparece.
|
|
*/
|
|
|
|
require_once __DIR__ . '/_helpers.php';
|
|
requireMethod('GET');
|
|
requireTurnero();
|
|
|
|
$turnoId = (int)($_GET['turno_id'] ?? 0);
|
|
if (!$turnoId) jsonError('turno_id requerido');
|
|
|
|
$pdo = db();
|
|
|
|
$t = $pdo->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',
|
|
],
|
|
]);
|