Files
whatsapp/modules/turnero/api/get_consentimientos.php
Lizandro GuarnizoandClaude Sonnet 4.6 e480e14974 Corregir fuga de tomas_detalle entre formularios del mismo turno
\$tomasDetalle solo se inicializaba dentro del if(\$esTomaProg), así que un
formulario que NO es de toma progresiva (ej. "Datos Toma de Muestras")
heredaba el arreglo del formulario anterior en el mismo turno (ej.
"Control de Tomas de Muestras Prolongadas") si ese sí lo era. Se reinicia
en cada vuelta del loop junto con tomas_total/tomas_firmadas.

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

499 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;
$tomasDetalle = []; // reiniciar cada vuelta: si no, un formulario sin tomas hereda las del anterior
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);