- Archive v2.0 phases (07-10) under .planning/milestones/ - v2.1 REQUIREMENTS/ROADMAP (Phases 11-17: Offer Studio + Proposal AI) - DESIGN-SYSTEM.md (database-view UI contract for Phases 11-14) - Phase 11 CONTEXT, DISCUSSION-LOG, PATTERNS Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09 | 02 | execute | 1 |
|
|
true |
|
|
Purpose: Enable admins to compose quotes in 2-3 clicks, validate pricing server-side, and generate shareable public links for clients.
Output:
/admin/quotes/newpage 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
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/phases/09-quote-builder-client-acceptance/09-RESEARCH.md @.planning/phases/09-quote-builder-client-acceptance/09-01-SUMMARY.mdKey Interfaces (from Phase 8 schema)
Quote Header:
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):
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):
type OfferMicro = {
id: string;
macro_id: string;
internal_name: string;
public_name: string;
transformation_promise: string | null;
duration_months: number;
};
OfferPhase (from Phase 8):
type OfferPhase = {
id: string;
micro_id: string;
title: string;
description: string | null;
sort_order: number;
created_at: Date;
};
- 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.
<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>
After Phase 9 Plan 2 execution:- Server action compiles:
npm run buildpasses - Components compile: All four components render without errors
- Page loads:
/admin/quotes/newaccessible to authenticated admin - Form submits: Clicking "Salva Preventivo" calls createQuote
- Link generated: On success, public link displayed in copyable box
- Server validation works: Manually modifying total in browser before submit should be rejected
- Database: Quote + quote_items inserted to DB with correct structure
<success_criteria>
/admin/quotes/newpage 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>