Files
whatsapp/modules/turnero/api/guardar_toma.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 2eefe3f5c8 feat: ciclos configurables + gestión de exámenes en Tomas Prolongadas
Feature 1 — Ciclos configurables por paciente:
- ver_formulario_enviado.php: pantalla de configuración aparece la primera
  vez que el profesional abre el formulario (antes de firmar); permite
  seleccionar qué tomas realizar para cada protocolo. $_mpSkippedSepIds
  oculta las secciones no seleccionadas en render. $_mpMap se reconstruye
  desde los grupos filtrados para que next_firma_id salte tomas omitidas.
- configurar_tomas.php (nuevo): guarda _tomas_config en datos_respuestas
  del consentimiento; valida que cada grupo tenga al menos una toma.
- guardar_toma.php: aplica el filtro _tomas_config al array firmasCampos
  para que el conteo de tomas firmadas/pendientes sea correcto.

Feature 2 — Gestión de tipos de examen (admin):
- lab_tomas_config.php (nuevo): página admin que muestra los exámenes
  actuales con sus tomas y permite añadir nuevos tipos con su esquema
  completo (separador + hora + resultado + firma_profesional por toma).
- api/lab/save_tomas_config.php (nuevo): modifica el esquema JSON del
  formulario id=15; add_exam agrega opción al selector y los campos de
  cada toma; delete_exam elimina la opción y los separadores exclusivos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-10 10:19:11 -05:00

178 lines
7.0 KiB
PHP

<?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 ──────
// 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);
}
}
$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 $__g => $__ids) {
if (is_array($__ids)) foreach ($__ids as $__id) $__allowed[$__id] = true;
}
$firmasCampos = array_values(array_filter($firmasCampos, fn($f) => isset($__allowed[$f['id']])));
}
// ── 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'));