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; }
|
.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 ═══════════════════ */
|
/* ═══════ ESTILOS DE IMPRESIÓN ═══════════════════ */
|
||||||
@media print {
|
@media print {
|
||||||
body { background: #fff !important; font-size: 12px; }
|
body { background: #fff !important; font-size: 12px; }
|
||||||
@@ -283,11 +288,12 @@ function esc2(mixed $v): string {
|
|||||||
if (!$cid) continue;
|
if (!$cid) continue;
|
||||||
$fSvg = $datosCliente[$cid . '_svg'] ?? null;
|
$fSvg = $datosCliente[$cid . '_svg'] ?? null;
|
||||||
$fFoto = $datosCliente[$cid . '_foto'] ?? null;
|
$fFoto = $datosCliente[$cid . '_foto'] ?? null;
|
||||||
if (!$fSvg && !$fFoto) continue;
|
|
||||||
$isPro = ($tipo === 'firma_profesional');
|
$isPro = ($tipo === 'firma_profesional');
|
||||||
$fIcon = $isPro ? 'fa-user-md' : 'fa-signature';
|
$fIcon = $isPro ? 'fa-user-md' : 'fa-signature';
|
||||||
$fColor = $isPro ? '#198754' : '#1565c0';
|
$fColor = $isPro ? '#198754' : '#1565c0';
|
||||||
$fLabel = htmlspecialchars($campo['label'] ?? ($isPro ? 'Firma profesional' : 'Firma paciente'));
|
$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 ?>">
|
<div class="section-title mt-3" style="color:<?= $fColor ?>">
|
||||||
<i class="fas <?= $fIcon ?> me-1"></i><?= $fLabel ?>
|
<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 ?>">
|
<div class="firma-box" style="border-color:<?= $fColor ?>">
|
||||||
<img src="<?= htmlspecialchars($fSvg) ?>" alt="<?= $fLabel ?>">
|
<img src="<?= htmlspecialchars($fSvg) ?>" alt="<?= $fLabel ?>">
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php elseif ($fFoto): ?>
|
||||||
<?php if ($fFoto): ?>
|
|
||||||
<div class="firma-box mt-2" style="border-color:<?= $fColor ?>">
|
<div class="firma-box mt-2" style="border-color:<?= $fColor ?>">
|
||||||
<img src="<?= htmlspecialchars($fFoto) ?>" alt="Foto - <?= $fLabel ?>">
|
<img src="<?= htmlspecialchars($fFoto) ?>" alt="Foto - <?= $fLabel ?>">
|
||||||
</div>
|
</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 endif; ?>
|
||||||
<?php continue; endif;
|
<?php continue; endif;
|
||||||
|
|
||||||
@@ -422,5 +439,99 @@ function esc2(mixed $v): string {
|
|||||||
|
|
||||||
</div><!-- /doc-wrap -->
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user