diff --git a/modules/turnero/api/get_consentimientos.php b/modules/turnero/api/get_consentimientos.php index 0a40f65..477a123 100644 --- a/modules/turnero/api/get_consentimientos.php +++ b/modules/turnero/api/get_consentimientos.php @@ -48,6 +48,9 @@ $stmt = $pdo->prepare( 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 FROM turnero_consentimientos tc @@ -66,14 +69,31 @@ foreach ($consentimientos as &$c) { $decoded = json_decode($esquema, true); $campos = is_array($decoded) ? $decoded : ($decoded['campos'] ?? []); } - $tieneFirmaPro = !empty(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma_profesional')); - $tieneFirmaPac = !empty(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma')); + $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: más de 1 campo firma_profesional + $esTomaProg = count($camposFirmaPro) > 1; + $tomasTotal = count($camposFirmaPro); + $tomasFirm = 0; + if ($esTomaProg && $c['datos_respuestas']) { + $dr = json_decode($c['datos_respuestas'], true) ?? []; + foreach ($camposFirmaPro as $fp) { + if (!empty($dr[$fp['id']]) && strlen($dr[$fp['id']]) > 10) $tomasFirm++; + } + } + $c['requiere_firma_profesional'] = $tieneFirmaPro; $c['requiere_firma_paciente'] = !($tieneFirmaPro && !$tieneFirmaPac); - // Si el formulario está asignado al lugar destino → es de toma de muestras, no de recepción - $c['origen_lugar_id'] = in_array((int)$c['formulario_id'], $fidsDeLugar, true) - ? $lugarDestinoId : null; - unset($c['formulario_esquema']); + $c['origen_lugar_id'] = in_array((int)$c['formulario_id'], $fidsDeLugar, true) + ? $lugarDestinoId : null; + $c['es_toma_progresiva'] = $esTomaProg; + $c['tomas_total'] = $tomasTotal; + $c['tomas_firmadas'] = $tomasFirm; + // Exponer campos firma_profesional (ids) para el frontend + $c['campos_firma_pro'] = array_column($camposFirmaPro, 'id'); + unset($c['formulario_esquema'], $c['datos_respuestas']); } unset($c); @@ -202,6 +222,9 @@ if ($incluirSolicitud) { 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 FROM turnero_consentimientos tc @@ -218,13 +241,26 @@ if ($incluirSolicitud) { $decoded = json_decode($esquema, true); $campos = is_array($decoded) ? $decoded : ($decoded['campos'] ?? []); } - $tieneFirmaPro = !empty(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma_profesional')); - $tieneFirmaPac = !empty(array_filter($campos, fn($f) => ($f['tipo'] ?? '') === 'firma')); + $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')); + $esTomaProg = count($camposFirmaPro) > 1; + $tomasFirm = 0; + if ($esTomaProg && $c['datos_respuestas']) { + $dr = json_decode($c['datos_respuestas'], true) ?? []; + foreach ($camposFirmaPro as $fp) { + if (!empty($dr[$fp['id']]) && strlen($dr[$fp['id']]) > 10) $tomasFirm++; + } + } $c['requiere_firma_profesional'] = $tieneFirmaPro; $c['requiere_firma_paciente'] = !($tieneFirmaPro && !$tieneFirmaPac); - $c['origen_lugar_id'] = in_array((int)$c['formulario_id'], $fidsDeLugar, true) - ? $lugarDestinoId : null; - unset($c['formulario_esquema']); + $c['origen_lugar_id'] = in_array((int)$c['formulario_id'], $fidsDeLugar, true) + ? $lugarDestinoId : null; + $c['es_toma_progresiva'] = $esTomaProg; + $c['tomas_total'] = count($camposFirmaPro); + $c['tomas_firmadas'] = $tomasFirm; + $c['campos_firma_pro'] = array_column($camposFirmaPro, 'id'); + unset($c['formulario_esquema'], $c['datos_respuestas']); } unset($c); $respuesta['consentimientos'] = $consentimientos; diff --git a/modules/turnero/api/guardar_toma.php b/modules/turnero/api/guardar_toma.php new file mode 100644 index 0000000..bf110b6 --- /dev/null +++ b/modules/turnero/api/guardar_toma.php @@ -0,0 +1,152 @@ +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; +$datosMerge = array_merge($datosActuales, $datosInput); + +// ── Analizar esquema: campos firma_profesional en orden ────── +// Construir mapa: campo_id → minuto (o hora fija) leyendo el separador previo +$firmasCampos = []; // [ [id, minutos|null, hora_fija|null], ... ] en orden +$separadorLabel = null; + +foreach ($esquema as $campo) { + $tipo = $campo['tipo'] ?? ''; + if ($tipo === 'separador') { + $separadorLabel = $campo['label'] ?? ''; + } + if ($tipo === 'firma_profesional') { + $minutos = null; + $horaFija = null; + if ($separadorLabel !== null) { + // "Taukit · Minuto 10" → 10; "Toma · Minuto 60" → 60 + if (preg_match('/Minuto\s+(\d+)/i', $separadorLabel, $m)) { + $minutos = (int)$m[1]; + } + // "3:00 p.m." / "4:00 p.m." → hora fija + 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); + } + } + $firmasCampos[] = ['id' => $campo['id'], 'minutos' => $minutos, 'hora_fija' => $horaFija]; + } +} + +// ── Determinar estado y siguiente_toma_at ──────────────────── +$firmados = array_filter($firmasCampos, fn($f) => isset($datosMerge[$f['id']]) && strlen($datosMerge[$f['id']]) > 10); +$pendientes = array_filter($firmasCampos, fn($f) => !isset($datosMerge[$f['id']]) || strlen($datosMerge[$f['id']]) <= 10); +$pendientes = array_values($pendientes); + +$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 { + // Calcular cuándo es la siguiente toma + $next = $pendientes[0]; + if ($next['hora_fija'] !== null) { + // Hora fija en el día actual + $siguienteToma = date('Y-m-d') . ' ' . $next['hora_fija']; + } elseif ($next['minutos'] !== null && $tomaInicioAt) { + // Minuto relativo desde el inicio de la toma + $siguienteToma = date('Y-m-d H:i:s', strtotime($tomaInicioAt) + $next['minutos'] * 60); + } +} + +// ── 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')); diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php index c8b0204..6604ec8 100644 --- a/modules/turnero/views/lugar.php +++ b/modules/turnero/views/lugar.php @@ -265,11 +265,18 @@ try { padding: .5rem .7rem; border-radius: 10px; margin-bottom: .35rem; font-size: .84rem; border: 1px solid transparent; min-height: 46px; } - .consent-row.firmado { background: #f0fdf4; color: #166534; border-color: #bbf7d0; } - .consent-row.rechazado { background: #f8fafc; color: #64748b; border-color: #e2e8f0; } - .consent-row.enviado { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; } - .consent-row.pendiente { background: #fffbeb; color: #92400e; border-color: #fde68a; } - .consent-row.visto { background: #faf5ff; color: #6b21a8; border-color: #ddd6fe; } + .consent-row.firmado { background: #f0fdf4; color: #166534; border-color: #bbf7d0; } + .consent-row.rechazado { background: #f8fafc; color: #64748b; border-color: #e2e8f0; } + .consent-row.enviado { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; } + .consent-row.pendiente { background: #fffbeb; color: #92400e; border-color: #fde68a; } + .consent-row.visto { background: #faf5ff; color: #6b21a8; border-color: #ddd6fe; } + .consent-row.en_progreso { background: #fff7ed; color: #9a3412; border-color: #fed7aa; } + .consent-row.en_progreso.alerta { background: #fef2f2; color: #991b1b; border-color: #fca5a5; + animation: parpadeo-alerta 1s infinite; } + @keyframes parpadeo-alerta { 0%,100%{opacity:1} 50%{opacity:.6} } + .toma-progreso { font-size:.67rem; background:#ea580c; color:#fff; + border-radius:99px; padding:1px 7px; font-weight:700; white-space:nowrap; } + .toma-countdown { font-size:.67rem; color:inherit; opacity:.75; white-space:nowrap; } .consent-row .nom-form { flex: 1; font-weight: 500; } .consent-row .c-badge { font-size: .67rem; padding: 1px 8px; border-radius: 99px; border: 1px solid currentColor; font-weight: 700; opacity: .85; } @@ -1090,17 +1097,46 @@ async function cargarFichaSolicitud(turnoId) { const CONSENT_IC = { firmado:'fa-check-circle', rechazado:'fa-ban', enviado:'fa-envelope', visto:'fa-eye', pendiente:'fa-clock', + en_progreso:'fa-hourglass-half', }; const CONSENT_LBL = { firmado:'Firmado', rechazado:'Rechazado', enviado:'Enviado', visto:'Visto', pendiente:'Pendiente', + en_progreso:'En progreso', }; +// ── Tomas progresivas: actualiza countdowns cada segundo ────── +let _tomaTimers = {}; // consent_id → { siguiente_toma_at, es_alerta } +setInterval(() => { + const ahora = Date.now(); + for (const [cid, info] of Object.entries(_tomaTimers)) { + const row = document.querySelector(`[data-consent-id="${cid}"]`); + if (!row) continue; + const cdEl = row.querySelector('.toma-countdown'); + if (!cdEl) continue; + if (!info.siguiente_toma_at) { cdEl.textContent = ''; continue; } + const diff = Math.floor((info.siguiente_toma_at - ahora) / 1000); + if (diff <= 0) { + cdEl.textContent = '¡Hora de toma!'; + row.classList.add('alerta'); + if (!info.alertado) { + info.alertado = true; + try { new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAA...').play().catch(()=>{}); } catch(_) {} + } + } else { + row.classList.remove('alerta'); + const m = Math.floor(diff / 60), s = diff % 60; + cdEl.textContent = `próx. toma en ${m}:${String(s).padStart(2,'0')}`; + } + } +}, 1000); + function renderConsentimientos(lista) { // Mostrar consentimientos de examen (origen null) + los de esta estación lista = lista.filter(c => !c.origen_lugar_id || c.origen_lugar_id == lugarId); tieneConsent = lista.length > 0; hayPendientes = lista.some(c => !['firmado','rechazado'].includes(c.estado)); - const pendCount = lista.filter(c => !['firmado','rechazado'].includes(c.estado)).length; + // en_progreso bloquea finalizar pero no muestra aviso de "pendiente sin acción" + const pendCount = lista.filter(c => ['pendiente','enviado','visto'].includes(c.estado)).length; const listEl = document.getElementById('lista-consent'); const sinEl = document.getElementById('sin-consent'); @@ -1131,6 +1167,17 @@ function renderConsentimientos(lista) { banner.classList.toggle('d-none', !hayPendientes); if (btnFin) btnFin.disabled = hayPendientes; + // Actualizar mapa de timers para tomas progresivas + _tomaTimers = {}; + lista.forEach(c => { + if (c.es_toma_progresiva && c.estado === 'en_progreso') { + _tomaTimers[c.id] = { + siguiente_toma_at: c.siguiente_toma_at ? new Date(c.siguiente_toma_at).getTime() : null, + alertado: false, + }; + } + }); + listEl.innerHTML = lista.map(c => { const ya = ['firmado','rechazado'].includes(c.estado); const ico = CONSENT_IC[c.estado] || 'fa-clock'; @@ -1139,6 +1186,15 @@ function renderConsentimientos(lista) { const nomJs = JSON.stringify(c.formulario_nombre || 'Consentimiento'); const idJs = parseInt(c.id) || 0; const tId = parseInt(c.turno_id) || (turnoActivo?.id) || 0; + const fId = parseInt(c.formulario_id) || 0; + + // Badge de progreso para tomas progresivas + let progresoBadge = ''; + if (c.es_toma_progresiva && c.tomas_total > 0) { + progresoBadge = `${c.tomas_firmadas}/${c.tomas_total}`; + } + const countdownEl = (c.es_toma_progresiva && c.estado === 'en_progreso') + ? `` : ''; // Botón ver const btnVer = (ya && c.token) @@ -1149,8 +1205,15 @@ function renderConsentimientos(lista) { // Acción de firma según modo let btnAccion = ''; - if (!ya && c.token) { - if (LUGAR_FORM_MODO === 'embebido') { + const puedeFirmar = !ya || (c.es_toma_progresiva && c.estado === 'en_progreso'); + if (puedeFirmar && c.token) { + if (c.es_toma_progresiva) { + // Toma progresiva: siempre abre el modal embebido para firmar la siguiente toma + btnAccion = ``; + } else if (LUGAR_FORM_MODO === 'embebido') { btnAccion = `