Improve web signing flow and locale handling
This commit is contained in:
@@ -48,19 +48,21 @@ export default function DocumentsPage() {
|
||||
const [creatingFlow, setCreatingFlow] = useState(false);
|
||||
const [deletingInvitationId, setDeletingInvitationId] = useState(null);
|
||||
|
||||
const loadDocs = () => {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
listDocuments({ limit: 50 }),
|
||||
listSignedDocuments(),
|
||||
])
|
||||
.then(([documentsResponse, signedResponse]) => {
|
||||
setDocs(documentsResponse.data.data.rows);
|
||||
setTotal(documentsResponse.data.data.total);
|
||||
setSignedDocs(signedResponse.data.data || []);
|
||||
})
|
||||
.catch(() => toast.error('Error al cargar documentos'))
|
||||
.finally(() => setLoading(false));
|
||||
const loadDocs = async ({ silent = false } = {}) => {
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
const [documentsResponse, signedResponse] = await Promise.all([
|
||||
listDocuments({ limit: 50 }),
|
||||
listSignedDocuments(),
|
||||
]);
|
||||
setDocs(documentsResponse.data.data.rows);
|
||||
setTotal(documentsResponse.data.data.total);
|
||||
setSignedDocs(signedResponse.data.data || []);
|
||||
} catch {
|
||||
toast.error('Error al cargar documentos');
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { loadDocs(); }, []);
|
||||
@@ -79,9 +81,12 @@ export default function DocumentsPage() {
|
||||
if (file.type !== 'application/pdf') { toast.error('Solo se permiten archivos PDF'); return; }
|
||||
setUploading(true);
|
||||
try {
|
||||
await uploadDocument(file);
|
||||
const { data } = await uploadDocument(file);
|
||||
const createdDoc = data.data;
|
||||
setDocs((currentDocs) => [createdDoc, ...currentDocs.filter((doc) => doc.id !== createdDoc.id)]);
|
||||
setTotal((currentTotal) => currentTotal + 1);
|
||||
toast.success('Documento subido correctamente');
|
||||
loadDocs();
|
||||
void loadDocs({ silent: true });
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error || 'Error al subir documento');
|
||||
} finally {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user