feat(09-01): UI layer — property address modal input, profile display, PreparePanel pre-seed

- ClientModal: add defaultPropertyAddress prop, property address input field after email
- ClientProfileClient: add propertyAddress to Props type, display address under email when non-null, pass defaultPropertyAddress to edit modal
- documents/[docId]/page.tsx: extend client select to include propertyAddress, pass as clientPropertyAddress to PreparePanel
- PreparePanel: add clientPropertyAddress prop, lazy-initialize textFillData with { propertyAddress } when client has address
This commit is contained in:
Chandler Copeland
2026-03-21 12:15:27 -06:00
parent baa1c785a5
commit fa9981edd9
4 changed files with 45 additions and 44 deletions

View File

@@ -10,6 +10,7 @@ interface PreparePanelProps {
currentStatus: string; currentStatus: string;
agentDownloadUrl?: string | null; agentDownloadUrl?: string | null;
signedAt?: Date | null; signedAt?: Date | null;
clientPropertyAddress?: string | null;
} }
function parseEmails(raw: string | undefined): string[] { function parseEmails(raw: string | undefined): string[] {
@@ -20,14 +21,16 @@ function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
} }
export function PreparePanel({ docId, defaultEmail, clientName, currentStatus, agentDownloadUrl, signedAt }: PreparePanelProps) { export function PreparePanel({ docId, defaultEmail, clientName, currentStatus, agentDownloadUrl, signedAt, clientPropertyAddress }: PreparePanelProps) {
const router = useRouter(); const router = useRouter();
const [recipients, setRecipients] = useState(defaultEmail ?? ''); const [recipients, setRecipients] = useState(defaultEmail ?? '');
// Sync if defaultEmail arrives after initial render (streaming / hydration timing) // Sync if defaultEmail arrives after initial render (streaming / hydration timing)
useEffect(() => { useEffect(() => {
if (defaultEmail) setRecipients(defaultEmail); if (defaultEmail) setRecipients(defaultEmail);
}, [defaultEmail]); }, [defaultEmail]);
const [textFillData, setTextFillData] = useState<Record<string, string>>({}); const [textFillData, setTextFillData] = useState<Record<string, string>>(
() => (clientPropertyAddress ? { propertyAddress: clientPropertyAddress } : {} as Record<string, string>)
);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null); const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);

View File

