Files
2026-03-21 12:09:21 -06:00

294 lines
15 KiB
Markdown

---
phase: 09-client-property-address
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/db/schema.ts
- src/lib/actions/clients.ts
- src/app/portal/_components/ClientModal.tsx
- src/app/portal/_components/ClientProfileClient.tsx
- src/app/portal/(protected)/documents/[docId]/page.tsx
- src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
autonomous: false
requirements:
- CLIENT-04
- CLIENT-05
must_haves:
truths:
- "Agent can add or edit a property address on any client profile from the portal (create and edit modes in ClientModal)"
- "Property address is persisted to the database as NULL when left blank, not as empty string"
- "Property address is displayed on the client profile page when non-null"
- "When opening a prepared document for a client who has a property address, the PreparePanel's text fill area arrives pre-populated with the address under the key 'propertyAddress'"
- "Clients without a property address work identically to before — no regressions on existing data"
artifacts:
- path: "src/lib/db/schema.ts"
provides: "propertyAddress: text('property_address') nullable column on clients pgTable"
contains: "property_address"
- path: "drizzle/0007_property_address.sql"
provides: "ALTER TABLE clients ADD COLUMN property_address text migration"
contains: "property_address"
- path: "src/lib/actions/clients.ts"
provides: "createClient and updateClient server actions extended with propertyAddress"
contains: "propertyAddress"
- path: "src/app/portal/_components/ClientModal.tsx"
provides: "Property address input field in create/edit modal"
contains: "propertyAddress"
- path: "src/app/portal/_components/ClientProfileClient.tsx"
provides: "Property address display in profile card"
contains: "propertyAddress"
- path: "src/app/portal/(protected)/documents/[docId]/page.tsx"
provides: "propertyAddress included in client select query, passed to PreparePanel"
contains: "propertyAddress"
- path: "src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
provides: "clientPropertyAddress prop accepted, seeds textFillData state on mount"
contains: "clientPropertyAddress"
key_links:
- from: "src/lib/db/schema.ts"
to: "drizzle/0007_property_address.sql"
via: "npm run db:generate then npm run db:migrate"
pattern: "property_address"
- from: "src/app/portal/(protected)/documents/[docId]/page.tsx"
to: "src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
via: "clientPropertyAddress prop"
pattern: "clientPropertyAddress=\\{.*propertyAddress"
- from: "src/app/portal/_components/ClientProfileClient.tsx"
to: "src/app/portal/_components/ClientModal.tsx"
via: "defaultPropertyAddress prop on edit modal call"
pattern: "defaultPropertyAddress=\\{client\\.propertyAddress"
---
<objective>
Add a property address field to client profiles: DB column, server actions, form UI, profile display, and pre-seed the PreparePanel text fill state so the address flows into the AI pre-fill pipeline in Phase 13.
Purpose: Phase 13 AI pre-fill (AI-02) needs structured client data (name + property address) to populate text fields. This phase is the data sourcing layer — no AI work yet, just capturing and plumbing the data.
Output: Nullable `property_address` TEXT column on `clients` table, extended server actions, ClientModal form field, profile display, and PreparePanel state seed.
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@src/lib/db/schema.ts
@src/lib/actions/clients.ts
@src/app/portal/_components/ClientModal.tsx
@src/app/portal/_components/ClientProfileClient.tsx
@src/app/portal/(protected)/documents/[docId]/page.tsx
@src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
<interfaces>
<!-- Key existing patterns. Executor should use these directly — no exploration needed. -->
From src/lib/db/schema.ts (clients table — existing pattern):
```typescript
export const clients = pgTable("clients", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
email: text("email").notNull(),
// propertyAddress goes here — nullable, no .notNull(), no .default()
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
// Existing nullable pattern in same file: sentAt: timestamp("sent_at"), filePath: text("file_path")
```
From src/lib/actions/clients.ts (Zod schema + action pattern):
```typescript
const clientSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Valid email address required"),
// propertyAddress: z.string().optional() — add here
});
// After parse: parsed.data.propertyAddress || null (coerce "" → null before DB write)
// revalidatePath("/portal/clients") and revalidatePath("/portal/clients/" + id) already present — keep both
```
From src/app/portal/_components/ClientModal.tsx (useActionState form pattern):
```typescript
type ClientModalProps = {
isOpen: boolean; onClose: () => void; mode?: "create" | "edit";
clientId?: string; defaultName?: string; defaultEmail?: string;
// defaultPropertyAddress?: string; — add here
};
// Add input after email input: name="propertyAddress", defaultValue={defaultPropertyAddress}
// Placeholder: "123 Main St, Salt Lake City, UT 84101"
```
From src/app/portal/_components/ClientProfileClient.tsx (display + edit modal pattern):
```typescript
type Props = {
client: { id: string; name: string; email: string }; // add: propertyAddress?: string | null
docs: DocumentRow[];
};
// Edit modal call site: add defaultPropertyAddress={client.propertyAddress ?? undefined}
// Profile display: {client.propertyAddress && <p className="text-sm text-gray-500">{client.propertyAddress}</p>}
```
From src/app/portal/(protected)/documents/[docId]/page.tsx (client data select):
```typescript
// Existing: db.select({ email: clients.email, name: clients.name })
// Extend to: db.select({ email: clients.email, name: clients.name, propertyAddress: clients.propertyAddress })
// Pass to PreparePanel: clientPropertyAddress={docClient?.propertyAddress ?? null}
```
From src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx (textFillData state):
```typescript
interface PreparePanelProps {
// ... existing props
clientPropertyAddress?: string | null; // add
}
// Lazy initializer — runs ONCE on mount:
const [textFillData, setTextFillData] = useState<Record<string, string>>(
() => clientPropertyAddress ? { propertyAddress: clientPropertyAddress } : {}
);
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Schema column, migration, and server action extension</name>
<files>
src/lib/db/schema.ts
src/lib/actions/clients.ts
drizzle/0007_property_address.sql (generated by drizzle-kit)
</files>
<action>
1. In `src/lib/db/schema.ts`, add `propertyAddress: text("property_address")` to the `clients` pgTable definition after the `email` column. No `.notNull()`, no `.default()` — purely nullable. Match the pattern of existing nullable columns (`sentAt`, `filePath`).
2. Run migration generation: `npm run db:generate` from the project root. This produces `drizzle/0007_property_address.sql` with `ALTER TABLE "clients" ADD COLUMN "property_address" text;` and updates `drizzle/meta/_journal.json` and the snapshot. Do NOT write the SQL file manually — always use drizzle-kit.
3. Apply the migration: `npm run db:migrate`. Verify the column now exists.
4. In `src/lib/actions/clients.ts`, extend the Zod `clientSchema` to add: `propertyAddress: z.string().optional()`.
5. In `createClient`, after `safeParse` succeeds, add `propertyAddress: parsed.data.propertyAddress || null` to the `.values({...})` object. The `|| null` coercion ensures empty string from FormData becomes NULL in the DB — not an empty string.
6. In `updateClient`, add `propertyAddress: parsed.data.propertyAddress || null` to the `.set({...})` object alongside `name`, `email`, `updatedAt`. The existing `revalidatePath` calls cover both list and profile pages — no new paths needed.
CRITICAL: Do not add `.notNull()` or `.default('')` to the column. Existing client rows have no address — a NOT NULL default would fail the migration.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red && npm run db:generate 2>&1 | tail -5 && npm run db:migrate 2>&1 | tail -5 && npx tsc --noEmit 2>&1 | head -20</automated>
</verify>
<done>
drizzle/0007_property_address.sql exists and contains `ADD COLUMN "property_address" text`. Migration applied without errors. TypeScript compilation passes with no new errors in schema.ts or clients.ts.
</done>
</task>
<task type="auto">
<name>Task 2: UI layer — modal input, profile display, and PreparePanel pre-seed</name>
<files>
src/app/portal/_components/ClientModal.tsx
src/app/portal/_components/ClientProfileClient.tsx
src/app/portal/(protected)/documents/[docId]/page.tsx
src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
</files>
<action>
1. **ClientModal.tsx** — Add `defaultPropertyAddress?: string` to `ClientModalProps` type. Add a new input field after the email field:
```tsx
<div>
<label htmlFor="propertyAddress" className="block text-sm font-medium text-gray-700 mb-1">
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 focus:border-[var(--gold)] focus:ring-[var(--gold)]"
placeholder="123 Main St, Salt Lake City, UT 84101"
/>
</div>
```
Match the exact className pattern used by the existing name and email inputs in this file. The field is optional — no required attribute, no validation message needed.
2. **ClientProfileClient.tsx** — Update the `Props` type's `client` object to include `propertyAddress?: string | null`. In the header card section (after the email paragraph), add:
```tsx
{client.propertyAddress && (
<p className="text-sm text-gray-500 mt-1">{client.propertyAddress}</p>
)}
```
Update the `<ClientModal>` edit call site to pass `defaultPropertyAddress={client.propertyAddress ?? undefined}`.
3. **documents/[docId]/page.tsx** — Find the `db.select({...})` call that fetches client data for `PreparePanel`. Extend the select to include `propertyAddress: clients.propertyAddress`. Pass the value to `PreparePanel` as `clientPropertyAddress={docClient?.propertyAddress ?? null}`. Add the prop to the `PreparePanel` JSX element call.
4. **PreparePanel.tsx** — Add `clientPropertyAddress?: string | null` to `PreparePanelProps`. Change the `textFillData` state initialization from `useState<Record<string, string>>({})` (or equivalent) to a lazy initializer:
```tsx
const [textFillData, setTextFillData] = useState<Record<string, string>>(
() => (clientPropertyAddress ? { propertyAddress: clientPropertyAddress } : {})
);
```
This lazy initializer runs exactly once on mount and pre-seeds the map when the value is available. If the client has no address, `textFillData` initializes to `{}` as before — zero behavior change for the existing flow.
CRITICAL: Do NOT change anything in `/api/documents/[id]/prepare/route.ts` — it already accepts `textFillData: Record<string, string>` and passes it through untouched.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red && npx tsc --noEmit 2>&1 | head -30</automated>
</verify>
<done>
TypeScript compiles clean across all four modified files. ClientModal renders a Property Address input. ClientProfileClient displays address when non-null. PreparePanel receives and seeds the prop. No regressions in existing client create/edit flows.
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Human verification — property address end-to-end</name>
<action>Pause for human verification of the complete property address feature across all touchpoints.</action>
<what-built>
Property address field: DB column + migration, extended server actions, ClientModal input (create + edit), ClientProfileClient display, DocumentPage client select extension, PreparePanel state pre-seed.
</what-built>
<how-to-verify>
1. Navigate to /portal/clients — click "Add Client". Confirm the modal has a "Property Address" field below Email. Create a new test client with an address (e.g., "456 Oak Ave, Provo, UT 84601"). Save — client appears in list.
2. Click the new client to open their profile. Confirm the property address appears under the email in the header card.
3. Click "Edit Client" — confirm the modal opens with the address pre-filled in the Property Address field. Change the address and save. Confirm the profile now shows the updated address.
4. Create a second client with NO property address. Confirm their profile shows no address field (no empty line or placeholder).
5. Upload or use an existing document assigned to the client with an address. Open the document's prepare page. Confirm the TextFillForm/PreparePanel shows a "propertyAddress" row pre-populated with the client's address — without the agent typing it.
6. Open a document assigned to the client WITHOUT an address. Confirm PreparePanel text fill area is empty as before.
Expected: All 6 checks pass. No console errors. Existing clients and documents unaffected.
</how-to-verify>
<verify>
<automated>MISSING — human verification required for visual/interactive UI checks</automated>
</verify>
<done>Human types "approved" after all 6 verification checks pass.</done>
<resume-signal>Type "approved" to complete Phase 9, or describe any issues found.</resume-signal>
</task>
</tasks>
<verification>
- `npm run db:migrate` succeeds: `ALTER TABLE "clients" ADD COLUMN "property_address" text` applied cleanly
- `npx tsc --noEmit` passes with zero new errors across all modified files
- ClientModal renders Property Address input in both create and edit modes
- Existing clients (no address) display and edit without errors
- PreparePanel state initialized with `{ propertyAddress: "..." }` when client has address, `{}` when null
- No changes to `/api/documents/[id]/prepare/route.ts` or `lib/pdf/prepare-document.ts`
</verification>
<success_criteria>
- Agent can add/edit property address in ClientModal — persisted as NULL (not "") when blank
- Property address displays on client profile when non-null; hidden when null
- PreparePanel arrives pre-seeded with client's property address as `textFillData.propertyAddress`
- TypeScript clean: `npx tsc --noEmit` passes
- Phase 9 human verification approved (checkpoint)
- Requirements CLIENT-04 and CLIENT-05 complete
</success_criteria>
<output>
After completion, create `.planning/phases/09-client-property-address/09-01-SUMMARY.md` following the template at @/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</output>