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', ], ]);