firma: tabs canvas/foto, drag&drop, fallback seguro, aviso builder
This commit is contained in:
+75
-20
@@ -301,6 +301,8 @@ const API = 'api/lab/submit_formulario.php';
|
||||
let _data = null; // respuesta de verificar token
|
||||
let _cfg = {}; // config de diseño del laboratorio
|
||||
let _firmaDibujada = false;
|
||||
let _firmaModosGlobal = null; // modos del topbar (columna firma_modos en DB)
|
||||
let _hasInlineFirma = false; // true si el esquema tiene campos tipo firma/firma_profesional
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// HELPERS
|
||||
@@ -357,6 +359,20 @@ const firma = (() => {
|
||||
canvas.addEventListener('touchstart', e => { e.preventDefault(); drawing=true; const p=getPos(e); lastX=p.x; lastY=p.y; }, {passive:false});
|
||||
canvas.addEventListener('touchmove', e => { e.preventDefault(); if (!drawing) return; trazo(getPos(e)); }, {passive:false});
|
||||
canvas.addEventListener('touchend', ()=> { drawing=false; ctx.beginPath(); });
|
||||
|
||||
// Drag & drop en área de foto global
|
||||
const fotoDrop = $('firma-foto-drop');
|
||||
if (fotoDrop) {
|
||||
fotoDrop.addEventListener('dragover', e => { e.preventDefault(); fotoDrop.classList.add('dragover'); });
|
||||
fotoDrop.addEventListener('dragleave', () => fotoDrop.classList.remove('dragover'));
|
||||
fotoDrop.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
fotoDrop.classList.remove('dragover');
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (!file || !file.type.startsWith('image/')) return;
|
||||
_onFotoChange({ files: [file] });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function trazo(p) {
|
||||
@@ -438,7 +454,11 @@ function renderCampos(esquema, prefilled) {
|
||||
return;
|
||||
}
|
||||
if (c.tipo === 'firma' || c.tipo === 'firma_profesional') {
|
||||
html += renderFirmaInline(c.id, c.modos || ['canvas', 'foto'], c.label, c.required, c.tipo === 'firma_profesional');
|
||||
// firma del paciente usa firma_modos global (topbar); profesional usa sus propios modos
|
||||
const modos = (c.tipo === 'firma' && _firmaModosGlobal)
|
||||
? _firmaModosGlobal
|
||||
: (c.modos || ['canvas']);
|
||||
html += renderFirmaInline(c.id, modos, c.label, c.required, c.tipo === 'firma_profesional');
|
||||
return;
|
||||
}
|
||||
if (c.tipo === 'parrafo') {
|
||||
@@ -553,6 +573,7 @@ const _fw = {}; // { fieldId: { canvas, ctx, hasFirma, foto, drawing, lastX, la
|
||||
function renderFirmaInline(fid, modos, label, required, isPro) {
|
||||
const hasCanvas = (modos||[]).includes('canvas');
|
||||
const hasFoto = (modos||[]).includes('foto');
|
||||
const hasBoth = hasCanvas && hasFoto;
|
||||
const borderC = isPro ? '#198754' : '#1565c0';
|
||||
const bgC = isPro ? '#f0fff4' : '#fff';
|
||||
const iconC = isPro ? 'fa-user-md' : 'fa-signature';
|
||||
@@ -565,6 +586,16 @@ function renderFirmaInline(fid, modos, label, required, isPro) {
|
||||
</label>`;
|
||||
let html = `<div class="mb-4" id="fw-${fid}"><hr class="mb-3">${lbl}`;
|
||||
|
||||
// Tabs cuando ambos modos están activos
|
||||
if (hasBoth) {
|
||||
html += `<div class="firma-modo-btns mb-2">
|
||||
<button type="button" class="firma-modo-btn activo" id="fw-${fid}-tab-canvas"
|
||||
onclick="firmaWidgetTab('${fid}','canvas')">✍️ Dibujar</button>
|
||||
<button type="button" class="firma-modo-btn" id="fw-${fid}-tab-foto"
|
||||
onclick="firmaWidgetTab('${fid}','foto')">📷 Foto</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (hasCanvas) {
|
||||
html += `<div id="fw-${fid}-canvas-area">
|
||||
<p class="text-muted small mb-2">✍️ Dibuja la firma con el dedo o el mouse.</p>
|
||||
@@ -581,11 +612,11 @@ function renderFirmaInline(fid, modos, label, required, isPro) {
|
||||
}
|
||||
|
||||
if (hasFoto) {
|
||||
html += `<div id="fw-${fid}-foto-area" class="${hasCanvas ? 'mt-3' : ''}">
|
||||
<p class="text-muted small mb-2">📷 Adjunta una foto de la firma.</p>
|
||||
html += `<div id="fw-${fid}-foto-area"${hasBoth ? ' style="display:none"' : (hasCanvas ? ' class="mt-3"' : '')}>
|
||||
<p class="text-muted small mb-2">📷 Adjunta una foto de la firma o arrastra la imagen aquí.</p>
|
||||
<label class="firma-foto-drop" id="fw-${fid}-foto-drop" for="fw-${fid}-foto-input">
|
||||
<i class="fas fa-camera fa-2x d-block mb-2"></i>
|
||||
Toca para abrir cámara o galería
|
||||
Toca para abrir cámara / galería, o arrastra aquí
|
||||
</label>
|
||||
<input type="file" id="fw-${fid}-foto-input" accept="image/*" capture="environment"
|
||||
class="d-none" onchange="firmaWidgetFoto('${fid}', this)">
|
||||
@@ -651,6 +682,31 @@ function firmaWidgetInit(fid) {
|
||||
canvas.addEventListener('touchstart', start, { passive: false });
|
||||
canvas.addEventListener('touchmove', move, { passive: false });
|
||||
canvas.addEventListener('touchend', end);
|
||||
|
||||
// Drag & drop en área de foto del widget
|
||||
const fotoDrop = document.getElementById('fw-' + fid + '-foto-drop');
|
||||
if (fotoDrop) {
|
||||
fotoDrop.addEventListener('dragover', e => { e.preventDefault(); fotoDrop.classList.add('dragover'); });
|
||||
fotoDrop.addEventListener('dragleave', () => fotoDrop.classList.remove('dragover'));
|
||||
fotoDrop.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
fotoDrop.classList.remove('dragover');
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (!file || !file.type.startsWith('image/')) return;
|
||||
firmaWidgetFoto(fid, { files: [file] });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function firmaWidgetTab(fid, tab) {
|
||||
const ca = document.getElementById('fw-' + fid + '-canvas-area');
|
||||
const fa = document.getElementById('fw-' + fid + '-foto-area');
|
||||
const bt = document.getElementById('fw-' + fid + '-tab-canvas');
|
||||
const bf = document.getElementById('fw-' + fid + '-tab-foto');
|
||||
if (ca) ca.style.display = tab === 'canvas' ? '' : 'none';
|
||||
if (fa) fa.style.display = tab === 'foto' ? '' : 'none';
|
||||
if (bt) bt.classList.toggle('activo', tab === 'canvas');
|
||||
if (bf) bf.classList.toggle('activo', tab === 'foto');
|
||||
}
|
||||
|
||||
function firmaWidgetLimpiar(fid) {
|
||||
@@ -845,33 +901,32 @@ const formCliente = {
|
||||
}
|
||||
|
||||
// Campos
|
||||
// firma_modos global: autoridad para campos tipo 'firma' (topbar del builder)
|
||||
const fModosStr = form.firma_modos || '';
|
||||
_firmaModosGlobal = fModosStr
|
||||
? fModosStr.split(',').map(m => m.trim()).filter(Boolean)
|
||||
: null;
|
||||
|
||||
const hasInlineFirma = form.esquema_decoded.some(c => c.tipo === 'firma' || c.tipo === 'firma_profesional');
|
||||
_hasInlineFirma = hasInlineFirma;
|
||||
renderCampos(form.esquema_decoded, prefilled);
|
||||
|
||||
// Firma (se inicializa DESPUÉS de mostrar el form para que offsetWidth sea correcto)
|
||||
if (form.permite_firma) {
|
||||
// Firma global (solo si NO hay firmas inline y permite_firma está activo)
|
||||
if (form.permite_firma && !hasInlineFirma) {
|
||||
show('firma-container');
|
||||
if (form.requiere_firma) show('firma-req-badge');
|
||||
// Configurar modos de firma: primero columna firma_modos, luego campo en esquema
|
||||
const firmaCampo = form.esquema_decoded.find(c => c.tipo === 'firma');
|
||||
const fModosStr = form.firma_modos || '';
|
||||
const firmaModosArr = fModosStr
|
||||
? fModosStr.split(',').map(m => m.trim()).filter(Boolean)
|
||||
: (firmaCampo?.modos ?? null);
|
||||
firma.setModos(firmaModosArr);
|
||||
firma.setModos(_firmaModosGlobal);
|
||||
}
|
||||
|
||||
hide('loading-screen');
|
||||
show('form-screen');
|
||||
|
||||
if (form.permite_firma) {
|
||||
// Pequeño delay para asegurar que el layout ya renderizó
|
||||
if (form.permite_firma && !hasInlineFirma) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
firma.init();
|
||||
form.esquema_decoded.forEach(c => {
|
||||
if (c.tipo === 'firma' || c.tipo === 'firma_profesional') firmaWidgetInit(c.id);
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
}
|
||||
if (hasInlineFirma) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
form.esquema_decoded.forEach(c => {
|
||||
if (c.tipo === 'firma' || c.tipo === 'firma_profesional') firmaWidgetInit(c.id);
|
||||
@@ -933,7 +988,7 @@ const formCliente = {
|
||||
return `El campo "${c.label}" es obligatorio.`;
|
||||
}
|
||||
}
|
||||
if (_data.formulario.requiere_firma && !firma.obtenerSVG() && !firma.obtenerFoto()) {
|
||||
if (_data.formulario.requiere_firma && !_hasInlineFirma && !firma.obtenerSVG() && !firma.obtenerFoto()) {
|
||||
$('firma-container').scrollIntoView({block:'center', behavior:'smooth'});
|
||||
return 'La firma es obligatoria en este formulario.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user