--- 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" --- 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 @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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; }; ``` Task 1: Create server action for quote creation (createQuote) src/lib/quote-actions.ts 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. npm run build 2>&1 | grep -c "error TS" | xargs test 0 -eq && echo "✓ Actions compile" createQuote action handles validation, server-side total recalculation, and atomic quote+items creation. Error messages localized to Italian. Returns token and public link. Task 2: Create QuoteBuilderForm component (two-column layout) src/components/admin/quotes/QuoteBuilderForm.tsx, src/components/admin/quotes/OfferSelector.tsx, src/components/admin/quotes/PriceOverrideInput.tsx, src/components/admin/quotes/QuotePreview.tsx 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. npm run build 2>&1 | grep -c "error TS" | xargs test 0 -eq && echo "✓ Components compile" Four component files created. Two-column form renders with client/offer selection, price input, and live preview. Form submission calls createQuote action. Task 3: Create /admin/quotes/new page and wire form src/app/admin/quotes/new/page.tsx, src/app/admin/quotes/new/actions.ts 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. curl -s http://localhost:3000/admin/quotes/new 2>&1 | grep -q "Crea Preventivo" && echo "✓ Page loads" Page and form wired. Admin can navigate to /admin/quotes/new and see the two-column builder form. ## 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) | 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 - `/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) After execution, create `.planning/phases/09-quote-builder-client-acceptance/09-02-SUMMARY.md`