Files
red/.planning/phases/09-client-property-address/09-01-PLAN.md
2026-03-21 12:09:21 -06:00

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
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
false
CLIENT-04
CLIENT-05
truths artifacts key_links
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
path provides contains
src/lib/db/schema.ts propertyAddress: text('property_address') nullable column on clients pgTable property_address
path provides contains
drizzle/0007_property_address.sql ALTER TABLE clients ADD COLUMN property_address text migration property_address
path provides contains
src/lib/actions/clients.ts createClient and updateClient server actions extended with propertyAddress propertyAddress
path provides contains
src/app/portal/_components/ClientModal.tsx Property address input field in create/edit modal propertyAddress
path provides contains
src/app/portal/_components/ClientProfileClient.tsx Property address display in profile card propertyAddress
path provides contains
src/app/portal/(protected)/documents/[docId]/page.tsx propertyAddress included in client select query, passed to PreparePanel propertyAddress
path provides contains
src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx clientPropertyAddress prop accepted, seeds textFillData state on mount clientPropertyAddress
from to via pattern
src/lib/db/schema.ts drizzle/0007_property_address.sql npm run db:generate then npm run db:migrate property_address
from to via pattern
src/app/portal/(protected)/documents/[docId]/page.tsx src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx clientPropertyAddress prop clientPropertyAddress={.*propertyAddress
from to via pattern
src/app/portal/_components/ClientProfileClient.tsx src/app/portal/_components/ClientModal.tsx defaultPropertyAddress prop on edit modal call defaultPropertyAddress={client.propertyAddress
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.

<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 } : {}
);
Task 1: Schema column, migration, and server action extension src/lib/db/schema.ts src/lib/actions/clients.ts drizzle/0007_property_address.sql (generated by drizzle-kit) 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.
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
Property Address
``` 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.
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 --noEmit passes
  • Phase 9 human verification approved (checkpoint)
  • Requirements CLIENT-04 and CLIENT-05 complete </success_criteria>
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