Refine web mobile download prompt and document pagination
This commit is contained in:
@@ -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'];
|
||||
|
||||
@@ -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 = [
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'image',
|
||||
label: 'Imagen',
|
||||
icon: ImageIcon,
|
||||
desc: 'Sube tu firma manuscrita o imagen personalizada',
|
||||
preview: ({ name, date, image }) => (
|
||||
<div className="sig-preview sig-preview--image">
|
||||
<div className="sp-img-bar" />
|
||||
<div className="sp-img-area">
|
||||
{image
|
||||
? <img src={image} alt="firma" className="sp-sig-img" />
|
||||
: <span className="sp-img-placeholder">Tu firma aquí</span>
|
||||
}
|
||||
</div>
|
||||
<div className="sp-divider-v" />
|
||||
<div className="sp-info">
|
||||
<strong>{name}</strong>
|
||||
<span>Firmado digitalmente</span>
|
||||
<span>{date}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<div className="style-picker">
|
||||
<label className="field-label-standalone">Estilo de firma visual</label>
|
||||
@@ -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({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{value === 'image' && (
|
||||
<div className="image-upload-area">
|
||||
<button type="button" className="btn btn-ghost btn-sm" onClick={() => fileRef.current.click()}>
|
||||
<ImageIcon size={14} /> {signatureImage ? 'Cambiar imagen' : 'Subir firma (PNG/JPG)'}
|
||||
</button>
|
||||
{signatureImage && (
|
||||
<button type="button" className="btn btn-ghost btn-sm danger" onClick={() => onImageChange(null)}>
|
||||
Quitar imagen
|
||||
</button>
|
||||
)}
|
||||
<small className="field-hint">Fondo transparente (PNG) recomendado para mejor resultado</small>
|
||||
<input ref={fileRef} type="file" accept="image/png,image/jpeg,image/webp" style={{ display: 'none' }} onChange={handleImageFile} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? 'Ingresando...' : 'Iniciar sesión'}
|
||||
</button>
|
||||
<a className="btn btn-secondary btn-full auth-download-btn" href={androidApkUrl}>
|
||||
<Download size={14} />
|
||||
Descargar app Android
|
||||
</a>
|
||||
{showAndroidDownload && (
|
||||
<a className="btn btn-secondary btn-full auth-download-btn" href={androidApkUrl}>
|
||||
<Download size={14} />
|
||||
Descargar app Android
|
||||
</a>
|
||||
)}
|
||||
</form>
|
||||
<p className="auth-link">
|
||||
¿No tienes cuenta? <Link to="/register">Registrarse</Link>
|
||||
|
||||
@@ -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() {
|
||||
<p>{total} documento{total !== 1 ? 's' : ''} original{total !== 1 ? 'es' : ''}</p>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<div className="documents-page-size">
|
||||
<label htmlFor="documents-page-size">Mostrar</label>
|
||||
<select
|
||||
id="documents-page-size"
|
||||
value={pageSize}
|
||||
onChange={(e) => setPageSize(Number(e.target.value))}
|
||||
>
|
||||
{pageSizeOptions.map((option) => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button className="btn btn-ghost" onClick={loadDocs} title="Actualizar"><RefreshCw size={16} /></button>
|
||||
<button className="btn btn-primary" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
|
||||
<Upload size={16} /> {uploading ? 'Subiendo...' : 'Subir PDF'}
|
||||
@@ -194,7 +218,7 @@ export default function DocumentsPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{docs.map((doc) => {
|
||||
{pagedDocs.map((doc) => {
|
||||
const s = STATUS_LABELS[doc.status] || STATUS_LABELS.pending;
|
||||
return (
|
||||
<tr key={doc.id}>
|
||||
@@ -223,6 +247,15 @@ export default function DocumentsPage() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{docs.length > 0 && (
|
||||
<div className="audit-pagination">
|
||||
<span>Página {docsPage} de {docsTotalPages}</span>
|
||||
<div className="header-actions">
|
||||
<button className="btn btn-secondary" disabled={docsPage <= 1} onClick={() => setDocsPage((current) => Math.max(1, current - 1))}>Anterior</button>
|
||||
<button className="btn btn-secondary" disabled={docsPage >= docsTotalPages} onClick={() => setDocsPage((current) => Math.min(docsTotalPages, current + 1))}>Siguiente</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="documents-section">
|
||||
@@ -248,7 +281,7 @@ export default function DocumentsPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signedDocs.map((doc) => (
|
||||
{pagedSignedDocs.map((doc) => (
|
||||
<tr key={doc.id}>
|
||||
<td className="doc-name"><FileText size={14} /> {doc.signed_filename}</td>
|
||||
<td>{doc.document_title}</td>
|
||||
@@ -268,6 +301,15 @@ export default function DocumentsPage() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{signedDocs.length > 0 && (
|
||||
<div className="audit-pagination">
|
||||
<span>Página {signedPage} de {signedTotalPages}</span>
|
||||
<div className="header-actions">
|
||||
<button className="btn btn-secondary" disabled={signedPage <= 1} onClick={() => setSignedPage((current) => Math.max(1, current - 1))}>Anterior</button>
|
||||
<button className="btn btn-secondary" disabled={signedPage >= signedTotalPages} onClick={() => setSignedPage((current) => Math.min(signedTotalPages, current + 1))}>Siguiente</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user