Compare commits
21 Commits
58ab0311df
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d17393d5f | |||
| 848441d593 | |||
| 99a5350008 | |||
| 82f3d2cbf9 | |||
| ff7ff92d84 | |||
| b6c169ffbb | |||
| 8d090dc8ca | |||
| 7fc4707724 | |||
| bc28441531 | |||
| 3c9aefa10e | |||
| 81bd8d3c59 | |||
| fdde9cdb5d | |||
| 5ed70766b9 | |||
| 2a0f272af1 | |||
| e67e7aa122 | |||
| f19f8bc899 | |||
| dfcc0ca37b | |||
| 8308e3537e | |||
| 8bc244855c | |||
| 5d7d610322 | |||
| 92d934bb36 |
@@ -170,3 +170,110 @@ Always suggest when a skill can be applied.
|
||||
## Goal
|
||||
|
||||
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
|
||||
|
||||
+82
-1
@@ -2,10 +2,91 @@
|
||||
|
||||
## Versión actual
|
||||
|
||||
- Versión base: `1.2.15`
|
||||
- Versión base: `1.2.35`
|
||||
|
||||
## 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
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## Versión actual
|
||||
|
||||
- Base actual: `1.2.15`
|
||||
- Base actual: `1.2.34`
|
||||
- Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION)
|
||||
- Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md)
|
||||
|
||||
|
||||
+9
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## Versión actual
|
||||
|
||||
- Base actual: `1.2.15`
|
||||
- Base actual: `1.2.34`
|
||||
- Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION)
|
||||
- Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md)
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
9. Reinicia servicios:
|
||||
- `sudo systemctl restart linkedsign-frontend`
|
||||
- `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:
|
||||
- `find /opt/apps/linkedsign -type f -iname '*.md' -delete`
|
||||
11. Verifica:
|
||||
@@ -48,6 +53,9 @@
|
||||
- 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/`.
|
||||
- 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:
|
||||
- `/opt/apps/linkedsign/backend/.env`
|
||||
- `/opt/apps/linkedsign/storage/`
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## Versión actual
|
||||
|
||||
- Base actual: `1.2.15`
|
||||
- Base actual: `1.2.34`
|
||||
- Fuente de verdad: [VERSION](/home/mgonzalez/desarrollo/react/linkedsign/VERSION)
|
||||
- Historial de cambios: [CHANGELOG.md](/home/mgonzalez/desarrollo/react/linkedsign/CHANGELOG.md)
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ android {
|
||||
applicationId "net.linkedcorp.linkedsign"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 10215
|
||||
versionName "1.2.15"
|
||||
versionCode 10235
|
||||
versionName "1.2.35"
|
||||
|
||||
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkedsign-backend",
|
||||
"version": "1.2.15",
|
||||
"version": "1.2.35",
|
||||
"private": true,
|
||||
"description": "Initial backend for LinkedSign",
|
||||
"main": "src/server.js",
|
||||
|
||||
@@ -5,6 +5,7 @@ const signaturesRepository = require('./signatures.repository');
|
||||
const documentsRepository = require('../documents/documents.repository');
|
||||
const certificatesRepository = require('../certificates/certificates.repository');
|
||||
const certificatesService = require('../certificates/certificates.service');
|
||||
const usersRepository = require('../users/users.repository');
|
||||
const { signPdf } = require('../signing/signing.service');
|
||||
const auditService = require('../audit/audit.service');
|
||||
const env = require('../../config/env');
|
||||
@@ -32,6 +33,12 @@ const sign = async ({ documentId, certificateId, certPassword, reason, location,
|
||||
const verificationCode = uuidv4();
|
||||
const frontendBaseUrl = (context.frontendUrl || env.frontendUrl || '').replace(/\/$/, '');
|
||||
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;
|
||||
try {
|
||||
@@ -44,11 +51,11 @@ const sign = async ({ documentId, certificateId, certPassword, reason, location,
|
||||
location: location || '',
|
||||
page: page || 0,
|
||||
rect: rect || null,
|
||||
signatureStyle: signatureStyle || 'classic',
|
||||
qrPosition: qrPosition || 'left',
|
||||
qrBackground: qrBackground || 'transparent',
|
||||
qrBorder: qrBorder || 'none',
|
||||
signatureImageBase64: signatureImageBase64 || null,
|
||||
signatureStyle: effectiveSignatureStyle,
|
||||
qrPosition: effectiveQrPosition,
|
||||
qrBackground: effectiveQrBackground,
|
||||
qrBorder: effectiveQrBorder,
|
||||
signatureImageBase64: effectiveSignatureImageBase64,
|
||||
verificationUrl,
|
||||
clientLocale: clientLocale || context.clientLocale || null,
|
||||
clientTimeZone: clientTimeZone || context.clientTimeZone || null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
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 { SignPdf } = require('@signpdf/signpdf');
|
||||
const { P12Signer } = require('@signpdf/signer-p12');
|
||||
@@ -33,11 +33,48 @@ function truncate(text, font, size, maxWidth) {
|
||||
}
|
||||
|
||||
function getSignatureWidth(style, signerName = '') {
|
||||
if (style !== 'qr') return 200;
|
||||
if (style === 'qr' || style === 'firmaec') {
|
||||
const normalizedName = String(signerName).trim();
|
||||
const calculatedWidth = 78 + (normalizedName.length * 4.9);
|
||||
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 } = {}) {
|
||||
const now = new Date();
|
||||
@@ -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) ──────────────────────────────────────
|
||||
async function drawStyleClassic(pdfDoc, page, sx, sy, sw, sh, { signerName, reason, dateStr, timeStr, location }) {
|
||||
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 });
|
||||
}
|
||||
|
||||
// ── 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 ─────────────────────────────────────────────────────────
|
||||
const signPdf = async ({
|
||||
pdfPath, p12Buffer, p12Password,
|
||||
@@ -300,6 +427,7 @@ const signPdf = async ({
|
||||
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 === '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);
|
||||
|
||||
// ── Save page with visual stamp ───────────────────────────────────
|
||||
@@ -307,16 +435,24 @@ const signPdf = async ({
|
||||
|
||||
// ── Add cryptographic signature placeholder ───────────────────────
|
||||
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({
|
||||
pdfDoc: pdfDocForSign,
|
||||
pdfPage: placeholderPage,
|
||||
reason: reason || 'Firma digital',
|
||||
contactInfo: signerName,
|
||||
name: signerName,
|
||||
location: location || '',
|
||||
signatureLength: 32768,
|
||||
rect: [sx, sy, sx + sw, sy + sh],
|
||||
pageIndex: Math.min(page, pdfDocForSign.getPageCount() - 1),
|
||||
widgetRect,
|
||||
});
|
||||
if (!useInvisibleWidget) {
|
||||
hideSignatureWidgetBorder(pdfDocForSign, placeholderPageIndex);
|
||||
}
|
||||
|
||||
const pdfWithPlaceholder = Buffer.from(await pdfDocForSign.save({ useObjectStreams: false }));
|
||||
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
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_BACKGROUNDS = ['transparent', 'soft'];
|
||||
const ALLOWED_QR_BORDERS = ['none', 'line'];
|
||||
const LEGACY_SIGNATURE_STYLE_MAP = {
|
||||
image: 'qr',
|
||||
firmaecuador: 'firmaec',
|
||||
'firma ec': 'firmaec',
|
||||
};
|
||||
|
||||
const getSignaturePreferences = async (userId) => {
|
||||
return usersRepository.getSignaturePreferences(userId);
|
||||
};
|
||||
|
||||
const updateSignaturePreferences = async (userId, { signatureStyle, qrPosition, qrBackground, qrBorder, signatureImageBase64 }) => {
|
||||
if (!ALLOWED_SIGNATURE_STYLES.includes(signatureStyle)) {
|
||||
const error = new Error('Invalid signature style.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
const rawSignatureStyle = String(signatureStyle || '').trim().toLowerCase();
|
||||
let normalizedSignatureStyle = LEGACY_SIGNATURE_STYLE_MAP[rawSignatureStyle] || rawSignatureStyle || 'qr';
|
||||
if (!ALLOWED_SIGNATURE_STYLES.includes(normalizedSignatureStyle)) {
|
||||
normalizedSignatureStyle = rawSignatureStyle.includes('firma') ? 'firmaec' : 'qr';
|
||||
}
|
||||
|
||||
if (!ALLOWED_QR_POSITIONS.includes(qrPosition)) {
|
||||
const error = new Error('Invalid QR position.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// Preferences should be resilient to stale clients or partially populated forms.
|
||||
const resolvedQrPosition = ALLOWED_QR_POSITIONS.includes(qrPosition) ? qrPosition : 'left';
|
||||
const resolvedQrBackground = ALLOWED_QR_BACKGROUNDS.includes(qrBackground) ? qrBackground : 'transparent';
|
||||
const resolvedQrBorder = ALLOWED_QR_BORDERS.includes(qrBorder) ? qrBorder : 'none';
|
||||
|
||||
return usersRepository.setSignaturePreferences(userId, {
|
||||
signatureStyle,
|
||||
qrPosition,
|
||||
qrBackground,
|
||||
qrBorder,
|
||||
signatureStyle: normalizedSignatureStyle,
|
||||
qrPosition: resolvedQrPosition,
|
||||
qrBackground: resolvedQrBackground,
|
||||
qrBorder: resolvedQrBorder,
|
||||
signatureImageBase64: signatureImageBase64 || null,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.2.15",
|
||||
"version": "1.2.35",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { FileText, Shield, PenTool, LayoutDashboard, LogOut, Palette, Menu, X, KeyRound, Users, Settings, ScrollText, Scale } from 'lucide-react';
|
||||
import ChangePasswordModal from '../ui/ChangePasswordModal';
|
||||
import packageInfo from '../../../package.json';
|
||||
|
||||
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' }} />
|
||||
</div>
|
||||
<div className="sidebar-brand-copy">
|
||||
<span>LinkedSign</span>
|
||||
<small>Desarrollo de Soluciones</small>
|
||||
<span>LinkedSign <em>v{packageInfo.version}</em></span>
|
||||
<small>Firma Electrónica @LinkedCorp</small>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,12 +13,20 @@ const SIG_W = 200;
|
||||
const SIG_H = 60;
|
||||
|
||||
const getSignatureWidth = (style, signerName = '') => {
|
||||
if (style !== 'qr') return SIG_W;
|
||||
if (style !== 'qr' && style !== 'firmaec') return SIG_W;
|
||||
const normalizedName = signerName.trim();
|
||||
const calculatedWidth = 78 + (normalizedName.length * 4.9);
|
||||
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({
|
||||
pdfBlob,
|
||||
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') {
|
||||
return (
|
||||
<div className="sig-preview-body sig-preview-body--image">
|
||||
@@ -225,8 +266,8 @@ export default function PdfSignaturePlacer({
|
||||
{sigBox && (
|
||||
<div
|
||||
className={`sig-preview-box sig-preview-box--${signatureStyle}`}
|
||||
data-background={signatureStyle === 'qr' ? qrBackground : undefined}
|
||||
data-border={signatureStyle === 'qr' ? qrBorder : undefined}
|
||||
data-background={signatureStyle === 'qr' || signatureStyle === 'firmaec' ? qrBackground : undefined}
|
||||
data-border={signatureStyle === 'qr' || signatureStyle === 'firmaec' ? qrBorder : undefined}
|
||||
style={{ left: sigBox.x, top: sigBox.y, width: signatureWidth, height: SIG_H }}
|
||||
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 extraWidth = Math.max(0, name.trim().length - 18) * 4;
|
||||
@@ -54,7 +54,7 @@ const STYLES = [
|
||||
</svg>
|
||||
<span className="sp-qr-hint">Escanear</span>
|
||||
</div>
|
||||
<div className="sp-divider-v" />
|
||||
{qrBorder === 'line' && <div className="sp-divider-v" />}
|
||||
</>
|
||||
)}
|
||||
<div className="sp-info sp-info--qr">
|
||||
@@ -64,7 +64,7 @@ const STYLES = [
|
||||
</div>
|
||||
{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}>
|
||||
<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 =>
|
||||
@@ -84,6 +84,61 @@ const STYLES = [
|
||||
</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',
|
||||
label: 'Profesional',
|
||||
@@ -145,7 +200,7 @@ export default function SignatureStylePicker({
|
||||
})}
|
||||
</div>
|
||||
<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-inline-group">
|
||||
<button
|
||||
|
||||
+111
-7
@@ -134,6 +134,16 @@ select {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
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 {
|
||||
@@ -1069,17 +1079,17 @@ select {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sign-form-card > .field + .btn-inline-toggle {
|
||||
margin-top: 10px;
|
||||
.sign-certificate-field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.sign-form-card > .btn-inline-toggle + .btn-full {
|
||||
margin-top: 10px;
|
||||
.sign-details-toggle {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sign-form-card > .btn-inline-toggle + .field,
|
||||
.sign-form-card > .info-box + .btn-full {
|
||||
margin-top: 6px;
|
||||
.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; }
|
||||
@@ -1504,6 +1514,18 @@ select {
|
||||
border-color: transparent;
|
||||
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'] {
|
||||
background: rgba(242, 244, 255, 0.96);
|
||||
}
|
||||
@@ -1868,6 +1890,88 @@ select {
|
||||
flex-direction: column;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -286,7 +286,7 @@ export default function SignaturesPage() {
|
||||
)}
|
||||
{uploadingDocument && <small className="field-hint">Subiendo documento...</small>}
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="field sign-certificate-field">
|
||||
<label><Shield size={14} /> Certificado digital</label>
|
||||
<select name="certificateId" value={form.certificateId} onChange={handleChange} required>
|
||||
<option value="">Selecciona un certificado...</option>
|
||||
@@ -296,7 +296,7 @@ export default function SignaturesPage() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-inline-toggle"
|
||||
className="btn btn-ghost btn-inline-toggle sign-details-toggle"
|
||||
onClick={() => setShowAdvancedOptions((current) => !current)}
|
||||
>
|
||||
{showAdvancedOptions ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
@@ -316,13 +316,13 @@ export default function SignaturesPage() {
|
||||
<h4>Estilo aplicado</h4>
|
||||
<p>La firma usará tu configuración guardada en el menú <strong>Estilo Firma</strong>.</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' && <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>Posición QR:</strong> {form.qrPosition === 'left' ? 'Izquierda' : 'Derecha'}</p>}
|
||||
{(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Fondo:</strong> {form.qrBackground === 'transparent' ? 'Transparente' : 'Suave'}</p>}
|
||||
{(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Borde:</strong> {form.qrBorder === 'none' ? 'Sin borde' : 'Con borde'}</p>}
|
||||
</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 →
|
||||
</button>
|
||||
</div>
|
||||
@@ -425,9 +425,9 @@ export default function SignaturesPage() {
|
||||
<p><strong>Razón:</strong> {form.reason}</p>
|
||||
{form.location && <p><strong>Ubicación:</strong> {form.location}</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' && <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>Posición QR:</strong> {form.qrPosition === 'left' ? 'Izquierda' : 'Derecha'}</p>}
|
||||
{(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Fondo:</strong> {form.qrBackground === 'transparent' ? 'Transparente' : 'Suave'}</p>}
|
||||
{(form.signatureStyle === 'qr' || form.signatureStyle === 'firmaec') && <p><strong>Borde:</strong> {form.qrBorder === 'none' ? 'Sin borde' : 'Con borde'}</p>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user