From 775c6547906b177c017a3543451b5e168f145813 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:58:42 -0500 Subject: [PATCH] cambios --- form_cliente.php | 197 ++++++++++++++++++++++++++++++++++++- lab_formulario_builder.php | 41 ++++++-- lab_formularios.php | 7 +- ver_formulario_enviado.php | 30 ++++++ 4 files changed, 263 insertions(+), 12 deletions(-) diff --git a/form_cliente.php b/form_cliente.php index a95e826..9b2eea1 100644 --- a/form_cliente.php +++ b/form_cliente.php @@ -50,7 +50,11 @@ $token = trim($_GET['t'] ?? ''); cursor:crosshair; touch-action:none; background:#fff; display:block; width:100%; height:150px; } #firma-canvas.empty { border-style:dashed; border-color:#adb5bd; } - + /* ── Firma inline por campo ──────────────────────────────── */ + .fw-canvas { cursor:crosshair; touch-action:none; + background:#fff; display:block; width:100%; height:140px; + border-radius:8px; } + .fw-canvas.empty { border-style:dashed !important; } /* ── Firma modos ─────────────────────────────── */ .firma-modo-btns { display:flex; gap:8px; margin-bottom:12px; } .firma-modo-btn { flex:1; padding:7px 0; border:2px solid #dee2e6; border-radius:8px; @@ -433,7 +437,10 @@ function renderCampos(esquema, prefilled) { `; return; } - if (c.tipo === 'firma') return; // se maneja aparte + if (c.tipo === 'firma' || c.tipo === 'firma_profesional') { + html += renderFirmaInline(c.id, c.modos || ['canvas', 'foto'], c.label, c.required, c.tipo === 'firma_profesional'); + return; + } if (c.tipo === 'parrafo') { const ws = c.flujoLibre ? 'normal' : 'pre-wrap'; html += `
${esc(c.contenido||'')}
`; @@ -538,6 +545,158 @@ function renderCampos(esquema, prefilled) { cont.innerHTML = html; } +// ══════════════════════════════════════════════════════════════════════ +// FIRMAS INLINE (por campo) +// ══════════════════════════════════════════════════════════════════════ +const _fw = {}; // { fieldId: { canvas, ctx, hasFirma, foto, drawing, lastX, lastY } } + +function renderFirmaInline(fid, modos, label, required, isPro) { + const hasCanvas = (modos||[]).includes('canvas'); + const hasFoto = (modos||[]).includes('foto'); + const borderC = isPro ? '#198754' : '#1565c0'; + const bgC = isPro ? '#f0fff4' : '#fff'; + const iconC = isPro ? 'fa-user-md' : 'fa-signature'; + const badge = isPro + ? `Profesional` + : `Paciente`; + const star = required ? '*' : ''; + const lbl = ``; + let html = `

${lbl}`; + + if (hasCanvas) { + html += `
+

✍️ Dibuja la firma con el dedo o el mouse.

+ +
+ + Sin firma +
+
`; + } + + if (hasFoto) { + html += `
+

📷 Adjunta una foto de la firma.

