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.
+
+
+
![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 ``;
+ ✍️ 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 {
= esc2($campo['label'] ?? '') ?>
+
+
+
+ = $titLabel ?>
+
+
+
+
 ?>)
+
+
+
+
+
 ?>)
+
+
+
+
Información del envío