Refine external signing flow and mobile PDF placement
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user