---
plan_id: 05-02
phase: 5
wave: 2
title: "Offer catalog admin CRUD — /admin/offers (macro + micro + services + multi-select assignment)"
type: execute
depends_on: [05-01]
files_modified:
  - src/app/admin/offers/page.tsx
  - src/app/admin/offers/actions.ts
  - src/lib/offer-queries.ts
  - src/components/admin/NavBar.tsx
requirements_addressed: [OFFER-01, OFFER-02, OFFER-03]
autonomous: true
must_haves:
  truths:
    - "Admin can create a macro-offer with internal_name, public_name, transformation_promise"
    - "Admin can create a micro-offer as a child of a macro, with internal_name, public_name, transformation_promise, duration_months"
    - "Admin can create offer services with name, price, transformation_description"
    - "Admin can assign services to a micro-offer via a checkbox list (many-to-many)"
    - "NavBar has an 'Offerte' link pointing to /admin/offers"
  artifacts:
    - path: "src/app/admin/offers/page.tsx"
      provides: "RSC page listing macros, their micros, and offer services"
      contains: "OfferMacroForm, OfferServiceForm"
    - path: "src/app/admin/offers/actions.ts"
      provides: "Server actions for all offer catalog mutations"
      contains: "createMacro, createMicro, createOfferService, updateMicroServices"
    - path: "src/lib/offer-queries.ts"
      provides: "Read queries for offer catalog"
      contains: "getCatalogWithMicros, getAllOfferServices"
    - path: "src/components/admin/NavBar.tsx"
      provides: "NavBar with Offerte link"
      contains: "/admin/offers"
  key_links:
    - from: "src/app/admin/offers/page.tsx"
      to: "src/lib/offer-queries.ts"
      via: "getCatalogWithMicros() server import"
      pattern: "getCatalogWithMicros"
    - from: "src/app/admin/offers/actions.ts"
      to: "src/db/schema.ts"
      via: "offer_macros, offer_micros, offer_services, offer_micro_services imports"
      pattern: "offer_macros|offer_micro_services"
---

<objective>
Build the full admin offer catalog at `/admin/offers`: create/list macro-offers, create/list micro-offers (children of macros), create/list offer services, and assign services to micro-offers via a checkbox list.

Purpose: This is the data entry point for the entire offer system. Without catalog data, Plans 03 and 04 have nothing to assign or display.
Output: `/admin/offers` page fully functional; server actions for all mutations; query layer in `offer-queries.ts`; NavBar updated.
</objective>

<execution_context>
@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/workflows/execute-plan.md
@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md
@/Users/simonecavalli/IAMCAVALLI/.planning/STATE.md
@/Users/simonecavalli/IAMCAVALLI/.planning/phases/05-offer-system/05-RESEARCH.md
@/Users/simonecavalli/IAMCAVALLI/.planning/phases/05-offer-system/05-01-SUMMARY.md

<interfaces>
<!-- From src/db/schema.ts (after 05-01): -->
```typescript
export const offer_macros = pgTable("offer_macros", {
  id: text("id").primaryKey().$defaultFn(() => nanoid()),
  internal_name: text("internal_name").notNull(),
  public_name: text("public_name").notNull(),
  transformation_promise: text("transformation_promise"),
  sort_order: integer("sort_order").notNull().default(0),
  created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});

export const offer_micros = pgTable("offer_micros", {
  id: text("id").primaryKey().$defaultFn(() => nanoid()),
  macro_id: text("macro_id").notNull().references(() => offer_macros.id, { onDelete: "cascade" }),
  internal_name: text("internal_name").notNull(),
  public_name: text("public_name").notNull(),
  transformation_promise: text("transformation_promise"),
  duration_months: integer("duration_months").notNull().default(1),
  sort_order: integer("sort_order").notNull().default(0),
});

export const offer_services = pgTable("offer_services", {
  id: text("id").primaryKey().$defaultFn(() => nanoid()),
  name: text("name").notNull(),
  price: numeric("price", { precision: 10, scale: 2 }).notNull(),
  transformation_description: text("transformation_description"),
  active: boolean("active").notNull().default(true),
});

export const offer_micro_services = pgTable("offer_micro_services", {
  micro_id: text("micro_id").notNull(),
  service_id: text("service_id").notNull(),
}, (t) => ({ pk: primaryKey({ columns: [t.micro_id, t.service_id] }) }));

export type OfferMacro = typeof offer_macros.$inferSelect;
export type OfferMicro = typeof offer_micros.$inferSelect;
export type OfferService = typeof offer_services.$inferSelect;
```

