From df6eb76bd0fc9b49dd57c5c831e7cc9917bd401b Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Fri, 20 Mar 2026 00:04:08 -0600 Subject: [PATCH] feat(05-03): create TextFillForm and PreparePanel client components - TextFillForm: key-value pair builder (up to 10 rows, individually removable) - PreparePanel: client selector + text fill form + Prepare and Send button - PreparePanel calls POST /api/documents/[id]/prepare and calls router.refresh() on success - PreparePanel shows read-only message for non-Draft documents - Rule 1 fix: PdfViewer page.scale -> scale (PageCallback has no .scale property; use state var) --- .../[docId]/_components/PdfViewer.tsx | 2 +- .../[docId]/_components/PreparePanel.tsx | 95 +++++++++++++++++++ .../[docId]/_components/TextFillForm.tsx | 68 +++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx create mode 100644 teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx index e2c09b7..a955856 100644 --- a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx +++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx @@ -81,7 +81,7 @@ export function PdfViewer({ docId }: { docId: string }) { originalHeight: Math.max(page.view[1], page.view[3]), width: page.width, height: page.height, - scale: page.scale, + scale, // use the current scale state value (PageCallback has no .scale property) }); }} /> diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx new file mode 100644 index 0000000..9e7a32c --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx @@ -0,0 +1,95 @@ +'use client'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { TextFillForm } from './TextFillForm'; + +interface Client { id: string; name: string; email: string; } + +interface PreparePanelProps { + docId: string; + clients: Client[]; + currentClientId: string; + currentStatus: string; +} + +export function PreparePanel({ docId, clients, currentClientId, currentStatus }: PreparePanelProps) { + const router = useRouter(); + const [assignedClientId, setAssignedClientId] = useState(currentClientId); + const [textFillData, setTextFillData] = useState>({}); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null); + + // Don't show the panel if already sent/signed + const canPrepare = currentStatus === 'Draft'; + + async function handlePrepare() { + setLoading(true); + setResult(null); + try { + const res = await fetch(`/api/documents/${docId}/prepare`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ textFillData, assignedClientId }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: 'Unknown error' })); + setResult({ ok: false, message: err.error ?? 'Prepare failed' }); + } else { + setResult({ ok: true, message: 'Document prepared successfully. Status updated to Sent.' }); + router.refresh(); // Update the page to reflect new status + } + } catch (e) { + setResult({ ok: false, message: String(e) }); + } finally { + setLoading(false); + } + } + + if (!canPrepare) { + return ( +
+ Document status is {currentStatus} — preparation is only available for Draft documents. +
+ ); + } + + return ( +
+

Prepare Document

+ +
+ + +
+ +
+ + +
+ + + + {result && ( +

+ {result.message} +

+ )} +
+ ); +} diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx new file mode 100644 index 0000000..6cb7e3f --- /dev/null +++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/TextFillForm.tsx @@ -0,0 +1,68 @@ +'use client'; +import { useState } from 'react'; + +interface TextRow { label: string; value: string; } + +interface TextFillFormProps { + onChange: (data: Record) => void; +} + +export function TextFillForm({ onChange }: TextFillFormProps) { + const [rows, setRows] = useState([{ label: '', value: '' }]); + + function updateRow(index: number, field: 'label' | 'value', val: string) { + const next = rows.map((r, i) => i === index ? { ...r, [field]: val } : r); + setRows(next); + onChange(Object.fromEntries(next.filter(r => r.label).map(r => [r.label, r.value]))); + } + + function addRow() { + if (rows.length >= 10) return; + setRows([...rows, { label: '', value: '' }]); + } + + function removeRow(index: number) { + const next = rows.filter((_, i) => i !== index); + setRows(next.length ? next : [{ label: '', value: '' }]); + onChange(Object.fromEntries(next.filter(r => r.label).map(r => [r.label, r.value]))); + } + + return ( +
+

+ Field label = AcroForm field name in the PDF (e.g. "PropertyAddress"). Leave blank to skip. +

+ {rows.map((row, i) => ( +
+ updateRow(i, 'label', e.target.value)} + className="flex-1 border rounded px-2 py-1 text-sm" + /> + updateRow(i, 'value', e.target.value)} + className="flex-1 border rounded px-2 py-1 text-sm" + /> + +
+ ))} + {rows.length < 10 && ( + + )} +
+ ); +}