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