---
phase: "03"
plan: "02"
type: execute
wave: 2
depends_on:
  - "03-01"
files_modified:
  - src/app/admin/catalog/page.tsx
  - src/app/admin/catalog/actions.ts
  - src/components/admin/catalog/ServiceTable.tsx
  - src/components/admin/catalog/ServiceForm.tsx
  - src/components/admin/NavBar.tsx
autonomous: true
requirements:
  - CAT-01

must_haves:
  truths:
    - "Admin can navigate to /admin/catalog from the NavBar ('Catalogo' link visible between Statistiche and Esci)"
    - "Admin can see a table of all services with columns Nome | Descrizione | Prezzo | Stato | Azioni"
    - "Admin can add a new service via an inline form (name, optional description, unit price) — it appears in the table after save"
    - "Admin can click 'Modifica' on a row and edit name, description, price inline — changes persist after save"
    - "Admin can click 'Disattiva' to soft-delete a service (active=false) — row shows 'Disattivato' badge at 50% opacity"
    - "Admin can click 'Riattiva' on a disabled service to re-enable it"
    - "Inactive services remain visible in the table (with badge) but are excluded from the quote builder dropdown"
  artifacts:
    - path: "src/app/admin/catalog/page.tsx"
      provides: "Service catalog page — server component, fetches all services, renders table"
      contains: "getAllServices"
    - path: "src/app/admin/catalog/actions.ts"
      provides: "Server Actions: createService, updateService, toggleServiceActive"
      exports: ["createService", "updateService", "toggleServiceActive"]
    - path: "src/components/admin/catalog/ServiceTable.tsx"
      provides: "Table with per-row inline edit and active toggle"
      contains: "ServiceTable"
    - path: "src/components/admin/catalog/ServiceForm.tsx"
      provides: "Add-new-service form rendered above table"
      contains: "ServiceForm"
    - path: "src/components/admin/NavBar.tsx"
      provides: "NavBar with Catalogo link added"
      contains: "/admin/catalog"
  key_links:
    - from: "src/components/admin/catalog/ServiceForm.tsx"
      to: "src/app/admin/catalog/actions.ts createService"
      via: "form action"
      pattern: "createService"
    - from: "src/components/admin/catalog/ServiceTable.tsx"
      to: "src/app/admin/catalog/actions.ts updateService / toggleServiceActive"
      via: "Server Action calls in useTransition"
      pattern: "updateService|toggleServiceActive"
    - from: "src/app/admin/catalog/page.tsx"
      to: "src/lib/admin-queries.ts getAllServices (new function)"
      via: "await getAllServices()"
      pattern: "getAllServices"
---

<objective>
Deliver the complete `/admin/catalog` page: NavBar link, page layout, table with inline edit, add-service form, and soft-delete toggle. This is a self-contained vertical slice — after this plan executes, the admin can manage the service catalog end-to-end.

Purpose: Fulfills CAT-01 (service database with prices). Provides the catalog data that Wave 2's Quote Builder (plan 03-03) will query for its dropdown.
Output: 5 new/modified files — a fully functional service catalog page.
</objective>

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

<context>
@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md
@/Users/simonecavalli/IAMCAVALLI/.planning/phases/03-service-catalog-quote-builder/03-01-SUMMARY.md

<interfaces>
<!-- Analog: src/app/admin/page.tsx — follow this exact page structure -->
```typescript
// Server component, fetches data, renders table + header
export default async function AdminDashboard() {
  const clients = await getAllClientsWithPayments();
  return (
    <div>
      <div className="flex items-center justify-between mb-6">
        <h1 className="text-2xl font-bold text-[#1a1a1a]">Clienti</h1>
        <Button asChild><Link href="/admin/clients/new">+ Nuovo cliente</Link></Button>
      </div>
      {/* table */}
    </div>
  );
}
```

