---
phase: 09
plan: 02
type: execute
wave: 1
depends_on:
  - 09-01
files_modified:
  - src/app/admin/quotes/new/page.tsx
  - src/app/admin/quotes/new/actions.ts
  - src/components/admin/quotes/QuoteBuilderForm.tsx
  - src/components/admin/quotes/OfferSelector.tsx
  - src/components/admin/quotes/PriceOverrideInput.tsx
  - src/components/admin/quotes/QuotePreview.tsx
  - src/lib/quote-actions.ts
autonomous: true
requirements:
  - QUOTE-01
user_setup: []
must_haves:
  truths:
    - "Admin can select a client and offer on /admin/quotes/new"
    - "Two-column form shows inputs on left and live preview on right"
    - "Form submit calls createQuote server action with validated data"
    - "Server action recalculates total and rejects if mismatch"
    - "On success, public link /quote/[token] is displayed for copy"
  artifacts:
    - path: src/lib/quote-actions.ts
      provides: "createQuote server action with price validation"
      exports: ["createQuote", "getOfferWithPhases"]
    - path: src/components/admin/quotes/QuoteBuilderForm.tsx
      provides: "Form state management and submission"
      min_lines: 80
    - path: src/app/admin/quotes/new/page.tsx
      provides: "Admin quote builder page at /admin/quotes/new"
      contains: "QuoteBuilderForm"
  key_links:
    - from: "src/app/admin/quotes/new/page.tsx"
      to: "src/components/admin/quotes/QuoteBuilderForm.tsx"
      via: "import and render"
      pattern: "import.*QuoteBuilderForm"
    - from: "src/components/admin/quotes/QuoteBuilderForm.tsx"
      to: "src/lib/quote-actions.ts"
      via: "server action call"
      pattern: "await createQuote"
    - from: "src/lib/quote-actions.ts"
      to: "src/db/schema.ts"
      via: "quote insert and validation"
      pattern: "db.insert.*quotes"
---

<objective>
Implement the admin Quote Builder UI (`/admin/quotes/new`) with two-column form (left: inputs, right: live preview) and server actions for creating quotes. Admin selects client, offer, and overrides pricing; saves generates nanoid token and creates public link.

Purpose: Enable admins to compose quotes in 2-3 clicks, validate pricing server-side, and generate shareable public links for clients.

Output:
- `/admin/quotes/new` page with form and live preview
- Server action `createQuote()` validates and saves quote + quote_items to DB
- Generated public link `/quote/[token]` shareable via email (Phase 12)
- Quote saved as draft; state can be updated to "sent" when link shared
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/09-quote-builder-client-acceptance/09-RESEARCH.md
@.planning/phases/09-quote-builder-client-acceptance/09-01-SUMMARY.md

### Key Interfaces (from Phase 8 schema)

Quote Header:
```typescript
type Quote = {
  id: string;
  client_id: string | null;
  offer_micro_id: string;
  token: string;
  state: "draft" | "sent" | "viewed" | "accepted" | "rejected";
  accepted_total: string;
  accepted_at: Date | null;
  client_email: string | null;
  client_notes: string | null;
  created_at: Date;
  updated_at: Date;
};
```

Quote Item (line):
```typescript
type QuoteItem = {
  id: string;
  quote_id: string;
  offer_phase_id: string;
  service_id: string | null;
  quantity: string;
  unit_price: string;
  subtotal: string;
  custom_label: string | null;
};
```

OfferMicro (from Phase 5):
```typescript
type OfferMicro = {
  id: string;
  macro_id: string;
  internal_name: string;
  public_name: string;
  transformation_promise: string | null;
  duration_months: number;
};
```

OfferPhase (from Phase 8):
```typescript
type OfferPhase = {
  id: string;
  micro_id: string;
  title: string;
  description: string | null;
  sort_order: number;
  created_at: Date;
};
```
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create server action for quote creation (createQuote)</name>
  <files>src/lib/quote-actions.ts</files>
  <action>
Create a new server action file with the core quote creation logic. Key responsibilities:

- Validate input via Zod schema (client, offer, items array)
- Verify client and offer exist in database
- **Recalculate total server-side** by summing all quote_items subtotals (quantity × unit_price)
- Reject if client-submitted total doesn't match calculated total (0.01 tolerance for rounding)
- Generate nanoid(21) token — 21 characters, ~122-bit entropy
- Create atomic transaction: insert quote header, then insert quote_items
- Return success/error + public link `/quote/[token]`

Security per CLAUDE.md:
- Never trust client-side pricing calculation
- Server-side recalculation is THE security boundary
- If attacker modifies total before submit, server action rejects it
- Tokens are collision-free and unguessable via nanoid

Error handling: Localize error messages to Italian (client not found, offer not found, total mismatch).

Use existing patterns from quote-validators.ts where applicable; reference createQuoteSchema and quoteItemSchema for Zod integration.
  </action>
  <verify>
    <automated>npm run build 2>&1 | grep -c "error TS" | xargs test 0 -eq && echo "✓ Actions compile"</automated>
  </verify>
  <done>createQuote action handles validation, server-side total recalculation, and atomic quote+items creation. Error messages localized to Italian. Returns token and public link.</done>
</task>