+ + + +
+ + Sin foto +
+
`; + } + + html += `
`; + return html; +} + +function firmaWidgetInit(fid) { + const canvas = document.getElementById('fw-' + fid + '-canvas'); + if (!canvas) return; + const ratio = window.devicePixelRatio || 1; + canvas.width = canvas.offsetWidth * ratio; + canvas.height = canvas.offsetHeight * ratio; + const ctx = canvas.getContext('2d'); + ctx.scale(ratio, ratio); + ctx.strokeStyle = '#1a1a2e'; + ctx.lineWidth = 2.5; + ctx.lineCap = 'round'; + _fw[fid] = { canvas, ctx, drawing: false, hasFirma: false, foto: null, lastX: 0, lastY: 0 }; + + function pos(e) { + const r = canvas.getBoundingClientRect(); + const src = e.touches?.[0] ?? e; + return { x: src.clientX - r.left, y: src.clientY - r.top }; + } + function start(e) { + e.preventDefault(); + const s = _fw[fid]; s.drawing = true; + const p = pos(e); s.lastX = p.x; s.lastY = p.y; + } + function move(e) { + e.preventDefault(); + const s = _fw[fid]; + if (!s.drawing) return; + const p = pos(e); + s.ctx.beginPath(); s.ctx.moveTo(s.lastX, s.lastY); + s.ctx.lineTo(p.x, p.y); s.ctx.stroke(); + s.lastX = p.x; s.lastY = p.y; + if (!s.hasFirma) { + s.hasFirma = true; + canvas.classList.remove('empty'); + canvas.style.borderStyle = 'solid'; + const st = document.getElementById('fw-' + fid + '-status'); + if (st) { st.textContent = '✅ Firmado'; st.className = 'small text-success align-self-center fw-semibold'; } + } + } + function end() { if (_fw[fid]) _fw[fid].drawing = false; } + canvas.addEventListener('mousedown', start); + canvas.addEventListener('mousemove', move); + canvas.addEventListener('mouseup', end); + canvas.addEventListener('touchstart', start, { passive: false }); + canvas.addEventListener('touchmove', move, { passive: false }); + canvas.addEventListener('touchend', end); +} + +function firmaWidgetLimpiar(fid) { + 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.hasFirma = false; + s.canvas.classList.add('empty'); + s.canvas.style.borderStyle = 'dashed'; + const st = document.getElementById('fw-' + fid + '-status'); + if (st) { st.textContent = 'Sin firma'; st.className = 'small text-muted align-self-center'; } +} + +function firmaWidgetFoto(fid, input) { + const file = input.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = e => { + if (!_fw[fid]) _fw[fid] = {}; + _fw[fid].foto = e.target.result; + const p = document.getElementById('fw-' + fid + '-foto-preview'); + const d = document.getElementById('fw-' + fid + '-foto-drop'); + const s = document.getElementById('fw-' + fid + '-foto-status'); + if (p) { p.src = e.target.result; p.style.display = 'block'; } + if (d) d.style.display = 'none'; + if (s) { s.textContent = '✅ Foto lista'; s.className = 'small text-success align-self-center fw-semibold'; } + }; + reader.readAsDataURL(file); +} + +function firmaWidgetLimpiarFoto(fid) { + if (_fw[fid]) _fw[fid].foto = null; + const p = document.getElementById('fw-' + fid + '-foto-preview'); + const d = document.getElementById('fw-' + fid + '-foto-drop'); + const s = document.getElementById('fw-' + fid + '-foto-status'); + const i = document.getElementById('fw-' + fid + '-foto-input'); + if (p) { p.src = ''; p.style.display = 'none'; } + if (d) d.style.display = ''; + if (s) { s.textContent = 'Sin foto'; s.className = 'small text-muted align-self-center'; } + if (i) i.value = ''; +} + +function firmaWidgetObtenerSVG(fid) { return _fw[fid]?.hasFirma ? _fw[fid].canvas.toDataURL('image/png') : null; } +function firmaWidgetObtenerFoto(fid) { return _fw[fid]?.foto || null; } + // ══════════════════════════════════════════════════════════════════════ // formCliente // ══════════════════════════════════════════════════════════════════════ @@ -706,7 +865,18 @@ const formCliente = { if (form.permite_firma) { // Pequeño delay para asegurar que el layout ya renderizó - requestAnimationFrame(() => requestAnimationFrame(() => firma.init())); + requestAnimationFrame(() => requestAnimationFrame(() => { + firma.init(); + form.esquema_decoded.forEach(c => { + if (c.tipo === 'firma' || c.tipo === 'firma_profesional') firmaWidgetInit(c.id); + }); + })); + } else { + requestAnimationFrame(() => requestAnimationFrame(() => { + form.esquema_decoded.forEach(c => { + if (c.tipo === 'firma' || c.tipo === 'firma_profesional') firmaWidgetInit(c.id); + }); + })); } }, @@ -716,7 +886,7 @@ const formCliente = { const campos = _data.formulario.esquema_decoded; campos.forEach(c => { - if (c.tipo === 'separador' || c.tipo === 'firma' || c.tipo === 'parrafo') return; + if (c.tipo === 'separador' || c.tipo === 'firma' || c.tipo === 'firma_profesional' || c.tipo === 'parrafo') return; if (c.tipo === 'linked') { return; } // linked viene del prefilled // parrafo_inline: recolectar solo los inputs editables (los que el usuario llenó) @@ -739,6 +909,14 @@ const formCliente = { datos[c.id] = el ? el.value : ''; } }); + // Recopilar firmas inline (firma y firma_profesional por campo) + campos.forEach(c => { + if (c.tipo !== 'firma' && c.tipo !== 'firma_profesional') return; + const svg = firmaWidgetObtenerSVG(c.id); + const foto = firmaWidgetObtenerFoto(c.id); + if (svg) datos[c.id + '_svg'] = svg; + if (foto) datos[c.id + '_foto'] = foto; + }); return datos; }, @@ -746,7 +924,7 @@ const formCliente = { const campos = _data.formulario.esquema_decoded; for (const c of campos) { if (!c.required) continue; - if (c.tipo === 'separador' || c.tipo === 'linked' || c.tipo === 'firma' || c.tipo === 'parrafo' || c.tipo === 'parrafo_inline') continue; + if (c.tipo === 'separador' || c.tipo === 'linked' || c.tipo === 'firma' || c.tipo === 'firma_profesional' || c.tipo === 'parrafo' || c.tipo === 'parrafo_inline') continue; const v = datos[c.id]; const vacio = Array.isArray(v) ? v.length === 0 : !v || !String(v).trim(); if (vacio) { @@ -759,6 +937,15 @@ const formCliente = { $('firma-container').scrollIntoView({block:'center', behavior:'smooth'}); return 'La firma es obligatoria en este formulario.'; } + // Validar firmas inline requeridas + for (const c of campos) { + if (!c.required || (c.tipo !== 'firma' && c.tipo !== 'firma_profesional')) continue; + if (!firmaWidgetObtenerSVG(c.id) && !firmaWidgetObtenerFoto(c.id)) { + const el = document.getElementById('fw-' + c.id); + if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' }); + return `El campo "${c.label}" requiere firma.`; + } + } return null; }, diff --git a/lab_formulario_builder.php b/lab_formulario_builder.php index 5de8c0c..5b408e6 100644 --- a/lab_formulario_builder.php +++ b/lab_formulario_builder.php @@ -471,7 +471,8 @@ const TIPOS_CAMPO = [ { tipo:'select', label:'Lista desplegable', icon:'fa-list' }, { tipo:'radio', label:'Selección única', icon:'fa-dot-circle' }, { tipo:'checkbox', label:'Múltiple opción', icon:'fa-check-square' }, - { tipo:'firma', label:'Firma digital', icon:'fa-signature' }, + { tipo:'firma', label:'Firma del paciente', icon:'fa-signature' }, + { tipo:'firma_profesional', label:'Firma del profesional', icon:'fa-user-md' }, { tipo:'separador', label:'Separador / Título', icon:'fa-minus' }, { tipo:'parrafo', label:'Párrafo de texto', icon:'fa-paragraph' }, { tipo:'parrafo_inline', label:'Párrafo con campos', icon:'fa-align-left' }, @@ -584,8 +585,9 @@ function agregarCampo({ tipo, linked }) { } else if (['select','radio','checkbox'].includes(tipo)) { campo = { id, tipo, label:'Campo sin título', options:['Opción 1','Opción 2'], required:false }; } else if (tipo === 'firma') { - const _mc = []; if($('tb-firma-canvas').checked)_mc.push('canvas'); if($('tb-firma-foto').checked)_mc.push('foto'); - campo = { id, tipo:'firma', label:'Firma del paciente', modos: _mc.length ? _mc : ['canvas','foto'] }; + campo = { id, tipo:'firma', label:'Firma del paciente', modos: ['canvas','foto'] }; + } else if (tipo === 'firma_profesional') { + campo = { id, tipo:'firma_profesional', label:'Firma del profesional', modos: ['canvas'] }; } else if (tipo === 'parrafo') { campo = { id, tipo:'parrafo', contenido:'Escribe aquí el texto del párrafo...', flujoLibre:false }; } else if (tipo === 'parrafo_inline') { @@ -606,7 +608,7 @@ function agregarCampo({ tipo, linked }) { const TIPO_ICON = { texto:'fa-font', textarea:'fa-align-left', numero:'fa-hashtag', fecha:'fa-calendar', hora:'fa-clock', select:'fa-list', radio:'fa-dot-circle', - checkbox:'fa-check-square', firma:'fa-signature', separador:'fa-minus', linked:'fa-link', + checkbox:'fa-check-square', firma:'fa-signature', firma_profesional:'fa-user-md', separador:'fa-minus', linked:'fa-link', parrafo:'fa-paragraph', parrafo_inline:'fa-align-left', lista_marcable:'fa-list-ol' }; @@ -732,7 +734,11 @@ function renderCampoPreview(c) { } if (c.tipo === 'firma') { return `
${lbl} -
✍️ Firma
`; +
✍️ Firma del paciente
`; + } + if (c.tipo === 'firma_profesional') { + return `
${lbl} +
✍️ Firma del profesional
`; } if (c.tipo === 'parrafo') { const ws = c.flujoLibre ? 'normal' : 'pre-wrap'; @@ -837,6 +843,21 @@ function abrirEditorCampo(idx) { `; + } else if (c.tipo === 'firma' || c.tipo === 'firma_profesional') { + const cm = c.modos || ['canvas']; + html += `
+ +
+
+ + +
+
+ + +
+
+
`; } $('campo-editor-body').innerHTML = html; @@ -882,6 +903,15 @@ function aplicarCampo() { const items = document.getElementById('ce-items'); if (items) c.items = items.value.split('\n').map(s=>s.trim()).filter(Boolean); + const modoCanvas = document.getElementById('ce-modo-canvas'); + const modoFoto = document.getElementById('ce-modo-foto'); + if (modoCanvas !== null || modoFoto !== null) { + const mc = []; + if (modoCanvas?.checked) mc.push('canvas'); + if (modoFoto?.checked) mc.push('foto'); + if (mc.length) c.modos = mc; + } + _bsCampo.hide(); renderCanvas(); renderPreview(); @@ -988,7 +1018,6 @@ async function guardar() { const usarGlobal = $('tb-usar-global').checked; // Sincronizar modos ANTES de serializar el esquema const _modos = []; if($('tb-firma-canvas').checked)_modos.push('canvas'); if($('tb-firma-foto').checked)_modos.push('foto'); - _campos = _campos.map(c => c.tipo === 'firma' ? { ...c, modos: _modos.length ? _modos : ['canvas'] } : c); const datos = { nombre, descripcion: $('tb-descripcion').value.trim() || null, diff --git a/lab_formularios.php b/lab_formularios.php index 435e59b..cfd6f76 100644 --- a/lab_formularios.php +++ b/lab_formularios.php @@ -747,7 +747,12 @@ function renderCampoPreview(c, valores) { if (c.tipo === 'firma') { return `
${lbl}
- ✍️ Área de firma
`; + ✍️ Área de firma (paciente)`; + } + if (c.tipo === 'firma_profesional') { + return `
${lbl} +
+ ✍️ Área de firma (profesional)
`; } if (c.tipo === 'parrafo') { const txt = (c.contenido || ''); diff --git a/ver_formulario_enviado.php b/ver_formulario_enviado.php index b05dcf3..272df50 100644 --- a/ver_formulario_enviado.php +++ b/ver_formulario_enviado.php @@ -277,6 +277,7 @@ function esc2(mixed $v): string {
+ + +
+ +
+ +
+ <?= $titLabel ?> +
+ + +
+ Foto - <?= $titLabel ?> +
+ + +
Información del envío