<!-- From src/app/admin/catalog/actions.ts (existing pattern to mirror): -->
```typescript
"use server";
async function requireAdmin() {
  const session = await getServerSession(authOptions);
  if (!session) throw new Error("Non autorizzato");
}
// All actions: call requireAdmin() first, then zod parse, then db mutation, then revalidatePath("/admin/offers")
```

<!-- From src/components/admin/NavBar.tsx (existing, to be extended): -->
```typescript
// Add between Catalogo and Impostazioni:
<Link href="/admin/offers" className="text-sm text-white/70 hover:text-white transition-colors">
  Offerte
</Link>
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: offer-queries.ts + server actions (actions.ts)</name>
  <files>
    src/lib/offer-queries.ts
    src/app/admin/offers/actions.ts
  </files>

  <read_first>
    - src/db/schema.ts — confirm column names for offer_macros, offer_micros, offer_services, offer_micro_services (after 05-01)
    - src/app/admin/catalog/actions.ts — copy requireAdmin() pattern, zod schema pattern, revalidatePath usage
    - src/lib/admin-queries.ts — copy import style (db, eq, inArray, asc, sql from drizzle-orm)
  </read_first>

  <action>
**Create `src/lib/offer-queries.ts`:**

```typescript
import { db } from "@/db";
import {
  offer_macros, offer_micros, offer_services, offer_micro_services,
} from "@/db/schema";
import type { OfferMacro, OfferMicro, OfferService } from "@/db/schema";
import { eq, asc, inArray, sql } from "drizzle-orm";

export type MicroWithServices = OfferMicro & {
  services: Array<{ id: string; name: string; price: string }>;
  cumulative_price: string;
};

export type MacroWithMicros = OfferMacro & {
  micros: MicroWithServices[];
};

export async function getCatalogWithMicros(): Promise<MacroWithMicros[]> {
  const macros = await db
    .select()
    .from(offer_macros)
    .orderBy(asc(offer_macros.sort_order), asc(offer_macros.created_at));

  if (macros.length === 0) return [];

  const macroIds = macros.map((m) => m.id);

  const micros = await db
    .select()
    .from(offer_micros)
    .where(inArray(offer_micros.macro_id, macroIds))
    .orderBy(asc(offer_micros.sort_order));

  const microIds = micros.map((m) => m.id);

  if (microIds.length === 0) {
    return macros.map((m) => ({ ...m, micros: [] }));
  }

  // Fetch junction rows + service data in one query
  const assignments = await db
    .select({
      micro_id: offer_micro_services.micro_id,
      service_id: offer_services.id,
      name: offer_services.name,
      price: offer_services.price,
    })
    .from(offer_micro_services)
    .innerJoin(offer_services, eq(offer_micro_services.service_id, offer_services.id))
    .where(inArray(offer_micro_services.micro_id, microIds));

  // Cumulative price per micro
  const cumulRows = await db
    .select({
      micro_id: offer_micro_services.micro_id,
      cumulative_price: sql<string>`coalesce(sum(${offer_services.price}::numeric), 0)`,
    })
    .from(offer_micro_services)
    .innerJoin(offer_services, eq(offer_micro_services.service_id, offer_services.id))
    .where(inArray(offer_micro_services.micro_id, microIds))
    .groupBy(offer_micro_services.micro_id);

  const cumulMap = new Map(cumulRows.map((r) => [r.micro_id, r.cumulative_price]));

  const microsWithServices: MicroWithServices[] = micros.map((micro) => ({
    ...micro,
    services: assignments
      .filter((a) => a.micro_id === micro.id)
      .map((a) => ({ id: a.service_id, name: a.name, price: String(a.price) })),
    cumulative_price: cumulMap.get(micro.id) ?? "0",
  }));

  return macros.map((macro) => ({
    ...macro,
    micros: microsWithServices.filter((m) => m.macro_id === macro.id),
  }));
}

export async function getAllOfferServices(): Promise<OfferService[]> {
  return db
    .select()
    .from(offer_services)
    .where(eq(offer_services.active, true))
    .orderBy(asc(offer_services.name));
}

// Returns assigned service IDs for a given micro-offer (used by ServiceCheckboxList)
export async function getMicroAssignedServiceIds(microId: string): Promise<string[]> {
  const rows = await db
    .select({ service_id: offer_micro_services.service_id })
    .from(offer_micro_services)
    .where(eq(offer_micro_services.micro_id, microId));
  return rows.map((r) => r.service_id);
}
```

**Create `src/app/admin/offers/actions.ts`:**

```typescript
"use server";

import { db } from "@/db";
import {
  offer_macros, offer_micros, offer_services, offer_micro_services,
} from "@/db/schema";
import { revalidatePath } from "next/cache";
import { eq, inArray } from "drizzle-orm";
import { z } from "zod";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

async function requireAdmin() {
  const session = await getServerSession(authOptions);
  if (!session) throw new Error("Non autorizzato");
}

// ── Macro-offer CRUD ─────────────────────────────────────────────────────────

const macroSchema = z.object({
  internal_name: z.string().min(1, "Nome interno richiesto"),
  public_name: z.string().min(1, "Nome pubblico richiesto"),
  transformation_promise: z.string().optional(),
});

export async function createMacro(formData: FormData) {
  await requireAdmin();
  const parsed = macroSchema.safeParse({
    internal_name: formData.get("internal_name"),
    public_name: formData.get("public_name"),
    transformation_promise: formData.get("transformation_promise") ?? "",
  });
  if (!parsed.success) throw new Error(parsed.error.issues[0].message);
  await db.insert(offer_macros).values({
    internal_name: parsed.data.internal_name,
    public_name: parsed.data.public_name,
    transformation_promise: parsed.data.transformation_promise || null,
  });
  revalidatePath("/admin/offers");
}

export async function deleteMacro(macroId: string) {
  await requireAdmin();
  await db.delete(offer_macros).where(eq(offer_macros.id, macroId));
  revalidatePath("/admin/offers");
}

// ── Micro-offer CRUD ─────────────────────────────────────────────────────────

const microSchema = z.object({
  macro_id: z.string().min(1, "Macro offerta richiesta"),
  internal_name: z.string().min(1, "Nome interno richiesto"),
  public_name: z.string().min(1, "Nome pubblico richiesto"),
  transformation_promise: z.string().optional(),
  duration_months: z.coerce.number().int().min(1, "Durata minima 1 mese"),
});

export async function createMicro(formData: FormData) {
  await requireAdmin();
  const parsed = microSchema.safeParse({
    macro_id: formData.get("macro_id"),
    internal_name: formData.get("internal_name"),
    public_name: formData.get("public_name"),
    transformation_promise: formData.get("transformation_promise") ?? "",
    duration_months: formData.get("duration_months"),
  });
  if (!parsed.success) throw new Error(parsed.error.issues[0].message);
  await db.insert(offer_micros).values({
    macro_id: parsed.data.macro_id,
    internal_name: parsed.data.internal_name,
    public_name: parsed.data.public_name,
    transformation_promise: parsed.data.transformation_promise || null,
    duration_months: parsed.data.duration_months,
  });
  revalidatePath("/admin/offers");
}

export async function deleteMicro(microId: string) {
  await requireAdmin();
  // offer_micro_services will cascade; project_offers will restrict (DB constraint)
  await db.delete(offer_micros).where(eq(offer_micros.id, microId));
  revalidatePath("/admin/offers");
}

// ── Offer service CRUD ───────────────────────────────────────────────────────

const offerServiceSchema = z.object({
  name: z.string().min(1, "Nome richiesto"),
  price: z.coerce.number().min(0, "Prezzo non può essere negativo"),
  transformation_description: z.string().optional(),
});

export async function createOfferService(formData: FormData) {
  await requireAdmin();
  const parsed = offerServiceSchema.safeParse({
    name: formData.get("name"),
    price: formData.get("price"),
    transformation_description: formData.get("transformation_description") ?? "",
  });
  if (!parsed.success) throw new Error(parsed.error.issues[0].message);
  await db.insert(offer_services).values({
    name: parsed.data.name,
    price: parsed.data.price.toFixed(2),
    transformation_description: parsed.data.transformation_description || null,
  });
  revalidatePath("/admin/offers");
}

export async function toggleOfferServiceActive(serviceId: string, active: boolean) {
  await requireAdmin();
  await db.update(offer_services).set({ active }).where(eq(offer_services.id, serviceId));
  revalidatePath("/admin/offers");
}

// ── Multi-select service assignment ─────────────────────────────────────────

// Replaces all assignments for a micro with the provided serviceIds (upsert-replace pattern)
export async function updateMicroOfferServices(microId: string, serviceIds: string[]) {
  await requireAdmin();
  // Delete existing assignments for this micro
  await db.delete(offer_micro_services).where(eq(offer_micro_services.micro_id, microId));
  // Re-insert with new set (empty array = unassign all)
  if (serviceIds.length > 0) {
    await db.insert(offer_micro_services).values(
      serviceIds.map((serviceId) => ({ micro_id: microId, service_id: serviceId }))
    );
  }
  revalidatePath("/admin/offers");
}
```
  </action>

  <verify>
    <automated>cd /Users/simonecavalli/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20</automated>
  </verify>

  <acceptance_criteria>
    - `npx tsc --noEmit` exits 0
    - `grep -c "requireAdmin" src/app/admin/offers/actions.ts` returns 5 or more (every exported action calls it)
    - `grep "revalidatePath" src/app/admin/offers/actions.ts` returns at least 5 matches (every mutating action revalidates)
    - `grep "getCatalogWithMicros\|getAllOfferServices\|getMicroAssignedServiceIds" src/lib/offer-queries.ts` returns 3 matches
  </acceptance_criteria>

  <done>offer-queries.ts and actions.ts created; TypeScript compiles; every action guards with requireAdmin()</done>
</task>

<task type="auto">
  <name>Task 2: /admin/offers RSC page + NavBar update</name>
  <files>
    src/app/admin/offers/page.tsx
    src/components/admin/NavBar.tsx
  </files>

  <read_first>
    - src/app/admin/catalog/page.tsx — read for the admin page structure pattern (RSC, revalidate = 0, form patterns)
    - src/components/admin/NavBar.tsx — read full file before editing (must preserve all existing links)
    - src/lib/offer-queries.ts — confirm getCatalogWithMicros and getAllOfferServices signatures (just created in Task 1)
    - src/app/admin/offers/actions.ts — confirm action names (just created in Task 1)
  </read_first>

  <action>
**Create `src/app/admin/offers/page.tsx`:**

This is an RSC page. No `"use client"` at the top level. Inline client sub-components that need interactivity are permissible as separate files or as local `"use client"` components in the same file using the pattern established by existing admin pages.

The page has three sections:
1. **Macro-offers** — list with "create" form inline; each macro shows its micro-offer children
2. **Micro-offers** per macro — create form inside each macro card; each micro shows assigned services and a `ServiceCheckboxList`
3. **Offer Services catalog** — list all offer services + create form at bottom

For `ServiceCheckboxList`: this is a `"use client"` component. Create it as a named export in the same page file or as a separate file at `src/components/admin/offers/ServiceCheckboxList.tsx`. It uses `useState(Set<string>)` + `useTransition` + `router.refresh()` pattern from the research (Pattern 3).

```typescript
// src/components/admin/offers/ServiceCheckboxList.tsx
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { updateMicroOfferServices } from "@/app/admin/offers/actions";

export function ServiceCheckboxList({
  allServices,
  assignedIds,
  microId,
}: {
  allServices: Array<{ id: string; name: string; price: string }>;
  assignedIds: string[];
  microId: string;
}) {
  const [selected, setSelected] = useState(new Set(assignedIds));
  const [isPending, startTransition] = useTransition();
  const router = useRouter();

  function toggle(serviceId: string) {
    const next = new Set(selected);
    if (next.has(serviceId)) next.delete(serviceId);
    else next.add(serviceId);
    setSelected(next);
    startTransition(async () => {
      await updateMicroOfferServices(microId, [...next]);
      router.refresh();
    });
  }

  return (
    <div className="space-y-1">
      {allServices.map((svc) => (
        <label key={svc.id} className="flex items-center gap-2 cursor-pointer text-sm">
          <input
            type="checkbox"
            checked={selected.has(svc.id)}
            onChange={() => toggle(svc.id)}
            disabled={isPending}
            className="rounded"
          />
          <span>{svc.name}</span>
          <span className="ml-auto text-xs text-[#71717a]">€{parseFloat(svc.price).toFixed(2)}</span>
        </label>
      ))}
      {allServices.length === 0 && (
        <p className="text-xs text-[#71717a]">Nessun servizio nel catalogo ancora.</p>
      )}
    </div>
  );
}
```

**Page structure for `src/app/admin/offers/page.tsx`:**

```typescript
import { getCatalogWithMicros, getAllOfferServices } from "@/lib/offer-queries";
import { createMacro, createMicro, createOfferService, deleteMacro, deleteMicro, toggleOfferServiceActive } from "./actions";
import { ServiceCheckboxList } from "@/components/admin/offers/ServiceCheckboxList";

export const revalidate = 0;

