feat: olvidar firma, editar tiempos tomas, obs en header, fix dispositivos

- feat(turnero): Botón "Olvidar" en lugar.php y recepcion.php para descartar firma de consentimiento completo (nuevo endpoint resetear_consentimiento.php)
- feat(turnero): Botón "Olvidar" por toma individual en prolongadas para borrar firma+hora de una toma específica (nuevo endpoint resetear_toma.php)
- feat(turnero): Editar tiempos en tomas en_progreso: hora_campo signed muestra readonly con botón olvidar, permite re-editar tras olvidar
- feat(turnero): Guardar tiempos (draft save) también en estado en_progreso
- feat(turnero): Observaciones de tomas prolongadas se muestran en header de tarjeta firmada (campo obs_campo en _mpMap)
- fix(turnero): Token del equipo se sobreescribía en tabla — ip permitía solo un registro con ip='' (UNIQUE); ahora ip es nullable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-28 07:37:09 -05:00
co-authored by Claude Sonnet 4.6
parent 8e9d7750a7
commit ff52fa5ba7
6 changed files with 278 additions and 9 deletions
@@ -0,0 +1,40 @@
<?php
/**
* POST /modules/turnero/api/resetear_consentimiento.php
* Descarta firma y datos de un consentimiento → estado 'enviado' para re-firma.
* Body JSON: { token: string }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireTurnero();
$body = inputJson();
$token = trim($body['token'] ?? '');
if (!$token) jsonError('token requerido.');
$pdo = db();
$stmt = $pdo->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.');
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* POST /modules/turnero/api/resetear_toma.php
* Borra campos específicos de datos_respuestas (firma + hora de una toma).
* Body JSON: { token: string, campos: string[] }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireTurnero();
$body = inputJson();
$token = trim($body['token'] ?? '');
$campos = $body['campos'] ?? [];
if (!$token) jsonError('token requerido.');
if (!is_array($campos) || empty($campos)) jsonError('campos requerido.');
$pdo = db();
$stmt = $pdo->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.');
+2 -2
View File
@@ -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(
+21 -1
View File
@@ -1633,6 +1633,12 @@ function renderConsentimientos(lista) {
<i class="fas fa-trash"></i>
</button>`;
const btnOlvidar = (ya && c.token && c.estado !== 'rechazado')
? `<button class="btn btn-outline-warning" title="Olvidar firma — permite re-firmar"
onclick="_olvidarConsentimiento('${token}', ${nomJs.replace(/"/g,'&quot;')})">
<i class="fas fa-undo"></i>
</button>` : '';
const motivoRow = (c.estado === 'cerrado_anticipado' && c.cierre_anticipado?.motivo)
? `<div style="font-size:.75rem;color:#92400e;background:#fef3c7;border-radius:4px;padding:3px 8px;margin-top:3px;width:100%">
<i class="fas fa-comment-alt me-1"></i>${escHtml(c.cierre_anticipado.motivo)}
@@ -1644,13 +1650,27 @@ function renderConsentimientos(lista) {
${progresoBadge}
${countdownEl}
<span class="c-badge">${label}</span>
<div class="acciones-consent">${btnAccion}${btnVer}${btnQuitar}</div>
<div class="acciones-consent">${btnAccion}${btnVer}${btnOlvidar}${btnQuitar}</div>
</div>
${motivoRow}
</div>`;
}).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) {
+21 -1
View File
@@ -2016,6 +2016,12 @@ function renderConsentimientos(lista) {
<i class="fab fa-whatsapp"></i> WhatsApp
</button>`;
const btnOlvidar = (ya && c.token && c.estado !== 'rechazado')
? `<button class="btn btn-outline-warning btn-sm" title="Olvidar firma — permite re-firmar"
onclick="_olvidarConsentimientoRec('${token}', '${escHtml(c.formulario_nombre || '')}')">
<i class="fas fa-undo"></i>
</button>` : '';
// 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) {
<i class="fas ${m.ico}"></i>
<span class="c-nom">${nom}</span>
<span class="c-badge">${m.lbl}</span>
<div class="consent-acciones">${btnFirmar}${btnWa}</div>
<div class="consent-acciones">${btnFirmar}${btnWa}${btnOlvidar}</div>
</div>
${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;