guardar_toma.php (única ruta de firma de F-LAB-28 Tomas Prolongadas) nunca registró quién firmaba: solo guardaba el SVG por campo. De los 111 registros del formulario, 0 tienen identidad del firmante y no hay traza en ninguna tabla de auditoría, así que no es recuperable retroactivamente. Ahora el nombre/cédula se resuelven desde la sesión en el servidor (adminId()) y se guardan por toma — cada toma puede firmarla un profesional distinto, y esta es la única fuente confiable. El intento anterior dependía de que el navegador enviara window._profNombre. historial.php abría el modal sin compact=1&zoom=1.1, que sí usa bandeja; por eso el mismo documento se veía en lista larga en vez de la grilla con tarjetas de toma. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
220 lines
8.8 KiB
PHP
220 lines
8.8 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;
|
|
|
|
// 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'));
|