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);