---
phase: "02-admin-area-interactive-features"
plan: 02
type: execute
wave: 2
depends_on:
  - "02-01"
files_modified:
  - src/app/admin/page.tsx
  - src/app/admin/layout.tsx
  - src/app/admin/clients/new/page.tsx
  - src/app/admin/clients/new/actions.ts
  - src/lib/admin-queries.ts
  - src/components/admin/ClientRow.tsx
  - src/components/admin/NavBar.tsx
autonomous: true
requirements:
  - ADMIN-01
  - ADMIN-02

must_haves:
  truths:
    - "Admin can see a list of all clients at /admin with name, brand, and payment status badges"
    - "Admin can create a new client via /admin/clients/new form; on submit the client row + two payment rows are inserted and the secret link (token) is auto-generated"
    - "After creating a client, admin is redirected to /admin (or /admin/clients/[id] for detail)"
    - "The new client's shareable link /c/[token] is visible to the admin immediately after creation"
    - "Payment status badges for Acconto and Saldo are visible in the client list row"
  artifacts:
    - path: "src/app/admin/page.tsx"
      provides: "Admin client list — Server Component fetching all clients with payments"
      contains: "export default async function"
    - path: "src/app/admin/layout.tsx"
      provides: "Admin layout with minimal NavBar (logo + Clienti link + logout button)"
      contains: "NavBar"
    - path: "src/app/admin/clients/new/page.tsx"
      provides: "New client form page"
      min_lines: 30
    - path: "src/app/admin/clients/new/actions.ts"
      provides: "Server Action: createClient() — inserts client + 2 payment rows"
      contains: "createClient"
    - path: "src/lib/admin-queries.ts"
      provides: "Admin-side DB query functions (getAllClientsWithPayments)"
      contains: "getAllClientsWithPayments"
  key_links:
    - from: "src/app/admin/page.tsx"
      to: "src/lib/admin-queries.ts"
      via: "getAllClientsWithPayments()"
      pattern: "getAllClientsWithPayments"
    - from: "src/app/admin/clients/new/page.tsx"
      to: "src/app/admin/clients/new/actions.ts"
      via: "createClient Server Action"
      pattern: "createClient"
    - from: "createClient action"
      to: "clients + payments tables"
      via: "db.insert(clients) + db.insert(payments) x2"
      pattern: "db.insert"
---

<objective>
**Admin Client List + Create Client:** Build the admin home page (client list with payment badges) and the new client creation form. The create form auto-generates the nanoid secret token, inserts the client row, and creates two payment rows (Acconto 50% / Saldo 50%) in a single Server Action.

Purpose: Deliver the first end-to-end admin capability — admin can enter a client's details and immediately get a shareable /c/[token] link. Implements ADMIN-01 (client list with status) and the creation half of ADMIN-02 (per D-05 Server Actions, D-07 list→detail layout, D-09 minimal nav).

Output: /admin shows all clients with payment badges; /admin/clients/new creates a client and two payment stubs.
</objective>

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

<context>
@.planning/ROADMAP.md
@.planning/phases/02-admin-area-interactive-features/02-CONTEXT.md
@.planning/phases/02-admin-area-interactive-features/02-01-SUMMARY.md

<interfaces>
<!-- Exact schema exports from src/db/schema.ts (do not re-read the file) -->
```typescript
export const clients = pgTable("clients", {
  id: text("id").primaryKey().$defaultFn(() => nanoid()),
  name: text("name").notNull(),
  brand_name: text("brand_name").notNull(),
  brief: text("brief").notNull(),
  token: text("token").notNull().unique().$defaultFn(() => nanoid()),
  accepted_total: numeric("accepted_total", { precision: 10, scale: 2 }).default("0"),
  created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});

export const payments = pgTable("payments", {
  id: text("id").primaryKey().$defaultFn(() => nanoid()),
  client_id: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }),
  label: text("label").notNull(),          // "Acconto 50%" | "Saldo 50%"
  amount: numeric("amount", { precision: 10, scale: 2 }).notNull(),
  status: text("status").notNull().default("da_saldare"), // da_saldare | inviata | saldato
  paid_at: timestamp("paid_at", { withTimezone: true }),
});

export type Client = typeof clients.$inferSelect;
export type NewClient = typeof clients.$inferInsert;
export type Payment = typeof payments.$inferSelect;
```

