prepare( "SELECT tc.id, tc.estado, tc.datos_respuestas, tc.toma_inicio_at, t.sesion_id, f.esquema FROM turnero_consentimientos tc JOIN turnero_turnos t ON t.id = tc.turno_id JOIN lab_formularios f ON f.id = tc.formulario_id WHERE tc.turno_id = ? AND tc.formulario_id = ?" ); $stmt->execute([$turnoId, $formularioId]); $tc = $stmt->fetch(PDO::FETCH_ASSOC); if (!$tc) jsonError('Consentimiento no encontrado.', 404); $esquema = json_decode($tc['esquema'] ?? '[]', true); if (!is_array($esquema)) $esquema = []; // ── Merge datos_respuestas existentes + nuevos ─────────────── $datosActuales = []; if ($tc['datos_respuestas']) { $d = json_decode($tc['datos_respuestas'], true); if (is_array($d)) $datosActuales = $d; } // Guardar svg en el campo firmado $datosInput[$campoId] = $svg; // Identidad del firmante — resuelta en el servidor desde la sesión, no desde el // cliente: cada toma puede firmarla un profesional distinto y esta es la única // fuente confiable de quién la hizo. $__uid = adminId(); if ($__uid) { $stmtPro = $pdo->prepare( "SELECT au.full_name, au.cedula AS au_cedula, le.nombre_completo AS pro_nombre, le.numero_documento AS pro_documento FROM admin_users au LEFT JOIN lab_enfermeras le ON le.id = au.enfermera_id WHERE au.id = ? LIMIT 1" ); $stmtPro->execute([$__uid]); $pro = $stmtPro->fetch(PDO::FETCH_ASSOC) ?: []; $proNombre = ($pro['pro_nombre'] ?? null) ?: ($pro['full_name'] ?? null); $proCedula = ($pro['au_cedula'] ?? null) ?: ($pro['pro_documento'] ?? null); if ($proNombre) { $datosInput[$campoId . '_pro_nombre'] = $proNombre; $datosInput['_pro_nombre'] = $proNombre; // respaldo global } if ($proCedula) { $datosInput[$campoId . '_pro_cedula'] = $proCedula; $datosInput['_pro_cedula'] = $proCedula; } $datosInput[$campoId . '_pro_id'] = $__uid; } $datosMerge = array_merge($datosActuales, $datosInput); // ── Analizar esquema: campos firma_profesional en orden ────── // Solo incluye firmas cuya sección condicional coincide con el examen seleccionado. $firmasCampos = []; $separadorLabel = null; $curCondField = null; $curCondVals = []; foreach ($esquema as $campo) { $tipo = $campo['tipo'] ?? ''; if ($tipo === 'separador') { $separadorLabel = $campo['label'] ?? ''; $cond = $campo['condicion'] ?? null; if ($cond) { $curCondField = $cond['campo_id'] ?? null; $curCondVals = $cond['valores'] ?? ($cond['valor'] ? [$cond['valor']] : []); } else { $curCondField = null; $curCondVals = []; } } if ($tipo === 'firma_profesional') { // Filtrar por condición del separador padre if ($curCondField !== null && !empty($curCondVals)) { $ctrlVal = $datosMerge[$curCondField] ?? null; $ctrlArr = is_array($ctrlVal) ? $ctrlVal : ($ctrlVal !== null ? [$ctrlVal] : []); if (empty(array_intersect($curCondVals, $ctrlArr))) continue; } $minutos = null; $horaFija = null; if ($separadorLabel !== null) { if (preg_match('/Minuto\s+(\d+)/i', $separadorLabel, $m)) { $minutos = (int)$m[1]; } elseif (preg_match('/(\d+):(\d+)\s*(a\.?m\.?|p\.?m\.?)/i', $separadorLabel, $m)) { $h = (int)$m[1]; $min = (int)$m[2]; $pm = strtolower(preg_replace('/[^apm]/i', '', $m[3])) === 'pm'; if ($pm && $h < 12) $h += 12; $horaFija = sprintf('%02d:%02d:00', $h, $min); } elseif (stripos($separadorLabel, 'Otro') !== false) { $otroVal = trim((string)($datosMerge['_otro_hora'] ?? '')); $minutos = is_numeric($otroVal) ? (int)$otroVal : PHP_INT_MAX; } } $firmasCampos[] = ['id' => $campo['id'], 'minutos' => $minutos, 'hora_fija' => $horaFija]; } } // ── Aplicar filtro de ciclos configurados (tomas_config) ──── $__tcFirmas = $datosMerge['_tomas_config'] ?? null; if (is_array($__tcFirmas)) { $__allowed = []; foreach ($__tcFirmas as $__v) { if (!is_array($__v)) continue; $__inner = reset($__v); if (is_array($__inner)) { // nested: { examName: { groupKey: [fids] } } foreach ($__v as $__fids) foreach ($__fids as $__f) $__allowed[$__f] = true; } else { // flat: { groupKey: [fids] } foreach ($__v as $__f) $__allowed[$__f] = true; } } $firmasCampos = array_values(array_filter($firmasCampos, fn($f) => isset($__allowed[$f['id']]))); } // Ordenar cronológicamente para que el siguiente pendiente sea el más próximo usort($firmasCampos, fn($a, $b) => ($a['minutos'] ?? PHP_INT_MAX) <=> ($b['minutos'] ?? PHP_INT_MAX)); // ── Determinar estado y siguiente_toma_at ──────────────────── // Acepta firma guardada con clave campo_id O campo_id_svg (compatibilidad registros históricos) $hasFirma = fn($f) => (isset($datosMerge[$f['id']]) && strlen($datosMerge[$f['id']]) > 10) || (isset($datosMerge[$f['id'].'_svg']) && strlen($datosMerge[$f['id'].'_svg']) > 10); $firmados = array_filter($firmasCampos, $hasFirma); $pendientes = array_values(array_filter($firmasCampos, fn($f) => !$hasFirma($f))); $todasFirmadas = empty($pendientes); $siguienteToma = null; $nuevoEstado = 'en_progreso'; $tomaInicioAt = $tc['toma_inicio_at']; if (empty($tc['toma_inicio_at'])) { $tomaInicioAt = date('Y-m-d H:i:s'); } if ($todasFirmadas) { $nuevoEstado = 'firmado'; } else { $next = $pendientes[0]; if ($next['hora_fija'] !== null) { $siguienteToma = date('Y-m-d') . ' ' . $next['hora_fija']; } elseif ($next['minutos'] !== null && $tomaInicioAt) { $siguienteToma = date('Y-m-d H:i:s', strtotime($tomaInicioAt) + $next['minutos'] * 60); } // Si la siguiente toma ya pasó o es ahora (minuto 0), no enviar countdown if ($siguienteToma !== null && $siguienteToma <= date('Y-m-d H:i:s')) { $siguienteToma = null; } } // ── Persistir ──────────────────────────────────────────────── $pdo->prepare( "UPDATE turnero_consentimientos SET datos_respuestas = ?, estado = ?, siguiente_toma_at = ?, toma_inicio_at = COALESCE(toma_inicio_at, ?), firmado_at = IF(? = 'firmado', NOW(), firmado_at), firmado_profesional_at = IF(? = 'firmado', NOW(), firmado_profesional_at) WHERE turno_id = ? AND formulario_id = ?" )->execute([ json_encode($datosMerge, JSON_UNESCAPED_UNICODE), $nuevoEstado, $siguienteToma, date('Y-m-d H:i:s'), $nuevoEstado, $nuevoEstado, $turnoId, $formularioId, ]); notificarSSE((int)$tc['sesion_id']); $totalFirmas = count($firmasCampos); $firmadasCount = count($firmados) + ($todasFirmadas ? 0 : 1); // incluye la recién guardada jsonOk([ 'estado' => $nuevoEstado, 'siguiente_toma_at'=> $siguienteToma, 'tomas_firmadas' => min($firmadasCount, $totalFirmas), 'tomas_total' => $totalFirmas, 'completado' => $todasFirmadas, ], $todasFirmadas ? 'Formulario completado.' : 'Toma guardada. Próxima: ' . ($siguienteToma ?? 'pendiente'));