Compare commits

...

24 Commits

Author SHA1 Message Date
Milton Gonzalez 8d17393d5f Remove FirmaEC debug markers and document backend restart 2026-05-22 16:59:05 -05:00
Milton Gonzalez 848441d593 Add FirmaEC PDF render trace log in 1.2.34 2026-05-22 16:39:46 -05:00
Milton Gonzalez 99a5350008 Fix FirmaEC stamp preview borderless layout and bump to 1.2.33 2026-05-22 16:31:28 -05:00
Milton Gonzalez 82f3d2cbf9 Add control marker to both FirmaEC PDF header lines in 1.2.32 2026-05-22 15:26:58 -05:00
Milton Gonzalez ff7ff92d84 Move FirmaEC control marker to Firmado line in 1.2.31 2026-05-22 15:24:30 -05:00
Milton Gonzalez b6c169ffbb Add FirmaEC control marker and bump to 1.2.30 2026-05-22 15:22:14 -05:00
Milton Gonzalez 8d090dc8ca Increase FirmaEC name size and bump to 1.2.29 2026-05-22 15:13:49 -05:00
Milton Gonzalez 7fc4707724 Add FirmaEC PDF marker text and bump to 1.2.28 2026-05-22 14:55:37 -05:00
Milton Gonzalez bc28441531 Split FirmaEC names after second word for 4+ words in 1.2.27 2026-05-22 14:46:10 -05:00
Milton Gonzalez 3c9aefa10e Show app version in sidebar and split 4-word FirmaEC names in 1.2.26 2026-05-22 14:31:10 -05:00
Milton Gonzalez 81bd8d3c59 Use stored signature preferences during signing and bump to 1.2.25 2026-05-22 14:19:27 -05:00
Milton Gonzalez fdde9cdb5d Use invisible signature widget for borderless FirmaEC and bump to 1.2.24 2026-05-22 14:10:43 -05:00
Milton Gonzalez 5ed70766b9 Hide signature widget border for FirmaEC and bump to 1.2.23 2026-05-22 14:02:56 -05:00
Milton Gonzalez 2a0f272af1 Fix borderless FirmaEC rendering and bump to 1.2.22 2026-05-22 12:02:44 -05:00
Milton Gonzalez e67e7aa122 Honor borderless FirmaEC signatures 2026-05-22 11:50:08 -05:00
Milton Gonzalez f19f8bc899 Fallback unknown signature styles when saving preferences 2026-05-21 22:43:10 -05:00
Milton Gonzalez dfcc0ca37b Normalize FirmaEC signature style values 2026-05-21 22:34:16 -05:00
Milton Gonzalez 8308e3537e Relax signature preference validation for FirmaEC 2026-05-21 22:28:44 -05:00
Milton Gonzalez 8bc244855c Add FirmaEC signature style and bump to 1.2.17 2026-05-21 22:07:22 -05:00
Milton Gonzalez 5d7d610322 Update sidebar brand subtitle 2026-04-27 16:56:33 -05:00
Milton Gonzalez 92d934bb36 Refine sign now spacing and bump to 1.2.16 2026-04-27 16:06:33 -05:00
Milton Gonzalez 58ab0311df Increase sign now spacing and bump to 1.2.15 2026-04-27 15:15:33 -05:00
Milton Gonzalez 199e62e641 Bump version to 1.2.14 2026-04-27 15:09:28 -05:00
Milton Gonzalez 505bfa0efd Adjust spacing in sign now menu 2026-04-27 15:03:46 -05:00
17 changed files with 622 additions and 66 deletions
+107
View File
@@ -170,3 +170,110 @@ Always suggest when a skill can be applied.
## Goal ## Goal
Build a secure, scalable, production-ready digital signing platform suitable for multi-user environments and legal compliance. Build a secure, scalable, production-ready digital signing platform suitable for multi-user environments and legal compliance.
---
## Guía rápida: nuevo tipo de estampado
Cuando se cree o ajuste un nuevo estampado visual de firma, revisar siempre estas capas:
### 1. Vista previa en configuración
- Archivo: `frontend/src/components/ui/SignatureStylePicker.jsx`
- Aquí se agrega el nuevo estilo al arreglo `STYLES`
- Cambiar:
- `id`
- `label`
- `desc`
- `preview(...)`
### 2. Vista previa al colocar la firma sobre el PDF
- Archivo: `frontend/src/components/ui/PdfSignaturePlacer.jsx`
- Aquí se dibuja el estampado que el usuario ve antes de firmar
- Cambiar:
- rama `if (signatureStyle === 'nuevo-estilo')`
- ancho dinámico si aplica en `getSignatureWidth(...)`
- partición de nombre o textos fijos si aplica
### 3. Estilos CSS de las vistas previas
- Archivo: `frontend/src/index.css`
- Aquí viven:
- `.sig-preview-*`
- `.sp-*`
- Cambiar:
- borde
- fondo
- tipografía
- separadores
- layout del QR y textos
### 4. PDF final real que se incrusta al firmar
- Archivo: `backend/src/modules/signing/signing.service.js`
- Este es el archivo clave del sello visible real dentro del PDF
- Cambiar:
- función nueva tipo `drawStyleNuevo(...)`
- selección del estilo en `signPdf(...)`
- helper de líneas/nombre si aplica
- QR, bordes, divisor, tipografía y textos fijos
### 5. Flujo backend que llama al generador PDF
- Firma normal web:
- `backend/src/modules/signatures/signatures.service.js`
- Firma por invitación:
- `backend/src/modules/invitations/invitations.service.js`
- PDF legal firmado:
- `backend/src/modules/legal/legal.service.js`
Si el estampado no cambia en el PDF final, verificar primero que el flujo usado realmente pase por `backend/src/modules/signing/signing.service.js`.
### 6. Orden correcto de trabajo
1. Ajustar preview en `SignatureStylePicker.jsx`
2. Ajustar preview de estampado en `PdfSignaturePlacer.jsx`
3. Ajustar CSS en `frontend/src/index.css`
4. Ajustar sello PDF real en `backend/src/modules/signing/signing.service.js`
5. Validar:
- `cd frontend && npm run build`
- `cd backend && node -e "require('./src/app')"`
6. Si va a producción:
- `commit`
- `push`
- deploy web
### 7. Regla práctica importante
Si el cambio se ve en la web pero no en el PDF firmado:
- casi seguro falta tocar `backend/src/modules/signing/signing.service.js`
- o el backend runtime de producción quedó desalineado respecto a `source`
### 8. Verificación obligatoria en producción para cambios backend
Cuando el cambio afecta el PDF final o cualquier lógica backend:
1. hacer deploy
2. reiniciar `linkedsign-backend`
3. confirmar que el `PID` cambió:
- `ps -ef | grep 'node .*src/server.js' | grep -v grep`
4. revisar que el proceso activo escucha en `:4000`
5. recién después hacer la prueba funcional
Caso ya confirmado en producción:
- el archivo en disco puede tener el fix correcto
- pero si `linkedsign-backend` no reinicia de verdad, el proceso sigue firmando con código viejo en memoria
### 9. Método recomendado para depurar un estampado nuevo
Si un sello no refleja cambios y hay duda de ruta:
1. agregar una marca visual temporal en `backend/src/modules/signing/signing.service.js`
2. desplegar
3. reiniciar backend y confirmar cambio de `PID`
4. firmar un PDF nuevo
5. comprobar si la marca aparece en el PDF final
6. quitar la marca temporal antes de cerrar el trabajo
+90 -1
View File
@@ -2,10 +2,99 @@
## Versión actual ## Versión actual
- Versión base: `1.2.13` - Versión base: `1.2.35`
## Historial ## Historial
### 1.2.35 - 2026-05-22
- `FirmaEC` retira las marcas temporales de control del PDF final y mantiene documentado el flujo correcto para depurar nuevos estampados y verificar reinicio real del backend en producción.
### 1.2.34 - 2026-05-22
- Backend agrega el marcador de log `FIRMAEC_PDF_RENDER` para rastrear en producción si `drawStyleFirmaEC` realmente corre durante la firma PDF.
### 1.2.33 - 2026-05-22
- El estampado web de `FirmaEC` ya no muestra recuadro cuando está en `Sin borde` y parte el nombre en `2 + 2` desde la vista de colocación.
### 1.2.32 - 2026-05-22
- `FirmaEC` agrega la marca `0` al inicio de ambas líneas superiores del sello PDF para validar la ruta real de impresión.
### 1.2.31 - 2026-05-22
- La marca de control `0` de `FirmaEC` pasa a la línea `Firmado electronicamente por:` del sello PDF.
### 1.2.30 - 2026-05-22
- `FirmaEC` agrega la marca de control `0` al inicio del texto superior del sello PDF y devuelve el tamaño del nombre a `8.5`.
### 1.2.29 - 2026-05-22
- `FirmaEC` aumenta el tamaño del nombre visible en el PDF de `8.5` a `12.5`.
### 1.2.28 - 2026-05-22
- `FirmaEC` agrega la marca temporal `prueba` debajo del nombre en el sello PDF para verificar que el backend activo de firma sea el correcto.
### 1.2.27 - 2026-05-22
- `FirmaEC` parte el nombre después de la segunda palabra cuando el certificado trae 4 o más palabras, para que el salto de línea sí se vea en el PDF final.
### 1.2.26 - 2026-05-22
- El sidebar web muestra la versión activa junto a `LinkedSign`.
- `FirmaEC` divide nombres de 4 palabras en `2 + 2` dentro del sello visible del PDF.
### 1.2.25 - 2026-05-22
- La firma normal usa las preferencias visuales guardadas del usuario al momento de firmar, evitando que un estado viejo del frontend vuelva a enviar `FirmaEC` con borde.
### 1.2.24 - 2026-05-22
- `FirmaEC` sin borde ahora usa un widget criptográfico invisible en el PDF final, evitando el rectángulo residual alrededor del sello.
### 1.2.23 - 2026-05-22
- El PDF firmado oculta el borde visible del widget criptográfico de firma, para que `FirmaEC` respete de verdad la opción `Sin borde`.
### 1.2.22 - 2026-05-22
- `FirmaEC` deja de mostrar líneas de borde y separadores cuando la preferencia está en `Sin borde`, tanto al colocar la firma como en el PDF final.
### 1.2.21 - 2026-05-22
- `FirmaEC` respeta la opción `Sin borde` tanto en la vista previa web como en la firma visible del PDF.
### 1.2.20 - 2026-05-21
- Backend deja de rechazar estilos visuales desconocidos al guardar preferencias y los normaliza hacia `FirmaEC` o `QR` según corresponda.
### 1.2.19 - 2026-05-21
- Backend normaliza estilos de firma antes de validar, aceptando variantes de texto y evitando fallos intermitentes al guardar `FirmaEC`.
### 1.2.18 - 2026-05-21
- Backend vuelve tolerante el guardado de preferencias de firma para aceptar clientes parciales o legados y evitar errores al guardar `FirmaEC`.
### 1.2.17 - 2026-05-21
- Web y backend agregan el estilo de firma `FirmaEC`, con vista previa configurable y sello visible con QR de validación.
### 1.2.16 - 2026-04-27
- Web aplica separación explícita entre `Certificado digital`, `Mostrar detalles` y `Elegir posición de firma` en `Firmar ahora`.
### 1.2.15 - 2026-04-27
- Web aumenta de forma visible la separación entre `Certificado digital`, `Mostrar detalles` y `Elegir posición de firma` en `Firmar ahora`.
### 1.2.14 - 2026-04-27
- Web mejora la separación vertical en `Firmar ahora` entre el certificado, el botón `Mostrar detalles` y el botón de elegir posición de firma.
### 1.2.13 - 2026-04-27 ### 1.2.13 - 2026-04-27
- Web y Android ocultan por defecto los detalles avanzados de `Firmar ahora` y los muestran solo bajo demanda con botón de ojo. - Web y Android ocultan por defecto los detalles avanzados de `Firmar ahora` y los muestran solo bajo demanda con botón de ojo.
+1 -1
View File
@@ -2,7 +2,7 @@
## Versión actual ## Versión actual
- Base actual: `1.0.1` - Base actual: `1.2.34`
- Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION) - Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION)
- Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md) - Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md)
+9 -1
View File
@@ -2,7 +2,7 @@
## Versión actual ## Versión actual
- Base actual: `1.0.1` - Base actual: `1.2.34`
- Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION) - Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION)
- Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md) - Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md)
@@ -33,6 +33,11 @@
9. Reinicia servicios: 9. Reinicia servicios:
- `sudo systemctl restart linkedsign-frontend` - `sudo systemctl restart linkedsign-frontend`
- `sudo systemctl restart linkedsign-backend` - `sudo systemctl restart linkedsign-backend`
- Si hubo cambios en `backend/`, este reinicio es obligatorio
- Verifica que el backend sí reinició de verdad:
- `ps -ef | grep 'node .*src/server.js' | grep -v grep`
- confirma que cambió el `PID`
- `sudo systemctl status linkedsign-backend --no-pager -n 20`
10. Limpia archivos `.md` en producción: 10. Limpia archivos `.md` en producción:
- `find /opt/apps/linkedsign -type f -iname '*.md' -delete` - `find /opt/apps/linkedsign -type f -iname '*.md' -delete`
11. Verifica: 11. Verifica:
@@ -48,6 +53,9 @@
- Si cambias rutas del APK o storage, revisa también `PRODUCCION.md`. - Si cambias rutas del APK o storage, revisa también `PRODUCCION.md`.
- En producción no debe quedar código fuente Android; solo el APK release en `/opt/apps/linkedsign/android/apk/`. - En producción no debe quedar código fuente Android; solo el APK release en `/opt/apps/linkedsign/android/apk/`.
- Si en el despliegue hay cambios Android, recuerda que a producción solo pasa el `APK release` versionado; no el proyecto Android completo. - Si en el despliegue hay cambios Android, recuerda que a producción solo pasa el `APK release` versionado; no el proyecto Android completo.
- En cambios de backend no basta con copiar archivos o correr el script de deploy:
- el proceso `node src/server.js` debe reiniciarse sí o sí
- si el `PID` no cambia, el backend puede seguir firmando con código viejo en memoria aunque el archivo en disco ya esté actualizado
- El deploy web no debe tocar ni reemplazar: - El deploy web no debe tocar ni reemplazar:
- `/opt/apps/linkedsign/backend/.env` - `/opt/apps/linkedsign/backend/.env`
- `/opt/apps/linkedsign/storage/` - `/opt/apps/linkedsign/storage/`
+1 -1
View File
@@ -2,7 +2,7 @@
## Versión actual ## Versión actual
- Base actual: `1.0.1` - Base actual: `1.2.34`
- Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION) - Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION)
- Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md) - Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md)
+1 -1
View File
@@ -1 +1 @@
1.2.13 1.2.35
+2 -2
View File
@@ -18,8 +18,8 @@ android {
applicationId "net.linkedcorp.linkedsign" applicationId "net.linkedcorp.linkedsign"
minSdk 26 minSdk 26
targetSdk 35 targetSdk 35
versionCode 10213 versionCode 10235
versionName "1.2.13" versionName "1.2.35"
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\"" buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "linkedsign-backend", "name": "linkedsign-backend",
"version": "1.2.13", "version": "1.2.35",
"private": true, "private": true,
"description": "Initial backend for LinkedSign", "description": "Initial backend for LinkedSign",
"main": "src/server.js", "main": "src/server.js",
@@ -5,6 +5,7 @@ const signaturesRepository = require('./signatures.repository');
const documentsRepository = require('../documents/documents.repository'); const documentsRepository = require('../documents/documents.repository');
const certificatesRepository = require('../certificates/certificates.repository'); const certificatesRepository = require('../certificates/certificates.repository');
const certificatesService = require('../certificates/certificates.service'); const certificatesService = require('../certificates/certificates.service');
const usersRepository = require('../users/users.repository');
const { signPdf } = require('../signing/signing.service'); const { signPdf } = require('../signing/signing.service');
const auditService = require('../audit/audit.service'); const auditService = require('../audit/audit.service');
const env = require('../../config/env'); const env = require('../../config/env');
@@ -32,6 +33,12 @@ const sign = async ({ documentId, certificateId, certPassword, reason, location,
const verificationCode = uuidv4(); const verificationCode = uuidv4();
const frontendBaseUrl = (context.frontendUrl || env.frontendUrl || '').replace(/\/$/, ''); const frontendBaseUrl = (context.frontendUrl || env.frontendUrl || '').replace(/\/$/, '');
const verificationUrl = `${frontendBaseUrl}/verify/${verificationCode}`; const verificationUrl = `${frontendBaseUrl}/verify/${verificationCode}`;
const storedSignaturePreferences = await usersRepository.getSignaturePreferences(userId);
const effectiveSignatureStyle = storedSignaturePreferences.signatureStyle || signatureStyle || 'classic';
const effectiveQrPosition = storedSignaturePreferences.qrPosition || qrPosition || 'left';
const effectiveQrBackground = storedSignaturePreferences.qrBackground || qrBackground || 'transparent';
const effectiveQrBorder = storedSignaturePreferences.qrBorder || qrBorder || 'none';
const effectiveSignatureImageBase64 = storedSignaturePreferences.signatureImageBase64 || signatureImageBase64 || null;
let signedFilename, signedPath; let signedFilename, signedPath;
try { try {
@@ -44,11 +51,11 @@ const sign = async ({ documentId, certificateId, certPassword, reason, location,
location: location || '', location: location || '',
page: page || 0, page: page || 0,
rect: rect || null, rect: rect || null,
signatureStyle: signatureStyle || 'classic', signatureStyle: effectiveSignatureStyle,
qrPosition: qrPosition || 'left', qrPosition: effectiveQrPosition,
qrBackground: qrBackground || 'transparent', qrBackground: effectiveQrBackground,
qrBorder: qrBorder || 'none', qrBorder: effectiveQrBorder,
signatureImageBase64: signatureImageBase64 || null, signatureImageBase64: effectiveSignatureImageBase64,
verificationUrl, verificationUrl,
clientLocale: clientLocale || context.clientLocale || null, clientLocale: clientLocale || context.clientLocale || null,
clientTimeZone: clientTimeZone || context.clientTimeZone || null, clientTimeZone: clientTimeZone || context.clientTimeZone || null,
+144 -8
View File
@@ -1,7 +1,7 @@
const fs = require('fs/promises'); const fs = require('fs/promises');
const path = require('path'); const path = require('path');
const QRCode = require('qrcode'); const QRCode = require('qrcode');
const { PDFDocument, rgb, StandardFonts } = require('pdf-lib'); const { PDFDocument, PDFArray, PDFName, PDFNumber, rgb, StandardFonts } = require('pdf-lib');
const { pdflibAddPlaceholder } = require('@signpdf/placeholder-pdf-lib'); const { pdflibAddPlaceholder } = require('@signpdf/placeholder-pdf-lib');
const { SignPdf } = require('@signpdf/signpdf'); const { SignPdf } = require('@signpdf/signpdf');
const { P12Signer } = require('@signpdf/signer-p12'); const { P12Signer } = require('@signpdf/signer-p12');
@@ -33,10 +33,47 @@ function truncate(text, font, size, maxWidth) {
} }
function getSignatureWidth(style, signerName = '') { function getSignatureWidth(style, signerName = '') {
if (style !== 'qr') return 200; if (style === 'qr' || style === 'firmaec') {
const normalizedName = String(signerName).trim(); const normalizedName = String(signerName).trim();
const calculatedWidth = 78 + (normalizedName.length * 4.9); const calculatedWidth = 78 + (normalizedName.length * 4.9);
return Math.max(170, Math.min(270, Math.round(calculatedWidth))); return Math.max(170, Math.min(270, Math.round(calculatedWidth)));
}
return 200;
}
function wrapToLines(text, font, size, maxWidth, maxLines) {
const words = text.split(' ');
const lines = [];
let current = '';
for (const word of words) {
if (lines.length >= maxLines) break;
const test = current ? `${current} ${word}` : word;
if (font.widthOfTextAtSize(test, size) <= maxWidth) {
current = test;
} else {
if (current) lines.push(current);
current = truncate(word, font, size, maxWidth);
}
}
if (current && lines.length < maxLines) lines.push(current);
return lines;
}
function getFirmaEcNameLines(signerName, font, size, maxWidth) {
const words = String(signerName || '')
.trim()
.toUpperCase()
.split(/\s+/)
.filter(Boolean);
if (words.length >= 4) {
return [
words.slice(0, 2).join(' '),
words.slice(2).join(' '),
].map((line) => truncate(line, font, size, maxWidth));
}
return wrapToLines(words.join(' '), font, size, maxWidth, 2);
} }
function buildLocalizedSignatureTimestamp({ locale, timeZone } = {}) { function buildLocalizedSignatureTimestamp({ locale, timeZone } = {}) {
@@ -67,6 +104,34 @@ function buildLocalizedSignatureTimestamp({ locale, timeZone } = {}) {
} }
} }
function hideSignatureWidgetBorder(pdfDoc, pageIndex) {
const page = pdfDoc.getPages()[pageIndex];
if (!page) return;
const annotations = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
if (!annotations || annotations.size() === 0) return;
const widgetRef = annotations.get(annotations.size() - 1);
const widget = pdfDoc.context.lookup(widgetRef);
if (!widget) return;
const zeroBorder = PDFArray.withContext(pdfDoc.context);
zeroBorder.push(PDFNumber.of(0));
zeroBorder.push(PDFNumber.of(0));
zeroBorder.push(PDFNumber.of(0));
widget.set(PDFName.of('Border'), zeroBorder);
widget.set(PDFName.of('BS'), pdfDoc.context.obj({
Type: 'Border',
W: 0,
S: 'S',
}));
widget.set(PDFName.of('MK'), pdfDoc.context.obj({
BC: [],
BG: [],
}));
}
// ── Style: CLASSIC (blue bar + DS badge) ────────────────────────────────────── // ── Style: CLASSIC (blue bar + DS badge) ──────────────────────────────────────
async function drawStyleClassic(pdfDoc, page, sx, sy, sw, sh, { signerName, reason, dateStr, timeStr, location }) { async function drawStyleClassic(pdfDoc, page, sx, sy, sw, sh, { signerName, reason, dateStr, timeStr, location }) {
const f1 = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const f1 = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
@@ -240,6 +305,68 @@ async function drawStyleImage(pdfDoc, page, sx, sy, sw, sh, { signerName, reason
if (location) page.drawText(truncate(location, f2, 6, textW), { x: textX, y: sy + sh - 59, size: 6, font: f2, color: C.lightGray }); if (location) page.drawText(truncate(location, f2, 6, textW), { x: textX, y: sy + sh - 59, size: 6, font: f2, color: C.lightGray });
} }
// ── Style: FIRMAEC (QR izquierda + texto monoespaciado estilo FirmaEC Ecuador) ─
async function drawStyleFirmaEC(pdfDoc, page, sx, sy, sw, sh, { signerName, verificationUrl, qrPosition, qrBackground, qrBorder }) {
const fBold = await pdfDoc.embedFont(StandardFonts.CourierBold);
const fReg = await pdfDoc.embedFont(StandardFonts.Courier);
const useSoftBackground = qrBackground === 'soft';
const useBorder = qrBorder === 'line';
const qrOnRight = qrPosition === 'right';
const qrContent = verificationUrl || [
'FIRMA DIGITAL VERIFICADA',
`Firmante: ${signerName}`,
].join('\n');
const qrPng = await QRCode.toBuffer(qrContent, {
type: 'png',
width: 120,
margin: 1,
color: { dark: '#000000', light: '#ffffff' },
});
const qrImage = await pdfDoc.embedPng(qrPng);
const qrSize = Math.min(sh - 10, 50);
// Background
const bgColor = useSoftBackground ? C.bgBlue : C.white;
page.drawRectangle({ x: sx, y: sy, width: sw, height: sh, color: bgColor });
if (useBorder) {
page.drawRectangle({
x: sx,
y: sy,
width: sw,
height: sh,
borderColor: rgb(0.15, 0.15, 0.15),
borderWidth: 0.85,
});
}
// QR image
const qrX = qrOnRight ? sx + sw - qrSize - 4 : sx + 4;
const qrY = sy + (sh - qrSize) / 2;
page.drawImage(qrImage, { x: qrX, y: qrY, width: qrSize, height: qrSize });
// Vertical divider belongs to the bordered variant only.
const divX = qrOnRight ? qrX - 4 : sx + qrSize + 8;
if (useBorder) {
page.drawLine({ start: { x: divX, y: sy + 6 }, end: { x: divX, y: sy + sh - 6 }, thickness: 0.5, color: rgb(0.55, 0.55, 0.58) });
}
// Text column
const textX = qrOnRight ? sx + 6 : divX + 5;
const textW = sw - qrSize - 18;
page.drawText('Validar unicamente en FirmaEC.', { x: textX, y: sy + sh - 12, size: 6, font: fReg, color: rgb(0.12, 0.12, 0.14) });
page.drawText('Firmado electronicamente por:', { x: textX, y: sy + sh - 21, size: 6, font: fReg, color: rgb(0.12, 0.12, 0.14) });
// Name — bold, uppercase, word-wrapped up to 2 lines
const nameSize = 8.5;
const nameLines = getFirmaEcNameLines(signerName, fBold, nameSize, textW);
nameLines.forEach((line, i) => {
page.drawText(line, { x: textX, y: sy + sh - 34 - (i * 13), size: nameSize, font: fBold, color: rgb(0.05, 0.05, 0.07) });
});
}
// ── Main sign function ───────────────────────────────────────────────────────── // ── Main sign function ─────────────────────────────────────────────────────────
const signPdf = async ({ const signPdf = async ({
pdfPath, p12Buffer, p12Password, pdfPath, p12Buffer, p12Password,
@@ -300,6 +427,7 @@ const signPdf = async ({
if (style === 'qr') await drawStyleQR(pdfDoc, tp, sx, sy, sw, sh, info); if (style === 'qr') await drawStyleQR(pdfDoc, tp, sx, sy, sw, sh, info);
else if (style === 'professional') await drawStyleProfessional(pdfDoc, tp, sx, sy, sw, sh, info); else if (style === 'professional') await drawStyleProfessional(pdfDoc, tp, sx, sy, sw, sh, info);
else if (style === 'image') await drawStyleImage(pdfDoc, tp, sx, sy, sw, sh, info); else if (style === 'image') await drawStyleImage(pdfDoc, tp, sx, sy, sw, sh, info);
else if (style === 'firmaec') await drawStyleFirmaEC(pdfDoc, tp, sx, sy, sw, sh, info);
else await drawStyleClassic(pdfDoc, tp, sx, sy, sw, sh, info); else await drawStyleClassic(pdfDoc, tp, sx, sy, sw, sh, info);
// ── Save page with visual stamp ─────────────────────────────────── // ── Save page with visual stamp ───────────────────────────────────
@@ -307,16 +435,24 @@ const signPdf = async ({
// ── Add cryptographic signature placeholder ─────────────────────── // ── Add cryptographic signature placeholder ───────────────────────
const pdfDocForSign = await PDFDocument.load(pdfWithStamp); const pdfDocForSign = await PDFDocument.load(pdfWithStamp);
const placeholderPageIndex = Math.min(page, pdfDocForSign.getPageCount() - 1);
const placeholderPage = pdfDocForSign.getPages()[placeholderPageIndex];
const useInvisibleWidget = style === 'firmaec' && qrBorder === 'none';
const widgetRect = useInvisibleWidget
? [0, 0, 0, 0]
: [sx, sy, sx + sw, sy + sh];
pdflibAddPlaceholder({ pdflibAddPlaceholder({
pdfDoc: pdfDocForSign, pdfPage: placeholderPage,
reason: reason || 'Firma digital', reason: reason || 'Firma digital',
contactInfo: signerName, contactInfo: signerName,
name: signerName, name: signerName,
location: location || '', location: location || '',
signatureLength: 32768, signatureLength: 32768,
rect: [sx, sy, sx + sw, sy + sh], widgetRect,
pageIndex: Math.min(page, pdfDocForSign.getPageCount() - 1),
}); });
if (!useInvisibleWidget) {
hideSignatureWidgetBorder(pdfDocForSign, placeholderPageIndex);
}
const pdfWithPlaceholder = Buffer.from(await pdfDocForSign.save({ useObjectStreams: false })); const pdfWithPlaceholder = Buffer.from(await pdfDocForSign.save({ useObjectStreams: false }));
+18 -26
View File
@@ -1,44 +1,36 @@
const usersRepository = require('./users.repository'); const usersRepository = require('./users.repository');
const ALLOWED_SIGNATURE_STYLES = ['classic', 'qr', 'professional']; const ALLOWED_SIGNATURE_STYLES = ['classic', 'qr', 'professional', 'firmaec'];
const ALLOWED_QR_POSITIONS = ['left', 'right']; const ALLOWED_QR_POSITIONS = ['left', 'right'];
const ALLOWED_QR_BACKGROUNDS = ['transparent', 'soft']; const ALLOWED_QR_BACKGROUNDS = ['transparent', 'soft'];
const ALLOWED_QR_BORDERS = ['none', 'line']; const ALLOWED_QR_BORDERS = ['none', 'line'];
const LEGACY_SIGNATURE_STYLE_MAP = {
image: 'qr',
firmaecuador: 'firmaec',
'firma ec': 'firmaec',
};
const getSignaturePreferences = async (userId) => { const getSignaturePreferences = async (userId) => {
return usersRepository.getSignaturePreferences(userId); return usersRepository.getSignaturePreferences(userId);
}; };
const updateSignaturePreferences = async (userId, { signatureStyle, qrPosition, qrBackground, qrBorder, signatureImageBase64 }) => { const updateSignaturePreferences = async (userId, { signatureStyle, qrPosition, qrBackground, qrBorder, signatureImageBase64 }) => {
if (!ALLOWED_SIGNATURE_STYLES.includes(signatureStyle)) { const rawSignatureStyle = String(signatureStyle || '').trim().toLowerCase();
const error = new Error('Invalid signature style.'); let normalizedSignatureStyle = LEGACY_SIGNATURE_STYLE_MAP[rawSignatureStyle] || rawSignatureStyle || 'qr';
error.statusCode = 400; if (!ALLOWED_SIGNATURE_STYLES.includes(normalizedSignatureStyle)) {
throw error; normalizedSignatureStyle = rawSignatureStyle.includes('firma') ? 'firmaec' : 'qr';
} }
if (!ALLOWED_QR_POSITIONS.includes(qrPosition)) { // Preferences should be resilient to stale clients or partially populated forms.
const error = new Error('Invalid QR position.'); const resolvedQrPosition = ALLOWED_QR_POSITIONS.includes(qrPosition) ? qrPosition : 'left';
error.statusCode = 400; const resolvedQrBackground = ALLOWED_QR_BACKGROUNDS.includes(qrBackground) ? qrBackground : 'transparent';
throw error; const resolvedQrBorder = ALLOWED_QR_BORDERS.includes(qrBorder) ? qrBorder : 'none';
}
if (!ALLOWED_QR_BACKGROUNDS.includes(qrBackground)) {
const error = new Error('Invalid QR background.');
error.statusCode = 400;
throw error;
}
if (!ALLOWED_QR_BORDERS.includes(qrBorder)) {
const error = new Error('Invalid QR border.');
error.statusCode = 400;
throw error;
}
return usersRepository.setSignaturePreferences(userId, { return usersRepository.setSignaturePreferences(userId, {
signatureStyle, signatureStyle: normalizedSignatureStyle,
qrPosition, qrPosition: resolvedQrPosition,
qrBackground, qrBackground: resolvedQrBackground,
qrBorder, qrBorder: resolvedQrBorder,
signatureImageBase64: signatureImageBase64 || null, signatureImageBase64: signatureImageBase64 || null,
}); });
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "1.2.13", "version": "1.2.35",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+3 -2
View File
@@ -3,6 +3,7 @@ import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { FileText, Shield, PenTool, LayoutDashboard, LogOut, Palette, Menu, X, KeyRound, Users, Settings, ScrollText, Scale } from 'lucide-react'; import { FileText, Shield, PenTool, LayoutDashboard, LogOut, Palette, Menu, X, KeyRound, Users, Settings, ScrollText, Scale } from 'lucide-react';
import ChangePasswordModal from '../ui/ChangePasswordModal'; import ChangePasswordModal from '../ui/ChangePasswordModal';
import packageInfo from '../../../package.json';
const navSections = [ const navSections = [
{ {
@@ -86,8 +87,8 @@ export default function Layout({ children }) {
<img src="/app-icon-96.png" alt="LinkedSign" width="32" height="32" style={{ display: 'block' }} /> <img src="/app-icon-96.png" alt="LinkedSign" width="32" height="32" style={{ display: 'block' }} />
</div> </div>
<div className="sidebar-brand-copy"> <div className="sidebar-brand-copy">
<span>LinkedSign</span> <span>LinkedSign <em>v{packageInfo.version}</em></span>
<small>Desarrollo de Soluciones</small> <small>Firma Electrónica @LinkedCorp</small>
</div> </div>
<button <button
type="button" type="button"
@@ -13,12 +13,20 @@ const SIG_W = 200;
const SIG_H = 60; const SIG_H = 60;
const getSignatureWidth = (style, signerName = '') => { const getSignatureWidth = (style, signerName = '') => {
if (style !== 'qr') return SIG_W; if (style !== 'qr' && style !== 'firmaec') return SIG_W;
const normalizedName = signerName.trim(); const normalizedName = signerName.trim();
const calculatedWidth = 78 + (normalizedName.length * 4.9); const calculatedWidth = 78 + (normalizedName.length * 4.9);
return Math.max(170, Math.min(270, Math.round(calculatedWidth))); return Math.max(170, Math.min(270, Math.round(calculatedWidth)));
}; };
const getFirmaEcPreviewLines = (signerName = '') => {
const words = String(signerName).trim().toUpperCase().split(/\s+/).filter(Boolean);
if (words.length >= 4) {
return [words.slice(0, 2).join(' '), words.slice(2).join(' ')];
}
return [words.join(' ')];
};
export default function PdfSignaturePlacer({ export default function PdfSignaturePlacer({
pdfBlob, pdfBlob,
signerName, signerName,
@@ -162,6 +170,39 @@ export default function PdfSignaturePlacer({
); );
} }
if (signatureStyle === 'firmaec') {
const nameLines = getFirmaEcPreviewLines(signerName);
return (
<div className="sig-preview-body sig-preview-body--firmaec">
{qrPosition === 'left' && (
<>
<div className="sig-preview-qr-box sig-preview-qr-box--firmaec" data-background={qrBackground}>
<div className="sig-preview-qr-pattern sig-preview-qr-pattern--firmaec" />
</div>
{qrBorder === 'line' && <div className="sig-preview-divider" />}
</>
)}
<div className="sig-preview-text sig-preview-text--firmaec">
<small>Validar unicamente en FirmaEC.</small>
<small>Firmado electronicamente por:</small>
<strong>
{nameLines.map((line, index) => (
<span key={`${line}-${index}`}>{line}</span>
))}
</strong>
</div>
{qrPosition === 'right' && (
<>
{qrBorder === 'line' && <div className="sig-preview-divider" />}
<div className="sig-preview-qr-box sig-preview-qr-box--firmaec" data-background={qrBackground}>
<div className="sig-preview-qr-pattern sig-preview-qr-pattern--firmaec" />
</div>
</>
)}
</div>
);
}
if (signatureStyle === 'image') { if (signatureStyle === 'image') {
return ( return (
<div className="sig-preview-body sig-preview-body--image"> <div className="sig-preview-body sig-preview-body--image">
@@ -225,8 +266,8 @@ export default function PdfSignaturePlacer({
{sigBox && ( {sigBox && (
<div <div
className={`sig-preview-box sig-preview-box--${signatureStyle}`} className={`sig-preview-box sig-preview-box--${signatureStyle}`}
data-background={signatureStyle === 'qr' ? qrBackground : undefined} data-background={signatureStyle === 'qr' || signatureStyle === 'firmaec' ? qrBackground : undefined}
data-border={signatureStyle === 'qr' ? qrBorder : undefined} data-border={signatureStyle === 'qr' || signatureStyle === 'firmaec' ? qrBorder : undefined}
style={{ left: sigBox.x, top: sigBox.y, width: signatureWidth, height: SIG_H }} style={{ left: sigBox.x, top: sigBox.y, width: signatureWidth, height: SIG_H }}
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
> >
@@ -1,4 +1,4 @@
import { QrCode, Briefcase, Stamp, PanelsLeftBottom, PanelsRightBottom, CircleOff, Square } from 'lucide-react'; import { QrCode, Briefcase, Stamp, PanelsLeftBottom, PanelsRightBottom, CircleOff, Square, ShieldCheck } from 'lucide-react';
const getQrPreviewWidth = (name = '') => { const getQrPreviewWidth = (name = '') => {
const extraWidth = Math.max(0, name.trim().length - 18) * 4; const extraWidth = Math.max(0, name.trim().length - 18) * 4;
@@ -54,7 +54,7 @@ const STYLES = [
</svg> </svg>
<span className="sp-qr-hint">Escanear</span> <span className="sp-qr-hint">Escanear</span>
</div> </div>
<div className="sp-divider-v" /> {qrBorder === 'line' && <div className="sp-divider-v" />}
</> </>
)} )}
<div className="sp-info sp-info--qr"> <div className="sp-info sp-info--qr">
@@ -64,7 +64,7 @@ const STYLES = [
</div> </div>
{qrPosition === 'right' && ( {qrPosition === 'right' && (
<> <>
<div className="sp-divider-v" /> {qrBorder === 'line' && <div className="sp-divider-v" />}
<div className="sp-qr-box" data-background={qrBackground} data-border={qrBorder}> <div className="sp-qr-box" data-background={qrBackground} data-border={qrBorder}>
<svg viewBox="0 0 10 10" className="sp-qr-svg" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 10 10" className="sp-qr-svg" xmlns="http://www.w3.org/2000/svg">
{[0,1,2,3,4,5,6,7,8,9].map(r => {[0,1,2,3,4,5,6,7,8,9].map(r =>
@@ -84,6 +84,61 @@ const STYLES = [
</div> </div>
), ),
}, },
{
id: 'firmaec',
label: 'FirmaEC',
icon: ShieldCheck,
desc: 'Formato oficial FirmaEC: QR de validación + nombre en negrita',
preview: ({ name, qrPosition = 'left', qrBackground = 'transparent', qrBorder = 'none' }) => (
<div
className="sig-preview sig-preview--firmaec"
data-background={qrBackground}
data-border={qrBorder}
>
{qrPosition === 'left' && (
<>
<div className="sp-qr-box sp-qr-box--firmaec" data-background={qrBackground}>
<svg viewBox="0 0 10 10" className="sp-qr-svg" xmlns="http://www.w3.org/2000/svg">
{[0,1,2,3,4,5,6,7,8,9].map(r =>
[0,1,2,3,4,5,6,7,8,9].map(c => {
const v = ((r * 3 + c * 7 + r ^ c) % 3 === 0);
return v ? <rect key={`${r}-${c}`} x={c} y={r} width={1} height={1} fill="#000" /> : null;
})
)}
<rect x={0} y={0} width={3} height={3} fill="none" stroke="#000" strokeWidth={0.3} />
<rect x={7} y={0} width={3} height={3} fill="none" stroke="#000" strokeWidth={0.3} />
<rect x={0} y={7} width={3} height={3} fill="none" stroke="#000" strokeWidth={0.3} />
</svg>
</div>
{qrBorder === 'line' && <div className="sp-divider-v" />}
</>
)}
<div className="sp-firmaec-text">
<span>Validar únicamente en FirmaEC.</span>
<span>Firmado electrónicamente por:</span>
<strong>{name.toUpperCase()}</strong>
</div>
{qrPosition === 'right' && (
<>
{qrBorder === 'line' && <div className="sp-divider-v" />}
<div className="sp-qr-box sp-qr-box--firmaec" data-background={qrBackground}>
<svg viewBox="0 0 10 10" className="sp-qr-svg" xmlns="http://www.w3.org/2000/svg">
{[0,1,2,3,4,5,6,7,8,9].map(r =>
[0,1,2,3,4,5,6,7,8,9].map(c => {
const v = ((r * 3 + c * 7 + r ^ c) % 3 === 0);
return v ? <rect key={`${r}-${c}`} x={c} y={r} width={1} height={1} fill="#000" /> : null;
})
)}
<rect x={0} y={0} width={3} height={3} fill="none" stroke="#000" strokeWidth={0.3} />
<rect x={7} y={0} width={3} height={3} fill="none" stroke="#000" strokeWidth={0.3} />
<rect x={0} y={7} width={3} height={3} fill="none" stroke="#000" strokeWidth={0.3} />
</svg>
</div>
</>
)}
</div>
),
},
{ {
id: 'professional', id: 'professional',
label: 'Profesional', label: 'Profesional',
@@ -145,7 +200,7 @@ export default function SignatureStylePicker({
})} })}
</div> </div>
<p className="style-card-desc">{s.desc}</p> <p className="style-card-desc">{s.desc}</p>
{s.id === 'qr' && isActive && ( {(s.id === 'qr' || s.id === 'firmaec') && isActive && (
<div className="style-card-inline-tools"> <div className="style-card-inline-tools">
<div className="style-inline-group"> <div className="style-inline-group">
<button <button
+120
View File
@@ -134,6 +134,16 @@ select {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--c-text); color: var(--c-text);
display: inline-flex;
align-items: baseline;
gap: 6px;
}
.sidebar-brand-copy span em {
font-size: 10px;
font-style: normal;
font-weight: 400;
color: var(--c-muted);
} }
.sidebar-brand-copy small { .sidebar-brand-copy small {
@@ -1037,6 +1047,9 @@ select {
border: 0.5px solid var(--c-border); border: 0.5px solid var(--c-border);
border-radius: 12px; border-radius: 12px;
padding: 24px; padding: 24px;
display: flex;
flex-direction: column;
gap: 14px;
} }
.select-action-row { .select-action-row {
@@ -1066,6 +1079,19 @@ select {
gap: 8px; gap: 8px;
} }
.sign-certificate-field {
margin-bottom: 14px;
}
.sign-details-toggle {
margin-bottom: 16px;
}
.sign-form-card > .info-box + .sign-place-button,
.sign-place-button {
margin-top: 0;
}
.sign-form-card form { display: flex; flex-direction: column; gap: 14px; } .sign-form-card form { display: flex; flex-direction: column; gap: 14px; }
.sign-info-panel { display: flex; flex-direction: column; gap: 14px; } .sign-info-panel { display: flex; flex-direction: column; gap: 14px; }
@@ -1488,6 +1514,18 @@ select {
border-color: transparent; border-color: transparent;
box-shadow: none; box-shadow: none;
} }
.sig-preview-box--firmaec {
background: transparent;
border-color: transparent;
box-shadow: none;
}
.sig-preview-box--firmaec[data-background='soft'] {
background: rgba(242, 244, 255, 0.96);
}
.sig-preview-box--firmaec[data-border='line'] {
border-color: rgba(55, 65, 81, 0.7);
box-shadow: 0 2px 8px rgba(31, 41, 55, 0.16);
}
.sig-preview-box--qr[data-background='soft'] { .sig-preview-box--qr[data-background='soft'] {
background: rgba(242, 244, 255, 0.96); background: rgba(242, 244, 255, 0.96);
} }
@@ -1852,6 +1890,88 @@ select {
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
} }
.sig-preview--firmaec {
display: flex;
align-items: stretch;
background: #fff;
border: 1px solid transparent;
padding: 8px;
gap: 8px;
}
.sig-preview--firmaec[data-background='soft'] {
background: #eef2ff;
}
.sig-preview--firmaec[data-border='line'] {
border-color: rgba(0, 0, 0, 0.45);
border-width: 1.5px;
}
.sp-qr-box--firmaec {
background: #fff;
}
.sp-qr-box--firmaec[data-background='soft'] {
background: #f5f5f5;
}
.sp-firmaec-text {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
flex: 1;
min-width: 0;
font-family: 'Courier New', Courier, monospace;
}
.sp-firmaec-text span {
font-size: 8.5px;
color: #1a1a1a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sp-firmaec-text strong {
font-size: 11px;
font-weight: 800;
color: #0d0d0d;
white-space: normal;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-family: 'Courier New', Courier, monospace;
}
.sig-preview-body--firmaec {
display: flex;
align-items: center;
gap: 8px;
height: 100%;
}
.sig-preview-qr-box--firmaec {
background: #fff;
}
.sig-preview-qr-pattern--firmaec {
background:
linear-gradient(90deg, #111 20%, transparent 20% 40%, #111 40% 60%, transparent 60% 80%, #111 80%),
linear-gradient(#111 20%, transparent 20% 40%, #111 40% 60%, transparent 60% 80%, #111 80%);
opacity: 0.9;
}
.sig-preview-text--firmaec {
font-family: 'Courier New', Courier, monospace;
gap: 3px;
}
.sig-preview-text--firmaec strong {
text-transform: uppercase;
display: flex;
flex-direction: column;
line-height: 1.15;
}
.sig-preview-text--firmaec strong span {
display: block;
}
.sig-preview-divider {
width: 1px;
height: 70%;
background: rgba(85, 85, 88, 0.58);
}
.sig-preview--image { .sig-preview--image {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
@@ -286,7 +286,7 @@ export default function SignaturesPage() {
)} )}
{uploadingDocument && <small className="field-hint">Subiendo documento...</small>} {uploadingDocument && <small className="field-hint">Subiendo documento...</small>}
</div> </div>
<div className="field"> <div className="field sign-certificate-field">
<label><Shield size={14} /> Certificado digital</label> <label><Shield size={14} /> Certificado digital</label>
<select name="certificateId" value={form.certificateId} onChange={handleChange} required> <select name="certificateId" value={form.certificateId} onChange={handleChange} required>
<option value="">Selecciona un certificado...</option> <option value="">Selecciona un certificado...</option>
@@ -296,7 +296,7 @@ export default function SignaturesPage() {
</div> </div>
<button <button
type="button" type="button"
className="btn btn-ghost btn-inline-toggle" className="btn btn-ghost btn-inline-toggle sign-details-toggle"
onClick={() => setShowAdvancedOptions((current) => !current)} onClick={() => setShowAdvancedOptions((current) => !current)}
> >
{showAdvancedOptions ? <EyeOff size={16} /> : <Eye size={16} />} {showAdvancedOptions ? <EyeOff size={16} /> : <Eye size={16} />}
@@ -316,13 +316,13 @@ export default function SignaturesPage() {
<h4>Estilo aplicado</h4> <h4>Estilo aplicado</h4>
<p>La firma usará tu configuración guardada en el menú <strong>Estilo Firma</strong>.</p> <p>La firma usará tu configuración guardada en el menú <strong>Estilo Firma</strong>.</p>
<p><strong>Tipo:</strong> {form.signatureStyle}</p> <p><strong>Tipo:</strong> {form.signatureStyle}</p>
{form.signatureStyle === 'qr' && <p><strong>Posición QR:</strong> {form.qrPosition === 'left' ? 'Izquierda' : 'Derecha'}</p>} {(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Posición QR:</strong> {form.qrPosition === 'left' ? 'Izquierda' : 'Derecha'}</p>}
{form.signatureStyle === 'qr' && <p><strong>Fondo:</strong> {form.qrBackground === 'transparent' ? 'Transparente' : 'Suave'}</p>} {(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Fondo:</strong> {form.qrBackground === 'transparent' ? 'Transparente' : 'Suave'}</p>}
{form.signatureStyle === 'qr' && <p><strong>Borde:</strong> {form.qrBorder === 'none' ? 'Sin borde' : 'Con borde'}</p>} {(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Borde:</strong> {form.qrBorder === 'none' ? 'Sin borde' : 'Con borde'}</p>}
</div> </div>
</> </>
)} )}
<button className="btn btn-primary btn-full" onClick={handleGoToPlace}> <button className="btn btn-primary btn-full sign-place-button" onClick={handleGoToPlace}>
<MapPin size={16} /> Elegir posición de firma → <MapPin size={16} /> Elegir posición de firma →
</button> </button>
</div> </div>
@@ -425,9 +425,9 @@ export default function SignaturesPage() {
<p><strong>Razón:</strong> {form.reason}</p> <p><strong>Razón:</strong> {form.reason}</p>
{form.location && <p><strong>Ubicación:</strong> {form.location}</p>} {form.location && <p><strong>Ubicación:</strong> {form.location}</p>}
<p><strong>Estilo visual:</strong> {form.signatureStyle}</p> <p><strong>Estilo visual:</strong> {form.signatureStyle}</p>
{form.signatureStyle === 'qr' && <p><strong>Posición QR:</strong> {form.qrPosition === 'left' ? 'Izquierda' : 'Derecha'}</p>} {(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Posición QR:</strong> {form.qrPosition === 'left' ? 'Izquierda' : 'Derecha'}</p>}
{form.signatureStyle === 'qr' && <p><strong>Fondo:</strong> {form.qrBackground === 'transparent' ? 'Transparente' : 'Suave'}</p>} {(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Fondo:</strong> {form.qrBackground === 'transparent' ? 'Transparente' : 'Suave'}</p>}
{form.signatureStyle === 'qr' && <p><strong>Borde:</strong> {form.qrBorder === 'none' ? 'Sin borde' : 'Con borde'}</p>} {(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Borde:</strong> {form.qrBorder === 'none' ? 'Sin borde' : 'Con borde'}</p>}
</> </>
)} )}
</div> </div>