diff --git a/backend/src/modules/users/users.service.js b/backend/src/modules/users/users.service.js index 8cbf5a1..cddeee7 100644 --- a/backend/src/modules/users/users.service.js +++ b/backend/src/modules/users/users.service.js @@ -1,6 +1,6 @@ const usersRepository = require('./users.repository'); -const ALLOWED_SIGNATURE_STYLES = ['classic', 'qr', 'professional', 'image']; +const ALLOWED_SIGNATURE_STYLES = ['classic', 'qr', 'professional']; const ALLOWED_QR_POSITIONS = ['left', 'right']; const ALLOWED_QR_BACKGROUNDS = ['transparent', 'soft']; const ALLOWED_QR_BORDERS = ['none', 'line']; diff --git a/frontend/src/components/ui/SignatureStylePicker.jsx b/frontend/src/components/ui/SignatureStylePicker.jsx index 4489baf..8bc0a23 100644 --- a/frontend/src/components/ui/SignatureStylePicker.jsx +++ b/frontend/src/components/ui/SignatureStylePicker.jsx @@ -1,5 +1,4 @@ -import { useRef } from 'react'; -import { QrCode, Briefcase, ImageIcon, Stamp, PanelsLeftBottom, PanelsRightBottom, CircleOff, Square } from 'lucide-react'; +import { QrCode, Briefcase, Stamp, PanelsLeftBottom, PanelsRightBottom, CircleOff, Square } from 'lucide-react'; const getQrPreviewWidth = (name = '') => { const extraWidth = Math.max(0, name.trim().length - 18) * 4; @@ -102,36 +101,11 @@ const STYLES = [ ), }, - { - id: 'image', - label: 'Imagen', - icon: ImageIcon, - desc: 'Sube tu firma manuscrita o imagen personalizada', - preview: ({ name, date, image }) => ( -
-
-
- {image - ? firma - : Tu firma aquí - } -
-
-
- {name} - Firmado digitalmente - {date} -
-
- ), - }, ]; export default function SignatureStylePicker({ value, onChange, - signatureImage, - onImageChange, signerName, qrPosition = 'left', onQrPositionChange, @@ -140,19 +114,8 @@ export default function SignatureStylePicker({ qrBorder = 'none', onQrBorderChange, }) { - const fileRef = useRef(); const today = new Date().toLocaleDateString('es-EC'); - const handleImageFile = (e) => { - const file = e.target.files[0]; - if (!file) return; - if (!file.type.startsWith('image/')) return; - const reader = new FileReader(); - reader.onload = (ev) => onImageChange(ev.target.result); - reader.readAsDataURL(file); - e.target.value = ''; - }; - return (
@@ -176,7 +139,6 @@ export default function SignatureStylePicker({ {s.preview({ name: signerName || 'Milton González H.', date: today, - image: signatureImage, qrPosition, qrBackground, qrBorder, @@ -267,21 +229,6 @@ export default function SignatureStylePicker({ ); })}
- - {value === 'image' && ( -
- - {signatureImage && ( - - )} - Fondo transparente (PNG) recomendado para mejor resultado - -
- )}
); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 3b75def..3cf6876 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -633,6 +633,21 @@ select { padding: 6px 10px; } +.documents-page-size { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--gray-700); + font-size: 12px; +} + +.documents-page-size select { + border: 1px solid var(--gray-300); + border-radius: 8px; + background: #fff; + padding: 6px 10px; +} + .audit-table-header, .audit-table-row { grid-template-columns: minmax(220px, 1.2fr) 180px 120px minmax(260px, 1.5fr); diff --git a/frontend/src/pages/auth/LoginPage.jsx b/frontend/src/pages/auth/LoginPage.jsx index d0c2b23..cb38d66 100644 --- a/frontend/src/pages/auth/LoginPage.jsx +++ b/frontend/src/pages/auth/LoginPage.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { Download, PenTool } from 'lucide-react'; @@ -11,6 +11,10 @@ export default function LoginPage() { const [loading, setLoading] = useState(false); const apiBaseUrl = (import.meta.env.VITE_API_URL || 'http://localhost:4000').replace(/\/$/, ''); const androidApkUrl = `${apiBaseUrl}/downloads/android-apk`; + const showAndroidDownload = useMemo(() => { + if (typeof navigator === 'undefined') return false; + return /Android/i.test(navigator.userAgent || ''); + }, []); const handleChange = (e) => setForm((f) => ({ ...f, [e.target.name]: e.target.value })); @@ -60,10 +64,12 @@ export default function LoginPage() { - - - Descargar app Android - + {showAndroidDownload && ( + + + Descargar app Android + + )}

¿No tienes cuenta? Registrarse diff --git a/frontend/src/pages/documents/DocumentsPage.jsx b/frontend/src/pages/documents/DocumentsPage.jsx index 01c4ed6..299fed4 100644 --- a/frontend/src/pages/documents/DocumentsPage.jsx +++ b/frontend/src/pages/documents/DocumentsPage.jsx @@ -22,9 +22,13 @@ const STATUS_LABELS = { export default function DocumentsPage() { const navigate = useNavigate(); + const pageSizeOptions = [5, 10, 15, 20]; const [docs, setDocs] = useState([]); const [signedDocs, setSignedDocs] = useState([]); const [total, setTotal] = useState(0); + const [pageSize, setPageSize] = useState(5); + const [docsPage, setDocsPage] = useState(1); + const [signedPage, setSignedPage] = useState(1); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(); @@ -53,6 +57,14 @@ export default function DocumentsPage() { useEffect(() => { loadDocs(); }, []); + useEffect(() => { setDocsPage(1); }, [pageSize, docs.length]); + useEffect(() => { setSignedPage(1); }, [pageSize, signedDocs.length]); + + const docsTotalPages = Math.max(1, Math.ceil(docs.length / pageSize)); + const signedTotalPages = Math.max(1, Math.ceil(signedDocs.length / pageSize)); + const pagedDocs = docs.slice((docsPage - 1) * pageSize, docsPage * pageSize); + const pagedSignedDocs = signedDocs.slice((signedPage - 1) * pageSize, signedPage * pageSize); + const handleUpload = async (e) => { const file = e.target.files[0]; if (!file) return; @@ -160,6 +172,18 @@ export default function DocumentsPage() {

{total} documento{total !== 1 ? 's' : ''} original{total !== 1 ? 'es' : ''}

+
+ + +
)} + {docs.length > 0 && ( +
+ Página {docsPage} de {docsTotalPages} +
+ + +
+
+ )}
@@ -248,7 +281,7 @@ export default function DocumentsPage() { - {signedDocs.map((doc) => ( + {pagedSignedDocs.map((doc) => ( {doc.signed_filename} {doc.document_title} @@ -268,6 +301,15 @@ export default function DocumentsPage() { )} + {signedDocs.length > 0 && ( +
+ Página {signedPage} de {signedTotalPages} +
+ + +
+
+ )}
)} diff --git a/frontend/src/pages/signature-style/SignatureStylePage.jsx b/frontend/src/pages/signature-style/SignatureStylePage.jsx index 5f264bc..4ec24dd 100644 --- a/frontend/src/pages/signature-style/SignatureStylePage.jsx +++ b/frontend/src/pages/signature-style/SignatureStylePage.jsx @@ -4,24 +4,24 @@ import toast from 'react-hot-toast'; import SignatureStylePicker from '../../components/ui/SignatureStylePicker'; import { useAuth } from '../../contexts/AuthContext'; +const normalizeSignatureStyle = (value) => (value === 'image' ? 'qr' : (value || 'qr')); + export default function SignatureStylePage() { const { user, updateSignatureAppearancePreference } = useAuth(); const [form, setForm] = useState({ - signatureStyle: user?.signaturePreferences?.signatureStyle || 'qr', + signatureStyle: normalizeSignatureStyle(user?.signaturePreferences?.signatureStyle), qrPosition: user?.signaturePreferences?.qrPosition || 'left', qrBackground: user?.signaturePreferences?.qrBackground || 'transparent', qrBorder: user?.signaturePreferences?.qrBorder || 'none', - signatureImageBase64: user?.signaturePreferences?.signatureImageBase64 || null, }); const [saving, setSaving] = useState(false); useEffect(() => { setForm({ - signatureStyle: user?.signaturePreferences?.signatureStyle || 'qr', + signatureStyle: normalizeSignatureStyle(user?.signaturePreferences?.signatureStyle), qrPosition: user?.signaturePreferences?.qrPosition || 'left', qrBackground: user?.signaturePreferences?.qrBackground || 'transparent', qrBorder: user?.signaturePreferences?.qrBorder || 'none', - signatureImageBase64: user?.signaturePreferences?.signatureImageBase64 || null, }); }, [user?.signaturePreferences]); @@ -66,8 +66,6 @@ export default function SignatureStylePage() { onQrBackgroundChange={(qrBackground) => setForm((currentForm) => ({ ...currentForm, qrBackground }))} qrBorder={form.qrBorder} onQrBorderChange={(qrBorder) => setForm((currentForm) => ({ ...currentForm, qrBorder }))} - signatureImage={form.signatureImageBase64} - onImageChange={(signatureImageBase64) => setForm((currentForm) => ({ ...currentForm, signatureImageBase64 }))} signerName={user?.name || 'Firmante'} /> diff --git a/frontend/src/pages/signatures/SignaturesPage.jsx b/frontend/src/pages/signatures/SignaturesPage.jsx index 1ac3c66..1ec65c2 100644 --- a/frontend/src/pages/signatures/SignaturesPage.jsx +++ b/frontend/src/pages/signatures/SignaturesPage.jsx @@ -10,6 +10,7 @@ import PdfSignaturePlacer from '../../components/ui/PdfSignaturePlacer'; import { useAuth } from '../../contexts/AuthContext'; const STEPS = ['Selección', 'Posición', 'Firmar']; +const normalizeSignatureStyle = (value) => (value === 'image' ? 'qr' : (value || 'qr')); export default function SignaturesPage() { const { user } = useAuth(); @@ -21,11 +22,10 @@ export default function SignaturesPage() { certificateId: '', reason: 'Firmado digitalmente', location: '', - signatureStyle: user?.signaturePreferences?.signatureStyle || 'qr', + signatureStyle: normalizeSignatureStyle(user?.signaturePreferences?.signatureStyle), qrPosition: user?.signaturePreferences?.qrPosition || 'left', qrBackground: user?.signaturePreferences?.qrBackground || 'transparent', qrBorder: user?.signaturePreferences?.qrBorder || 'none', - signatureImageBase64: user?.signaturePreferences?.signatureImageBase64 || null, }); const [step, setStep] = useState(0); // 0=select, 1=place, 2=sign const [pdfBlob, setPdfBlob] = useState(null); @@ -52,11 +52,10 @@ export default function SignaturesPage() { if (!user?.signaturePreferences) return; setForm((currentForm) => ({ ...currentForm, - signatureStyle: user.signaturePreferences.signatureStyle || 'qr', + signatureStyle: normalizeSignatureStyle(user.signaturePreferences.signatureStyle), qrPosition: user.signaturePreferences.qrPosition || 'left', qrBackground: user.signaturePreferences.qrBackground || 'transparent', qrBorder: user.signaturePreferences.qrBorder || 'none', - signatureImageBase64: user.signaturePreferences.signatureImageBase64 || null, })); }, [user?.signaturePreferences]); @@ -100,7 +99,6 @@ export default function SignaturesPage() { qrPosition: form.qrPosition, qrBackground: form.qrBackground, qrBorder: form.qrBorder, - signatureImageBase64: form.signatureImageBase64, }; try { const { data } = await signDocument(payload);