feat: client-text and client-checkbox field types — signer fills text/checks boxes on signing page
This commit is contained in:
@@ -112,8 +112,10 @@ export async function POST(
|
||||
const { token } = await params;
|
||||
|
||||
// 1. Parse body
|
||||
const { signatures } = (await req.json()) as {
|
||||
const { signatures, clientTextValues = [], clientCheckboxValues = [] } = (await req.json()) as {
|
||||
signatures: Array<{ fieldId: string; dataURL: string }>;
|
||||
clientTextValues?: Array<{ fieldId: string; value: string }>;
|
||||
clientCheckboxValues?: Array<{ fieldId: string; checked: boolean }>;
|
||||
};
|
||||
|
||||
// 2. Verify JWT
|
||||
@@ -229,7 +231,57 @@ export async function POST(
|
||||
await writeFile(dateStampedPath, stampedBytes);
|
||||
}
|
||||
|
||||
// 8b. Build signaturesWithCoords — scoped to this signer's signable fields (Pitfall 4)
|
||||
// 8b. Embed client-text and client-checkbox values into the PDF
|
||||
let clientFieldStampedPath = dateStampedPath;
|
||||
const clientTextFields = (doc.signatureFields ?? []).filter((f) => {
|
||||
if (getFieldType(f) !== 'client-text') return false;
|
||||
if (signerEmail !== null) return f.signerEmail === signerEmail;
|
||||
return true;
|
||||
});
|
||||
const clientCheckboxFields = (doc.signatureFields ?? []).filter((f) => {
|
||||
if (getFieldType(f) !== 'client-checkbox') return false;
|
||||
if (signerEmail !== null) return f.signerEmail === signerEmail;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (clientTextFields.length > 0 || clientCheckboxFields.length > 0) {
|
||||
const pdfBytes = await readFile(clientFieldStampedPath);
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
for (const field of clientTextFields) {
|
||||
const val = clientTextValues.find(v => v.fieldId === field.id)?.value ?? '';
|
||||
if (!val) continue;
|
||||
const page = pages[field.page - 1];
|
||||
if (!page) continue;
|
||||
page.drawText(val, {
|
||||
x: field.x + 4,
|
||||
y: field.y + field.height / 2 - 4,
|
||||
size: 10, font: helvetica, color: rgb(0.05, 0.05, 0.05),
|
||||
maxWidth: field.width - 8,
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of clientCheckboxFields) {
|
||||
const checked = clientCheckboxValues.find(v => v.fieldId === field.id)?.checked ?? false;
|
||||
if (!checked) continue;
|
||||
const page = pages[field.page - 1];
|
||||
if (!page) continue;
|
||||
// Draw a bold checkmark centred in the field box
|
||||
page.drawText('✓', {
|
||||
x: field.x + field.width / 2 - 5,
|
||||
y: field.y + field.height / 2 - 5,
|
||||
size: 14, font: helvetica, color: rgb(0.02, 0.52, 0.78),
|
||||
});
|
||||
}
|
||||
|
||||
const stampedBytes = await pdfDoc.save();
|
||||
clientFieldStampedPath = `${workingAbsPath}.clientfields_${payload.jti}.tmp`;
|
||||
await writeFile(clientFieldStampedPath, stampedBytes);
|
||||
}
|
||||
|
||||
// 8c. Build signaturesWithCoords — scoped to this signer's signable fields (Pitfall 4)
|
||||
// text/checkbox/date are embedded at prepare time; the client was never shown these as interactive fields
|
||||
// CRITICAL: x/y/width/height come ONLY from the DB — never from the request body
|
||||
const signableFields = (doc.signatureFields ?? []).filter((f) => {
|
||||
@@ -258,16 +310,19 @@ export async function POST(
|
||||
// 9. Embed signatures into PDF — input: date-stamped working PDF, output: this signer's partial
|
||||
let pdfHash: string;
|
||||
try {
|
||||
pdfHash = await embedSignatureInPdf(dateStampedPath, partialAbsPath, signaturesWithCoords);
|
||||
pdfHash = await embedSignatureInPdf(clientFieldStampedPath, partialAbsPath, signaturesWithCoords);
|
||||
} catch (err) {
|
||||
console.error('[sign/POST] embedSignatureInPdf failed:', err);
|
||||
return NextResponse.json({ error: 'pdf-embed-failed' }, { status: 500 });
|
||||
}
|
||||
|
||||
// 9.5. Clean up temporary date-stamped file if it was created
|
||||
// 9.5. Clean up temporary files
|
||||
if (dateStampedPath !== workingAbsPath) {
|
||||
unlink(dateStampedPath).catch(() => {});
|
||||
}
|
||||
if (clientFieldStampedPath !== dateStampedPath && clientFieldStampedPath !== workingAbsPath) {
|
||||
unlink(clientFieldStampedPath).catch(() => {});
|
||||
}
|
||||
|
||||
// 10. Log pdf_hash_computed audit event
|
||||
await logAuditEvent({
|
||||
|
||||
@@ -70,9 +70,11 @@ async function persistFields(docId: string, fields: SignatureFieldData[]) {
|
||||
const PALETTE_TOKENS: Array<{ id: SignatureFieldType; label: string; color: string }> = [
|
||||
{ id: 'client-signature', label: 'Signature', color: '#2563eb' }, // blue
|
||||
{ id: 'initials', label: 'Initials', color: '#7c3aed' }, // purple
|
||||
{ id: 'checkbox', label: 'Checkbox', color: '#059669' }, // green
|
||||
{ id: 'checkbox', label: 'Checkbox', color: '#059669' }, // green (agent fills)
|
||||
{ id: 'date', label: 'Date', color: '#d97706' }, // amber
|
||||
{ id: 'text', label: 'Text', color: '#64748b' }, // slate
|
||||
{ id: 'text', label: 'Text', color: '#64748b' }, // slate (agent fills)
|
||||
{ id: 'client-text', label: 'Client Text', color: '#0891b2' }, // cyan (signer fills)
|
||||
{ id: 'client-checkbox', label: 'Client Checkbox', color: '#0284c7' }, // sky (signer checks)
|
||||
{ id: 'agent-signature', label: 'Agent Signature', color: '#dc2626' }, // red
|
||||
{ id: 'agent-initials', label: 'Agent Initials', color: '#ea580c' }, // orange
|
||||
];
|
||||
@@ -282,13 +284,13 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
const rawY = ghostRect.top - refRect.top;
|
||||
|
||||
// Determine the field type from the dnd-kit active.id (token id IS the SignatureFieldType)
|
||||
const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature', 'agent-initials']);
|
||||
const validTypes = new Set<string>(['client-signature', 'initials', 'text', 'checkbox', 'date', 'agent-signature', 'agent-initials', 'client-text', 'client-checkbox']);
|
||||
const droppedType: SignatureFieldType = validTypes.has(active.id as string)
|
||||
? (active.id as SignatureFieldType)
|
||||
: 'client-signature';
|
||||
|
||||
// Checkbox fields are square (24x24pt). All other types: 144x36pt.
|
||||
const isCheckbox = droppedType === 'checkbox';
|
||||
const isCheckbox = droppedType === 'checkbox' || droppedType === 'client-checkbox';
|
||||
const fieldW = isCheckbox ? 24 : 144;
|
||||
const fieldH = isCheckbox ? 24 : 36;
|
||||
|
||||
@@ -686,11 +688,10 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (readOnly) return;
|
||||
if (getFieldType(field) === 'text') {
|
||||
e.stopPropagation(); // prevent DroppableZone's deselect handler from firing
|
||||
if (getFieldType(field) === 'text' || getFieldType(field) === 'client-text') {
|
||||
e.stopPropagation();
|
||||
onFieldSelect?.(field.id);
|
||||
} else {
|
||||
// Non-text field click: deselect any selected text field
|
||||
onFieldSelect?.(null);
|
||||
}
|
||||
}}
|
||||
@@ -703,17 +704,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
value={currentValue}
|
||||
onChange={(e) => onFieldValueChange?.(field.id, e.target.value)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
fontSize: '10px',
|
||||
color: fieldColor,
|
||||
width: '100%',
|
||||
cursor: 'text',
|
||||
padding: 0,
|
||||
}}
|
||||
style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', fontSize: '10px', color: fieldColor, width: '100%', cursor: 'text', padding: 0 }}
|
||||
placeholder="Type value..."
|
||||
/>
|
||||
) : (
|
||||
@@ -721,8 +712,25 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
{currentValue || fieldLabel}
|
||||
</span>
|
||||
)
|
||||
) : fieldType === 'client-text' ? (
|
||||
// Agent types a hint stored in textFillData — shown as placeholder on signing page
|
||||
isSelected ? (
|
||||
<input
|
||||
data-no-move
|
||||
autoFocus
|
||||
value={currentValue}
|
||||
onChange={(e) => onFieldValueChange?.(field.id, e.target.value)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', fontSize: '10px', color: fieldColor, width: '100%', cursor: 'text', padding: 0 }}
|
||||
placeholder="Hint for signer (e.g. Full legal name)..."
|
||||
/>
|
||||
) : (
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', pointerEvents: 'none', fontStyle: currentValue ? 'normal' : 'italic', opacity: currentValue ? 1 : 0.6 }}>
|
||||
{currentValue || 'Client text — click to add hint'}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
fieldType !== 'checkbox' && (
|
||||
fieldType !== 'checkbox' && fieldType !== 'client-checkbox' && (
|
||||
<span style={{ pointerEvents: 'none' }}>{fieldLabel}</span>
|
||||
)
|
||||
)}
|
||||
@@ -760,7 +768,7 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
{!readOnly && fieldType !== 'checkbox' && (
|
||||
{!readOnly && fieldType !== 'checkbox' && fieldType !== 'client-checkbox' && (
|
||||
<>
|
||||
{resizeHandle('nw')}
|
||||
{resizeHandle('ne')}
|
||||
|
||||
@@ -44,10 +44,13 @@ export function SigningPageClient({
|
||||
}: SigningPageClientProps) {
|
||||
const router = useRouter();
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
// Map from page number (1-indexed) to rendered dimensions
|
||||
const [pageDimensions, setPageDimensions] = useState<Record<number, PageDimensions>>({});
|
||||
// Map of fieldId -> dataURL for signed fields
|
||||
// Signature / initials captures
|
||||
const [signedFields, setSignedFields] = useState<Map<string, string>>(new Map());
|
||||
// Client-text values: fieldId -> string
|
||||
const [textValues, setTextValues] = useState<Map<string, string>>(new Map());
|
||||
// Client-checkbox values: fieldId -> boolean
|
||||
const [checkboxValues, setCheckboxValues] = useState<Map<string, boolean>>(new Map());
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Modal state
|
||||
@@ -136,16 +139,28 @@ export function SigningPageClient({
|
||||
}, [signatureFields, signedFields]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const requiredFields = signatureFields.filter(
|
||||
const sigFields = signatureFields.filter(
|
||||
(f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials'
|
||||
);
|
||||
if (signedFields.size < requiredFields.length || submitting) return;
|
||||
const clientTextFields = signatureFields.filter((f) => getFieldType(f) === 'client-text');
|
||||
const clientCheckboxFields = signatureFields.filter((f) => getFieldType(f) === 'client-checkbox');
|
||||
|
||||
// All required: signatures + text fields must have a value + checkboxes must be checked
|
||||
if (signedFields.size < sigFields.length) return;
|
||||
if (clientTextFields.some((f) => !textValues.get(f.id)?.trim())) return;
|
||||
if (clientCheckboxFields.some((f) => !checkboxValues.get(f.id))) return;
|
||||
if (submitting) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/sign/${token}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ signatures: signaturesRef.current }),
|
||||
body: JSON.stringify({
|
||||
signatures: signaturesRef.current,
|
||||
clientTextValues: Array.from(textValues.entries()).map(([fieldId, value]) => ({ fieldId, value })),
|
||||
clientCheckboxValues: Array.from(checkboxValues.entries()).map(([fieldId, checked]) => ({ fieldId, checked })),
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
router.push(`/sign/${token}/confirmed`);
|
||||
@@ -293,57 +308,74 @@ export function SigningPageClient({
|
||||
renderTextLayer={false}
|
||||
/>
|
||||
|
||||
{/* Signature field overlays — rendered once page dimensions are known */}
|
||||
{/* Field overlays */}
|
||||
{dims &&
|
||||
fieldsOnPage.map((field) => {
|
||||
// Only render interactive overlays for client-signature and initials fields
|
||||
// text/checkbox/date are embedded at prepare time — no client interaction needed
|
||||
const ft = getFieldType(field);
|
||||
const isInteractive = ft === 'client-signature' || ft === 'initials';
|
||||
if (!isInteractive) return null;
|
||||
|
||||
const isSigned = signedFields.has(field.id);
|
||||
const baseStyle = getFieldOverlayStyle(field, dims);
|
||||
const animationStyle: React.CSSProperties = ft === 'initials'
|
||||
? { animation: 'pulse-border-purple 2s infinite' }
|
||||
: { animation: 'pulse-border 2s infinite' };
|
||||
const fieldOverlayStyle = { ...baseStyle, ...animationStyle };
|
||||
const sigDataURL = signedFields.get(field.id);
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
id={`field-${field.id}`}
|
||||
className={isSigned ? 'signing-field-signed' : ''}
|
||||
style={fieldOverlayStyle}
|
||||
onClick={() => handleFieldClick(field.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={ft === 'initials'
|
||||
? `Initials field${isSigned ? ' (initialed)' : ' — click to initial'}`
|
||||
: `Signature field${isSigned ? ' (signed)' : ' — click to sign'}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleFieldClick(field.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Show signature/initials preview when signed */}
|
||||
{isSigned && sigDataURL && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img
|
||||
src={sigDataURL}
|
||||
alt={ft === 'initials' ? 'Initials' : 'Signature'}
|
||||
|
||||
// ── Signature / initials ──
|
||||
if (ft === 'client-signature' || ft === 'initials') {
|
||||
const isSigned = signedFields.has(field.id);
|
||||
const animStyle: React.CSSProperties = ft === 'initials'
|
||||
? { animation: 'pulse-border-purple 2s infinite' }
|
||||
: { animation: 'pulse-border 2s infinite' };
|
||||
const sigDataURL = signedFields.get(field.id);
|
||||
return (
|
||||
<div key={field.id} id={`field-${field.id}`}
|
||||
className={isSigned ? 'signing-field-signed' : ''}
|
||||
style={{ ...baseStyle, ...animStyle }}
|
||||
onClick={() => handleFieldClick(field.id)}
|
||||
role="button" tabIndex={0}
|
||||
aria-label={ft === 'initials' ? `Initials field${isSigned ? ' (initialed)' : ' — click to initial'}` : `Signature field${isSigned ? ' (signed)' : ' — click to sign'}`}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleFieldClick(field.id); } }}
|
||||
>
|
||||
{isSigned && sigDataURL && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={sigDataURL} alt={ft === 'initials' ? 'Initials' : 'Signature'} style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Client-text: signer types a value ──
|
||||
if (ft === 'client-text') {
|
||||
const val = textValues.get(field.id) ?? '';
|
||||
const hint = field.hint || '';
|
||||
return (
|
||||
<div key={field.id} style={{ ...baseStyle, cursor: 'text', animation: val ? 'none' : 'pulse-border 2s infinite', border: val ? '1px solid #0891b2' : undefined }}>
|
||||
<input
|
||||
type="text"
|
||||
value={val}
|
||||
onChange={(e) => setTextValues(prev => new Map(prev).set(field.id, e.target.value))}
|
||||
placeholder={hint || 'Type here…'}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
display: 'block',
|
||||
width: '100%', height: '100%', border: 'none', outline: 'none',
|
||||
background: val ? 'rgba(8,145,178,0.05)' : 'rgba(8,145,178,0.1)',
|
||||
fontSize: '11px', padding: '2px 4px', color: '#0c4a6e',
|
||||
fontFamily: 'Georgia, serif',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Client-checkbox: signer checks it ──
|
||||
if (ft === 'client-checkbox') {
|
||||
const checked = checkboxValues.get(field.id) ?? false;
|
||||
return (
|
||||
<div key={field.id}
|
||||
style={{ ...baseStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', animation: checked ? 'none' : 'pulse-border 2s infinite', border: checked ? '1px solid #0284c7' : undefined, background: checked ? 'rgba(2,132,199,0.08)' : 'rgba(2,132,199,0.12)', borderRadius: '3px' }}
|
||||
onClick={() => setCheckboxValues(prev => new Map(prev).set(field.id, !prev.get(field.id)))}
|
||||
role="checkbox" aria-checked={checked} tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setCheckboxValues(prev => new Map(prev).set(field.id, !prev.get(field.id))); } }}
|
||||
>
|
||||
{checked && <span style={{ fontSize: '16px', color: '#0284c7', fontWeight: 'bold', lineHeight: 1 }}>✓</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null; // text/checkbox/date/agent fields handled at prepare time
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -358,6 +390,10 @@ export function SigningPageClient({
|
||||
(f) => getFieldType(f) === 'client-signature' || getFieldType(f) === 'initials'
|
||||
).length}
|
||||
signed={signedFields.size}
|
||||
extraRequired={
|
||||
signatureFields.filter((f) => getFieldType(f) === 'client-text').filter((f) => !textValues.get(f.id)?.trim()).length +
|
||||
signatureFields.filter((f) => getFieldType(f) === 'client-checkbox').filter((f) => !checkboxValues.get(f.id)).length
|
||||
}
|
||||
onJumpToNext={handleJumpToNext}
|
||||
onSubmit={handleSubmit}
|
||||
submitting={submitting}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
interface SigningProgressBarProps {
|
||||
total: number;
|
||||
signed: number;
|
||||
extraRequired?: number; // unfilled client-text + unchecked client-checkbox fields
|
||||
onJumpToNext: () => void;
|
||||
onSubmit: () => void;
|
||||
submitting: boolean;
|
||||
@@ -11,64 +12,43 @@ interface SigningProgressBarProps {
|
||||
export function SigningProgressBar({
|
||||
total,
|
||||
signed,
|
||||
extraRequired = 0,
|
||||
onJumpToNext,
|
||||
onSubmit,
|
||||
submitting,
|
||||
}: SigningProgressBarProps) {
|
||||
const allSigned = signed >= total;
|
||||
const sigsDone = signed >= total;
|
||||
const allDone = sigsDone && extraRequired === 0;
|
||||
|
||||
let statusText = `${signed} of ${total} signature${total !== 1 ? 's' : ''} complete`;
|
||||
if (sigsDone && extraRequired > 0) {
|
||||
statusText = `${extraRequired} field${extraRequired !== 1 ? 's' : ''} still need${extraRequired === 1 ? 's' : ''} to be filled`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 50,
|
||||
backgroundColor: '#1B2B4B',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 24px',
|
||||
boxShadow: '0 -2px 8px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '15px' }}>
|
||||
{signed} of {total} signature{total !== 1 ? 's' : ''} complete
|
||||
</span>
|
||||
<div style={{
|
||||
position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 50,
|
||||
backgroundColor: '#1B2B4B', color: '#fff',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '12px 24px', boxShadow: '0 -2px 8px rgba(0,0,0,0.15)',
|
||||
}}>
|
||||
<span style={{ fontSize: '15px' }}>{statusText}</span>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
{!allSigned && (
|
||||
<button
|
||||
onClick={onJumpToNext}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #C9A84C',
|
||||
color: '#C9A84C',
|
||||
padding: '8px 18px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{!sigsDone && (
|
||||
<button onClick={onJumpToNext} style={{
|
||||
backgroundColor: 'transparent', border: '1px solid #C9A84C', color: '#C9A84C',
|
||||
padding: '8px 18px', borderRadius: '4px', cursor: 'pointer', fontSize: '14px',
|
||||
}}>
|
||||
Jump to Next
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={!allSigned || submitting}
|
||||
style={{
|
||||
backgroundColor: allSigned ? '#C9A84C' : '#555',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
padding: '8px 22px',
|
||||
borderRadius: '4px',
|
||||
cursor: allSigned ? 'pointer' : 'not-allowed',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
opacity: submitting ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{submitting ? 'Submitting...' : 'Submit Signature'}
|
||||
<button onClick={onSubmit} disabled={!allDone || submitting} style={{
|
||||
backgroundColor: allDone ? '#C9A84C' : '#555', color: '#fff', border: 'none',
|
||||
padding: '8px 22px', borderRadius: '4px',
|
||||
cursor: allDone ? 'pointer' : 'not-allowed', fontSize: '14px', fontWeight: 'bold',
|
||||
opacity: submitting ? 0.7 : 1,
|
||||
}}>
|
||||
{submitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,8 @@ const FIELD_HEIGHTS: Record<SignatureFieldType, number> = {
|
||||
'date': 12,
|
||||
'text': 12,
|
||||
'checkbox': 14,
|
||||
'client-text': 12,
|
||||
'client-checkbox': 14,
|
||||
};
|
||||
|
||||
// Width clamping — use the exact measured blank width but stay within these bounds
|
||||
@@ -49,6 +51,8 @@ const SIZE_LIMITS: Record<SignatureFieldType, { minW: number; maxW: number }> =
|
||||
'date': { minW: 50, maxW: 130 },
|
||||
'text': { minW: 30, maxW: 280 },
|
||||
'checkbox': { minW: 14, maxW: 20 },
|
||||
'client-text': { minW: 30, maxW: 280 },
|
||||
'client-checkbox': { minW: 14, maxW: 20 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,13 @@ import { relations } from "drizzle-orm";
|
||||
export type SignatureFieldType =
|
||||
| 'client-signature'
|
||||
| 'initials'
|
||||
| 'text'
|
||||
| 'checkbox'
|
||||
| 'date'
|
||||
| 'text' // agent fills at prepare time
|
||||
| 'checkbox' // agent fills at prepare time
|
||||
| 'date' // auto-stamped with signing date
|
||||
| 'agent-signature'
|
||||
| 'agent-initials';
|
||||
| 'agent-initials'
|
||||
| 'client-text' // signer types a value on the signing page
|
||||
| 'client-checkbox'; // signer checks/unchecks on the signing page
|
||||
|
||||
export interface SignatureFieldData {
|
||||
id: string;
|
||||
@@ -19,6 +21,7 @@ export interface SignatureFieldData {
|
||||
height: number; // PDF points (default: 36 — 0.5 inches)
|
||||
type?: SignatureFieldType; // Optional — v1.0 documents have no type; fallback = 'client-signature'
|
||||
signerEmail?: string; // Optional — absent = legacy single-signer or agent-owned field
|
||||
hint?: string; // Optional label shown to signer for client-text / client-checkbox fields
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user