firma_profesional: canvas en ver_formulario_enviado + endpoint firmar_profesional
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user