feat(firma): integración Topaz SigWeb en ver_formulario_enviado.php

- CSS/HTML: mismo overlay #topaz-overlay con modal de estado
- JS: módulo topaz con target={canvas, onAccept} para canvas
  del profesional (.fpw-canvas) y del paciente turnero (.turnero-cv)
- Botón "Tableta" en canvas del paciente (.turnero-topaz) y del
  profesional (.fpw-topaz); el usuario sigue confirmando manualmente

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-03 17:11:29 -05:00
co-authored by Claude Sonnet 4.6
parent 9e19c1c8c7
commit 67efd470de
+177 -1
View File
@@ -359,6 +359,28 @@ function esc2(mixed $v): string {
.doc-footer { padding: 10px 22px; }
@page { margin: 1cm; }
}
/* ── Topaz SigWeb overlay ─────────────────────────── */
#topaz-overlay { display:none; position:fixed; inset:0; z-index:9999;
background:rgba(0,0,0,.55); align-items:center;
justify-content:center; padding:16px; }
.topaz-modal { background:#fff; border-radius:16px; width:100%;
max-width:400px; box-shadow:0 12px 40px rgba(0,0,0,.3); overflow:hidden; }
.topaz-modal-hdr { background:linear-gradient(135deg,#1565c0,#0288d1);
color:#fff; padding:14px 18px; font-weight:700; font-size:.95rem;
display:flex; align-items:center; gap:8px; }
.topaz-modal-body { padding:20px 18px; }
.topaz-pad-area { border:2px dashed #adb5bd; border-radius:10px; padding:24px 16px;
text-align:center; background:#f8fafc; min-height:100px;
display:flex; flex-direction:column; align-items:center;
justify-content:center; gap:6px; transition:border-color .2s; }
.topaz-pad-area.has-sig { border-color:#198754; border-style:solid; background:#f0fff4; }
.topaz-pts-badge { font-size:.8rem; color:#64748b; }
.topaz-modal-footer { display:flex; gap:8px; justify-content:flex-end;
padding:12px 18px; border-top:1px solid #f1f5f9; flex-wrap:wrap; }
.btn-topaz { font-size:.83rem; padding:6px 13px; border-radius:8px; border:1.5px solid #0288d1;
background:#fff; color:#0288d1; cursor:pointer; font-weight:600;
display:inline-flex; align-items:center; gap:5px; transition:all .15s; }
.btn-topaz:hover { background:#e0f2fe; }
</style>
</head>
<body>
@@ -553,6 +575,9 @@ function esc2(mixed $v): string {
<button class="btn btn-outline-secondary btn-sm turnero-limpiar">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button class="btn-topaz turnero-topaz" title="Usar pad biométrico Topaz">
<i class="fas fa-tablet-alt"></i>Tableta
</button>
<button class="btn btn-success fw-semibold turnero-firmar">
<i class="fas fa-check-circle me-1"></i>Confirmar: <?= $fLabel ?>
</button>
@@ -575,8 +600,11 @@ function esc2(mixed $v): string {
data-formulario="<?= isset($tcRow) ? (int)$tcRow['formulario_id'] : '' ?>">
<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">
<div class="mt-2 d-flex gap-2 flex-wrap">
<button class="btn btn-outline-secondary btn-sm fpw-clear"><i class="fas fa-eraser me-1"></i>Limpiar</button>
<button class="btn-topaz fpw-topaz" title="Usar pad biométrico Topaz">
<i class="fas fa-tablet-alt"></i>Tableta
</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>
@@ -897,6 +925,11 @@ function esc2(mixed $v): string {
msg.textContent = '';
});
var btnTopazPro = widget.querySelector('.fpw-topaz');
if (btnTopazPro) btnTopazPro.addEventListener('click', function() {
topaz.activar({ canvas: canvas, onAccept: function() {} });
});
btnSave.addEventListener('click', function() {
const svg = canvas.toDataURL('image/png');
if (svg.length < 1000) {
@@ -1027,6 +1060,11 @@ document.querySelectorAll('.turnero-firma-item').forEach(function(widget) {
msgEl.textContent = '';
});
var btnTopazPac = widget.querySelector('.turnero-topaz');
if (btnTopazPac) btnTopazPac.addEventListener('click', function() {
topaz.activar({ canvas: canvas, onAccept: function() {} });
});
if (btnFirm) btnFirm.addEventListener('click', function() {
var png = canvas.toDataURL('image/png');
if (png.length < 1500) {
@@ -1083,5 +1121,143 @@ document.querySelectorAll('.turnero-firma-item').forEach(function(widget) {
});
});
</script>
<!-- ── Topaz SigWeb overlay ───────────────────────────────────────── -->
<div id="topaz-overlay" style="display:none">
<div class="topaz-modal">
<div class="topaz-modal-hdr">
<i class="fas fa-tablet-alt"></i> Pad biométrico Topaz
</div>
<div class="topaz-modal-body">
<div class="topaz-pad-area" id="topaz-pad-area">
<i class="fas fa-signature fa-2x" style="color:#adb5bd" id="topaz-pad-icon"></i>
<div id="topaz-status-msg" style="font-size:.9rem;color:#64748b;font-weight:600">
Firme en el pad biométrico
</div>
<div class="topaz-pts-badge">Trazos: <span id="topaz-pts">0</span></div>
</div>
</div>
<div class="topaz-modal-footer">
<button class="btn btn-outline-secondary btn-sm" onclick="topaz.cancelar()">
<i class="fas fa-times me-1"></i>Cancelar
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="topaz.limpiarPad()">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button class="btn btn-success btn-sm fw-semibold" id="topaz-btn-aceptar"
onclick="topaz.aceptar()" disabled>
<i class="fas fa-check me-1"></i>Aceptar firma
</button>
</div>
</div>
</div>
<script>
// ── Topaz SigWeb ────────────────────────────────────────────────────
const topaz = (() => {
const SIGWEB_URL = 'http://localhost:47289/SigWeb/SigWebTablet.js';
let _loaded = false, _ctx = null, _target = null, _poll = null;
// _target: { type: 'turnero'|'pro', canvas: CanvasElement, onAccept: fn }
function _call(fn, ...args) {
if (_ctx && typeof _ctx[fn] === 'function') return _ctx[fn](...args);
const g = window[fn];
if (typeof g === 'function') return _ctx ? g(...args, _ctx) : g(...args);
throw new Error('SigWeb: ' + fn + ' no encontrado');
}
function _loadScript() {
if (_loaded) return Promise.resolve(true);
return new Promise(res => {
const s = document.createElement('script');
s.src = SIGWEB_URL;
s.onload = () => { _loaded = true; res(true); };
s.onerror = () => res(false);
document.head.appendChild(s);
});
}
function _setStatus(msg, ok) {
const el = document.getElementById('topaz-status-msg');
if (el) { el.textContent = msg; el.style.color = ok ? '#198754' : '#64748b'; }
const area = document.getElementById('topaz-pad-area');
if (area) area.classList.toggle('has-sig', !!ok);
const icon = document.getElementById('topaz-pad-icon');
if (icon) icon.style.color = ok ? '#198754' : '#adb5bd';
}
async function activar(target) {
const ok = await _loadScript();
if (!ok) {
alert('No se detectó SigWeb.\nInstala el servicio Topaz SigWeb y vuelve a intentarlo.');
return;
}
_target = target;
try {
_ctx = typeof SigWebTablet !== 'undefined' ? new SigWebTablet() : null;
_call('SetImageXSize', 500);
_call('SetImageYSize', 150);
_call('SetImagePenWidth', 3);
_call('SetTabletState', 1);
_call('ClearTablet');
} catch(e) {
alert('Error al activar el pad: ' + e.message); return;
}
document.getElementById('topaz-overlay').style.display = 'flex';
document.getElementById('topaz-btn-aceptar').disabled = true;
document.getElementById('topaz-pts').textContent = '0';
_setStatus('Firme en el pad biométrico', false);
_poll = setInterval(() => {
try {
const pts = _call('GetSigTotalPoints');
document.getElementById('topaz-pts').textContent = pts;
document.getElementById('topaz-btn-aceptar').disabled = pts === 0;
if (pts > 0) _setStatus('✅ Firma detectada — presione Aceptar', true);
} catch(e) { _stopPoll(); }
}, 400);
}
function _stopPoll() { if (_poll) { clearInterval(_poll); _poll = null; } }
function _cerrar() {
_stopPoll();
try { if (_ctx) _call('SetTabletState', 0); } catch(e) {}
_ctx = null; _target = null;
document.getElementById('topaz-overlay').style.display = 'none';
}
function cancelar() { _cerrar(); }
function limpiarPad() {
try { _call('ClearTablet'); } catch(e) {}
document.getElementById('topaz-pts').textContent = '0';
document.getElementById('topaz-btn-aceptar').disabled = true;
_setStatus('Firme en el pad biométrico', false);
}
function aceptar() {
let b64;
try { b64 = _call('GetSigImageB64'); } catch(e) {
alert('Error al capturar la firma: ' + e.message); _cerrar(); return;
}
if (!b64) { alert('No se capturó ninguna firma.'); return; }
const target = _target;
_cerrar();
const img = new Image();
img.onload = () => {
const c = target.canvas;
const ctx = c.getContext('2d');
ctx.clearRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0, c.width, c.height);
c.classList.add('has-sig');
if (typeof target.onAccept === 'function') target.onAccept();
};
img.src = 'data:image/png;base64,' + b64;
}
return { activar, cancelar, limpiarPad, aceptar };
})();
</script>
</body>
</html>