Files
whatsapp/modules/turnero/api/get_consentimientos.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 2ba13add34 Corregir Tomas Prolongadas: filtrado de secciones, firmante por toma, detalle en historial
Tres bugs relacionados en el formulario "Control de Tomas de Muestras
Prolongadas" (F-LAB-28), detectados en el turno E017:

1. El modal de documentos del historial abría el documento sin &embed=1,
   por lo que el filtrado de tomas irrelevantes (según examen seleccionado)
   nunca se aplicaba — se mostraban las 16 secciones posibles en vez de
   solo las 2-3 que aplicaban, con textos "Pendiente de firma" de más.

2. El guardado de firma en tomas progresivas usa un endpoint distinto
   (_guardarFirmaMP → guardar_toma.php) que no pasaba por el fix de
   nombre/cédula del firmante aplicado antes solo a _guardarFirmaPro.
   Como además una toma progresiva puede pasar por varios profesionales
   en el transcurso de horas, ahora se guarda identidad por-toma
   (campoId + '_pro_nombre'/'_pro_cedula'), no solo global.

3. El historial no mostraba quién atendió cada toma. get_consentimientos.php
   ahora expone tomas_detalle (etiqueta, hora, firmante) por consentimiento
   de toma progresiva, y el panel de documentos del historial lo renderiza
   en una tabla debajo del pill del formulario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-03 14:26:28 -05:00

498 lines
24 KiB
PHP

