Refine external signing flow and mobile PDF placement

This commit is contained in:
2026-04-08 23:45:47 -05:00
parent f1bfa5aafc
commit 331dd40e76
3 changed files with 78 additions and 39 deletions
@@ -183,12 +183,6 @@ const addInviteesToFlow = async (documentId, { invitees }, ownerUserId, context
throw error; throw error;
} }
if (flow.status === 'completed') {
const error = new Error('La cadena ya fue completada. Crea una nueva si necesitas más firmas.');
error.statusCode = 409;
throw error;
}
const { document } = await ensureOwnerCanInvite(documentId, ownerUserId); const { document } = await ensureOwnerCanInvite(documentId, ownerUserId);
const existingInvitees = await invitationsRepository.listInvitationsByFlow(flow.id); const existingInvitees = await invitationsRepository.listInvitationsByFlow(flow.id);
const currentCount = existingInvitees.length; const currentCount = existingInvitees.length;
@@ -198,11 +192,26 @@ const addInviteesToFlow = async (documentId, { invitees }, ownerUserId, context
throw error; throw error;
} }
const existingEmails = new Set(existingInvitees.map((item) => String(item.invitee_email).toLowerCase()));
const normalizedInvitees = invitees.map((invitee) => ({ const normalizedInvitees = invitees.map((invitee) => ({
name: invitee.name.trim(), name: invitee.name.trim(),
email: invitee.email.trim().toLowerCase(), email: invitee.email.trim().toLowerCase(),
})); }));
const payloadEmails = new Set();
for (const invitee of normalizedInvitees) {
if (payloadEmails.has(invitee.email)) {
const error = new Error(`El correo ${invitee.email} está repetido en la solicitud.`);
error.statusCode = 409;
throw error;
}
payloadEmails.add(invitee.email);
}
const existingEmails = new Set(
flow.status === 'completed'
? []
: existingInvitees.map((item) => String(item.invitee_email).toLowerCase())
);
for (const invitee of normalizedInvitees) { for (const invitee of normalizedInvitees) {
if (existingEmails.has(invitee.email)) { if (existingEmails.has(invitee.email)) {
@@ -1,4 +1,4 @@
import { useState, useRef, useCallback } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf'; import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css'; import 'react-pdf/dist/Page/TextLayer.css';
@@ -31,10 +31,33 @@ export default function PdfSignaturePlacer({
const [numPages, setNumPages] = useState(null); const [numPages, setNumPages] = useState(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [sigBox, setSigBox] = useState(null); // { x, y } in container px const [sigBox, setSigBox] = useState(null); // { x, y } in container px
const stageRef = useRef(null);
const containerRef = useRef(null); const containerRef = useRef(null);
const [pageSize, setPageSize] = useState(null); // rendered px size const [pageSize, setPageSize] = useState(null); // rendered px size
const [pageRenderWidth, setPageRenderWidth] = useState(600);
const signatureWidth = getSignatureWidth(signatureStyle, signerName); const signatureWidth = getSignatureWidth(signatureStyle, signerName);
useEffect(() => {
const stage = stageRef.current;
if (!stage) return undefined;
const updateWidth = () => {
const nextWidth = Math.max(280, Math.min(600, Math.floor(stage.clientWidth || 600)));
setPageRenderWidth(nextWidth);
};
updateWidth();
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', updateWidth);
return () => window.removeEventListener('resize', updateWidth);
}
const observer = new ResizeObserver(() => updateWidth());
observer.observe(stage);
return () => observer.disconnect();
}, []);
const getCanvasMetrics = () => { const getCanvasMetrics = () => {
const container = containerRef.current; const container = containerRef.current;
const canvas = container?.querySelector('.react-pdf__Page__canvas'); const canvas = container?.querySelector('.react-pdf__Page__canvas');
@@ -59,12 +82,12 @@ export default function PdfSignaturePlacer({
setPageSize({ width: page.width, height: page.height }); setPageSize({ width: page.width, height: page.height });
}; };
const handleContainerClick = (e) => { const handlePointerDown = (event) => {
const metrics = getCanvasMetrics(); const metrics = getCanvasMetrics();
if (!metrics) return; if (!metrics) return;
const x = e.clientX - metrics.canvasRect.left; const x = event.clientX - metrics.canvasRect.left;
const y = e.clientY - metrics.canvasRect.top; const y = event.clientY - metrics.canvasRect.top;
const clampedX = Math.min(Math.max(x, 0), metrics.width - signatureWidth); const clampedX = Math.min(Math.max(x, 0), metrics.width - signatureWidth);
const clampedY = Math.min(Math.max(y, 0), metrics.height - SIG_H); const clampedY = Math.min(Math.max(y, 0), metrics.height - SIG_H);
@@ -177,16 +200,17 @@ export default function PdfSignaturePlacer({
</div> </div>
</div> </div>
<div ref={stageRef} className="pdf-placer-stage">
<div <div
ref={containerRef} ref={containerRef}
className="pdf-placer-canvas" className="pdf-placer-canvas"
onClick={handleContainerClick} onPointerDown={handlePointerDown}
style={{ cursor: 'crosshair', position: 'relative', display: 'inline-block' }} style={{ cursor: 'crosshair', position: 'relative', display: 'inline-block' }}
> >
<Document file={pdfBlob} onLoadSuccess={handleDocLoad}> <Document file={pdfBlob} onLoadSuccess={handleDocLoad}>
<Page <Page
pageNumber={currentPage} pageNumber={currentPage}
width={600} width={pageRenderWidth}
onLoadSuccess={(p) => setPageSize({ onLoadSuccess={(p) => setPageSize({
width: p.width, height: p.height, width: p.width, height: p.height,
pdfWidth: p.originalWidth, pdfHeight: p.originalHeight, pdfWidth: p.originalWidth, pdfHeight: p.originalHeight,
@@ -200,13 +224,14 @@ export default function PdfSignaturePlacer({
data-background={signatureStyle === 'qr' ? qrBackground : undefined} data-background={signatureStyle === 'qr' ? qrBackground : undefined}
data-border={signatureStyle === 'qr' ? qrBorder : undefined} data-border={signatureStyle === 'qr' ? qrBorder : undefined}
style={{ left: sigBox.x, top: sigBox.y, width: signatureWidth, height: SIG_H }} style={{ left: sigBox.x, top: sigBox.y, width: signatureWidth, height: SIG_H }}
onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
> >
{signatureStyle === 'classic' && <div className="sig-preview-bar" />} {signatureStyle === 'classic' && <div className="sig-preview-bar" />}
{renderPreview()} {renderPreview()}
</div> </div>
)} )}
</div> </div>
</div>
{sigBox && ( {sigBox && (
<div className="pdf-placer-actions"> <div className="pdf-placer-actions">
+5
View File
@@ -1425,6 +1425,10 @@ select {
.pdf-placer-pages { display: flex; align-items: center; gap: 8px; } .pdf-placer-pages { display: flex; align-items: center; gap: 8px; }
.pdf-placer-pages .btn { color: #fff; border-color: rgba(255,255,255,.2); padding: 4px 10px; } .pdf-placer-pages .btn { color: #fff; border-color: rgba(255,255,255,.2); padding: 4px 10px; }
.pdf-placer-stage {
width: 100%;
}
.pdf-placer-canvas { .pdf-placer-canvas {
border: 2px dashed var(--primary); border: 2px dashed var(--primary);
border-radius: 0 0 8px 8px; border-radius: 0 0 8px 8px;
@@ -2323,6 +2327,7 @@ select {
.pdf-placer-canvas { .pdf-placer-canvas {
overflow: auto; overflow: auto;
max-width: 100%;
} }
.pdf-placer-canvas .react-pdf__Page, .pdf-placer-canvas .react-pdf__Page,