up
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user