--- 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