<task type="auto">
  <name>Task 2: Create QuoteBuilderForm component (two-column layout)</name>
  <files>src/components/admin/quotes/QuoteBuilderForm.tsx, src/components/admin/quotes/OfferSelector.tsx, src/components/admin/quotes/PriceOverrideInput.tsx, src/components/admin/quotes/QuotePreview.tsx  <action>
Create four new component files that make up the two-column form UI:

**src/components/admin/quotes/QuoteBuilderForm.tsx** — Parent component managing form state:
- Uses React Hook Form + Zod for validation
- Manages three sections: client select, offer select, price overrides
- Left column: form inputs (client dropdown, offer dropdown, price fields)
- Right column: live preview (selected offer summary + calculated total)
- On form submit: call createQuote server action
- On success: display public link in a copiable text box + "Copy Link" button
- Handles form errors (display via Form/FormMessage from shadcn/ui)

**src/components/admin/quotes/OfferSelector.tsx** — Dropdown to select offer_micro:
- Fetches all offer_macros with their offer_micros (via server component query)
- Displays as grouped dropdown: "Entry Offer" > [Entry A, Entry B, Entry C]
- On selection change: trigger preview update in parent

**src/components/admin/quotes/PriceOverrideInput.tsx** — Input for final accepted_total:
- Single numeric input field
- On blur: parent calculates preview total and compares to this input
- Visual feedback: green checkmark if matches, red X if mismatch (but allow save — server validates)

**src/components/admin/quotes/QuotePreview.tsx** — Right column preview:
- Displays selected offer name, public name, transformation promise
- Lists phases (Discovery, Strategy, Execution)
- For each phase, lists the services that will be included
- Shows live calculated total as admin enters price overrides
- Displays estimated revenue and duration

All components use shadcn/ui (Form, Input, Select, Button, Card) for consistency with existing admin UI.
Use tailwind grid-cols-2 for two-column layout in QuoteBuilderForm.
  </action>
  <verify>
    <automated>npm run build 2>&1 | grep -c "error TS" | xargs test 0 -eq && echo "✓ Components compile"</automated>
  </verify>
  <done>Four component files created. Two-column form renders with client/offer selection, price input, and live preview. Form submission calls createQuote action.</done>
</task>

<task type="auto">
  <name>Task 3: Create /admin/quotes/new page and wire form</name>
  <files>src/app/admin/quotes/new/page.tsx, src/app/admin/quotes/new/actions.ts  <action>
Create the page file and optional actions helper:

**src/app/admin/quotes/new/page.tsx** — Page component:
- Server component that imports QuoteBuilderForm
- Renders page title "Crea Preventivo"
- Renders QuoteBuilderForm centered in a Card
- Wraps in AdminLayout (existing pattern from /admin/clients, /admin/projects)
- Page has Auth.js guard via middleware (existing /admin/* protection)

**src/app/admin/quotes/new/actions.ts** — Optional re-export of quote actions:
- Can be empty or re-export createQuote from lib/quote-actions.ts
- Keeps page-level action imports clean if needed by form

The form will be a client component ("use client") managing form state with React Hook Form.
  </action>
  <verify>
    <automated>curl -s http://localhost:3000/admin/quotes/new 2>&1 | grep -q "Crea Preventivo" && echo "✓ Page loads"</automated>
  </verify>
  <done>Page and form wired. Admin can navigate to /admin/quotes/new and see the two-column builder form.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Admin → Form Input | Form data can be manipulated before submit |
| Client Browser → Server | Pricing total can be modified in network inspector before sending |
| Server → Database | Quote must be immutable once accepted |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-09-01 | Tampering | Price total modified before submit | mitigate | Server-side recalculation in createQuote action; reject if mismatch between item sum and submitted total |
| T-09-02 | Tampering | Quote item quantity/unit_price tampered | mitigate | Zod schema validates numeric fields; server recalculates before saving |
| T-09-03 | Elevation | Non-admin access to /admin/quotes/new | mitigate | Auth.js middleware guards /admin/* routes (existing infrastructure) |
| T-09-04 | Information Disclosure | offer_micro details over-exposed | accept | Offer details are internal-facing; not visible to public (per Phase 9 Task 5) |

</threat_model>

<verification>
After Phase 9 Plan 2 execution:

1. Server action compiles: `npm run build` passes
2. Components compile: All four components render without errors
3. Page loads: `/admin/quotes/new` accessible to authenticated admin
4. Form submits: Clicking "Salva Preventivo" calls createQuote
5. Link generated: On success, public link displayed in copyable box
6. Server validation works: Manually modifying total in browser before submit should be rejected
7. Database: Quote + quote_items inserted to DB with correct structure
</verification>

<success_criteria>
- `/admin/quotes/new` page accessible and renders form
- Two-column layout (left: inputs, right: preview) visually distinct
- Form validates client and offer selection (required fields)
- Form submit calls createQuote server action
- Server action validates data and recalculates total server-side
- On success: public `/quote/[token]` link displayed and copyable
- Quote saved to DB with state="draft", token unique, accepted_total immutable
- On error: user-friendly error message displayed (Italian localization)
</success_criteria>

<output>
After execution, create `.planning/phases/09-quote-builder-client-acceptance/09-02-SUMMARY.md`
</output>
