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}
-
${btnAccion}${btnVer}${btnQuitar}
+
${btnAccion}${btnVer}${btnOlvidar}${btnQuitar}
${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}
- ${btnFirmar}${btnWa}
+ ${btnFirmar}${btnWa}${btnOlvidar}
${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);
?>
+
+
+
+
+
+
+
+
+