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;
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user