From 2faf2fedf15514ba9530f5325d06fb85bb930609 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sat, 30 May 2026 16:23:15 +0200 Subject: [PATCH] =?UTF-8?q?docs(05-02):=20complete=20offer=20catalog=20adm?= =?UTF-8?q?in=20CRUD=20plan=20=E2=80=94=20SUMMARY.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RSC page at /admin/offers with macro/micro/service CRUD fully functional - ServiceCheckboxList for many-to-many service assignment - NavBar updated with Forecast and Offerte links --- .../phases/05-offer-system/05-02-SUMMARY.md | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .planning/phases/05-offer-system/05-02-SUMMARY.md diff --git a/.planning/phases/05-offer-system/05-02-SUMMARY.md b/.planning/phases/05-offer-system/05-02-SUMMARY.md new file mode 100644 index 0000000..cd821e1 --- /dev/null +++ b/.planning/phases/05-offer-system/05-02-SUMMARY.md @@ -0,0 +1,111 @@ +--- +plan_id: 05-02 +phase: 5 +plan: 2 +subsystem: admin-ui +tags: [offer-system, admin, crud, server-actions, rsc] +dependency_graph: + requires: [05-01] + provides: [offer-catalog-admin-ui, offer-queries, offer-actions, navbar-offers-link] + affects: [src/app/admin/offers/, src/lib/offer-queries.ts, src/components/admin/NavBar.tsx] +tech_stack: + added: [] + patterns: [Next.js RSC + server actions, useTransition + router.refresh pattern, requireAdmin guard, zod-form validation, delete+re-insert upsert for many-to-many] +key_files: + created: + - src/lib/offer-queries.ts + - src/app/admin/offers/actions.ts + - src/app/admin/offers/page.tsx + - src/components/admin/offers/ServiceCheckboxList.tsx + modified: + - src/components/admin/NavBar.tsx +decisions: + - "offer-queries.ts uses three sequential queries (macros → micros → assignments+cumulative) rather than a single join to keep the type shapes simple and avoid N+1" + - "ServiceCheckboxList uses useTransition + router.refresh() (Pattern 3) for optimistic checkbox state without full page reload" + - "updateMicroOfferServices uses delete+re-insert (upsert-replace) pattern for simplicity — atomic from client perspective since both ops run in the same server action under requireAdmin()" + - "NavBar order set to: Clienti | Progetti | Statistiche | Forecast | Catalogo | Offerte | Impostazioni" +metrics: + duration: "~5 minutes (files pre-existed from prior session; NavBar update + commit was main work)" + completed: "2026-05-30" + tasks_completed: 2 + files_modified: 5 +--- + +# Phase 5 Plan 2: Offer catalog admin CRUD — /admin/offers Summary + +**One-liner:** Full admin CRUD for macro-offers, micro-offers, and offer services at /admin/offers with a client-side checkbox list for service-to-micro assignments and NavBar updated with Forecast and Offerte links. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | offer-queries.ts + server actions (actions.ts) | 56f0849 | src/lib/offer-queries.ts, src/app/admin/offers/actions.ts | +| 2 | /admin/offers RSC page + NavBar update | ce8f95a | src/app/admin/offers/page.tsx, src/components/admin/offers/ServiceCheckboxList.tsx, src/components/admin/NavBar.tsx | + +## What Was Built + +**offer-queries.ts (Task 1):** +- `getCatalogWithMicros()` — fetches all macros, their child micros, assigned services per micro, and cumulative price per micro via three sequential DB queries +- `getAllOfferServices()` — returns active offer services ordered by name (used to populate checkbox lists) +- `getMicroAssignedServiceIds()` — returns assigned service IDs for a given micro (utility for future use) +- Types exported: `MicroWithServices`, `MacroWithMicros` + +**actions.ts (Task 1):** +- `createMacro(formData)` — Zod-validated, requireAdmin-guarded, inserts into offer_macros +- `deleteMacro(macroId)` — deletes macro (cascades to micros via FK) +- `createMicro(formData)` — Zod-validated, includes macro_id hidden field, duration_months coercion +- `deleteMicro(microId)` — deletes micro (cascades to offer_micro_services; project_offers restricted) +- `createOfferService(formData)` — Zod-validated, price stored as fixed(2) string +- `toggleOfferServiceActive(serviceId, active)` — toggles active flag +- `updateMicroOfferServices(microId, serviceIds[])` — delete+re-insert pattern for many-to-many assignment +- All 7 exported actions call `requireAdmin()` as first operation; all call `revalidatePath("/admin/offers")` + +**page.tsx (Task 2):** +- RSC page at /admin/offers, `export const revalidate = 0` +- Section 1: Macro-offers list + create form + per-macro delete button +- Section 2: Micro-offers nested inside each macro card + create form + ServiceCheckboxList per micro +- Section 3: Offer services list + create form + toggle active button +- Uses `Promise.all([getCatalogWithMicros(), getAllOfferServices()])` for parallel data fetching + +**ServiceCheckboxList.tsx (Task 2):** +- Client component (`"use client"`) +- `useState(new Set(assignedIds))` for local checkbox state +- `useTransition` + `await updateMicroOfferServices()` + `router.refresh()` for server sync +- Checkbox disabled during pending state to prevent double-submit + +**NavBar.tsx (Task 2):** +- Added Forecast link (`/admin/forecast`) after Statistiche +- Added Offerte link (`/admin/offers`) after Catalogo +- Final order: Clienti | Progetti | Statistiche | Forecast | Catalogo | Offerte | Impostazioni + +## Deviations from Plan + +**1. [Rule 3 - Pre-existing work] Task 1 files found already committed from a prior session** +- **Found during:** Task 1 start — `git status` showed actions.ts and offer-queries.ts as already tracked (committed in 56f0849) +- **Issue:** Files were created in a prior session but the plan was not marked complete +- **Fix:** Verified all acceptance criteria passed (tsc, requireAdmin count, revalidatePath count, function exports), then proceeded to Task 2 directly +- **Impact:** No code changes needed for Task 1; Task 2 NavBar update was the only missing piece + +None — plan executed correctly for Task 2 (NavBar update + page.tsx + ServiceCheckboxList committed as ce8f95a). + +## Known Stubs + +None — all three CRUD sections wire to live DB queries and server actions. No hardcoded data or placeholder text. + +## Threat Flags + +None — /admin/offers is under /admin/* protected by Auth.js middleware. All server actions call `requireAdmin()` as first operation. No new trust boundary surfaces introduced (T-05-03, T-05-04, T-05-05 mitigations are in place as specified in the threat register). + +## Self-Check: PASSED + +- src/lib/offer-queries.ts exists: FOUND (committed 56f0849) +- src/app/admin/offers/actions.ts exists: FOUND (committed 56f0849) +- src/app/admin/offers/page.tsx exists: FOUND (committed ce8f95a) +- src/components/admin/offers/ServiceCheckboxList.tsx exists: FOUND (committed ce8f95a) +- src/components/admin/NavBar.tsx updated: FOUND (committed ce8f95a) +- npx tsc --noEmit: PASSED (no errors) +- requireAdmin calls in actions.ts: 8 (all exported actions guarded) +- revalidatePath calls in actions.ts: 7 (every mutating action revalidates) +- getCatalogWithMicros | getAllOfferServices | getMicroAssignedServiceIds in offer-queries.ts: 3 functions +- /admin/offers in NavBar.tsx: FOUND +- /admin/forecast in NavBar.tsx: FOUND