---
phase: 12
plan: 04
type: execute
wave: 4
depends_on: [1, 3]
files_modified:
  - src/app/admin/offers/page.tsx
  - src/components/admin/offers/OfferListClient.tsx
autonomous: true
requirements: [OFFER-18]

must_haves:
  truths:
    - "Admin sees a card grid of all offers, each showing internal name, short description, and category chip"
    - "Admin can filter the card grid by category (Tutti / Entry Offer / Signature Offer / Retainer Offer, derived dynamically from existing category values)"
    - "Admin can toggle 'Mostra offerte archiviate' to show/hide archived offers (hidden by default)"
    - "Admin can create a new offer via '+ Nuova Offerta', which creates an offer_macros row and the new card appears in the grid"
    - "Each card links to the Plan 05 editor route /admin/offers/[id]/edit"
  artifacts:
    - path: "src/app/admin/offers/page.tsx"
      provides: "Server component: fetches getOfferListCards() + getOfferFieldOptions(), renders OfferListClient"
    - path: "src/components/admin/offers/OfferListClient.tsx"
      provides: "Client component: category filter chips, archive toggle, card grid, '+ Nuova Offerta' button calling createOfferMacro"
      exports: ["OfferListClient"]
  key_links:
    - from: "src/components/admin/offers/OfferListClient.tsx"
      to: "/admin/offers/[id]/edit"
      via: "next/link Link per card"
      pattern: "admin/offers/\\$\\{.*\\}/edit"
    - from: "src/components/admin/offers/OfferListClient.tsx (+ Nuova Offerta)"
      to: "createOfferMacro server action (Plan 03)"
      via: "useTransition + router.refresh on success, new card appears in grid"
      pattern: "createOfferMacro"
---

<objective>
Redesign `/admin/offers` (currently a flat macro/micro CRUD list — the legacy page) into the
Phase 12 UI-SPEC card-grid list: filterable by category chips, archive toggle, "+ Nuova
Offerta" CTA, each card linking to the new per-offer editor route (`/admin/offers/[id]/edit`,
built in Plan 05).

Purpose: Deliver OFFER-18 (category-filterable, archive-aware offer list) per the locked
UI-SPEC (`12-UI-SPEC.md` section 1) and D-1 (full editor list+detail, CSV import deferred).
Output: `src/app/admin/offers/page.tsx` rewritten as the new list page;
`src/components/admin/offers/OfferListClient.tsx` (new) implementing filter/grid/CTA logic.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/12-offer-composition-drag-drop-csv-import/12-UI-SPEC.md
@src/app/admin/catalog/CatalogSearch.tsx
@src/components/admin/AdminSidebar.tsx
</context>

<interfaces>
<!-- From Plan 03 (src/lib/offer-queries.ts) — use directly, no further exploration needed. -->

```typescript
export type OfferListCard = Pick<OfferMacro, "id" | "internal_name" | "description" | "category" | "is_archived">;
export async function getOfferListCards(): Promise<OfferListCard[]>;

export type OfferFieldOptions = { categoria: string[]; ticket: string[]; tipo: string[]; obiettivo: string[] };
export async function getOfferFieldOptions(): Promise<OfferFieldOptions>;
```

<!-- From Plan 03 (src/app/admin/offers/actions.ts) — use directly. -->

```typescript
// Creates a new offer_macros row from FormData { internal_name, public_name?, description?, category? }
// public_name defaults to internal_name if omitted. Returns void (revalidates /admin/offers).
// NOTE: createOfferMacro does NOT return the new row's id (FormData server actions can't
// easily return values to client callers in the <form action={fn}> pattern). For the
// "+ Nuova Offerta" flow, Task 2 calls createOfferMacro via startTransition + FormData,
// then router.refresh() — the new card appears in the grid; admin clicks it to open the
// editor (Plan 05). No need to extend Plan 03's contract.
export async function createOfferMacro(formData: FormData): Promise<void>;
```