<!-- Analog: src/components/admin/DocumentRow.tsx — follow this inline edit pattern exactly -->
```typescript
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";

export function DocumentRow({ doc, clientId }) {
  const [editing, setEditing] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [, startTransition] = useTransition();
  const router = useRouter();

  function handleSave(fd: FormData) {
    setError(null);
    startTransition(async () => {
      try {
        await updateDocument(doc.id, clientId, fd);
        setEditing(false);
        router.refresh();
      } catch (e) {
        setError(e instanceof Error ? e.message : "Errore nel salvataggio");
      }
    });
  }
  // ...
}
```

<!-- Analog: src/app/admin/clients/[id]/actions.ts — Zod validation pattern -->
```typescript
"use server";
import { z } from "zod";
import { db } from "@/db";
import { revalidatePath } from "next/cache";

const clientSchema = z.object({
  name: z.string().min(1, "Nome richiesto"),
  brand_name: z.string().min(1, "Brand name richiesto"),
  brief: z.string(),
});

export async function updateClient(clientId: string, formData: FormData) {
  const parsed = clientSchema.safeParse({
    name: formData.get("name"),
    brand_name: formData.get("brand_name"),
    brief: formData.get("brief") ?? "",
  });
  if (!parsed.success) throw new Error(parsed.error.issues[0].message);
  await db.update(clients).set(parsed.data).where(eq(clients.id, clientId));
  revalidatePath("/admin");
}
```

<!-- NavBar current structure — add Catalogo link after Statistiche -->
```typescript
// src/components/admin/NavBar.tsx lines 7-29
export function NavBar() {
  return (
    <nav className="bg-[#1A463C] px-6 py-3 flex items-center justify-between">
      <div className="flex items-center gap-6">
        <span className="font-bold text-white tracking-tight">iamcavalli</span>
        <Link href="/admin" className="text-sm text-white/70 hover:text-white transition-colors">Clienti</Link>
        <Link href="/admin/analytics" className="text-sm text-white/70 hover:text-white transition-colors">Statistiche</Link>
        {/* ADD HERE: */}
        {/* <Link href="/admin/catalog" className="text-sm text-white/70 hover:text-white transition-colors">Catalogo</Link> */}
      </div>
      <Button variant="ghost" size="sm" onClick={() => signOut({ callbackUrl: "/admin/login" })}
        className="text-sm text-white/70 hover:text-white hover:bg-white/10">Esci</Button>
    </nav>
  );
}
```

<!-- Table + card styling from existing admin UI -->
```typescript
// Table container
<div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
  <table className="w-full text-sm">
    <thead className="bg-[#f9f9f9] border-b border-[#e5e7eb]">
      <tr>
        <th className="text-left py-3 px-4 font-medium text-[#71717a]">Colonna</th>
      </tr>
    </thead>
    <tbody>
      <tr className="border-b border-[#f4f4f5] hover:bg-[#f9f9f9]">
        <td className="py-3 px-4">...</td>
      </tr>
    </tbody>
  </table>
</div>

// Status badge — active
<span className="text-xs font-medium bg-[#1A463C]/10 text-[#1A463C] px-2 py-0.5 rounded-full">Attivo</span>
// Status badge — inactive
<span className="text-xs font-medium bg-[#f4f4f5] text-[#71717a] px-2 py-0.5 rounded-full">Disattivato</span>

// Currency display
€{parseFloat(amount).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
```

<!-- ServiceCatalog type (auto-generated from schema.ts) -->
```typescript
export type ServiceCatalog = typeof service_catalog.$inferSelect;
// Fields: id: string, name: string, description: string | null, unit_price: string, active: boolean
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Server Actions + getAllServices query</name>
  <read_first>
    - /Users/simonecavalli/IAMCAVALLI/src/app/admin/clients/[id]/actions.ts (Zod + Server Action pattern to replicate)
    - /Users/simonecavalli/IAMCAVALLI/src/lib/admin-queries.ts (add getAllServices here, following existing function style)
    - /Users/simonecavalli/IAMCAVALLI/src/db/schema.ts (confirm service_catalog fields after 03-01 changes)
  </read_first>
  <files>
    src/app/admin/catalog/actions.ts
    src/lib/admin-queries.ts
  </files>
  <action>
**Create `src/app/admin/catalog/actions.ts`** — three Server Actions following exact Zod+FormData pattern from `clients/[id]/actions.ts`:

```typescript
"use server";

import { db } from "@/db";
import { service_catalog } from "@/db/schema";
import { revalidatePath } from "next/cache";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

const serviceSchema = z.object({
  name: z.string().min(1, "Nome richiesto"),
  description: z.string().optional(),
  unit_price: z.coerce.number().min(0.01, "Prezzo deve essere maggiore di 0"),
});

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

export async function createService(formData: FormData) {
  await requireAdmin();
  const parsed = serviceSchema.safeParse({
    name: formData.get("name"),
    description: formData.get("description") ?? "",
    unit_price: formData.get("unit_price"),
  });
  if (!parsed.success) throw new Error(parsed.error.issues[0].message);
  await db.insert(service_catalog).values({
    name: parsed.data.name,
    description: parsed.data.description ?? null,
    unit_price: parsed.data.unit_price.toFixed(2),
  });
  revalidatePath("/admin/catalog");
}

export async function updateService(serviceId: string, formData: FormData) {
  await requireAdmin();
  const parsed = serviceSchema.safeParse({
    name: formData.get("name"),
    description: formData.get("description") ?? "",
    unit_price: formData.get("unit_price"),
  });
  if (!parsed.success) throw new Error(parsed.error.issues[0].message);
  await db
    .update(service_catalog)
    .set({
      name: parsed.data.name,
      description: parsed.data.description ?? null,
      unit_price: parsed.data.unit_price.toFixed(2),
    })
    .where(eq(service_catalog.id, serviceId));
  revalidatePath("/admin/catalog");
}

export async function toggleServiceActive(serviceId: string, active: boolean) {
  await requireAdmin();
  await db
    .update(service_catalog)
    .set({ active })
    .where(eq(service_catalog.id, serviceId));
  revalidatePath("/admin/catalog");
}
```

**Add `getAllServices()` to `src/lib/admin-queries.ts`** — append at end of file before the closing exports:

```typescript
export async function getAllServices(): Promise<ServiceCatalog[]> {
  return db
    .select()
    .from(service_catalog)
    .orderBy(asc(service_catalog.name));
}
```

Also add `service_catalog` to the imports at top of admin-queries.ts, and `ServiceCatalog` to the type imports. Add `asc` if not already imported from `drizzle-orm`.
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c 'export async function createService' src/app/admin/catalog/actions.ts</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c 'export async function updateService' src/app/admin/catalog/actions.ts</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c 'export async function toggleServiceActive' src/app/admin/catalog/actions.ts</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c 'export async function getAllServices' src/lib/admin-queries.ts</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20</automated>
    Expected: no output (zero errors)
  </verify>
  <done>
    Three Server Actions exported from `catalog/actions.ts`. `getAllServices()` added to `admin-queries.ts`. TypeScript compiles clean.
  </done>
</task>

<task type="auto">
  <name>Task 2: Service Catalog page + components (ServiceTable, ServiceForm) + NavBar link</name>
  <read_first>
    - /Users/simonecavalli/IAMCAVALLI/src/app/admin/page.tsx (page structure to mirror)
    - /Users/simonecavalli/IAMCAVALLI/src/components/admin/DocumentRow.tsx (inline edit pattern to replicate)
    - /Users/simonecavalli/IAMCAVALLI/src/components/admin/NavBar.tsx (current NavBar to add Catalogo link)
    - /Users/simonecavalli/IAMCAVALLI/src/app/admin/catalog/actions.ts (actions just created in Task 1)
  </read_first>
  <files>
    src/app/admin/catalog/page.tsx
    src/components/admin/catalog/ServiceTable.tsx
    src/components/admin/catalog/ServiceForm.tsx
    src/components/admin/NavBar.tsx
  </files>
  <action>
**Create `src/app/admin/catalog/page.tsx`** — Server Component mirroring `src/app/admin/page.tsx`:

```typescript
import { getAllServices } from "@/lib/admin-queries";
import { ServiceTable } from "@/components/admin/catalog/ServiceTable";
import { ServiceForm } from "@/components/admin/catalog/ServiceForm";