From src/db/index.ts:
```typescript
export const db = drizzle(client); // drizzle-orm/postgres-js
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create src/lib/admin-queries.ts and admin layout + NavBar component</name>
  <files>
    src/lib/admin-queries.ts
    src/app/admin/layout.tsx
    src/components/admin/NavBar.tsx
  </files>
  <action>
    Create `src/lib/admin-queries.ts` — all admin-side DB reads live here:
    ```typescript
    import { db } from "@/db";
    import { clients, payments } from "@/db/schema";
    import { eq } from "drizzle-orm";

    export type ClientWithPayments = {
      id: string;
      name: string;
      brand_name: string;
      token: string;
      accepted_total: string;
      created_at: Date;
      payments: Array<{
        id: string;
        label: string;
        status: string;
        amount: string;
      }>;
    };

    export async function getAllClientsWithPayments(): Promise<ClientWithPayments[]> {
      const allClients = await db
        .select()
        .from(clients)
        .orderBy(clients.created_at);

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

      const allPayments = await db
        .select()
        .from(payments);

      return allClients.map((c) => ({
        id: c.id,
        name: c.name,
        brand_name: c.brand_name,
        token: c.token,
        accepted_total: c.accepted_total ?? "0",
        created_at: c.created_at,
        payments: allPayments
          .filter((p) => p.client_id === c.id)
          .map((p) => ({
            id: p.id,
            label: p.label,
            status: p.status,
            amount: p.amount,
          })),
      }));
    }

    export async function getClientById(id: string) {
      const rows = await db
        .select()
        .from(clients)
        .where(eq(clients.id, id))
        .limit(1);
      return rows[0] ?? null;
    }
    ```

    Create `src/components/admin/NavBar.tsx` — minimal nav per D-09 (no sidebar):
    ```typescript
    "use client";

    import Link from "next/link";
    import { signOut } from "next-auth/react";
    import { Button } from "@/components/ui/button";

    export function NavBar() {
      return (
        <nav className="border-b border-gray-200 bg-white px-6 py-3 flex items-center justify-between">
          <div className="flex items-center gap-6">
            <span className="font-semibold text-gray-900">ClientHub</span>
            <Link
              href="/admin"
              className="text-sm text-gray-600 hover:text-gray-900 transition-colors"
            >
              Clienti
            </Link>
          </div>
          <Button
            variant="ghost"
            size="sm"
            onClick={() => signOut({ callbackUrl: "/admin/login" })}
            className="text-sm text-gray-500"
          >
            Esci
          </Button>
        </nav>
      );
    }
    ```

    Create `src/app/admin/layout.tsx` — wraps all /admin/* pages:
    ```typescript
    import { NavBar } from "@/components/admin/NavBar";

    export default function AdminLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <div className="min-h-screen bg-gray-50">
          <NavBar />
          <main className="max-w-5xl mx-auto px-6 py-8">{children}</main>
        </div>
      );
    }
    ```
  </action>
  <verify>
    <automated>test -f src/lib/admin-queries.ts && grep -q "getAllClientsWithPayments" src/lib/admin-queries.ts && echo "admin-queries.ts created"</automated>
    <automated>grep -q "getClientById" src/lib/admin-queries.ts && echo "getClientById exported"</automated>
    <automated>test -f src/components/admin/NavBar.tsx && grep -q "signOut" src/components/admin/NavBar.tsx && echo "NavBar with logout"</automated>
    <automated>test -f src/app/admin/layout.tsx && grep -q "NavBar" src/app/admin/layout.tsx && echo "Admin layout wraps NavBar"</automated>
    <automated>npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK"</automated>
  </verify>
  <done>
    - src/lib/admin-queries.ts exports getAllClientsWithPayments() and getClientById()
    - NavBar renders with "Clienti" link and "Esci" button
    - Admin layout wraps all /admin/* pages with NavBar + centered main content area
    - npm run build passes
  </done>
</task>

<task type="auto">
  <name>Task 2: Build /admin client list page and /admin/clients/new create-client flow</name>
  <files>
    src/app/admin/page.tsx
    src/components/admin/ClientRow.tsx
    src/app/admin/clients/new/page.tsx
    src/app/admin/clients/new/actions.ts
  </files>
  <action>
    Create `src/components/admin/ClientRow.tsx` — single row in client list table:
    ```typescript
    import Link from "next/link";
    import { Badge } from "@/components/ui/badge";
    import type { ClientWithPayments } from "@/lib/admin-queries";

    const statusConfig: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
      da_saldare: { label: "Da saldare", variant: "destructive" },
      inviata:    { label: "Inviata",    variant: "secondary" },
      saldato:    { label: "Saldato",    variant: "default" },
    };

    export function ClientRow({ client }: { client: ClientWithPayments }) {
      const acconto = client.payments.find((p) => p.label.includes("Acconto"));
      const saldo   = client.payments.find((p) => p.label.includes("Saldo"));

      return (
        <tr className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
          <td className="py-3 px-4">
            <Link
              href={`/admin/clients/${client.id}`}
              className="font-medium text-gray-900 hover:underline"
            >
              {client.name}
            </Link>
            <p className="text-xs text-gray-400">{client.brand_name}</p>
          </td>
          <td className="py-3 px-4 text-sm text-gray-600">
            € {parseFloat(client.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
          </td>
          <td className="py-3 px-4">
            {acconto && (
              <Badge variant={statusConfig[acconto.status]?.variant ?? "outline"}>
                Acconto: {statusConfig[acconto.status]?.label ?? acconto.status}
              </Badge>
            )}
          </td>
          <td className="py-3 px-4">
            {saldo && (
              <Badge variant={statusConfig[saldo.status]?.variant ?? "outline"}>
                Saldo: {statusConfig[saldo.status]?.label ?? saldo.status}
              </Badge>
            )}
          </td>
          <td className="py-3 px-4">
            <a
              href={`/c/${client.token}`}
              target="_blank"
              rel="noopener noreferrer"
              className="text-xs text-blue-600 hover:underline font-mono"
            >
              /c/{client.token.slice(0, 10)}…
            </a>
          </td>
        </tr>
      );
    }
    ```

    Create `src/app/admin/page.tsx` — Server Component, no client state:
    ```typescript
    import Link from "next/link";
    import { getAllClientsWithPayments } from "@/lib/admin-queries";
    import { ClientRow } from "@/components/admin/ClientRow";
    import { Button } from "@/components/ui/button";

    export const revalidate = 0; // always fresh — admin needs real-time data

    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-gray-900">Clienti</h1>
            <Button asChild>
              <Link href="/admin/clients/new">+ Nuovo cliente</Link>
            </Button>
          </div>

          {clients.length === 0 ? (
            <div className="text-center py-20 text-gray-400">
              <p>Nessun cliente ancora.</p>
              <p className="mt-2">
                <Link href="/admin/clients/new" className="text-blue-600 hover:underline">
                  Crea il primo cliente
                </Link>
              </p>
            </div>
          ) : (
            <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
              <table className="w-full text-sm">
                <thead className="bg-gray-50 border-b border-gray-200">
                  <tr>
                    <th className="text-left py-3 px-4 font-medium text-gray-600">Cliente</th>
                    <th className="text-left py-3 px-4 font-medium text-gray-600">Totale</th>
                    <th className="text-left py-3 px-4 font-medium text-gray-600">Acconto</th>
                    <th className="text-left py-3 px-4 font-medium text-gray-600">Saldo</th>
                    <th className="text-left py-3 px-4 font-medium text-gray-600">Link</th>
                  </tr>
                </thead>
                <tbody>
                  {clients.map((client) => (
                    <ClientRow key={client.id} client={client} />
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </div>
      );
    }
    ```

    Create `src/app/admin/clients/new/actions.ts` — Server Action (per D-05):
    ```typescript
    "use server";

    import { redirect } from "next/navigation";
    import { revalidatePath } from "next/cache";
    import { z } from "zod";
    import { db } from "@/db";
    import { clients, payments } from "@/db/schema";

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

    export async function createClient(formData: FormData) {
      const raw = {
        name: formData.get("name") as string,
        brand_name: formData.get("brand_name") as string,
        brief: formData.get("brief") as string,
      };

      const parsed = createClientSchema.safeParse(raw);
      if (!parsed.success) {
        // In v1 return errors as thrown string — form displays validation inline
        throw new Error(parsed.error.issues.map((i) => i.message).join(", "));
      }

      // Insert client — token and id are auto-generated by $defaultFn(() => nanoid())
      const [newClient] = await db
        .insert(clients)
        .values({
          name: parsed.data.name,
          brand_name: parsed.data.brand_name,
          brief: parsed.data.brief,
        })
        .returning({ id: clients.id, token: clients.token });

      // Always create two payment stubs per client — Acconto 50% and Saldo 50%
      // Amounts default to 0 until admin sets accepted_total; admin updates separately
      await db.insert(payments).values([
        {
          client_id: newClient.id,
          label: "Acconto 50%",
          amount: "0",
          status: "da_saldare",
        },
        {
          client_id: newClient.id,
          label: "Saldo 50%",
          amount: "0",
          status: "da_saldare",
        },
      ]);

      revalidatePath("/admin");
      redirect(`/admin/clients/${newClient.id}`);
    }
    ```

    Create `src/app/admin/clients/new/page.tsx` — form using the Server Action:
    ```typescript
    import { createClient } from "./actions";
    import { Button } from "@/components/ui/button";
    import { Input } from "@/components/ui/input";
    import { Label } from "@/components/ui/label";
    import { Textarea } from "@/components/ui/textarea";
    import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
    import Link from "next/link";

    export default function NewClientPage() {
      return (
        <div className="max-w-xl">
          <div className="mb-6">
            <Link href="/admin" className="text-sm text-gray-500 hover:text-gray-700">
              ← Clienti
            </Link>
          </div>
          <Card>
            <CardHeader>
              <CardTitle>Nuovo cliente</CardTitle>
            </CardHeader>
            <CardContent>
              <form action={createClient} className="space-y-4">
                <div className="space-y-1">
                  <Label htmlFor="name">Nome cliente</Label>
                  <Input
                    id="name"
                    name="name"
                    type="text"
                    placeholder="es. Marco Rossi"
                    required
                  />
                </div>
                <div className="space-y-1">
                  <Label htmlFor="brand_name">Nome brand</Label>
                  <Input
                    id="brand_name"
                    name="brand_name"
                    type="text"
                    placeholder="es. Rossi Studio"
                    required
                  />
                </div>
                <div className="space-y-1">
                  <Label htmlFor="brief">Brief del progetto</Label>
                  <Textarea
                    id="brief"
                    name="brief"
                    placeholder="Descrizione del progetto e degli obiettivi..."
                    rows={5}
                    required
                  />
                </div>
                <div className="flex gap-3 pt-2">
                  <Button type="submit">Crea cliente</Button>
                  <Button variant="outline" asChild>
                    <Link href="/admin">Annulla</Link>
                  </Button>
                </div>
              </form>
            </CardContent>
          </Card>
        </div>
      );
    }
    ```
  </action>
  <verify>
    <automated>test -f src/app/admin/page.tsx && grep -q "getAllClientsWithPayments" src/app/admin/page.tsx && echo "Admin page fetches clients"</automated>
    <automated>grep -q "ClientRow" src/app/admin/page.tsx && echo "ClientRow used in table"</automated>
    <automated>test -f src/app/admin/clients/new/actions.ts && grep -q '"use server"' src/app/admin/clients/new/actions.ts && echo "Server Action directive present"</automated>
    <automated>grep -q "db.insert(clients)" src/app/admin/clients/new/actions.ts && echo "client insert present"</automated>
    <automated>grep -q "Acconto 50%" src/app/admin/clients/new/actions.ts && grep -q "Saldo 50%" src/app/admin/clients/new/actions.ts && echo "both payment stubs inserted"</automated>
    <automated>grep -q "createClientSchema" src/app/admin/clients/new/actions.ts && echo "Zod validation present"</automated>
    <automated>test -f src/app/admin/clients/new/page.tsx && grep -q "action={createClient}" src/app/admin/clients/new/page.tsx && echo "form wired to Server Action"</automated>
    <automated>npm run build 2>&1 | grep -v "warning" | grep -qi "error" && echo "BUILD ERRORS" || echo "TypeScript OK"</automated>
  </verify>
  <done>
    - /admin shows table of clients with name, brand, totale, acconto badge, saldo badge, client link
    - Empty state shows "Nessun cliente ancora" with link to create
    - /admin/clients/new shows form with name, brand_name, brief fields
    - Submitting the form inserts client row (token auto-generated by nanoid) + 2 payment stubs
    - After creation, admin is redirected to /admin/clients/[id] (detail page — stub until Plan 03)
    - npm run build passes
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Admin browser → Server Action | createClient() runs server-side; input validated with Zod before any DB write |
| Admin browser → /admin/* | Middleware session guard (02-01) prevents unauthenticated access to all admin pages and Server Actions |
| Client token → DB | Token generated server-side by nanoid(), never user-supplied; cannot be guessed |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-06 | Tampering | createClient Server Action | mitigate | Zod validates all input fields before DB insert; malformed input throws, no partial writes |
| T-02-07 | Information Disclosure | Client list page | mitigate | /admin/* protected by middleware session guard from 02-01; unauthenticated requests never reach this Server Component |
| T-02-08 | Tampering | token generation | mitigate | token is $defaultFn(() => nanoid()) — server-generated, cryptographically random, never derived from user input |
| T-02-09 | Information Disclosure | ClientRow renders full token | accept | Token is shown truncated in UI for usability; full token accessible via /c/[token] link only — acceptable since admin has session auth |
</threat_model>

<verification>
After plan execution:
1. `npm run build` — no errors
2. Log in as admin, visit /admin — client table renders (empty or with seeded data)
3. Click "+ Nuovo cliente" → /admin/clients/new loads with form
4. Submit form with valid data → redirects to /admin/clients/[id] (stub page acceptable at this point)
5. Return to /admin → new client appears in table with "Da saldare" badges for Acconto and Saldo
6. Click the /c/[token] link in the table → opens client dashboard (Phase 1 output)
</verification>

<success_criteria>
- /admin shows all clients with payment status badges; empty state handled gracefully
- New client can be created via form; token is auto-generated server-side
- Two payment stubs (Acconto 50% / Saldo 50%) are created automatically on client creation
- Admin can immediately share /c/[token] after creating a client
- npm run build passes cleanly
</success_criteria>

<output>
After completion, create `.planning/phases/02-admin-area-interactive-features/02-02-SUMMARY.md`
</output>