<!-- From src/app/admin/catalog/CatalogSearch.tsx — reference pattern for a client component
wrapping a server-fetched list with client-side filter state (search/category chips). Read
this file's structure (useState + useMemo filter, "use client") before writing OfferListClient. -->
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Rewrite /admin/offers page.tsx as the new list page entry point</name>
  <files>src/app/admin/offers/page.tsx</files>
  <read_first>
    - src/app/admin/offers/page.tsx (current — full file, ~239 lines, the legacy macro/micro
      CRUD page being replaced)
    - src/app/admin/catalog/page.tsx (pattern: thin server component, `Promise.all` fetch,
      delegates to a client component for interactivity)
    - .planning/phases/12-offer-composition-drag-drop-csv-import/12-UI-SPEC.md (section 1:
      Offer List Page layout, header, filter row, card grid, empty state, copywriting
      contract table)
  </read_first>
  <action>
    Replace the entire contents of `src/app/admin/offers/page.tsx` with a thin server
    component matching the `catalog/page.tsx` pattern:

    ```typescript
    import { getOfferListCards, getOfferFieldOptions } from "@/lib/offer-queries";
    import { OfferListClient } from "@/components/admin/offers/OfferListClient";

    export const revalidate = 0;

    export default async function OffersPage() {
      const [cards, options] = await Promise.all([
        getOfferListCards(),
        getOfferFieldOptions(),
      ]);

      return (
        <div>
          <OfferListClient cards={cards} categoryOptions={options.categoria} />
        </div>
      );
    }
    ```

    The legacy macro/micro/offer_services CRUD UI previously on this page (forms for
    `createMacro`/`createMicro`/`createOfferService`, `ServiceCheckboxList`, etc.) is
    REMOVED from this route per D-1 (full editor redesign) and the UI-SPEC (section 1 — no
    legacy CRUD forms in the new list page). The underlying `offer_macros`/`offer_micros`
    legacy actions (`createMacro`, `deleteMacro`, `createMicro`, `deleteMicro`,
    `updateMicroOfferServices`) remain exported from `actions.ts` (Plan 03 did not remove
    them) but are no longer referenced from any page — this is intentional dead-export
    cleanup deferred to a future phase if needed (do NOT delete the exports now, only
    routes/usages, to avoid breaking `npx tsc --noEmit` if anything else imports them; grep
    confirms nothing else does, but leaving the exports is zero-risk and out of this plan's
    scope to remove).
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/Vault/IAMCAVALLI && npx tsc --noEmit 2>&1 | tail -20</automated>
  </verify>
  <acceptance_criteria>
    - `src/app/admin/offers/page.tsx` is a server component importing `getOfferListCards`, `getOfferFieldOptions`, and `OfferListClient`
    - `grep -c "ServiceCheckboxList\|createMacro\|createMicro\|createOfferService" src/app/admin/offers/page.tsx` == 0 (legacy forms removed from this page)
    - `npx tsc --noEmit` passes (will fail until Task 2 creates `OfferListClient` — run combined verification after both tasks)
  </acceptance_criteria>
  <done>page.tsx is a thin server component delegating to OfferListClient with the new data shape.</done>
</task>

<task type="auto">
  <name>Task 2: Build OfferListClient — category filter, archive toggle, card grid, create CTA</name>
  <files>src/components/admin/offers/OfferListClient.tsx</files>
  <read_first>
    - src/app/admin/catalog/CatalogSearch.tsx (client component pattern: "use client",
      useState filter state, useMemo derived list, search input)
    - src/components/ui/button.tsx, src/components/ui/badge.tsx, src/components/ui/input.tsx
      (exported component signatures — use these for CTA button, category chips/badges,
      "+ Nuova Offerta" form input)
    - .planning/phases/12-offer-composition-drag-drop-csv-import/12-UI-SPEC.md (section 1:
      exact layout ASCII diagram, color tokens #1A463C/#DEF168/#dc2626/#71717a/#e5e7eb,
      card structure, empty state copy, filter chip active/inactive styling, copywriting
      contract table for all literal strings)
  </read_first>
  <action>
    Create `src/components/admin/offers/OfferListClient.tsx`:

    ```typescript
    "use client";
    import { useState, useMemo, useTransition } from "react";
    import { useRouter } from "next/navigation";
    import Link from "next/link";
    import { createOfferMacro } from "@/app/admin/offers/actions";
    import type { OfferListCard } from "@/lib/offer-queries";

    export function OfferListClient({
      cards,
      categoryOptions,
    }: {
      cards: OfferListCard[];
      categoryOptions: string[];
    }) {
      // ... implementation below
    }
    ```

    Implementation requirements (per UI-SPEC section 1 + copywriting contract table):

    1. **State**: `activeCategory: string | null` (null = "Tutti"), `showArchived: boolean`
       (default `false`), `showCreateForm: boolean` + a controlled text input for the new
       offer's `internal_name`.

    2. **Filter row**:
       - Chip row: "Tutti" + one chip per `categoryOptions` value. Active chip: border
         `#1A463C`, text `#1A463C`. Inactive: border `#e5e7eb`, text `#71717a`. 8px gap
         (`gap-2`).
       - "Mostra offerte archiviate" checkbox + label (`#71717a` text), toggles
         `showArchived`.

    3. **Filtered list** (`useMemo`): `cards.filter(c => (activeCategory === null ||
       c.category === activeCategory) && (showArchived || !c.is_archived))`.

    4. **Card grid**: `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4`. Each card:
       - `Link href={`/admin/offers/${card.id}/edit`}` wrapping a `div` with white bg,
         `border border-[#e5e7eb] rounded-lg p-4 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)]
         cursor-pointer transition-shadow`.
       - `internal_name` as h3 (16px/600).
       - `description` (if present) as body text, `line-clamp-2`, `#71717a`.
       - Category badge: small chip showing `category` (if present) — reuse
         `src/components/ui/badge.tsx` `<Badge>` or a styled `<span>`.
       - If `is_archived`: red "Archiviata" badge (text `#dc2626`).

    5. **Empty state**: if `filteredCards.length === 0`, show centered block: heading
       "Nessuna offerta" (h3), body "Inizia creando la tua prima offerta" (`#71717a`), and
       the "+ Nuova Offerta" CTA (same as header).

    6. **"+ Nuova Offerta" CTA** (top-right of page header, "Offerte" h2 title to its left):
       - Clicking "+ Nuova Offerta" toggles `showCreateForm`, revealing a small inline
         `<form>` with one text `<input name="internal_name" required placeholder="Nome
         offerta...">` and a submit button "Crea".
       - On submit: `startTransition(async () => { await createOfferMacro(formData);
         setShowCreateForm(false); router.refresh(); })`. After `router.refresh()`, the new
         card appears in the grid — do NOT attempt to auto-navigate to the editor in this
         plan (acceptable per UI-SPEC: admin clicks the new card to open the editor). Button
         style: bg `#1A463C`, text white, per UI-SPEC.

    7. **Page header**: "Offerte" (h2, 20px/600, `#1a1a1a`) + "+ Nuova Offerta" button,
       `flex items-center justify-between`.

    All literal copy strings come from the UI-SPEC copywriting contract table verbatim
    (Italian): "Offerte", "+ Nuova Offerta", "Mostra offerte archiviate", "Tutti",
    "Nessuna offerta", "Inizia creando la tua prima offerta", "Archiviata", "Crea", "Nome
    offerta...".
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/Vault/IAMCAVALLI && npx tsc --noEmit 2>&1 | tail -30 && npx eslint src/app/admin/offers/page.tsx src/components/admin/offers/OfferListClient.tsx 2>&1 | tail -30</automated>
  </verify>
  <acceptance_criteria>
    - `npx tsc --noEmit` passes with zero errors (page.tsx + OfferListClient.tsx + rest of project)
    - `npx eslint src/app/admin/offers/page.tsx src/components/admin/offers/OfferListClient.tsx` reports zero errors
    - `grep -c "OfferListCard" src/components/admin/offers/OfferListClient.tsx` >= 1
    - `grep -c "createOfferMacro" src/components/admin/offers/OfferListClient.tsx` >= 1
    - `grep -c "admin/offers/\${" src/components/admin/offers/OfferListClient.tsx` >= 1 (card -> editor link)
    - `grep -c "Mostra offerte archiviate\|Nessuna offerta\|Nuova Offerta" src/components/admin/offers/OfferListClient.tsx` >= 3 (UI-SPEC copy present)
  </acceptance_criteria>
  <done>OfferListClient renders the category-filterable, archive-aware card grid with working "+ Nuova Offerta" creation; page.tsx + OfferListClient.tsx typecheck and lint clean.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Admin browser -> createOfferMacro (via "+ Nuova Offerta") | Free-text `internal_name` input crosses into a new `offer_macros` row |
| Admin browser -> /admin/offers/[id]/edit links | Card `id` values are server-fetched (not user input) — links are safe by construction |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-12-10 | Tampering | "+ Nuova Offerta" inline form | accept | `createOfferMacro` (Plan 03) already calls `requireAdmin()` + Zod-validates `internal_name` non-empty; this plan only adds the UI trigger, no new validation surface |
| T-12-11 | Information Disclosure | Offer list exposes `description`/`category`/`is_archived` for all offers | accept | Route is under `/admin/*`, Auth.js session-gated middleware (existing project-wide constraint); no change to access control in this plan |
</threat_model>

<verification>
1. `npx tsc --noEmit` passes for the full project.
2. `npx eslint` clean for both modified/created files.
3. `/admin/offers` renders a card grid sourced from `getOfferListCards()`, with category filter chips from `getOfferFieldOptions().categoria` and an archive toggle (default off).
4. "+ Nuova Offerta" creates a new `offer_macros` row via `createOfferMacro` and the new card appears after `router.refresh()`.
5. Each card links to `/admin/offers/${id}/edit` (Plan 05's route — may 404 until Plan 05 lands, but the link target is correct).
</verification>

<success_criteria>
- `/admin/offers` matches the UI-SPEC section 1 layout: header + CTA, category filter chips, archive toggle, card grid, empty state.
- OFFER-18 satisfied: list filterable by category, archive toggle hides archived offers by default.
- No legacy macro/micro CRUD forms remain on `/admin/offers`.
</success_criteria>

<output>
After completion, create `.planning/phases/12-offer-composition-drag-drop-csv-import/12-04-SUMMARY.md`
</output>
