From 8641253e85b54c192cab888cee192aa0ba04d0aa Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sun, 17 May 2026 11:46:11 +0200 Subject: [PATCH] docs(03-03): complete quote builder plan summary - SUMMARY.md for plan 03-03 (2/2 tasks complete) - Covers quote-actions.ts, QuoteTab.tsx, admin-queries extension, page.tsx wiring - Security verification: requireAdmin on all actions, 0 quote_items in client-view --- .../03-03-SUMMARY.md | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .planning/phases/03-service-catalog-quote-builder/03-03-SUMMARY.md diff --git a/.planning/phases/03-service-catalog-quote-builder/03-03-SUMMARY.md b/.planning/phases/03-service-catalog-quote-builder/03-03-SUMMARY.md new file mode 100644 index 0000000..c28af33 --- /dev/null +++ b/.planning/phases/03-service-catalog-quote-builder/03-03-SUMMARY.md @@ -0,0 +1,111 @@ +--- +phase: "03" +plan: "03" +subsystem: "quote-builder" +tags: [quote, admin, server-actions, drizzle, security] +dependency_graph: + requires: ["03-01"] + provides: ["quote-tab-ui", "quote-actions", "admin-quote-queries"] + affects: ["src/lib/admin-queries.ts", "src/app/admin/clients/[id]/page.tsx"] +tech_stack: + added: [] + patterns: ["Server Actions with requireAdmin guard", "useTransition for optimistic UI", "COALESCE SQL for label resolution", "leftJoin for optional catalog ref"] +key_files: + created: + - src/app/admin/clients/[id]/quote-actions.ts + - src/components/admin/tabs/QuoteTab.tsx + modified: + - src/lib/admin-queries.ts + - src/app/admin/clients/[id]/page.tsx +decisions: + - "QuoteTab is a Client Component (useTransition + useRouter) — actions called via startTransition, router.refresh() for revalidation" + - "updateAcceptedTotal in quote-actions.ts is separate from the one in actions.ts — scoped to quote tab, adds requireAdmin guard" + - "Service price pre-filled in catalog mode but editable — allows overriding price at quote time (snapshot semantics)" +metrics: + duration: "~15 min" + completed_date: "2026-05-17T09:45:11Z" + tasks_completed: 2 + tasks_total: 2 + files_created: 2 + files_modified: 2 +--- + +# Phase 03 Plan 03: Quote Builder Tab Summary + +**One-liner:** Admin quote builder tab with catalog dropdown, freeform toggle, items table with calculated total, and accepted_total editor — all backed by Zod-validated Server Actions with requireAdmin guard. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Server Actions + extend getClientFullDetail | db81829 | quote-actions.ts, admin-queries.ts | +| 2 | QuoteTab component + wire into client detail page | 48f81e7 | QuoteTab.tsx, page.tsx | + +## What Was Built + +**Task 1 — Server Actions + Query Layer** + +- `src/app/admin/clients/[id]/quote-actions.ts`: Three Server Actions exported: + - `addQuoteItem(clientId, formData)` — Zod validates service_id/custom_label, quantity, unit_price; computes subtotal; inserts into `quote_items` + - `removeQuoteItem(quoteItemId, clientId)` — deletes by item ID + - `updateAcceptedTotal(clientId, formData)` — writes to `clients.accepted_total` only (no payment row splitting — that stays in `actions.ts`) + - All three call `requireAdmin()` (getServerSession check) before any DB operation +- `src/lib/admin-queries.ts`: + - Added `QuoteItemWithLabel` type (COALESCE resolved label, snapshotted unit_price) + - Extended `ClientFullDetail` with `quoteItems: QuoteItemWithLabel[]` and `activeServices: ServiceCatalog[]` + - Added two queries in `getClientFullDetail()`: leftJoin for quote items with COALESCE label; active services ordered by name + - Security comment enforces that `client-view.ts` must never query `quote_items` (verified: 0 functional references) + +**Task 2 — UI Component + Page Wiring** + +- `src/components/admin/tabs/QuoteTab.tsx` (`"use client"`) — three sections: + - **Add items**: catalog dropdown (pre-fills unit_price on selection, editable) + freeform toggle (custom_label + price + qty) + - **Items table**: label, qty, unit_price, subtotal columns; "Rimuovi" button per row; "Totale calcolato" in bold footer + - **Accepted total**: editable numeric input with "Salva" button + helper text clarifying client sees only this value +- `src/app/admin/clients/[id]/page.tsx`: + - Import `QuoteTab` + - Destructure `quoteItems`, `activeServices` from `getClientFullDetail` result + - Added 5th `TabsTrigger value="quote"` with label "Preventivo" + - Added 5th `TabsContent value="quote"` rendering `` + +## Security Verification + +| Constraint | Status | +|------------|--------| +| T-03-03-01: requireAdmin on all Server Actions | Done — all three actions call `await requireAdmin()` first | +| T-03-03-02: Zod validation on formData numbers | Done — `quoteItemSchema` validates quantity + unit_price as `z.coerce.number().min(0.01)` | +| T-03-03-03: quote_items not in client-facing routes | Done — client-view.ts has 0 functional references to quote_items (only comments) | +| T-03-03-04: IDOR on removeQuoteItem | Mitigated by requireAdmin; future multi-admin scenario noted for future hardening | +| T-03-03-05: XSS in custom_label | Accepted — React JSX auto-escapes, no dangerouslySetInnerHTML used | +| T-03-03-06: calculated_total vs accepted_total confusion | Accepted — visual design enforces separation | + +## Deviations from Plan + +**1. [Rule 3 - Blocking] Build required DATABASE_URL env var not present in worktree** +- **Found during:** Task 2 build verification +- **Issue:** Worktree has no `.env.local`; build fails with "DATABASE_URL env var is required" at runtime collection phase +- **Fix:** Ran build with `DATABASE_URL=$(grep DATABASE_URL /path/.env.local ...)` from main repo — build passed clean +- **Impact:** None on code quality; worktree environment limitation only + +**2. [Rule 1 - Architecture] updateAcceptedTotal in quote-actions.ts does NOT update payment rows** +- **Found during:** Task 1 implementation +- **Rationale:** The existing `updateAcceptedTotal` in `actions.ts` splits the total 50/50 between payment rows. The quote tab version intentionally only writes to `clients.accepted_total` — this is the quote builder's domain. Payment row updates remain in the payments tab action. This preserves clean separation of concerns. + +## Known Stubs + +None — all three sections are fully wired to real Server Actions and real DB queries. + +## Threat Flags + +None — all new surface is admin-only, guarded by `requireAdmin()`, and consistent with the plan's threat model. + +## Self-Check: PASSED + +- `src/app/admin/clients/[id]/quote-actions.ts` exists and exports 3 Server Actions +- `src/components/admin/tabs/QuoteTab.tsx` exists and exports QuoteTab +- `src/lib/admin-queries.ts` modified with QuoteItemWithLabel type + quoteItems/activeServices in return +- `src/app/admin/clients/[id]/page.tsx` modified with Preventivo tab +- Commits db81829 and 48f81e7 verified in git log +- TypeScript: no errors +- Build: passes (with DATABASE_URL) +- client-view.ts: 0 functional references to quote_items