Improve web signing flow and locale handling

This commit is contained in:
2026-04-23 21:45:24 -05:00
parent b24d17b98e
commit 24fe8d0a0b
13 changed files with 312 additions and 42 deletions
+132 -14
View File
@@ -1,13 +1,14 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { listDocuments, downloadDocument } from '../../api/documents';
import { listDocuments, downloadDocument, uploadDocument } from '../../api/documents';
import { listCertificates } from '../../api/certificates';
import { signDocument, getSignatures, downloadSigned } from '../../api/signatures';
import { PenTool, FileText, Shield, CheckCircle, MapPin, Download } from 'lucide-react';
import { PenTool, FileText, Shield, CheckCircle, MapPin, Download, Check, Plus } from 'lucide-react';
import toast from 'react-hot-toast';
import PasswordModal from '../../components/ui/PasswordModal';
import PdfSignaturePlacer from '../../components/ui/PdfSignaturePlacer';
import { useAuth } from '../../contexts/AuthContext';
import { detectClientSignatureContext } from '../../utils/clientSignatureContext';
const STEPS = ['Selección', 'Posición', 'Firmar'];
const normalizeSignatureStyle = (value) => (value === 'image' ? 'qr' : (value || 'qr'));
@@ -15,13 +16,16 @@ const normalizeSignatureStyle = (value) => (value === 'image' ? 'qr' : (value ||
export default function SignaturesPage() {
const { user } = useAuth();
const [searchParams] = useSearchParams();
const clientSignatureContext = detectClientSignatureContext();
const initialDocumentId = searchParams.get('docId') || '';
const [docs, setDocs] = useState([]);
const [certs, setCerts] = useState([]);
const [documentPickerId, setDocumentPickerId] = useState(initialDocumentId);
const [form, setForm] = useState({
documentId: searchParams.get('docId') || '',
documentId: initialDocumentId,
certificateId: '',
reason: 'Firmado digitalmente',
location: '',
location: clientSignatureContext.defaultLocation,
signatureStyle: normalizeSignatureStyle(user?.signaturePreferences?.signatureStyle),
qrPosition: user?.signaturePreferences?.qrPosition || 'left',
qrBackground: user?.signaturePreferences?.qrBackground || 'transparent',
@@ -34,6 +38,9 @@ export default function SignaturesPage() {
const [result, setResult] = useState(null);
const [sigs, setSigs] = useState([]);
const [pendingSign, setPendingSign] = useState(null);
const [uploadingDocument, setUploadingDocument] = useState(false);
const [pendingLocalDocument, setPendingLocalDocument] = useState(null);
const documentInputRef = useRef(null);
useEffect(() => {
Promise.all([
@@ -59,16 +66,76 @@ export default function SignaturesPage() {
}));
}, [user?.signaturePreferences]);
useEffect(() => {
if (!clientSignatureContext.defaultLocation) return;
setForm((currentForm) => (
currentForm.location
? currentForm
: { ...currentForm, location: clientSignatureContext.defaultLocation }
));
}, [clientSignatureContext.defaultLocation]);
const handleChange = (e) => setForm(f => ({ ...f, [e.target.name]: e.target.value }));
const handleChooseDocument = () => {
if (!documentPickerId) {
toast.error('Selecciona un documento del listado');
return;
}
if (documentPickerId === '__local__') {
if (!pendingLocalDocument) {
toast.error('Selecciona un PDF local válido');
return;
}
setForm((currentForm) => ({ ...currentForm, documentId: '' }));
toast.success('PDF local seleccionado');
return;
}
setForm((currentForm) => ({ ...currentForm, documentId: documentPickerId }));
toast.success('Documento seleccionado');
};
const handleUploadFromSignFlow = async (event) => {
const file = event.target.files?.[0];
if (!file) return;
if (file.type !== 'application/pdf') {
toast.error('Solo se permiten archivos PDF');
event.target.value = '';
return;
}
setUploadingDocument(true);
try {
setPendingLocalDocument({
file,
title: file.name.replace(/\.pdf$/i, ''),
});
setDocumentPickerId('__local__');
setForm((currentForm) => ({ ...currentForm, documentId: '' }));
toast.success('Documento cargado localmente. Se guardará cuando completes la firma.');
} catch {
toast.error('No se pudo preparar el documento');
} finally {
setUploadingDocument(false);
event.target.value = '';
}
};
// Step 0 → 1: load PDF for placement
const handleGoToPlace = async () => {
if (!form.documentId || !form.certificateId) {
const usingLocalDocument = documentPickerId === '__local__' && pendingLocalDocument;
if ((!form.documentId && !usingLocalDocument) || !form.certificateId) {
toast.error('Selecciona documento y certificado'); return;
}
try {
const { data } = await downloadDocument(form.documentId);
setPdfBlob(data);
if (usingLocalDocument) {
setPdfBlob(pendingLocalDocument.file);
} else {
const { data } = await downloadDocument(form.documentId);
setPdfBlob(data);
}
setSigPlacement(null);
setStep(1);
} catch { toast.error('No se pudo cargar el documento'); }
@@ -81,18 +148,37 @@ export default function SignaturesPage() {
};
const doSign = async (certPassword = '', fromModal = false) => {
if (!form.documentId || !form.certificateId) {
const usingLocalDocument = documentPickerId === '__local__' && pendingLocalDocument;
if ((!form.documentId && !usingLocalDocument) || !form.certificateId) {
toast.error('Faltan datos de documento o certificado. Vuelve al paso 1.');
setStep(0);
return;
}
setSigning(true);
let workingDocumentId = Number(form.documentId) || null;
try {
if (!workingDocumentId && usingLocalDocument) {
const { data: uploadResponse } = await uploadDocument(pendingLocalDocument.file);
const createdDoc = uploadResponse.data;
workingDocumentId = Number(createdDoc.id);
setDocs((currentDocs) => [createdDoc, ...currentDocs.filter((doc) => doc.id !== createdDoc.id)]);
}
} catch (error) {
setSigning(false);
toast.error(error.response?.data?.error || 'No se pudo guardar el documento antes de firmar');
return;
}
const payload = {
documentId: Number(form.documentId),
documentId: workingDocumentId,
certificateId: Number(form.certificateId),
certPassword,
reason: form.reason,
location: form.location,
clientLocale: clientSignatureContext.locale,
clientTimeZone: clientSignatureContext.timeZone,
clientCountry: clientSignatureContext.countryCode,
page: sigPlacement?.page ?? 0,
rect: sigPlacement?.rect ?? null,
signatureStyle: form.signatureStyle,
@@ -105,6 +191,8 @@ export default function SignaturesPage() {
setResult(data.data);
toast.success('¡Documento firmado exitosamente!');
setDocs(prev => prev.filter(d => d.id !== payload.documentId));
setPendingLocalDocument(null);
setDocumentPickerId('');
setForm(f => ({ ...f, documentId: '', certificateId: '' }));
setSigPlacement(null);
setPdfBlob(null);
@@ -125,6 +213,9 @@ export default function SignaturesPage() {
const selectedCert = certs.find(c => c.id === Number(form.certificateId));
const selectedDoc = docs.find(d => d.id === Number(form.documentId));
const selectedDocumentTitle = documentPickerId === '__local__'
? pendingLocalDocument?.title
: selectedDoc?.title;
return (
<div className="page">
@@ -158,11 +249,38 @@ export default function SignaturesPage() {
<div className="sign-form-card">
<div className="field">
<label><FileText size={14} /> Documento</label>
<select name="documentId" value={form.documentId} onChange={handleChange} required>
<option value="">Selecciona un documento...</option>
{docs.map(d => <option key={d.id} value={d.id}>{d.title}</option>)}
</select>
<div className="select-action-row">
<select name="documentPickerId" value={documentPickerId} onChange={(event) => setDocumentPickerId(event.target.value)} required>
<option value="">Selecciona un documento...</option>
{docs.map(d => <option key={d.id} value={d.id}>{d.title}</option>)}
</select>
<div className="select-action-buttons">
<button type="button" className="btn btn-secondary btn-square" title="Elegir documento" onClick={handleChooseDocument}>
<Check size={16} />
</button>
<button
type="button"
className="btn btn-primary btn-square"
title="Agregar PDF"
onClick={() => documentInputRef.current?.click()}
disabled={uploadingDocument}
>
<Plus size={16} />
</button>
</div>
</div>
<input
ref={documentInputRef}
type="file"
accept=".pdf,application/pdf"
style={{ display: 'none' }}
onChange={handleUploadFromSignFlow}
/>
{docs.length === 0 && <small className="field-hint">No hay documentos pendientes.</small>}
{selectedDocumentTitle && (
<small className="field-hint">Seleccionado: {selectedDocumentTitle}</small>
)}
{uploadingDocument && <small className="field-hint">Subiendo documento...</small>}
</div>
<div className="field">
<label><Shield size={14} /> Certificado digital</label>