294 lines
15 KiB
Markdown
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>
|