firma_profesional: canvas en ver_formulario_enviado + endpoint firmar_profesional

This commit is contained in:
Lizandro Guarnizo
2026-03-17 15:55:56 -05:00
parent 922b129094
commit 8e62c29ca9
2 changed files with 198 additions and 3 deletions
+84
View File
@@ -0,0 +1,84 @@
<?php
/**
* firmar_profesional.php — Guarda la firma del profesional en un envío ya completado.
* Requiere: sesión activa (admin o enfermero).
* POST { envio_id: int, campo_id: string, svg: string }
*/
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../classes/Database.php';
header('Content-Type: application/json');
// ── Helpers ─────────────────────────────────────────────────────────
function ok(array $data = []): void {
echo json_encode(['ok' => true] + $data);
exit;
}
function err(string $msg, int $code = 400): void {
http_response_code($code);
echo json_encode(['ok' => false, 'error' => $msg]);
exit;
}
// ── Auth ─────────────────────────────────────────────────────────────
if (!isUserLoggedIn()) {
err('No autenticado', 401);
}
// ── Input ────────────────────────────────────────────────────────────
$body = json_decode(file_get_contents('php://input'), true);
if (!$body) $body = $_POST;
$envioId = (int)($body['envio_id'] ?? 0);
$campoId = trim($body['campo_id'] ?? '');
$svg = $body['svg'] ?? '';
if (!$envioId) err('envio_id requerido');
if ($campoId === '') err('campo_id requerido');
if ($svg === '') err('svg requerido');
// Validar que el SVG sea data URI de imagen (evita inyección de contenido)
if (!preg_match('/^data:image\/(svg\+xml|png|jpeg|webp);base64,/i', $svg)) {
err('Formato de firma no válido');
}
// ── Cargar envío ─────────────────────────────────────────────────────
$db = Database::getInstance();
$envio = $db->fetch('SELECT id, datos_cliente, estado, formulario_id FROM lab_form_envios WHERE id = ?', [$envioId]);
if (!$envio) err('Envío no encontrado', 404);
// Enfermero: solo puede firmar sus propios envíos
if (isEnfermero()) {
$uid = (int)($_SESSION['admin_user']['id'] ?? 0);
$propio = $db->fetch('SELECT id FROM lab_form_envios WHERE id = ? AND enviado_por = ?', [$envioId, $uid]);
if (!$propio) err('Sin acceso', 403);
}
// ── Validar que campo_id corresponde a una firma_profesional ─────────
$form = $db->fetch('SELECT esquema FROM lab_formularios WHERE id = ?', [$envio['formulario_id']]);
if (!$form) err('Formulario no encontrado', 404);
$esquema = json_decode($form['esquema'], true) ?? [];
$campoDef = null;
foreach ($esquema as $c) {
if (($c['id'] ?? '') === $campoId) { $campoDef = $c; break; }
}
if (!$campoDef || ($campoDef['tipo'] ?? '') !== 'firma_profesional') {
err('El campo no es una firma_profesional');
}
// ── Actualizar datos_cliente ─────────────────────────────────────────
$datos = json_decode($envio['datos_cliente'] ?? '{}', true) ?? [];
$datos[$campoId . '_svg'] = $svg;
$nuevosDatos = json_encode($datos, JSON_UNESCAPED_UNICODE);
$nuevoEstado = $envio['estado'] === 'firmado' ? 'firmado' : 'firmado';
$db->query(
'UPDATE lab_form_envios SET datos_cliente = ?, estado = ?, completado_en = COALESCE(completado_en, NOW()) WHERE id = ?',
[$nuevosDatos, $nuevoEstado, $envioId]
);
ok(['mensaje' => 'Firma del profesional guardada correctamente']);
+114 -3
View File
@@ -166,6 +166,11 @@ function esc2(mixed $v): string {
.hash-seal-header { background: #000 !important; -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
/* ── Canvas firma profesional ───────────────────── */
.firma-pro-widget { max-width: 520px; margin-top: 8px; }
.fpw-canvas { border: 2px solid #198754; border-radius: 8px; background: #f8fff9;
cursor: crosshair; display: block; max-width: 100%; touch-action: none; }
/* ═══════ ESTILOS DE IMPRESIÓN ═══════════════════ */
@media print {
body { background: #fff !important; font-size: 12px; }
@@ -283,11 +288,12 @@ function esc2(mixed $v): string {
if (!$cid) continue;
$fSvg = $datosCliente[$cid . '_svg'] ?? null;
$fFoto = $datosCliente[$cid . '_foto'] ?? null;
if (!$fSvg && !$fFoto) continue;
$isPro = ($tipo === 'firma_profesional');
$fIcon = $isPro ? 'fa-user-md' : 'fa-signature';
$fColor = $isPro ? '#198754' : '#1565c0';
$fLabel = htmlspecialchars($campo['label'] ?? ($isPro ? 'Firma profesional' : 'Firma paciente'));
// Firma paciente sin contenido: omitir
if (!$isPro && !$fSvg && !$fFoto) continue;
?>
<div class="section-title mt-3" style="color:<?= $fColor ?>">
<i class="fas <?= $fIcon ?> me-1"></i><?= $fLabel ?>
@@ -296,11 +302,22 @@ function esc2(mixed $v): string {
<div class="firma-box" style="border-color:<?= $fColor ?>">
<img src="<?= htmlspecialchars($fSvg) ?>" alt="<?= $fLabel ?>">
</div>
<?php endif; ?>
<?php if ($fFoto): ?>
<?php elseif ($fFoto): ?>
<div class="firma-box mt-2" style="border-color:<?= $fColor ?>">
<img src="<?= htmlspecialchars($fFoto) ?>" alt="Foto - <?= $fLabel ?>">
</div>
<?php elseif ($isPro): ?>
<!-- Canvas para que el profesional firme ahora -->
<div class="firma-pro-widget no-print" id="fpw-<?= htmlspecialchars($cid) ?>"
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>">
<p class="text-muted small mb-2"><i class="fas fa-pen me-1"></i>Dibuje su firma en el recuadro:</p>
<canvas class="fpw-canvas" width="500" height="150"></canvas>
<div class="mt-2 d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm fpw-clear"><i class="fas fa-eraser me-1"></i>Limpiar</button>
<button class="btn btn-success btn-sm fpw-save"><i class="fas fa-check me-1"></i>Guardar firma</button>
</div>
<div class="fpw-msg mt-2 small"></div>
</div>
<?php endif; ?>
<?php continue; endif;
@@ -422,5 +439,99 @@ function esc2(mixed $v): string {
</div><!-- /doc-wrap -->
<script>
/* ── Canvas firma del profesional ───────────────────────────────── */
(function () {
document.querySelectorAll('.firma-pro-widget').forEach(function (widget) {
const canvas = widget.querySelector('.fpw-canvas');
const ctx = canvas.getContext('2d');
const btnClear = widget.querySelector('.fpw-clear');
const btnSave = widget.querySelector('.fpw-save');
const msg = widget.querySelector('.fpw-msg');
const envioId = parseInt(widget.dataset.envio, 10);
const campoId = widget.dataset.campo;
let drawing = false;
// Ajustar resolución para HiDPI
(function scaleCanvas() {
const ratio = window.devicePixelRatio || 1;
const w = canvas.offsetWidth || canvas.width;
const h = canvas.offsetHeight || canvas.height;
canvas.width = w * ratio;
canvas.height = h * ratio;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.scale(ratio, ratio);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
})();
function getPos(e) {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
}
canvas.addEventListener('mousedown', function(e){ drawing=true; ctx.beginPath(); const p=getPos(e); ctx.moveTo(p.x,p.y); });
canvas.addEventListener('mousemove', function(e){ if(!drawing) return; const p=getPos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); });
canvas.addEventListener('mouseup', function(){ drawing=false; });
canvas.addEventListener('mouseleave', function(){ drawing=false; });
canvas.addEventListener('touchstart', function(e){ e.preventDefault(); drawing=true; ctx.beginPath(); const p=getPos(e); ctx.moveTo(p.x,p.y); }, {passive:false});
canvas.addEventListener('touchmove', function(e){ e.preventDefault(); if(!drawing) return; const p=getPos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); }, {passive:false});
canvas.addEventListener('touchend', function(){ drawing=false; });
btnClear.addEventListener('click', function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
msg.textContent = '';
});
btnSave.addEventListener('click', function() {
const svg = canvas.toDataURL('image/png');
// Verificar que no esté vacío (al menos 1000 bytes de data)
if (svg.length < 1000) {
msg.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-triangle me-1"></i>Por favor dibuje su firma antes de guardar.</span>';
return;
}
btnSave.disabled = true;
btnSave.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando...';
msg.textContent = '';
fetch('api/lab/firmar_profesional.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ envio_id: envioId, campo_id: campoId, svg: svg })
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (data.ok) {
// Reemplazar el canvas con la imagen firmada
const img = document.createElement('img');
img.src = svg;
img.alt = 'Firma profesional';
img.style.maxHeight = '140px';
img.style.maxWidth = '340px';
img.style.display = 'block';
const box = document.createElement('div');
box.className = 'firma-box';
box.style.borderColor = '#198754';
box.appendChild(img);
widget.replaceWith(box);
} else {
msg.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>' + (data.error||'Error al guardar') + '</span>';
btnSave.disabled = false;
btnSave.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
}
})
.catch(function() {
msg.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>Error de conexión.</span>';
btnSave.disabled = false;
btnSave.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
});
});
});
})();
</script>
</body>
</html>