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 _data = null; // respuesta de verificar token
|
||||||
let _cfg = {}; // config de diseño del laboratorio
|
let _cfg = {}; // config de diseño del laboratorio
|
||||||
let _firmaDibujada = false;
|
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
|
// 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('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('touchmove', e => { e.preventDefault(); if (!drawing) return; trazo(getPos(e)); }, {passive:false});
|
||||||
canvas.addEventListener('touchend', ()=> { drawing=false; ctx.beginPath(); });
|
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) {
|
function trazo(p) {
|
||||||
@@ -438,7 +454,11 @@ function renderCampos(esquema, prefilled) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (c.tipo === 'firma' || c.tipo === 'firma_profesional') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (c.tipo === 'parrafo') {
|
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) {
|
function renderFirmaInline(fid, modos, label, required, isPro) {
|
||||||
const hasCanvas = (modos||[]).includes('canvas');
|
const hasCanvas = (modos||[]).includes('canvas');
|
||||||
const hasFoto = (modos||[]).includes('foto');
|
const hasFoto = (modos||[]).includes('foto');
|
||||||
|
const hasBoth = hasCanvas && hasFoto;
|
||||||
const borderC = isPro ? '#198754' : '#1565c0';
|
const borderC = isPro ? '#198754' : '#1565c0';
|
||||||
const bgC = isPro ? '#f0fff4' : '#fff';
|
const bgC = isPro ? '#f0fff4' : '#fff';
|
||||||
const iconC = isPro ? 'fa-user-md' : 'fa-signature';
|
const iconC = isPro ? 'fa-user-md' : 'fa-signature';
|
||||||
@@ -565,6 +586,16 @@ function renderFirmaInline(fid, modos, label, required, isPro) {
|
|||||||
</label>`;
|
</label>`;
|
||||||
let html = `<div class="mb-4" id="fw-${fid}"><hr class="mb-3">${lbl}`;
|
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) {
|
if (hasCanvas) {
|
||||||
html += `<div id="fw-${fid}-canvas-area">
|
html += `<div id="fw-${fid}-canvas-area">
|
||||||
<p class="text-muted small mb-2">✍️ Dibuja la firma con el dedo o el mouse.</p>
|
<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) {
|
if (hasFoto) {
|
||||||
html += `<div id="fw-${fid}-foto-area" class="${hasCanvas ? 'mt-3' : ''}">
|
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.</p>
|
<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">
|
<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>
|
<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>
|
</label>
|
||||||
<input type="file" id="fw-${fid}-foto-input" accept="image/*" capture="environment"
|
<input type="file" id="fw-${fid}-foto-input" accept="image/*" capture="environment"
|
||||||
class="d-none" onchange="firmaWidgetFoto('${fid}', this)">
|
class="d-none" onchange="firmaWidgetFoto('${fid}', this)">
|
||||||
@@ -651,6 +682,31 @@ function firmaWidgetInit(fid) {
|
|||||||
canvas.addEventListener('touchstart', start, { passive: false });
|
canvas.addEventListener('touchstart', start, { passive: false });
|
||||||
canvas.addEventListener('touchmove', move, { passive: false });
|
canvas.addEventListener('touchmove', move, { passive: false });
|
||||||
canvas.addEventListener('touchend', end);
|
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) {
|
function firmaWidgetLimpiar(fid) {
|
||||||
@@ -845,33 +901,32 @@ const formCliente = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Campos
|
// 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);
|
renderCampos(form.esquema_decoded, prefilled);
|
||||||
|
|
||||||
// Firma (se inicializa DESPUÉS de mostrar el form para que offsetWidth sea correcto)
|
// Firma global (solo si NO hay firmas inline y permite_firma está activo)
|
||||||
if (form.permite_firma) {
|
if (form.permite_firma && !hasInlineFirma) {
|
||||||
show('firma-container');
|
show('firma-container');
|
||||||
if (form.requiere_firma) show('firma-req-badge');
|
if (form.requiere_firma) show('firma-req-badge');
|
||||||
// Configurar modos de firma: primero columna firma_modos, luego campo en esquema
|
firma.setModos(_firmaModosGlobal);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hide('loading-screen');
|
hide('loading-screen');
|
||||||
show('form-screen');
|
show('form-screen');
|
||||||
|
|
||||||
if (form.permite_firma) {
|
if (form.permite_firma && !hasInlineFirma) {
|
||||||
// Pequeño delay para asegurar que el layout ya renderizó
|
|
||||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
firma.init();
|
firma.init();
|
||||||
form.esquema_decoded.forEach(c => {
|
|
||||||
if (c.tipo === 'firma' || c.tipo === 'firma_profesional') firmaWidgetInit(c.id);
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
} else {
|
}
|
||||||
|
if (hasInlineFirma) {
|
||||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
form.esquema_decoded.forEach(c => {
|
form.esquema_decoded.forEach(c => {
|
||||||
if (c.tipo === 'firma' || c.tipo === 'firma_profesional') firmaWidgetInit(c.id);
|
if (c.tipo === 'firma' || c.tipo === 'firma_profesional') firmaWidgetInit(c.id);
|
||||||
@@ -933,7 +988,7 @@ const formCliente = {
|
|||||||
return `El campo "${c.label}" es obligatorio.`;
|
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'});
|
$('firma-container').scrollIntoView({block:'center', behavior:'smooth'});
|
||||||
return 'La firma es obligatoria en este formulario.';
|
return 'La firma es obligatoria en este formulario.';
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-13
@@ -844,20 +844,28 @@ function abrirEditorCampo(idx) {
|
|||||||
<label class="form-check-label small" for="ce-required">Requiere al menos una selección</label>
|
<label class="form-check-label small" for="ce-required">Requiere al menos una selección</label>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (c.tipo === 'firma' || c.tipo === 'firma_profesional') {
|
} else if (c.tipo === 'firma' || c.tipo === 'firma_profesional') {
|
||||||
const cm = c.modos || ['canvas'];
|
if (c.tipo === 'firma_profesional') {
|
||||||
html += `<div class="mb-2">
|
const cm = c.modos || ['canvas'];
|
||||||
<label class="form-label small fw-semibold">Modos de firma disponibles</label>
|
html += `<div class="mb-2">
|
||||||
<div class="d-flex gap-4">
|
<label class="form-label small fw-semibold">Modos de firma disponibles</label>
|
||||||
<div class="form-check">
|
<div class="d-flex gap-4">
|
||||||
<input type="checkbox" class="form-check-input" id="ce-modo-canvas" ${cm.includes('canvas')?'checked':''}>
|
<div class="form-check">
|
||||||
<label class="form-check-label small" for="ce-modo-canvas">✍️ Dibujar (canvas)</label>
|
<input type="checkbox" class="form-check-input" id="ce-modo-canvas" ${cm.includes('canvas')?'checked':''}>
|
||||||
|
<label class="form-check-label small" for="ce-modo-canvas">✍️ Dibujar (canvas)</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="ce-modo-foto" ${cm.includes('foto')?'checked':''}>
|
||||||
|
<label class="form-check-label small" for="ce-modo-foto">📷 Foto</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
</div>`;
|
||||||
<input type="checkbox" class="form-check-input" id="ce-modo-foto" ${cm.includes('foto')?'checked':''}>
|
} else {
|
||||||
<label class="form-check-label small" for="ce-modo-foto">📷 Foto</label>
|
html += `<div class="alert alert-info py-2 small mb-2">
|
||||||
</div>
|
<i class="fas fa-info-circle me-1"></i>
|
||||||
</div>
|
Los modos del paciente (<strong>✍️ Dibujar / 📷 Foto</strong>) se configuran
|
||||||
</div>`;
|
desde el panel superior <strong>↑</strong> (sección <em>Firma del paciente</em>).
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('campo-editor-body').innerHTML = html;
|
$('campo-editor-body').innerHTML = html;
|
||||||
|
|||||||
Reference in New Issue
Block a user