@@ -20,7 +20,7 @@ export default async function DocumentPage({
const [doc, docClient] = await Promise.all([ const [doc, docClient] = await Promise.all([
db.query.documents.findFirst({ where: eq(documents.id, docId) }), db.query.documents.findFirst({ where: eq(documents.id, docId) }),
db.select({ email: clients.email, name: clients.name }) db.select({ email: clients.email, name: clients.name, propertyAddress: clients.propertyAddress })
.from(clients) .from(clients)
.innerJoin(documents, eq(documents.clientId, clients.id)) .innerJoin(documents, eq(documents.clientId, clients.id))
.where(eq(documents.id, docId)) .where(eq(documents.id, docId))
@@ -65,6 +65,7 @@ export default async function DocumentPage({
currentStatus={doc.status} currentStatus={doc.status}
agentDownloadUrl={agentDownloadUrl} agentDownloadUrl={agentDownloadUrl}
signedAt={doc.signedAt ?? null} signedAt={doc.signedAt ?? null}
clientPropertyAddress={docClient?.propertyAddress ?? null}
/> />
</div> </div>
</div> </div>

View File

@@ -10,45 +10,28 @@ type ClientModalProps = {
clientId?: string; clientId?: string;
defaultName?: string; defaultName?: string;
defaultEmail?: string; defaultEmail?: string;
defaultPropertyAddress?: string;
}; };
export function ClientModal({ export function ClientModal({ isOpen, onClose, mode = "create", clientId, defaultName, defaultEmail, defaultPropertyAddress }: ClientModalProps) {
isOpen, const boundAction = mode === "edit" && clientId ? updateClient.bind(null, clientId) : createClient;
onClose,
mode = "create",
clientId,
defaultName,
defaultEmail,
}: ClientModalProps) {
const boundAction =
mode === "edit" && clientId
? updateClient.bind(null, clientId)
: createClient;
const [state, formAction, pending] = useActionState(boundAction, null); const [state, formAction, pending] = useActionState(boundAction, null);
useEffect(() => { useEffect(() => {
if (state?.success) { if (state?.success) onClose();
onClose();
}
}, [state, onClose]); }, [state, onClose]);
if (!isOpen) return null; if (!isOpen) return null;
const title = mode === "edit" ? "Edit Client" : "Add Client";
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div style={{ position: "fixed", inset: 0, zIndex: 50, display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "rgba(0,0,0,0.4)" }}>
<div className="bg-white rounded-xl shadow-xl p-6 w-full max-w-md"> <div style={{ backgroundColor: "white", borderRadius: "1rem", boxShadow: "0 8px 32px rgba(0,0,0,0.18)", padding: "2rem", width: "100%", maxWidth: "28rem" }}>
<h2 className="text-[var(--navy)] text-lg font-semibold mb-4"> <h2 style={{ color: "#1B2B4B", fontSize: "1.125rem", fontWeight: 700, marginBottom: "1.25rem" }}>
{title} {mode === "edit" ? "Edit Client" : "Add Client"}
</h2> </h2>
<form action={formAction} className="space-y-4"> <form action={formAction} style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div> <div>
<label <label htmlFor="name" style={{ display: "block", fontSize: "0.875rem", fontWeight: 500, color: "#1B2B4B", marginBottom: "0.375rem" }}>
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Name Name
</label> </label>
<input <input
@@ -57,14 +40,12 @@ export function ClientModal({
type="text" type="text"
defaultValue={defaultName} defaultValue={defaultName}
required required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--gold)]" className="block w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:border-transparent transition"
placeholder="Jane Smith"
/> />
</div> </div>
<div> <div>
<label <label htmlFor="email" style={{ display: "block", fontSize: "0.875rem", fontWeight: 500, color: "#1B2B4B", marginBottom: "0.375rem" }}>
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email Email
</label> </label>
<input <input
@@ -73,24 +54,37 @@ export function ClientModal({
type="email" type="email"
defaultValue={defaultEmail} defaultValue={defaultEmail}
required required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--gold)]" className="block w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:border-transparent transition"
placeholder="jane@example.com"
/> />
</div> </div>
{state?.error && ( <div>
<p className="text-red-500 text-sm">{state.error}</p> <label htmlFor="propertyAddress" style={{ display: "block", fontSize: "0.875rem", fontWeight: 500, color: "#1B2B4B", marginBottom: "0.375rem" }}>
)} Property Address
<div className="flex justify-end gap-3 pt-2"> </label>
<input
id="propertyAddress"
name="propertyAddress"
type="text"
defaultValue={defaultPropertyAddress}
className="block w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:border-transparent transition"
placeholder="123 Main St, Salt Lake City, UT 84101"
/>
</div>
{state?.error && <p style={{ color: "#DC2626", fontSize: "0.875rem" }}>{state.error}</p>}
<div style={{ display: "flex", justifyContent: "flex-end", gap: "0.75rem", paddingTop: "0.5rem" }}>
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50" className="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={pending} disabled={pending}
className="px-4 py-2 text-sm text-white bg-[var(--gold)] rounded-lg hover:opacity-90 disabled:opacity-50" className="px-4 py-2 text-sm font-semibold text-white rounded-lg transition hover:brightness-110 disabled:opacity-50"
style={{ backgroundColor: "#C9A84C" }}
> >
{pending ? "Saving..." : "Save"} {pending ? "Saving..." : "Save"}
</button> </button>

View File

@@ -20,7 +20,7 @@ type DocumentRow = {
}; };
type Props = { type Props = {
client: { id: string; name: string; email: string }; client: { id: string; name: string; email: string; propertyAddress?: string | null };
docs: DocumentRow[]; docs: DocumentRow[];
}; };
@@ -48,6 +48,9 @@ export function ClientProfileClient({ client, docs }: Props) {
<div> <div>
<h1 style={{ color: "#1B2B4B", fontSize: "1.5rem", fontWeight: 700, marginBottom: "0.25rem" }}>{client.name}</h1> <h1 style={{ color: "#1B2B4B", fontSize: "1.5rem", fontWeight: 700, marginBottom: "0.25rem" }}>{client.name}</h1>
<p style={{ color: "#6B7280", fontSize: "0.875rem" }}>{client.email}</p> <p style={{ color: "#6B7280", fontSize: "0.875rem" }}>{client.email}</p>
{client.propertyAddress && (
<p style={{ color: "#6B7280", fontSize: "0.875rem", marginTop: "0.25rem" }}>{client.propertyAddress}</p>
)}
</div> </div>
<div style={{ display: "flex", gap: "0.75rem" }}> <div style={{ display: "flex", gap: "0.75rem" }}>
<button <button
@@ -89,7 +92,7 @@ export function ClientProfileClient({ client, docs }: Props) {
)} )}
</div> </div>
<ClientModal isOpen={isEditOpen} onClose={() => setIsEditOpen(false)} mode="edit" clientId={client.id} defaultName={client.name} defaultEmail={client.email} /> <ClientModal isOpen={isEditOpen} onClose={() => setIsEditOpen(false)} mode="edit" clientId={client.id} defaultName={client.name} defaultEmail={client.email} defaultPropertyAddress={client.propertyAddress ?? undefined} />
{isAddDocOpen && ( {isAddDocOpen && (
<AddDocumentModal clientId={client.id} onClose={() => setIsAddDocOpen(false)} /> <AddDocumentModal clientId={client.id} onClose={() => setIsAddDocOpen(false)} />
)} )}