Refine web mobile download prompt and document pagination

This commit is contained in:
2026-04-07 16:57:17 -05:00
parent 007b5b4b9e
commit eec4977bb2
7 changed files with 79 additions and 73 deletions
+11 -5
View File
@@ -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>
+44 -2
View File
@@ -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);