15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-client-property-address | 01 | execute | 1 |
|
false |
|
|
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.
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_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
From src/lib/db/schema.ts (clients table — existing pattern):
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):
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):
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):
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):
// 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):
interface PreparePanelProps {
// ... existing props
clientPropertyAddress?: string | null; // add
}
// Lazy initializer — runs ONCE on mount:
const [textFillData, setTextFillData] = useState<Record<string, string>>(
() => clientPropertyAddress ? { propertyAddress: clientPropertyAddress } : {}
);
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.
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
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.
Task 2: UI layer — modal input, profile display, and PreparePanel pre-seed
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
1. **ClientModal.tsx** — Add `defaultPropertyAddress?: string` to `ClientModalProps` type. Add a new input field after the email field:
```tsx
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.
cd /Users/ccopeland/temp/red && npx tsc --noEmit 2>&1 | head -30
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.
Task 3: Human verification — property address end-to-end
Pause for human verification of the complete property address feature across all touchpoints.
Property address field: DB column + migration, extended server actions, ClientModal input (create + edit), ClientProfileClient display, DocumentPage client select extension, PreparePanel state pre-seed.
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.
MISSING — human verification required for visual/interactive UI checks
Human types "approved" after all 6 verification checks pass.
Type "approved" to complete Phase 9, or describe any issues found.
- `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`
<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 --noEmitpasses - Phase 9 human verification approved (checkpoint)
- Requirements CLIENT-04 and CLIENT-05 complete </success_criteria>