diff --git a/modules/turnero/api/resetear_consentimiento.php b/modules/turnero/api/resetear_consentimiento.php new file mode 100644 index 0000000..bca553b --- /dev/null +++ b/modules/turnero/api/resetear_consentimiento.php @@ -0,0 +1,40 @@ +prepare( + "SELECT tc.id, t.sesion_id + FROM turnero_consentimientos tc + JOIN turnero_turnos t ON t.id = tc.turno_id + WHERE tc.token = ? LIMIT 1" +); +$stmt->execute([$token]); +$tc = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$tc) jsonError('Consentimiento no encontrado.', 404); + +$pdo->prepare( + "UPDATE turnero_consentimientos + SET estado = 'enviado', + firma_svg = NULL, + firmado_at = NULL, + firma_profesional_svg = NULL, + firmado_profesional_at = NULL, + datos_respuestas = NULL, + siguiente_toma_at = NULL, + toma_inicio_at = NULL + WHERE token = ?" +)->execute([$token]); + +notificarSSE((int)$tc['sesion_id']); +jsonOk([], 'Consentimiento reiniciado correctamente.'); diff --git a/modules/turnero/api/resetear_toma.php b/modules/turnero/api/resetear_toma.php new file mode 100644 index 0000000..3ffc8db --- /dev/null +++ b/modules/turnero/api/resetear_toma.php @@ -0,0 +1,39 @@ +prepare( + "SELECT tc.id, tc.datos_respuestas, tc.estado, t.sesion_id + FROM turnero_consentimientos tc + JOIN turnero_turnos t ON t.id = tc.turno_id + WHERE tc.token = ? LIMIT 1" +); +$stmt->execute([$token]); +$tc = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$tc) jsonError('Consentimiento no encontrado.', 404); +if ($tc['estado'] !== 'en_progreso') jsonError('Solo se pueden olvidar tomas en estado en_progreso.'); + +$dr = json_decode($tc['datos_respuestas'] ?? '{}', true) ?? []; +foreach ($campos as $campo) { + unset($dr[$campo]); +} +$pdo->prepare( + "UPDATE turnero_consentimientos SET datos_respuestas=? WHERE token=?" +)->execute([json_encode($dr, JSON_UNESCAPED_UNICODE), $token]); + +notificarSSE((int)$tc['sesion_id']); +jsonOk([], 'Toma olvidada.'); diff --git a/modules/turnero/api/save_dispositivo.php b/modules/turnero/api/save_dispositivo.php index 54fcdfd..2dc8ac9 100644 --- a/modules/turnero/api/save_dispositivo.php +++ b/modules/turnero/api/save_dispositivo.php @@ -19,7 +19,7 @@ if ($delete) { jsonOk([], 'Dispositivo eliminado'); } -$ip = trim($input['ip'] ?? ''); +$ip = trim($input['ip'] ?? '') ?: null; $nombre = trim($input['nombre'] ?? ''); $lugarId = (int)($input['lugar_id'] ?? 0); $activo = (int)($input['activo'] ?? 1); @@ -27,7 +27,7 @@ $token = trim($input['token'] ?? '') ?: null; if ($nombre === '') jsonError('El nombre es requerido'); if ($lugarId <= 0) jsonError('Debes asignar un lugar'); -if ($ip === '' && $token === null) jsonError('Se requiere IP o token de dispositivo'); +if ($ip === null && $token === null) jsonError('Se requiere IP o token de dispositivo'); if ($id) { $stmt = db()->prepare( diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php index 350b7f6..cd5af58 100644 --- a/modules/turnero/views/lugar.php +++ b/modules/turnero/views/lugar.php @@ -1633,6 +1633,12 @@ function renderConsentimientos(lista) { `; + const btnOlvidar = (ya && c.token && c.estado !== 'rechazado') + ? `` : ''; + const motivoRow = (c.estado === 'cerrado_anticipado' && c.cierre_anticipado?.motivo) ? `
${escHtml(c.cierre_anticipado.motivo)} @@ -1644,13 +1650,27 @@ function renderConsentimientos(lista) { ${progresoBadge} ${countdownEl} ${label} - +
${motivoRow} `; }).join(''); } +// ── Olvidar (resetear) consentimiento ──────────────────────── +async function _olvidarConsentimiento(token, nombre) { + if (!confirm(`¿Seguro que deseas descartar la firma de "${nombre}"?\nEl paciente podrá volver a firmarlo.`)) return; + try { + const r = await fetch(API + 'resetear_consentimiento.php', { + method: 'POST', headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ token }) + }); + const j = await r.json(); + if (!j.ok) { mostrarError(j.error || 'Error al resetear'); return; } + cargarConsentimientos(); + } catch(e) { mostrarError(e.message); } +} + // ── Quitar consentimiento ───────────────────────────────────── let _mqcConsentId = null; function _abrirModalQuitarConsent(consentId, formNombre) { diff --git a/modules/turnero/views/recepcion.php b/modules/turnero/views/recepcion.php index 60c5d74..78f5f10 100644 --- a/modules/turnero/views/recepcion.php +++ b/modules/turnero/views/recepcion.php @@ -2016,6 +2016,12 @@ function renderConsentimientos(lista) { WhatsApp `; + const btnOlvidar = (ya && c.token && c.estado !== 'rechazado') + ? `` : ''; + // Fila de firma del profesional (solo si el formulario la requiere y el paciente ya firmó) let profRow = ''; if (c.estado === 'firmado' && c.requiere_firma_profesional) { @@ -2047,7 +2053,7 @@ function renderConsentimientos(lista) { ${nom} ${m.lbl} - + ${profRow} ${motivoRec} @@ -2206,6 +2212,20 @@ async function enviarConsentimientoUno(turnoId) { // ── Enviar consentimientos (legacy — usado por btn-enviar-consent) ── async function enviarConsentimientos() { return enviarConsentimientosTodos(); } +// ── Olvidar (resetear) consentimiento ──────────────────────── +async function _olvidarConsentimientoRec(token, nombre) { + if (!confirm(`¿Seguro que deseas descartar la firma de "${nombre}"?\nEl paciente podrá volver a firmarlo.`)) return; + try { + const r = await fetch('modules/turnero/api/resetear_consentimiento.php', { + method: 'POST', headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ token }) + }); + const j = await r.json(); + if (!j.ok) { alert(j.error || 'Error al resetear'); return; } + await cargarConsentimientos(); + } catch(e) { alert(e.message); } +} + // ── Pasar a lugar ───────────────────────────────────────────── async function pasarALugar() { if (!turnoActivo || !solicitudActiva) return; diff --git a/ver_formulario_enviado.php b/ver_formulario_enviado.php index 365353c..1f8dc4a 100644 --- a/ver_formulario_enviado.php +++ b/ver_formulario_enviado.php @@ -86,10 +86,14 @@ if ($modoTurnero) { echo json_encode(['ok' => true, 'mp_completado' => true]); exit; } if (!array_key_exists('firma_svg', $input) && isset($input['datos_respuestas']) && is_array($input['datos_respuestas'])) { - $existing = json_decode($tcRow['datos_respuestas'] ?? '{}', true) ?: []; - $db->getConnection() - ->prepare("UPDATE turnero_consentimientos SET datos_respuestas=? WHERE token=? AND estado='pendiente'") - ->execute([json_encode(array_merge($existing, $input['datos_respuestas']), JSON_UNESCAPED_UNICODE), $tokenTurnero]); + $existing = json_decode($tcRow['datos_respuestas'] ?? '{}', true) ?: []; + $allowSave = $tcRow['estado'] === 'pendiente' + || ($modoTurnero && $tcRow['estado'] === 'en_progreso'); + if ($allowSave) { + $db->getConnection() + ->prepare("UPDATE turnero_consentimientos SET datos_respuestas=? WHERE token=?") + ->execute([json_encode(array_merge($existing, $input['datos_respuestas']), JSON_UNESCAPED_UNICODE), $tokenTurnero]); + } echo json_encode(['ok' => true, 'draft' => true]); exit; } $firmaSvg = trim($input['firma_svg'] ?? ''); @@ -389,6 +393,46 @@ if (!empty($_mpMap)) { } } +// Mapa firma_id → obs_campo_id (campo texto/textarea que acompaña cada firma) +$_mpFirmaObs = []; +if (!empty($esquema) && !empty($_mpMap)) { + $_prevFirma = null; $_pendingObs = null; + foreach ($esquema as $_oc) { + $_ot = $_oc['tipo'] ?? ''; + if ($_ot === 'separador') { + $_prevFirma = null; $_pendingObs = null; + } elseif (($_ot === 'texto' || $_ot === 'textarea') && isset($_oc['id'])) { + if ($_prevFirma !== null) { + $_mpFirmaObs[$_prevFirma] = $_oc['id']; + $_prevFirma = null; + } else { + $_pendingObs = $_oc['id']; + } + } elseif ($_ot === 'firma_profesional' && isset($_oc['id']) && isset($_mpMap[$_oc['id']])) { + if ($_pendingObs !== null) { $_mpFirmaObs[$_oc['id']] = $_pendingObs; } + $_prevFirma = $_oc['id']; $_pendingObs = null; + } elseif (!in_array($_ot, ['hora'])) { + $_prevFirma = null; + } + } + foreach ($_mpFirmaObs as $_fid => $_obsId) { + if (isset($_mpMap[$_fid])) $_mpMap[$_fid]['obs_campo'] = $_obsId; + } +} + +// Horas de tomas ya firmadas → read-only en edición (no se pueden corregir si ya se firmó esa toma) +$_signedHoraCampos = []; +foreach ($_mpMap as $_fid => $_mpe) { + if ((!empty($datosCliente[$_fid]) && strlen($datosCliente[$_fid]) > 10) + || (!empty($datosCliente[$_fid . '_svg']) && strlen($datosCliente[$_fid . '_svg']) > 10)) { + if (!empty($_mpe['hora_campo'])) $_signedHoraCampos[$_mpe['hora_campo']] = true; + } +} +$_hcToFirmaIds = []; +foreach ($_mpMap as $_fid => $_mpe) { + if (!empty($_mpe['hora_campo'])) $_hcToFirmaIds[$_mpe['hora_campo']][] = $_fid; +} + // siguiente_toma_at para recuperar countdown si el modal fue cerrado y reabierto $_mpSiguienteTomAt = null; if ($modoTurnero && $embebido && !empty($_mpMap) && $_mpPrimeraPendiente !== null) { @@ -717,6 +761,7 @@ function _addExamWizardHtml(string $cid): string { .toma-card--locked .toma-card-num { background:#e2e8f0;color:#94a3b8; } .toma-card-lbl { font-weight:700;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .toma-card-hora { font-size:.75rem;opacity:.75;white-space:nowrap; } + .toma-card-obs { font-size:.72rem;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px; } .toma-card-badge { font-size:.7rem;padding:2px 8px;border-radius:20px;white-space:nowrap;flex-shrink:0; } .toma-badge-done { background:#dcfce7;color:#166534; } .toma-badge-active { background:rgba(255,255,255,.2);color:#fff;border:1px solid rgba(255,255,255,.35); } @@ -1242,12 +1287,30 @@ function _addExamWizardHtml(string $cid): string { $inputFullClass = ($compact && $tipo === 'texto') ? ' campo-full' : ''; $extraClase = htmlspecialchars($campo['clase'] ?? '', ENT_QUOTES); ?> + +
+ +
+ + + + +
+
+
>
+ + +
+ +
+