feat(firma): integración pad biométrico Topaz SigWeb
- CSS: overlay #topaz-overlay con modal centrado, estado has-sig - HTML: modal con área de estado, contador de trazos, botones Cancelar / Limpiar / Aceptar - JS: módulo topaz — carga SigWebTablet.js desde localhost:47289, compatible con SigWeb v1 (globals+ctx) y v2 (métodos en objeto), soporta fid='__global' para canvas principal e inline (fw-*) - Botón "Tableta" agregado en canvas area global y en renderFirmaInline para campos tipo firma/firma_profesional Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3d6147bf0d
commit
9e19c1c8c7
+195
-2
@@ -90,6 +90,29 @@ if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
||||
/* ── Loading screen ──────────────────────────────── */
|
||||
#loading-screen { min-height:50vh; display:flex; flex-direction:column;
|
||||
align-items:center; justify-content:center; gap:12px; }
|
||||
|
||||
/* ── 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>
|
||||
@@ -204,11 +227,15 @@ if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
||||
<div id="firma-canvas-area">
|
||||
<p class="text-muted small mb-2">✍️ Dibuja tu firma con el dedo o el mouse.</p>
|
||||
<canvas id="firma-canvas" class="empty"></canvas>
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<div class="d-flex gap-2 mt-2 flex-wrap">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
onclick="firma.limpiarCanvas()">
|
||||
<i class="fas fa-eraser me-1"></i>Limpiar firma
|
||||
</button>
|
||||
<button type="button" class="btn-topaz" onclick="topaz.activar('__global')"
|
||||
title="Usar pad biométrico Topaz">
|
||||
<i class="fas fa-tablet-alt"></i>Tableta
|
||||
</button>
|
||||
<span id="firma-status" class="small text-muted align-self-center">Sin firma</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,6 +321,36 @@ if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Topaz SigWeb overlay ───────────────────────────────────────── -->
|
||||
<div id="topaz-overlay">
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -648,11 +705,15 @@ function renderFirmaInline(fid, modos, label, required, isPro) {
|
||||
<p class="text-muted small mb-2">✍️ Dibuja la firma con el dedo o el mouse.</p>
|
||||
<canvas id="fw-${fid}-canvas" class="fw-canvas empty"
|
||||
style="border:2px dashed ${borderC};background:${bgC};"></canvas>
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<div class="d-flex gap-2 mt-2 flex-wrap">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
onclick="firmaWidgetLimpiar('${fid}')">
|
||||
<i class="fas fa-eraser me-1"></i>Limpiar
|
||||
</button>
|
||||
<button type="button" class="btn-topaz" onclick="topaz.activar('${fid}')"
|
||||
title="Usar pad biométrico Topaz">
|
||||
<i class="fas fa-tablet-alt"></i>Tableta
|
||||
</button>
|
||||
<span id="fw-${fid}-status" class="small text-muted align-self-center">Sin firma</span>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -1205,6 +1266,138 @@ function iniciarCondiciones(esquema) {
|
||||
evaluar();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// TOPAZ SIGWEB
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
const topaz = (() => {
|
||||
const SIGWEB_URL = 'http://localhost:47289/SigWeb/SigWebTablet.js';
|
||||
let _loaded = false, _ctx = null, _fid = null, _poll = null;
|
||||
|
||||
function _call(fn, ...args) {
|
||||
// SigWeb v1: global functions with ctx as last param
|
||||
// SigWeb v2: methods on ctx object
|
||||
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 = $('topaz-status-msg');
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.style.color = ok ? '#198754' : '#64748b';
|
||||
const area = $('topaz-pad-area');
|
||||
if (area) area.classList.toggle('has-sig', !!ok);
|
||||
const icon = $('topaz-pad-icon');
|
||||
if (icon) icon.style.color = ok ? '#198754' : '#adb5bd';
|
||||
}
|
||||
|
||||
async function activar(fid) {
|
||||
const ok = await _loadScript();
|
||||
if (!ok) {
|
||||
alert('No se detectó SigWeb.\nInstala el servicio Topaz SigWeb y vuelve a intentarlo.');
|
||||
return;
|
||||
}
|
||||
_fid = fid;
|
||||
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;
|
||||
}
|
||||
$('topaz-overlay').style.display = 'flex';
|
||||
$('topaz-btn-aceptar').disabled = true;
|
||||
$('topaz-pts').textContent = '0';
|
||||
_setStatus('Firme en el pad biométrico', false);
|
||||
|
||||
_poll = setInterval(() => {
|
||||
try {
|
||||
const pts = _call('GetSigTotalPoints');
|
||||
$('topaz-pts').textContent = pts;
|
||||
const hasSig = pts > 0;
|
||||
$('topaz-btn-aceptar').disabled = !hasSig;
|
||||
if (hasSig) _setStatus('✅ Firma detectada — presione Aceptar', true);
|
||||
} catch(e) { _stopPoll(); }
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function _stopPoll() { if (_poll) { clearInterval(_poll); _poll = null; } }
|
||||
|
||||
function _cerrarOverlay() {
|
||||
_stopPoll();
|
||||
try { if (_ctx) _call('SetTabletState', 0); } catch(e) {}
|
||||
_ctx = null; _fid = null;
|
||||
$('topaz-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
function cancelar() { _cerrarOverlay(); }
|
||||
|
||||
function limpiarPad() {
|
||||
try { _call('ClearTablet'); } catch(e) {}
|
||||
$('topaz-pts').textContent = '0';
|
||||
$('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);
|
||||
_cerrarOverlay(); return;
|
||||
}
|
||||
if (!b64) { alert('No se capturó ninguna firma.'); return; }
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const fid = _fid;
|
||||
_cerrarOverlay();
|
||||
|
||||
if (fid === '__global') {
|
||||
const c = $('firma-canvas');
|
||||
const cx = c.getContext('2d');
|
||||
const r = window.devicePixelRatio || 1;
|
||||
cx.clearRect(0, 0, c.width / r, c.height / r);
|
||||
cx.drawImage(img, 0, 0, c.offsetWidth, c.offsetHeight);
|
||||
c.classList.remove('empty');
|
||||
_firmaDibujada = true;
|
||||
const st = $('firma-status');
|
||||
if (st) { st.textContent = '✅ Firma lista (tableta)'; st.className = 'small text-success align-self-center fw-semibold'; }
|
||||
} else {
|
||||
const s = _fw[fid];
|
||||
if (!s?.canvas) return;
|
||||
const r = window.devicePixelRatio || 1;
|
||||
s.ctx.clearRect(0, 0, s.canvas.width / r, s.canvas.height / r);
|
||||
s.ctx.drawImage(img, 0, 0, s.canvas.offsetWidth, s.canvas.offsetHeight);
|
||||
s.hasFirma = true;
|
||||
s.canvas.classList.remove('empty');
|
||||
s.canvas.style.borderStyle = 'solid';
|
||||
const st = document.getElementById('fw-' + fid + '-status');
|
||||
if (st) { st.textContent = '✅ Firmado (tableta)'; st.className = 'small text-success align-self-center fw-semibold'; }
|
||||
}
|
||||
};
|
||||
img.src = 'data:image/png;base64,' + b64;
|
||||
}
|
||||
|
||||
return { activar, cancelar, limpiarPad, aceptar };
|
||||
})();
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// INIT
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user