export const revalidate = 0;

export default async function CatalogPage() {
  const services = await getAllServices();

  return (
    <div>
      <div className="flex items-center justify-between mb-6">
        <h1 className="text-2xl font-bold text-[#1a1a1a]">Catalogo Servizi</h1>
      </div>

      <div className="mb-6">
        <ServiceForm />
      </div>

      {services.length === 0 ? (
        <p className="text-sm text-[#71717a]">
          Nessun servizio nel catalogo. Aggiungi il primo servizio qui sopra.
        </p>
      ) : (
        <ServiceTable services={services} />
      )}
    </div>
  );
}
```

**Create `src/components/admin/catalog/ServiceForm.tsx`** — inline add-new-service form using Server Action:

```typescript
"use client";

import { useRef, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { createService } from "@/app/admin/catalog/actions";

export function ServiceForm() {
  const [open, setOpen] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [, startTransition] = useTransition();
  const router = useRouter();
  const formRef = useRef<HTMLFormElement>(null);

  function handleSubmit(fd: FormData) {
    setError(null);
    startTransition(async () => {
      try {
        await createService(fd);
        formRef.current?.reset();
        setOpen(false);
        router.refresh();
      } catch (e) {
        setError(e instanceof Error ? e.message : "Errore nel salvataggio");
      }
    });
  }

  if (!open) {
    return (
      <Button
        onClick={() => setOpen(true)}
        className="bg-[#1A463C] text-white hover:bg-[#1A463C]/90"
      >
        + Aggiungi servizio
      </Button>
    );
  }

  return (
    <div className="bg-white border border-[#e5e7eb] rounded-xl p-4 space-y-4">
      <h3 className="font-medium text-[#1a1a1a]">Nuovo servizio</h3>
      <form ref={formRef} action={handleSubmit} className="space-y-3">
        <div className="space-y-1">
          <Label htmlFor="name">Nome</Label>
          <Input id="name" name="name" placeholder="es. Strategia di brand" required />
        </div>
        <div className="space-y-1">
          <Label htmlFor="description">Descrizione (opzionale)</Label>
          <Input id="description" name="description" placeholder="es. Incluso: analisi competitor, posizionamento" />
        </div>
        <div className="space-y-1">
          <Label htmlFor="unit_price">Prezzo unitario (€)</Label>
          <Input
            id="unit_price"
            name="unit_price"
            type="number"
            step="0.01"
            min="0.01"
            placeholder="0.00"
            required
          />
        </div>
        {error && <p className="text-xs text-red-600">{error}</p>}
        <div className="flex gap-2">
          <Button type="submit" size="sm" className="bg-[#1A463C] text-white hover:bg-[#1A463C]/90">
            Aggiungi
          </Button>
          <Button
            type="button"
            variant="ghost"
            size="sm"
            onClick={() => { setOpen(false); setError(null); }}
          >
            Annulla
          </Button>
        </div>
      </form>
    </div>
  );
}
```

**Create `src/components/admin/catalog/ServiceTable.tsx`** — table with per-row inline edit, following DocumentRow pattern:

```typescript
"use client";

import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { updateService, toggleServiceActive } from "@/app/admin/catalog/actions";
import type { ServiceCatalog } from "@/db/schema";

function ServiceRow({ service }: { service: ServiceCatalog }) {
  const [editing, setEditing] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [, startTransition] = useTransition();
  const router = useRouter();

  function handleSave(fd: FormData) {
    setError(null);
    startTransition(async () => {
      try {
        await updateService(service.id, fd);
        setEditing(false);
        router.refresh();
      } catch (e) {
        setError(e instanceof Error ? e.message : "Errore nel salvataggio");
      }
    });
  }

  function handleToggle() {
    startTransition(async () => {
      await toggleServiceActive(service.id, !service.active);
      router.refresh();
    });
  }

  if (editing) {
    return (
      <tr>
        <td colSpan={5} className="px-4 py-3">
          <form action={handleSave} className="space-y-2 bg-[#f9f9f9] rounded-lg p-3 border border-[#1A463C]/20">
            <div className="flex gap-3 flex-wrap">
              <div className="flex-1 min-w-[140px] space-y-1">
                <Label htmlFor={`name-${service.id}`}>Nome</Label>
                <Input id={`name-${service.id}`} name="name" defaultValue={service.name} required />
              </div>
              <div className="flex-[2] min-w-[180px] space-y-1">
                <Label htmlFor={`desc-${service.id}`}>Descrizione</Label>
                <Input id={`desc-${service.id}`} name="description" defaultValue={service.description ?? ""} />
              </div>
              <div className="w-28 space-y-1">
                <Label htmlFor={`price-${service.id}`}>Prezzo (€)</Label>
                <Input
                  id={`price-${service.id}`}
                  name="unit_price"
                  type="number"
                  step="0.01"
                  min="0.01"
                  defaultValue={parseFloat(service.unit_price).toFixed(2)}
                  required
                />
              </div>
            </div>
            {error && <p className="text-xs text-red-600">{error}</p>}
            <div className="flex gap-2">
              <Button type="submit" size="sm" className="bg-[#1A463C] text-white hover:bg-[#1A463C]/90">Salva</Button>
              <Button type="button" variant="ghost" size="sm" onClick={() => { setEditing(false); setError(null); }}>Annulla</Button>
            </div>
          </form>
        </td>
      </tr>
    );
  }

  return (
    <tr className={`border-b border-[#f4f4f5] hover:bg-[#f9f9f9] transition-colors ${!service.active ? "opacity-50" : ""}`}>
      <td className="py-3 px-4 font-medium text-[#1a1a1a]">{service.name}</td>
      <td className="py-3 px-4 text-[#71717a] text-sm max-w-xs truncate">{service.description ?? "—"}</td>
      <td className="py-3 px-4 tabular-nums font-mono">
        €{parseFloat(service.unit_price).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
      </td>
      <td className="py-3 px-4">
        {service.active ? (
          <span className="text-xs font-medium bg-[#1A463C]/10 text-[#1A463C] px-2 py-0.5 rounded-full">Attivo</span>
        ) : (
          <span className="text-xs font-medium bg-[#f4f4f5] text-[#71717a] px-2 py-0.5 rounded-full">Disattivato</span>
        )}
      </td>
      <td className="py-3 px-4 text-right">
        <div className="flex items-center justify-end gap-2">
          <Button variant="ghost" size="sm" onClick={() => setEditing(true)}>Modifica</Button>
          <Button variant="ghost" size="sm" onClick={handleToggle}>
            {service.active ? "Disattiva" : "Riattiva"}
          </Button>
        </div>
      </td>
    </tr>
  );
}

export function ServiceTable({ services }: { services: ServiceCatalog[] }) {
  return (
    <div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
      <table className="w-full text-sm">
        <thead className="bg-[#f9f9f9] border-b border-[#e5e7eb]">
          <tr>
            <th className="text-left py-3 px-4 font-medium text-[#71717a]">Nome</th>
            <th className="text-left py-3 px-4 font-medium text-[#71717a]">Descrizione</th>
            <th className="text-left py-3 px-4 font-medium text-[#71717a]">Prezzo</th>
            <th className="text-left py-3 px-4 font-medium text-[#71717a]">Stato</th>
            <th className="py-3 px-4"></th>
          </tr>
        </thead>
        <tbody>
          {services.map((s) => (
            <ServiceRow key={s.id} service={s} />
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

**Modify `src/components/admin/NavBar.tsx`** — add Catalogo link after the Statistiche link:

```typescript
<Link href="/admin/catalog" className="text-sm text-white/70 hover:text-white transition-colors">
  Catalogo
</Link>
```

Insert this line immediately after the existing `<Link href="/admin/analytics" ...>Statistiche</Link>` line.
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c '/admin/catalog' src/components/admin/NavBar.tsx</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c 'export function ServiceTable' src/components/admin/catalog/ServiceTable.tsx</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c 'export function ServiceForm' src/components/admin/catalog/ServiceForm.tsx</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -c 'getAllServices' src/app/admin/catalog/page.tsx</automated>
    Expected: 1
    <automated>cd /Users/simonecavalli/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20</automated>
    Expected: no output (zero errors)
    <automated>cd /Users/simonecavalli/IAMCAVALLI && npm run build 2>&1 | tail -10</automated>
    Expected: "Compiled successfully" or "Route (app)" output with no errors
  </verify>
  <done>
    NavBar shows "Catalogo" link. `/admin/catalog` page renders. ServiceTable and ServiceForm compile. Full `npm run build` passes. Admin can navigate to `/admin/catalog` and see the table.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Admin browser → Server Actions (catalog/actions.ts) | FormData from admin form crosses to server; must be validated before DB write |
| /admin/catalog route → Auth.js session | All catalog routes inherit the `/admin/*` middleware session check from Phase 2; no additional guard needed at page level |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-02-01 | Spoofing | createService / updateService / toggleServiceActive | mitigate | `requireAdmin()` calls `getServerSession(authOptions)` at the top of every Server Action — rejects if no valid session |
| T-03-02-02 | Tampering | serviceSchema Zod validation | mitigate | `unit_price` validated as `z.coerce.number().min(0.01)` — prevents zero/negative prices; `name` requires min length 1 |
| T-03-02-03 | Tampering | updateService serviceId parameter | mitigate | serviceId is bound at call site in the Server Action closure — admin can only modify the row ID passed from the server-rendered page |
| T-03-02-04 | Information Disclosure | /admin/catalog page | accept | Page is behind Auth.js `/admin/*` middleware (enforced in Phase 2); service prices are admin-internal data, not client-facing |
| T-03-02-05 | Tampering | XSS in service name / description | accept | React JSX auto-escapes all string output; no `dangerouslySetInnerHTML` used; UI-SPEC forbids it |
</threat_model>

<verification>
After both tasks complete:
1. `grep '/admin/catalog' src/components/admin/NavBar.tsx` returns 1 match
2. `npx tsc --noEmit` exits clean
3. `npm run build` succeeds
4. Navigating to `/admin/catalog` (dev server) shows the catalog page with table headers and "Aggiungi servizio" button
5. Adding a service via the form makes it appear in the table
6. Clicking "Disattiva" changes badge to "Disattivato" and reduces row opacity
</verification>

<success_criteria>
- `/admin/catalog` route is accessible from NavBar and renders without error
- All three Server Actions (createService, updateService, toggleServiceActive) are exported from `catalog/actions.ts` with Zod validation and `requireAdmin()` guard
- ServiceTable renders per-row inline edit using the DocumentRow pattern
- Inactive services show "Disattivato" badge; active services show "Attivo" badge
- TypeScript and build both pass clean
</success_criteria>

<output>
After completion, create `.planning/phases/03-service-catalog-quote-builder/03-02-SUMMARY.md`
</output>