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:
@@ -10,6 +10,7 @@ interface PreparePanelProps {
|
||||
currentStatus: string;
|
||||
agentDownloadUrl?: string | null;
|
||||
signedAt?: Date | null;
|
||||
clientPropertyAddress?: string | null;
|
||||
}
|
||||
|
||||
function parseEmails(raw: string | undefined): string[] {
|
||||
@@ -20,14 +21,16 @@ function isValidEmail(email: string): boolean {
|
||||
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 [recipients, setRecipients] = useState(defaultEmail ?? '');
|
||||
// Sync if defaultEmail arrives after initial render (streaming / hydration timing)
|
||||
useEffect(() => {
|
||||
if (defaultEmail) setRecipients(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 [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async function DocumentPage({
|
||||
|
||||
const [doc, docClient] = await Promise.all([
|
||||
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)
|
||||
.innerJoin(documents, eq(documents.clientId, clients.id))
|
||||
.where(eq(documents.id, docId))
|
||||
@@ -65,6 +65,7 @@ export default async function DocumentPage({
|
||||
currentStatus={doc.status}
|
||||
agentDownloadUrl={agentDownloadUrl}
|
||||
signedAt={doc.signedAt ?? null}
|
||||
clientPropertyAddress={docClient?.propertyAddress ?? null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,45 +10,28 @@ type ClientModalProps = {
|
||||
clientId?: string;
|
||||
defaultName?: string;
|
||||
defaultEmail?: string;
|
||||
defaultPropertyAddress?: string;
|
||||
};
|
||||
|
||||
export function ClientModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
mode = "create",
|
||||
clientId,
|
||||
defaultName,
|
||||
defaultEmail,
|
||||
}: ClientModalProps) {
|
||||
const boundAction =
|
||||
mode === "edit" && clientId
|
||||
? updateClient.bind(null, clientId)
|
||||
: createClient;
|
||||
|
||||
export function ClientModal({ isOpen, onClose, mode = "create", clientId, defaultName, defaultEmail, defaultPropertyAddress }: ClientModalProps) {
|
||||
const boundAction = mode === "edit" && clientId ? updateClient.bind(null, clientId) : createClient;
|
||||
const [state, formAction, pending] = useActionState(boundAction, null);
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success) {
|
||||
onClose();
|
||||
}
|
||||
if (state?.success) onClose();
|
||||
}, [state, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const title = mode === "edit" ? "Edit Client" : "Add Client";
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div className="bg-white rounded-xl shadow-xl p-6 w-full max-w-md">
|
||||
<h2 className="text-[var(--navy)] text-lg font-semibold mb-4">
|
||||
{title}
|
||||
<div style={{ position: "fixed", inset: 0, zIndex: 50, display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "rgba(0,0,0,0.4)" }}>
|
||||
<div style={{ backgroundColor: "white", borderRadius: "1rem", boxShadow: "0 8px 32px rgba(0,0,0,0.18)", padding: "2rem", width: "100%", maxWidth: "28rem" }}>
|
||||
<h2 style={{ color: "#1B2B4B", fontSize: "1.125rem", fontWeight: 700, marginBottom: "1.25rem" }}>
|
||||
{mode === "edit" ? "Edit Client" : "Add Client"}
|
||||
</h2>
|
||||
<form action={formAction} className="space-y-4">
|
||||
<form action={formAction} style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
<label htmlFor="name" style={{ display: "block", fontSize: "0.875rem", fontWeight: 500, color: "#1B2B4B", marginBottom: "0.375rem" }}>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
@@ -57,14 +40,12 @@ export function ClientModal({
|
||||
type="text"
|
||||
defaultValue={defaultName}
|
||||
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>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
<label htmlFor="email" style={{ display: "block", fontSize: "0.875rem", fontWeight: 500, color: "#1B2B4B", marginBottom: "0.375rem" }}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
@@ -73,24 +54,37 @@ export function ClientModal({
|
||||
type="email"
|
||||
defaultValue={defaultEmail}
|
||||
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>
|
||||
{state?.error && (
|
||||
<p className="text-red-500 text-sm">{state.error}</p>
|
||||
)}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<div>
|
||||
<label htmlFor="propertyAddress" style={{ display: "block", fontSize: "0.875rem", fontWeight: 500, color: "#1B2B4B", marginBottom: "0.375rem" }}>
|
||||
Property Address
|
||||
</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
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
|
||||
@@ -20,7 +20,7 @@ type DocumentRow = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
client: { id: string; name: string; email: string };
|
||||
client: { id: string; name: string; email: string; propertyAddress?: string | null };
|
||||
docs: DocumentRow[];
|
||||
};
|
||||
|
||||
@@ -48,6 +48,9 @@ export function ClientProfileClient({ client, docs }: Props) {
|
||||
<div>
|
||||
<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>
|
||||
{client.propertyAddress && (
|
||||
<p style={{ color: "#6B7280", fontSize: "0.875rem", marginTop: "0.25rem" }}>{client.propertyAddress}</p>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "0.75rem" }}>
|
||||
<button
|
||||
@@ -89,7 +92,7 @@ export function ClientProfileClient({ client, docs }: Props) {
|
||||
)}
|
||||
</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 && (
|
||||
<AddDocumentModal clientId={client.id} onClose={() => setIsAddDocOpen(false)} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user