export default async function OffersPage() {
  const [catalog, allServices] = await Promise.all([
    getCatalogWithMicros(),
    getAllOfferServices(),
  ]);

  return (
    <div className="max-w-5xl mx-auto py-8 px-4 space-y-10">
      <h1 className="text-2xl font-bold text-[#1a1a1a]">Catalogo Offerte</h1>

      {/* ── Sezione macro-offerte ── */}
      <section>
        <h2 className="text-lg font-semibold text-[#1a1a1a] mb-4">Macro-Offerte</h2>

        {/* Create macro form */}
        <form action={createMacro} className="bg-white rounded-lg border border-[#e5e7eb] p-4 mb-6 space-y-3 max-w-lg">
          <p className="text-sm font-medium">Nuova Macro-Offerta</p>
          <input name="internal_name" placeholder="Nome interno (es. Entry Offer)" required
            className="w-full border rounded px-3 py-1.5 text-sm" />
          <input name="public_name" placeholder="Nome pubblico (es. Starter Branding)"
            required className="w-full border rounded px-3 py-1.5 text-sm" />
          <textarea name="transformation_promise" placeholder="Promessa di trasformazione"
            rows={2} className="w-full border rounded px-3 py-1.5 text-sm" />
          <button type="submit"
            className="bg-[#1A463C] text-white text-sm px-4 py-1.5 rounded hover:bg-[#163a31]">
            Aggiungi Macro
          </button>
        </form>

        {/* List macros */}
        <div className="space-y-8">
          {catalog.map((macro) => (
            <div key={macro.id} className="bg-white rounded-lg border border-[#e5e7eb] p-6">
              <div className="flex items-start justify-between mb-1">
                <div>
                  <p className="font-semibold text-[#1a1a1a]">{macro.internal_name}</p>
                  <p className="text-sm text-[#71717a]">Pubblico: {macro.public_name}</p>
                  {macro.transformation_promise && (
                    <p className="text-xs text-[#71717a] mt-1 italic">{macro.transformation_promise}</p>
                  )}
                </div>
                <form action={deleteMacro.bind(null, macro.id)}>
                  <button type="submit" className="text-xs text-red-600 hover:underline">Elimina</button>
                </form>
              </div>

              {/* Micro-offers under this macro */}
              <div className="mt-4 space-y-4 pl-4 border-l-2 border-[#e5e7eb]">
                <p className="text-xs font-semibold text-[#71717a] uppercase tracking-wider">Micro-Offerte</p>

                {macro.micros.map((micro) => (
                  <div key={micro.id} className="bg-[#f9f9f9] rounded-lg p-4 space-y-3">
                    <div className="flex items-start justify-between">
                      <div>
                        <p className="text-sm font-medium">{micro.internal_name}</p>
                        <p className="text-xs text-[#71717a]">Pubblico: {micro.public_name} · {micro.duration_months} {micro.duration_months === 1 ? "mese" : "mesi"}</p>
                        {micro.transformation_promise && (
                          <p className="text-xs text-[#71717a] italic">{micro.transformation_promise}</p>
                        )}
                        <p className="text-xs text-[#1a1a1a] mt-1 font-medium">
                          Prezzo cumulativo: €{parseFloat(micro.cumulative_price).toFixed(2)}
                        </p>
                      </div>
                      <form action={deleteMicro.bind(null, micro.id)}>
                        <button type="submit" className="text-xs text-red-600 hover:underline">Elimina</button>
                      </form>
                    </div>
                    {/* Service assignment checkbox list */}
                    <div>
                      <p className="text-xs font-medium text-[#71717a] mb-2">Servizi inclusi:</p>
                      <ServiceCheckboxList
                        allServices={allServices.map((s) => ({ id: s.id, name: s.name, price: String(s.price) }))}
                        assignedIds={micro.services.map((s) => s.id)}
                        microId={micro.id}
                      />
                    </div>
                  </div>
                ))}

                {/* Create micro form */}
                <form action={createMicro} className="bg-white rounded border border-[#e5e7eb] p-3 space-y-2">
                  <input type="hidden" name="macro_id" value={macro.id} />
                  <p className="text-xs font-medium">Nuova Micro-Offerta</p>
                  <input name="internal_name" placeholder="Nome interno" required
                    className="w-full border rounded px-2 py-1 text-xs" />
                  <input name="public_name" placeholder="Nome pubblico" required
                    className="w-full border rounded px-2 py-1 text-xs" />
                  <textarea name="transformation_promise" placeholder="Promessa di trasformazione"
                    rows={2} className="w-full border rounded px-2 py-1 text-xs" />
                  <div className="flex items-center gap-2">
                    <label className="text-xs">Durata (mesi):</label>
                    <input name="duration_months" type="number" min="1" defaultValue="1"
                      required className="w-16 border rounded px-2 py-1 text-xs" />
                  </div>
                  <button type="submit"
                    className="bg-[#1A463C] text-white text-xs px-3 py-1 rounded hover:bg-[#163a31]">
                    Aggiungi Micro
                  </button>
                </form>
              </div>
            </div>
          ))}

          {catalog.length === 0 && (
            <p className="text-sm text-[#71717a]">Nessuna macro-offerta ancora. Creane una sopra.</p>
          )}
        </div>
      </section>

      {/* ── Sezione servizi offerta ── */}
      <section>
        <h2 className="text-lg font-semibold text-[#1a1a1a] mb-4">Servizi Offerta</h2>
        <p className="text-sm text-[#71717a] mb-4">
          I servizi qui sono diversi dal catalogo preventivi — hanno una descrizione della trasformazione e vengono raggruppati nelle micro-offerte.
        </p>

        {/* List services */}
        <div className="bg-white rounded-lg border border-[#e5e7eb] divide-y divide-[#e5e7eb] mb-6">
          {allServices.map((svc) => (
            <div key={svc.id} className="flex items-center justify-between px-4 py-3">
              <div>
                <p className="text-sm font-medium">{svc.name}</p>
                {svc.transformation_description && (
                  <p className="text-xs text-[#71717a]">{svc.transformation_description}</p>
                )}
              </div>
              <div className="flex items-center gap-4">
                <span className="text-sm font-mono">€{parseFloat(String(svc.price)).toFixed(2)}</span>
                <form action={toggleOfferServiceActive.bind(null, svc.id, !svc.active)}>
                  <button type="submit" className="text-xs text-[#71717a] hover:text-[#1a1a1a]">
                    {svc.active ? "Disattiva" : "Attiva"}
                  </button>
                </form>
              </div>
            </div>
          ))}
          {allServices.length === 0 && (
            <p className="px-4 py-3 text-sm text-[#71717a]">Nessun servizio ancora.</p>
          )}
        </div>

        {/* Create service form */}
        <form action={createOfferService}
          className="bg-white rounded-lg border border-[#e5e7eb] p-4 space-y-3 max-w-lg">
          <p className="text-sm font-medium">Nuovo Servizio Offerta</p>
          <input name="name" placeholder="Nome servizio" required
            className="w-full border rounded px-3 py-1.5 text-sm" />
          <input name="price" type="number" step="0.01" min="0" placeholder="Prezzo (€)" required
            className="w-full border rounded px-3 py-1.5 text-sm" />
          <textarea name="transformation_description" placeholder="Descrizione trasformazione (marketing)"
            rows={2} className="w-full border rounded px-3 py-1.5 text-sm" />
          <button type="submit"
            className="bg-[#1A463C] text-white text-sm px-4 py-1.5 rounded hover:bg-[#163a31]">
            Aggiungi Servizio
          </button>
        </form>
      </section>
    </div>
  );
}
```

**Update `src/components/admin/NavBar.tsx`:**

Read the full file first. Add the "Offerte" link between the "Catalogo" link and the "Impostazioni" link. Also add a "Forecast" link between "Statistiche" and "Catalogo":

```tsx
// After the Statistiche link:
<Link href="/admin/forecast" className="text-sm text-white/70 hover:text-white transition-colors">
  Forecast