<?php
/**
* GET /modules/turnero/api/get_consentimientos.php
* Retorna el estado actualizado de los consentimientos de un turno.
* Usado por lugar.php (polling cada 5 s) y recepcion.php.
* No requiere autenticación (las pantallas internas ya están protegidas).
*
* Query params:
* turno_id int requerido
* incluir_solicitud int opcional 1 → también retorna solicitud, paciente y exámenes
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('GET');
$turnoId = isset($_GET['turno_id']) ? (int) $_GET['turno_id'] : 0;
$incluirSolicitud = isset($_GET['incluir_solicitud']) ? (bool)(int)$_GET['incluir_solicitud'] : false;
if ($turnoId <= 0) jsonError('turno_id inválido.');
$pdo = db();
// ── Verificar que el turno existe ─────────────────────────────
$stmt = $pdo->prepare("SELECT id, estado, lugar_destino_id, paciente_id FROM turnero_turnos WHERE id = ?");
$stmt->execute([$turnoId]);
$turno = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$turno) jsonError('Turno no encontrado.', 404);
// ── Formularios de lugar ──────────────────────────────────────────────────────
// Solo cuando el turno tiene lugar destino asignado. En recepción (NULL),
// fidsDeLugar queda vacío → todos los consentimientos reciben origen_lugar_id=null
// → pasan el filtro !c.origen_lugar_id de recepcion.php.
$lugarDestinoId = (int)($turno['lugar_destino_id'] ?? 0);
$fidsDeLugar = [];
if ($lugarDestinoId) {
$stmtL = $pdo->prepare(
"SELECT formulario_id FROM turnero_lugar_consentimientos WHERE lugar_id = ?"
);
$stmtL->execute([$lugarDestinoId]);
$fidsDeLugar = array_map('intval', $stmtL->fetchAll(PDO::FETCH_COLUMN));
}
// ── Consentimientos ───────────────────────────────────────────
$stmt = $pdo->prepare(
"SELECT tc.id,
tc.turno_id,
tc.formulario_id,
tc.token,
tc.estado,
tc.enviado_at,
tc.firmado_at,
(tc.firma_profesional_svg IS NOT NULL) AS tiene_firma_profesional,
tc.firmado_profesional_at,
tc.siguiente_toma_at,
tc.toma_inicio_at,
tc.datos_respuestas,
f.nombre AS formulario_nombre,
f.esquema AS formulario_esquema,
f.es_toma_progresiva AS formulario_es_toma_prog,
f.solo_profesional AS formulario_solo_profesional
FROM turnero_consentimientos tc
LEFT JOIN lab_formularios f ON f.id = tc.formulario_id
WHERE tc.turno_id = ?
ORDER BY tc.id ASC"
);
$stmt->execute([$turnoId]);
$consentimientos = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Compute flags desde el esquema; origen_lugar_id calculado según config actual
foreach ($consentimientos as &$c) {
$esquema = $c['formulario_esquema'] ?? null;
$campos = [];
if ($esquema) {
$decoded = json_decode($esquema, true);
$campos = is_array($decoded) ? $decoded : ($decoded['campos'] ?? []);
}
$camposFirmaPro = array_values(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma_profesional'));
$tieneFirmaPro = !empty($camposFirmaPro);
$tieneFirmaPac = !empty(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma'));
// Toma progresiva: override explícito en DB gana; si es NULL, usar heurística (>1 campo firma_profesional)
$overrideProg = $c['formulario_es_toma_prog'] ?? null;
$esTomaProg = $overrideProg !== null ? (bool)(int)$overrideProg : count($camposFirmaPro) > 1;
$dr = $c['datos_respuestas'] ? (json_decode($c['datos_respuestas'], true) ?? []) : [];
$tomasTotal = 0;
$tomasFirm = 0;
if ($esTomaProg) {
// Mapear cada firma_profesional a los valores de examen de su sección condicional
// (y a su etiqueta + campo hora, para poder mostrar el detalle por toma)
$firmaCondMap = [];
$curCondCampo = null;
$curCondVals = [];
$curLabel = null;
$curHoraId = null;
foreach ($campos as $campo) {
$ft = $campo['tipo'] ?? '';
if ($ft === 'separador') {
$cond = $campo['condicion'] ?? null;
$curCondCampo = $cond ? ($cond['campo_id'] ?? null) : null;
$curCondVals = $cond
? ($cond['valores'] ?? ($cond['valor'] ? [$cond['valor']] : []))
: [];
$curLabel = $campo['label'] ?? '';
$curHoraId = null;
} elseif ($ft === 'hora') {
$curHoraId = $campo['id'] ?? null;
} elseif ($ft === 'firma_profesional' && !empty($campo['id'])) {
$firmaCondMap[$campo['id']] = [
'campo' => $curCondCampo, 'valores' => $curCondVals,
'label' => $curLabel, 'hora_id' => $curHoraId,
];
}
}
// Filtrar por exámenes seleccionados en datos_respuestas
$relevantIds = [];
foreach ($firmaCondMap as $fid => $info) {
if ($info['campo'] === null) {
$relevantIds[] = $fid; // sin condición → siempre aplica
} else {
$ctrlVal = $dr[$info['campo']] ?? null;
$ctrlArr = is_array($ctrlVal) ? $ctrlVal : ($ctrlVal !== null ? [$ctrlVal] : []);
if (!empty(array_intersect($info['valores'], $ctrlArr))) {
$relevantIds[] = $fid;
}
}
}
// Si hay config de tomas guardada, restringir al subconjunto seleccionado
$tomasConfig = is_array($dr['_tomas_config'] ?? null) ? $dr['_tomas_config'] : null;
if ($tomasConfig !== null) {
$allowedFids = [];
foreach ($tomasConfig as $__v) {
if (!is_array($__v)) continue;
$__inner = reset($__v);
// nested: exam => { group => [fids] }; flat: group => [fids]
if (is_array($__inner)) { foreach ($__v as $__fids) foreach ($__fids as $__f) $allowedFids[] = $__f; }
else { foreach ($__v as $__f) $allowedFids[] = $__f; }
}
$allowedFids = array_unique($allowedFids);
$relevantIds = array_values(array_filter($relevantIds, fn($id) => in_array($id, $allowedFids, true)));
}
$tomasTotal = count($relevantIds);
$tomasDetalle = [];
foreach ($relevantIds as $fid) {
$firmado = (!empty($dr[$fid]) && strlen($dr[$fid]) > 10)
|| (!empty($dr[$fid.'_svg']) && strlen($dr[$fid.'_svg']) > 10);
if ($firmado) $tomasFirm++;
$info = $firmaCondMap[$fid] ?? [];
$horaId = $info['hora_id'] ?? null;
$tomasDetalle[] = [
'label' => $info['label'] ?? '',
'hora' => $horaId ? ($dr[$horaId] ?? null) : null,
'firmado' => $firmado,
'firmante' => $firmado ? ($dr[$fid . '_pro_nombre'] ?? $dr['_pro_nombre'] ?? null) : null,
];
}
}
$soloPro = !empty($c['formulario_solo_profesional']);
$c['requiere_firma_profesional'] = $tieneFirmaPro;
$c['requiere_firma_paciente'] = $soloPro ? false : !($tieneFirmaPro && !$tieneFirmaPac);
$c['origen_lugar_id'] = in_array((int)$c['formulario_id'], $fidsDeLugar, true)
? ($lugarDestinoId ?: -1) : null;
$c['es_toma_progresiva'] = $esTomaProg;
$c['tomas_total'] = $tomasTotal;
$c['tomas_firmadas'] = $tomasFirm;
$c['tomas_detalle'] = $tomasDetalle ?? [];
$c['cierre_anticipado'] = $dr['_cierre_anticipado'] ?? null;
// Exponer campos firma_profesional (ids) para el frontend
$c['campos_firma_pro'] = array_column($camposFirmaPro, 'id');
unset($c['formulario_esquema'], $c['formulario_es_toma_prog'], $c['formulario_solo_profesional'], $c['datos_respuestas']);
}
unset($c);
$respuesta = [
'turno_id' => $turnoId,
'turno_estado' => $turno['estado'],
'consentimientos' => $consentimientos,
];
// ── Datos adicionales si solicita solicitud ───────────────────
if ($incluirSolicitud) {
$stmt = $pdo->prepare(
"SELECT s.id, s.turno_id, s.paciente_id, s.lugar_id,
s.numero_orden,
s.total_cobrado, s.metodo_pago, s.observaciones, s.embarazada, s.solo_muestras, s.medico_id, s.creado_at,
l.nombre AS lugar_nombre,
CONCAT(m.nombres, ' ', m.apellidos) AS medico_nombre,
m.cod_especialidad AS medico_especialidad,
m.codigo AS medico_codigo
FROM turnero_solicitudes s
LEFT JOIN turnero_lugares l ON l.id = s.lugar_id
LEFT JOIN medicos m ON m.id = s.medico_id
WHERE s.turno_id = ?
LIMIT 1"
);
$stmt->execute([$turnoId]);
$solicitud = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
$paciente = null;
$examenes = [];
if ($solicitud) {
// Paciente
$stmt = $pdo->prepare(
"SELECT id, nombre_completo, tipo_documento, numero_documento,
fecha_nacimiento, telefono
FROM lab_pacientes
WHERE id = ?"
);
$stmt->execute([$solicitud['paciente_id']]);
$paciente = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
// Exámenes
$stmt = $pdo->prepare(
"SELECT et.id, et.codigo, et.nombre, et.categoria
FROM turnero_examen_items tei
JOIN exam_tipos et ON et.id = tei.exam_tipo_id
WHERE tei.solicitud_id = ?
ORDER BY et.categoria, et.nombre"
);
$stmt->execute([$solicitud['id']]);
$examenes = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Nota: en visitas de "solo entrega de muestras" no hay exámenes, así que la
// Fuente 1 (por examen) no aplica, pero la Fuente 2 (por lugar, ej. F-LAB-08
// Datos Toma de Muestras) sigue siendo obligatoria — se procesa más abajo igual.
// Auto-crear consentimientos requeridos desde DOS fuentes:
// 1. Por examen → exam_tipo_consentimientos
// 2. Por lugar → turnero_lugar_consentimientos
$requeridos = [];
$vistosFIds = [];
// Fuente 1: examen
if (!empty($examenes)) {
$examIds = array_column($examenes, 'id');
$ph = implode(',', array_fill(0, count($examIds), '?'));
$stmtE = $pdo->prepare(
"SELECT DISTINCT etc.formulario_id, f.nombre AS formulario_nombre
FROM exam_tipo_consentimientos etc
JOIN lab_formularios f ON f.id = etc.formulario_id
WHERE etc.exam_tipo_id IN ($ph) AND f.is_active = 1"
);
$stmtE->execute($examIds);
foreach ($stmtE->fetchAll(PDO::FETCH_ASSOC) as $r) {
$fid = (int)$r['formulario_id'];
if (!in_array($fid, $vistosFIds, true)) {
$requeridos[] = $r;
$vistosFIds[] = $fid;
}
}
}
// Fuente 2: lugar destino ($lugarDestinoId y $fidsDeLugar ya calculados arriba)
if ($lugarDestinoId) {
$stmtL = $pdo->prepare(
"SELECT DISTINCT tlc.formulario_id, f.nombre AS formulario_nombre
FROM turnero_lugar_consentimientos tlc
JOIN lab_formularios f ON f.id = tlc.formulario_id
WHERE tlc.lugar_id = ? AND f.is_active = 1"
);
$stmtL->execute([$lugarDestinoId]);
foreach ($stmtL->fetchAll(PDO::FETCH_ASSOC) as $r) {
$fid = (int)$r['formulario_id'];
if (!in_array($fid, $vistosFIds, true)) {
$requeridos[] = $r;
$vistosFIds[] = $fid;
}
}
}
// Auto-crear los registros que falten
if (!empty($requeridos)) {
$existFormIds = array_map('intval', array_column($consentimientos, 'formulario_id'));
$stmtIns = $pdo->prepare(
"INSERT IGNORE INTO turnero_consentimientos
(turno_id, formulario_id, token, estado, creado_por)
VALUES (?, ?, ?, 'pendiente', ?)"
);
$creados = 0;
foreach ($requeridos as $r) {
if (!in_array((int)$r['formulario_id'], $existFormIds, true)) {
$token = sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0,0xffff), mt_rand(0,0xffff),
mt_rand(0,0xffff),
mt_rand(0,0x0fff)|0x4000,
mt_rand(0,0x3fff)|0x8000,
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
);
$stmtIns->execute([$turnoId, (int)$r['formulario_id'], $token, adminId()]);
$creados++;
}
}
// Re-leer si se crearon nuevos registros y recalcular flags
if ($creados > 0) {
$stmtC = $pdo->prepare(
"SELECT tc.id, tc.turno_id, tc.formulario_id, tc.token,
tc.estado, tc.enviado_at, tc.firmado_at,
(tc.firma_profesional_svg IS NOT NULL) AS tiene_firma_profesional,
tc.firmado_profesional_at,
tc.siguiente_toma_at,
tc.toma_inicio_at,
tc.datos_respuestas,
f.nombre AS formulario_nombre,
f.esquema AS formulario_esquema,
f.es_toma_progresiva AS formulario_es_toma_prog,
f.solo_profesional AS formulario_solo_profesional
FROM turnero_consentimientos tc
LEFT JOIN lab_formularios f ON f.id = tc.formulario_id
WHERE tc.turno_id = ?
ORDER BY tc.id ASC"
);
$stmtC->execute([$turnoId]);
$consentimientos = $stmtC->fetchAll(PDO::FETCH_ASSOC);
foreach ($consentimientos as &$c) {
$esquema = $c['formulario_esquema'] ?? null;
$campos = [];
if ($esquema) {
$decoded = json_decode($esquema, true);
$campos = is_array($decoded) ? $decoded : ($decoded['campos'] ?? []);
}
$camposFirmaPro = array_values(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma_profesional'));
$tieneFirmaPro = !empty($camposFirmaPro);
$tieneFirmaPac = !empty(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma'));
$overrideProg = $c['formulario_es_toma_prog'] ?? null;
$esTomaProg = $overrideProg !== null ? (bool)(int)$overrideProg : count($camposFirmaPro) > 1;
$dr2 = $c['datos_respuestas'] ? (json_decode($c['datos_respuestas'], true) ?? []) : [];
$tomasTotal2 = 0;
$tomasFirm = 0;
if ($esTomaProg) {
$firmaCondMap2 = [];
$curC2 = null; $curV2 = [];
foreach ($campos as $campo2) {
$ft2 = $campo2['tipo'] ?? '';
if ($ft2 === 'separador') {
$cond2 = $campo2['condicion'] ?? null;
$curC2 = $cond2 ? ($cond2['campo_id'] ?? null) : null;
$curV2 = $cond2 ? ($cond2['valores'] ?? ($cond2['valor'] ? [$cond2['valor']] : [])) : [];
} elseif ($ft2 === 'firma_profesional' && !empty($campo2['id'])) {
$firmaCondMap2[$campo2['id']] = ['campo' => $curC2, 'valores' => $curV2];
}
}
$relevantIds2 = [];
foreach ($firmaCondMap2 as $fid2 => $info2) {
if ($info2['campo'] === null) {
$relevantIds2[] = $fid2;
} else {
$cv2 = $dr2[$info2['campo']] ?? null;
$ca2 = is_array($cv2) ? $cv2 : ($cv2 !== null ? [$cv2] : []);
if (!empty(array_intersect($info2['valores'], $ca2))) $relevantIds2[] = $fid2;
}
}
$cfg2 = is_array($dr2['_tomas_config'] ?? null) ? $dr2['_tomas_config'] : null;
if ($cfg2 !== null) {
$allowed2 = [];
foreach ($cfg2 as $__v2) {
if (!is_array($__v2)) continue;
$__i2 = reset($__v2);
if (is_array($__i2)) { foreach ($__v2 as $__fids2) foreach ($__fids2 as $__f2) $allowed2[] = $__f2; }
else { foreach ($__v2 as $__f2) $allowed2[] = $__f2; }
}
$allowed2 = array_unique($allowed2);
$relevantIds2 = array_values(array_filter($relevantIds2, fn($id) => in_array($id, $allowed2, true)));
}
$tomasTotal2 = count($relevantIds2);
foreach ($relevantIds2 as $fid2) {
if (!empty($dr2[$fid2]) && strlen($dr2[$fid2]) > 10) $tomasFirm++;
}
}
$soloPro2 = !empty($c['formulario_solo_profesional']);
$c['requiere_firma_profesional'] = $tieneFirmaPro;
$c['requiere_firma_paciente'] = $soloPro2 ? false : !($tieneFirmaPro && !$tieneFirmaPac);
$c['origen_lugar_id'] = in_array((int)$c['formulario_id'], $fidsDeLugar, true)
? $lugarDestinoId : null;
$c['es_toma_progresiva'] = $esTomaProg;
$c['tomas_total'] = $tomasTotal2;
$c['tomas_firmadas'] = $tomasFirm;
$c['cierre_anticipado'] = $dr2['_cierre_anticipado'] ?? null;
$c['campos_firma_pro'] = array_column($camposFirmaPro, 'id');
unset($c['formulario_esquema'], $c['formulario_es_toma_prog'], $c['formulario_solo_profesional'], $c['datos_respuestas']);
}
unset($c);
$respuesta['consentimientos'] = $consentimientos;
}
}
}
// Muestras pendientes / recibidas del turno
$muestras = [];
if ($solicitud) {
try {
$stmtM = $pdo->prepare(
"SELECT tm.id, tm.tipo_muestra, tm.estado, tm.motivo_rechazo, tm.recibida_at,
COALESCE(lt.nombre, tm.tipo_muestra) AS label,
0 AS es_pendiente_anterior, NULL AS solicitud_orden_anterior
FROM turnero_muestras tm
LEFT JOIN lab_tipos_muestra lt ON lt.codigo = tm.tipo_muestra
WHERE tm.solicitud_id = ?
ORDER BY tm.id ASC"
);
$stmtM->execute([$solicitud['id']]);
$muestras = $stmtM->fetchAll(PDO::FETCH_ASSOC);
// Si es visita de entrega de muestras, agregar pendientes de solicitudes anteriores
if (!empty($solicitud['solo_muestras']) && !empty($solicitud['paciente_id'])) {
$stmtPrev = $pdo->prepare(
"SELECT tm.id, tm.tipo_muestra, tm.estado, tm.motivo_rechazo, tm.recibida_at,
COALESCE(lt.nombre, tm.tipo_muestra) AS label,
1 AS es_pendiente_anterior,
ts.id AS solicitud_id_anterior,
ts.numero_orden AS solicitud_orden_anterior
FROM turnero_muestras tm
JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id
LEFT JOIN lab_tipos_muestra lt ON lt.codigo = tm.tipo_muestra
WHERE ts.paciente_id = ?
AND tm.solicitud_id != ?
AND tm.estado IN ('pendiente','rechazada')
ORDER BY tm.id ASC"
);
$stmtPrev->execute([$solicitud['paciente_id'], $solicitud['id']]);
$previas = $stmtPrev->fetchAll(PDO::FETCH_ASSOC);
$muestras = array_merge($previas, $muestras);
}
} catch (\Throwable $_) {
// Tabla aún no existe
}
}
// Fallback: sin solicitud pero el turno tiene paciente_id → buscar muestras pendientes igualmente
if (!$solicitud && !empty($turno['paciente_id'])) {
$pacId = (int)$turno['paciente_id'];
$stmt = $pdo->prepare(
"SELECT id, nombre_completo, tipo_documento, numero_documento,
fecha_nacimiento, telefono
FROM lab_pacientes WHERE id = ?"
);
$stmt->execute([$pacId]);
$paciente = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
try {
$stmtPrev = $pdo->prepare(
"SELECT tm.id, tm.tipo_muestra, tm.estado, tm.motivo_rechazo, tm.recibida_at,
COALESCE(lt.nombre, tm.tipo_muestra) AS label,
1 AS es_pendiente_anterior,
ts.id AS solicitud_id_anterior,
ts.numero_orden AS solicitud_orden_anterior
FROM turnero_muestras tm
JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id
LEFT JOIN lab_tipos_muestra lt ON lt.codigo = tm.tipo_muestra
WHERE ts.paciente_id = ?
AND tm.estado IN ('pendiente','rechazada')
ORDER BY tm.id ASC"
);
$stmtPrev->execute([$pacId]);
$muestras = $stmtPrev->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $_) {}
}
// Cargar exámenes de la solicitud original para cada muestra pendiente de visita anterior,
// para que al recibirla se vea el contexto completo del servicio (no solo el tipo de muestra)
$solIdsAnteriores = array_values(array_unique(array_filter(
array_column($muestras, 'solicitud_id_anterior')
)));
if ($solIdsAnteriores) {
$ph = implode(',', array_fill(0, count($solIdsAnteriores), '?'));
$stmtExAnt = $pdo->prepare(
"SELECT tei.solicitud_id, et.nombre, et.codigo
FROM turnero_examen_items tei
JOIN exam_tipos et ON et.id = tei.exam_tipo_id
WHERE tei.solicitud_id IN ($ph)
ORDER BY et.nombre ASC"
);
$stmtExAnt->execute($solIdsAnteriores);
$examPorSolicitud = [];
foreach ($stmtExAnt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$examPorSolicitud[(int)$row['solicitud_id']][] = ['nombre' => $row['nombre'], 'codigo' => $row['codigo']];
}
foreach ($muestras as &$m) {
if (!empty($m['solicitud_id_anterior'])) {
$m['examenes_anteriores'] = $examPorSolicitud[(int)$m['solicitud_id_anterior']] ?? [];
}
}
unset($m);
}
$respuesta['solicitud'] = $solicitud;
$respuesta['paciente'] = $paciente;
$respuesta['examenes'] = $examenes;
$respuesta['muestras'] = $muestras;
}
jsonOk($respuesta);