feat(turnero): tomas de muestras prolongadas con temporizador y progreso
- DB: ALTER TABLE turnero_consentimientos ADD siguiente_toma_at, toma_inicio_at; MODIFY estado para incluir 'en_progreso' - guardar_toma.php (nuevo): guarda firma parcial por campo_id, parsea el esquema para calcular siguiente_toma_at desde 'Minuto X' o hora fija, marca 'en_progreso' o 'firmado' según tomas restantes - get_consentimientos.php: expone siguiente_toma_at, es_toma_progresiva, tomas_total, tomas_firmadas, campos_firma_pro en cada consentimiento - lugar.php: estado en_progreso con badge X/Y, countdown en tiempo real, animación de alerta cuando el tiempo llega, botón 'Siguiente toma' que abre el modal embebido en modo toma_progresiva Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
cf8d011f09
commit
ea1f0e30c9
@@ -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;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/**
|
||||
* POST /modules/turnero/api/guardar_toma.php
|
||||
* Guarda una firma parcial en formularios de tomas prolongadas.
|
||||
* Calcula el próximo intervalo y actualiza estado + siguiente_toma_at.
|
||||
*
|
||||
* Body JSON:
|
||||
* turno_id int requerido
|
||||
* formulario_id int requerido
|
||||
* campo_id string requerido (id del campo firma_profesional firmado)
|
||||
* svg string requerido (data URI)
|
||||
* datos_respuestas object requerido (todos los valores actuales del form)
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireMethod('POST');
|
||||
requireTurnero();
|
||||
|
||||
$body = inputJson();
|
||||
$turnoId = (int)($body['turno_id'] ?? 0);
|
||||
$formularioId = (int)($body['formulario_id'] ?? 0);
|
||||
$campoId = trim($body['campo_id'] ?? '');
|
||||
$svg = $body['svg'] ?? '';
|
||||
$datosInput = is_array($body['datos_respuestas'] ?? null) ? $body['datos_respuestas'] : [];
|
||||
|
||||
if (!$turnoId) jsonError('turno_id requerido.');
|
||||
if (!$formularioId) jsonError('formulario_id requerido.');
|
||||
if (!$campoId) jsonError('campo_id requerido.');
|
||||
if (strlen($svg) < 100) jsonError('Firma requerida.');
|
||||
if (!preg_match('/^data:image\/(svg\+xml|png|jpeg|webp);base64,/i', $svg)) {
|
||||
jsonError('Formato de firma no válido.');
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// ── Cargar consentimiento y esquema ──────────────────────────
|
||||
$stmt = $pdo->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'));
|
||||
Reference in New Issue
Block a user