</Link>
// After the Catalogo link (before Impostazioni):
<Link href="/admin/offers" className="text-sm text-white/70 hover:text-white transition-colors">
  Offerte
</Link>
```

Final NavBar order: Clienti | Progetti | Statistiche | Forecast | Catalogo | Offerte | Impostazioni
  </action>

  <verify>
    <automated>cd /Users/simonecavalli/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20</automated>
  </verify>

  <acceptance_criteria>
    - `npx tsc --noEmit` exits 0
    - `grep "/admin/offers" src/components/admin/NavBar.tsx` matches (NavBar has Offerte link)
    - `grep "/admin/forecast" src/components/admin/NavBar.tsx` matches (NavBar has Forecast link)
    - `grep "ServiceCheckboxList" src/app/admin/offers/page.tsx` matches
    - `grep "getCatalogWithMicros\|getAllOfferServices" src/app/admin/offers/page.tsx` returns 2 matches
    - `grep "updateMicroOfferServices" src/components/admin/offers/ServiceCheckboxList.tsx` matches
    - File `src/app/admin/offers/page.tsx` exists
    - File `src/components/admin/offers/ServiceCheckboxList.tsx` exists
  </acceptance_criteria>

  <done>/admin/offers page renders macro-offers, micro-offers, services; ServiceCheckboxList multi-select works; NavBar has Offerte and Forecast links; TypeScript compiles</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Browser → Server Actions | Admin form submissions reach offer-actions.ts |
| Admin session → Server Actions | Only authenticated admin can mutate offer data |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-05-03 | Elevation of Privilege | actions.ts — all exported functions | mitigate | `requireAdmin()` as first call in every exported server action; throws if session absent |
| T-05-04 | Tampering | updateMicroOfferServices — delete+re-insert pattern | mitigate | Both operations run within the same server action under requireAdmin(); atomic from the client perspective |
| T-05-05 | Information Disclosure | /admin/offers page | accept | Page is under /admin/* — Auth.js session guard in middleware protects the route; no extra exposure |
</threat_model>

<verification>
After both tasks complete:
- `npx tsc --noEmit` exits 0
- Visit `/admin/offers` → page loads without error
- Create a macro-offer → appears in the list
- Create a micro-offer under the macro → appears nested
- Create an offer service → appears in services list
- Toggle a checkbox for a service on a micro → assignment persists on refresh
</verification>

<success_criteria>
1. Admin can create macro-offers with all three fields (internal_name, public_name, transformation_promise)
2. Admin can create micro-offers with all fields including duration_months
3. Admin can create offer services (separate from service_catalog)
4. Checkbox list correctly assigns/unassigns services to micro-offers
5. NavBar shows "Offerte" and "Forecast" links
</success_criteria>

<output>
After completion, create `.planning/phases/05-offer-system/05-02-SUMMARY.md` using the summary template.
</output>
