This commit is contained in:
Lizandro Guarnizo
2026-06-16 12:06:26 -05:00
parent 275170f9d5
commit 415ea3ef56
9 changed files with 348 additions and 47 deletions
@@ -0,0 +1,50 @@
<?php
/**
* POST /modules/turnero/api/firmar_profesional_consentimiento.php
* Guarda la firma del enfermero/profesional en un consentimiento del turnero.
*
* Body JSON:
* turno_id int requerido
* formulario_id int requerido
* svg string requerido (data URI SVG o PNG base64)
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
requireTurnero();
$body = inputJson();
$turnoId = (int)($body['turno_id'] ?? 0);
$formularioId = (int)($body['formulario_id'] ?? 0);
$svg = $body['svg'] ?? '';
if (!$turnoId) jsonError('turno_id requerido.');
if (!$formularioId) jsonError('formulario_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();
$stmt = $pdo->prepare(
"SELECT tc.id, tc.estado, t.sesion_id
FROM turnero_consentimientos tc
JOIN turnero_turnos t ON t.id = tc.turno_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);
if ($tc['estado'] !== 'firmado') jsonError('El paciente debe firmar primero antes de que el profesional pueda firmar.');
$stmt = $pdo->prepare(
"UPDATE turnero_consentimientos
SET firma_profesional_svg = ?, firmado_profesional_at = NOW()
WHERE turno_id = ? AND formulario_id = ?"
);
$stmt->execute([$svg, $turnoId, $formularioId]);
notificarSSE((int)$tc['sesion_id']);
jsonOk(['mensaje' => 'Firma del profesional guardada correctamente.']);
@@ -35,6 +35,8 @@ $stmt = $pdo->prepare(
tc.estado,
tc.enviado_at,
tc.firmado_at,
(tc.firma_profesional_svg IS NOT NULL) AS tiene_firma_profesional,
tc.firmado_profesional_at,
f.nombre AS formulario_nombre
FROM turnero_consentimientos tc
LEFT JOIN lab_formularios f ON f.id = tc.formulario_id
+128 -7
View File
@@ -168,6 +168,9 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
border-radius: 99px; border: 1px solid currentColor; opacity: .85; }
.consent-acciones { display: flex; gap: .3rem; flex-shrink: 0; }
.consent-acciones .btn { font-size: .72rem; padding: 2px 8px; border-radius: 7px; }
.consent-prof-row { display:flex; align-items:center; gap:.4rem; font-size:.75rem;
margin-top:.25rem; padding-top:.25rem; border-top:1px solid rgba(0,0,0,.07); }
.consent-prof-row .badge { font-size:.65rem; }
/* ── Barra de acciones ── */
.ficha-acciones {
@@ -425,6 +428,35 @@ require_once __DIR__ . '/../../../shared/components/sidebar.php';
</div><!-- /rec-layout -->
</main>
<!-- ═══ Modal firma enfermero ═══════════════════════════════════════ -->
<div class="modal fade" id="modalFirmaEnfermero" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title mb-0"><i class="fas fa-pen-nib me-1"></i>Firma del enfermero</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="text-muted small mb-2" id="fe-nombre-consentimiento"></p>
<canvas id="fe-canvas" width="440" height="180"
style="border:1px solid #cbd5e1;border-radius:8px;touch-action:none;cursor:crosshair;max-width:100%"></canvas>
<div class="d-flex justify-content-between mt-2">
<button class="btn btn-sm btn-outline-secondary" onclick="feCanvas.limpiar()">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<span class="text-muted small align-self-center">Firme en el recuadro</span>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary btn-sm" onclick="feGuardarFirma()">
<i class="fas fa-check me-1"></i>Guardar firma
</button>
</div>
</div>
</div>
</div>
<div class="rec-toast" id="rec-toast">
<span class="ico" id="toast-ico"><i class="fas fa-check-circle"></i></span>
<span id="toast-msg"></span>
@@ -875,10 +907,11 @@ function renderConsentimientos(lista) {
const token = escHtml(c.token || '');
const nom = escHtml(c.formulario_nombre || 'Consentimiento');
const fId = c.formulario_id;
const nomJs = (c.formulario_nombre || 'Consentimiento').replace(/'/g, "\\'");
// Botón de firma (solo si no está firmado/rechazado)
// Botón de firma paciente (solo si no está firmado/rechazado)
const btnFirmar = (!ya)
? `<button class="btn btn-outline-primary" onclick="firmarConsentimiento(${fId}, '${nom.replace(/'/g, "\\'")}')"
? `<button class="btn btn-outline-primary" onclick="firmarConsentimiento(${fId}, '${nomJs}')"
title="Abrir firma en nueva pestaña">
<i class="fas fa-external-link-alt"></i> Firmar
</button>` : '';
@@ -893,15 +926,103 @@ function renderConsentimientos(lista) {
<i class="fab fa-whatsapp"></i> WhatsApp
</button>`;
return `<div class="consent-item ${m.cls}">
<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>
// Fila de firma del profesional (solo visible si el paciente ya firmó)
let profRow = '';
if (c.estado === 'firmado') {
const tienePro = c.tiene_firma_profesional == 1 || c.tiene_firma_profesional === true;
if (tienePro) {
profRow = `<div class="consent-prof-row">
<i class="fas fa-user-nurse text-success"></i>
<span class="text-success">Firma enfermero</span>
<span class="badge bg-success ms-1">OK</span>
</div>`;
} else {
profRow = `<div class="consent-prof-row">
<i class="fas fa-user-nurse text-warning"></i>
<span class="text-warning fw-semibold">Falta firma enfermero</span>
<button class="btn btn-warning btn-sm ms-auto" style="font-size:.7rem;padding:1px 7px"
onclick="abrirFirmaEnfermero(${fId}, '${nomJs}')">
<i class="fas fa-pen-nib me-1"></i>Firmar
</button>
</div>`;
}
}
return `<div class="consent-item ${m.cls}" style="flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:.5rem;width:100%">
<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>
${profRow}
</div>`;
}).join('');
}
// ── Firma del enfermero: canvas modal ─────────────────────────
let _feFormularioId = null;
const feCanvas = {
_el: null, _ctx: null, _drawing: false,
init() {
this._el = document.getElementById('fe-canvas');
this._ctx = this._el.getContext('2d');
this._ctx.strokeStyle = '#1e293b';
this._ctx.lineWidth = 2;
this._ctx.lineCap = 'round';
this._el.addEventListener('pointerdown', e => { this._drawing = true; this._ctx.beginPath(); this._move(e); });
this._el.addEventListener('pointermove', e => { if (!this._drawing) return; this._ctx.lineTo(...this._pos(e)); this._ctx.stroke(); });
['pointerup','pointerleave'].forEach(ev => this._el.addEventListener(ev, () => { this._drawing = false; }));
},
_pos(e) { const r = this._el.getBoundingClientRect(); return [e.clientX - r.left, e.clientY - r.top]; },
_move(e) { const [x,y] = this._pos(e); this._ctx.moveTo(x, y); },
limpiar() { this._ctx.clearRect(0, 0, this._el.width, this._el.height); },
vacio() {
const d = this._ctx.getImageData(0, 0, this._el.width, this._el.height).data;
return !d.some(v => v !== 0);
},
png() { return this._el.toDataURL('image/png'); },
};
function abrirFirmaEnfermero(formularioId, nombre) {
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }
_feFormularioId = formularioId;
document.getElementById('fe-nombre-consentimiento').textContent = nombre;
if (!feCanvas._el) feCanvas.init();
feCanvas.limpiar();
const modal = bootstrap.Modal.getOrCreate(document.getElementById('modalFirmaEnfermero'));
modal.show();
}
async function feGuardarFirma() {
if (!turnoActivo || !_feFormularioId) return;
if (feCanvas.vacio()) { mostrarError('Dibuja la firma antes de guardar.'); return; }
const png = feCanvas.png();
const btn = document.querySelector('#modalFirmaEnfermero .btn-primary');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
try {
const res = await fetch(API + 'firmar_profesional_consentimiento.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: _feFormularioId, svg: png }),
});
const json = await res.json();
if (json.ok) {
bootstrap.Modal.getInstance(document.getElementById('modalFirmaEnfermero')).hide();
mostrarToast('Firma del enfermero guardada', 'success');
await refrescarConsentimientosLugar();
} else {
mostrarError(json.error || 'No se pudo guardar la firma.');
}
} catch(e) {
mostrarError('Error al guardar: ' + e.message);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
}
}
// ── Firmar en nueva pestaña ──────────────────────────────────
async function firmarConsentimiento(formularioId, nombre) {
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }