Files
simone 49ef45da83 fix(04): revision 1 — depends_on format + D-12 client list coverage
- 04-02/03/04: change depends_on from filename format to plan ID format
  ('04-01-PLAN.md' → '04-01') to match orchestrator expectations and
  Phase 3 precedent
- 04-02: add Task 3 implementing D-12 — /admin/clients list shows brand
  names below client name and LTV as sum of project accepted_totals;
  update getAllClientsWithPayments query and ClientRow component;
  add src/app/admin/page.tsx, src/lib/admin-queries.ts,
  src/components/admin/ClientRow.tsx to files_modified

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:10:25 +02:00

29 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-progetti-multi-project 02 execute 2
04-01
src/components/admin/NavBar.tsx
src/components/admin/ProjectRow.tsx
src/app/admin/projects/page.tsx
src/app/admin/projects/new/page.tsx
src/app/admin/projects/project-actions.ts
src/app/admin/clients/[id]/page.tsx
src/app/admin/page.tsx
src/lib/admin-queries.ts
src/components/admin/ClientRow.tsx
true
PROJ-01
PROJ-03
truths artifacts key_links
La navbar admin mostra i link Progetti e Impostazioni oltre a Clienti e Catalogo
La pagina /admin/projects elenca tutti i progetti con colonne Nome (+ cliente), Valore, Acconto, Saldo, Timer, €/h
Il bottone '+ Nuovo Progetto' in /admin/projects apre un form che chiede nome e selezione cliente
La pagina /admin/clients/[id] mostra cards dei progetti del cliente con bottone '+ Nuovo Progetto'
Cliccando una card progetto si naviga a /admin/projects/[id]
createProject e archiveProject sono server actions funzionanti
La lista /admin/clients mostra brand names dei progetti sotto il nome cliente e il Life Time Value (LTV = somma accepted_total di tutti i progetti del cliente) — D-12
path provides contains
src/components/admin/NavBar.tsx NavBar con link Progetti e Impostazioni href="/admin/projects"
path provides contains
src/components/admin/ProjectRow.tsx Riga progetto per la lista /admin/projects ProjectWithPayments
path provides contains
src/app/admin/projects/page.tsx Pagina lista tutti i progetti getAllProjectsWithPayments
path provides contains
src/app/admin/projects/new/page.tsx Form creazione progetto con selezione cliente createProject
path provides contains
src/app/admin/projects/project-actions.ts Server actions: createProject, archiveProject, updateProjectAcceptedTotal export async function createProject
path provides contains
src/app/admin/clients/[id]/page.tsx Pagina cliente modificata per mostrare project cards getClientWithProjects
path provides contains
src/app/admin/page.tsx Lista clienti con brand names secondari e LTV colonna — D-12 getAllClientsWithPayments
from to via pattern
src/app/admin/projects/page.tsx src/lib/admin-queries.ts getAllProjectsWithPayments() getAllProjectsWithPayments
from to via pattern
src/app/admin/clients/[id]/page.tsx src/lib/admin-queries.ts getClientWithProjects(id) getClientWithProjects
from to via pattern
src/components/admin/ProjectRow.tsx src/app/admin/timer-actions.ts TimerCell with project_id TimerCell
Admin projects list e client detail rewrite. Consegna la prima slice verticale visibile: l'admin può vedere tutti i progetti in /admin/projects, creare nuovi progetti da /admin/clients/[id] o dal form globale, e navigare ai workspace progetto.

Purpose: Rende operativa la struttura multi-project nell'area admin senza ancora richiedere il workspace completo del progetto (quello viene in 04-03). Dopo questo piano l'admin può creare e navigare progetti.

Output: NavBar aggiornata, /admin/projects funzionale, /admin/clients/[id] mostra project cards.

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

@/Users/simonecavalli/IAMCAVALLI/.planning/PROJECT.md @/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md @/Users/simonecavalli/IAMCAVALLI/CLAUDE.md @/Users/simonecavalli/IAMCAVALLI/.planning/phases/04-progetti-multi-project/04-01-SUMMARY.md

Da src/lib/admin-queries.ts (creato in 04-01):

export type ProjectWithPayments = {
  id: string;
  name: string;
  client: { id: string; name: string; slug: string | null };
  accepted_total: string;
  archived: boolean;
  created_at: Date;
  payments: Array<{ id: string; label: string; status: string; amount: string }>;
  activeTimerEntryId: string | null;
  activeTimerStartedAt: Date | null;
  totalTrackedSeconds: number;
};

export async function getAllProjectsWithPayments(includeArchived?: boolean): Promise<ProjectWithPayments[]>;
export async function getClientWithProjects(clientId: string): Promise<ClientWithProjects | null>;

export type ClientWithProjects = Client & {
  projects: Array<{ id: string; name: string; accepted_total: string; archived: boolean; created_at: Date }>;
};

Da src/app/admin/timer-actions.ts (da aggiornare in 04-03, ma TimerCell già usato):

// TimerCell props (da src/components/admin/TimerCell.tsx):
// clientId: string  ← NOTA: questo è un nome legacy, in ProjectRow passiamo project.id
// activeEntryId: string | null
// activeStartedAt: Date | null
// totalTrackedSeconds: number

Pattern ClientRow (da clonare per ProjectRow):

  • src/components/admin/ClientRow.tsx — usa statusConfig, Badge, TimerCell, Link
  • Colonne ClientRow: nome, token/link, LTV (accepted_total), acconto badge, saldo badge, timer
  • Colonne ProjectRow (D-14): Nome+Cliente, Valore, Acconto, Saldo, Timer, €/h

€/h in lista = accepted_total ÷ (totalTrackedSeconds / 3600). Se ore = 0, mostrare "—".

Pattern admin page (da src/app/admin/page.tsx):

  • export const revalidate = 0
  • Server component asincrono, chiama query, passa a Row component
Task 1: NavBar + ProjectRow + server actions (createProject, archiveProject, updateProjectAcceptedTotal) src/components/admin/NavBar.tsx src/components/admin/ProjectRow.tsx src/app/admin/projects/project-actions.ts

<read_first> - src/components/admin/NavBar.tsx — leggere struttura attuale (link presenti, stili, imports) - src/components/admin/ClientRow.tsx — leggere interamente: questo è il template ESATTO per ProjectRow - src/components/admin/TimerCell.tsx — leggere per capire la prop interface (clientId, activeEntryId, activeStartedAt, totalTrackedSeconds) - src/app/admin/clients/[id]/quote-actions.ts — pattern server action (requireAdmin, revalidatePath) </read_first>

**A. Aggiornare src/components/admin/NavBar.tsx**

Aggiungere i link "Progetti" e "Impostazioni" al NavBar esistente. Leggere il file per trovare dove sono i link esistenti (Clienti, Statistiche, Catalogo) e aggiungere nell'ordine:

  • Clienti (/admin)
  • Progetti (/admin/projects) ← NUOVO
  • Statistiche (/admin/analytics)
  • Catalogo (/admin/catalog)
  • Impostazioni (/admin/impostazioni) ← NUOVO

Ogni link usa il pattern esistente: <Link href="..." className="text-sm text-white/70 hover:text-white transition-colors">.

B. Creare src/components/admin/ProjectRow.tsx

Clonare ClientRow.tsx sostituendo:

  • ClientWithPaymentsProjectWithPayments (import da @/lib/admin-queries)
  • Colonna nome: project.name in bold, project.client.name in testo secondario xs
  • Rimuovere colonna token/link cliente (non si mostra il link pubblico nella lista progetti)
  • Colonna valore: €{parseFloat(project.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
  • Colonna Acconto: badge per project.payments.find(p => p.label.toLowerCase().includes("acconto"))
  • Colonna Saldo: badge per project.payments.find(p => p.label.toLowerCase().includes("saldo"))
  • Colonna Timer: <TimerCell clientId={project.id} activeEntryId={project.activeTimerEntryId} activeStartedAt={project.activeTimerStartedAt} totalTrackedSeconds={project.totalTrackedSeconds} />
  • Colonna €/h: calcolo inline — const hours = project.totalTrackedSeconds / 3600; const eurPerHour = hours > 0 ? parseFloat(project.accepted_total) / hours : null; — mostrare €{eurPerHour.toFixed(2)}/h oppure se null

Link cliccabile sul nome: <Link href={"/admin/projects/" + project.id}>.

Usare gli stessi statusConfig di ClientRow per i badge pagamento.

C. Creare src/app/admin/projects/project-actions.ts

"use server";

import { revalidatePath } from "next/cache";
import { requireAdmin } from "@/lib/auth"; // stesso pattern delle altre actions
import { db } from "@/db";
import { projects, clients } from "@/db/schema";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";

export async function createProject(fd: FormData): Promise<{ projectId: string }> {
  await requireAdmin();
  const name = String(fd.get("name") ?? "").trim();
  const clientId = String(fd.get("client_id") ?? "").trim();

  if (!name) throw new Error("Nome progetto obbligatorio");
  if (!clientId) throw new Error("Cliente obbligatorio");

  // Verify client exists
  const clientRows = await db
    .select({ id: clients.id })
    .from(clients)
    .where(eq(clients.id, clientId))
    .limit(1);
  if (clientRows.length === 0) throw new Error("Cliente non trovato");

  const id = nanoid();
  await db.insert(projects).values({ id, client_id: clientId, name });

  revalidatePath("/admin/projects");
  revalidatePath(`/admin/clients/${clientId}`);
  return { projectId: id };
}

export async function archiveProject(projectId: string): Promise<void> {
  await requireAdmin();
  await db.update(projects).set({ archived: true }).where(eq(projects.id, projectId));
  revalidatePath("/admin/projects");
}

export async function unarchiveProject(projectId: string): Promise<void> {
  await requireAdmin();
  await db.update(projects).set({ archived: false }).where(eq(projects.id, projectId));
  revalidatePath("/admin/projects");
}

export async function updateProjectAcceptedTotal(projectId: string, acceptedTotal: string): Promise<void> {
  await requireAdmin();
  await db.update(projects).set({ accepted_total: acceptedTotal }).where(eq(projects.id, projectId));
  revalidatePath(`/admin/projects/${projectId}`);
}

NOTA: Verificare il path di requireAdmin leggendo un altro actions file (es. quote-actions.ts) — usare lo stesso import esatto.

npx tsc --noEmit 2>&1 | head -20

<acceptance_criteria> - src/components/admin/NavBar.tsx contains href="/admin/projects" (grep) - src/components/admin/NavBar.tsx contains href="/admin/impostazioni" (grep) - src/components/admin/ProjectRow.tsx exists e contains ProjectWithPayments (grep) - src/components/admin/ProjectRow.tsx contains totalTrackedSeconds / 3600 (formula €/h) (grep) - src/app/admin/projects/project-actions.ts exports createProject, archiveProject, unarchiveProject, updateProjectAcceptedTotal (grep: grep "export async function" src/app/admin/projects/project-actions.ts) - TypeScript compila senza errori </acceptance_criteria>

NavBar aggiornata, ProjectRow pronto, server actions create

Task 2: /admin/projects list page + /admin/projects/new form + /admin/clients/[id] project cards src/app/admin/projects/page.tsx src/app/admin/projects/new/page.tsx src/app/admin/clients/[id]/page.tsx

<read_first> - src/app/admin/page.tsx — template esatto per la struttura della lista (revalidate, table, map su rows) - src/app/admin/clients/[id]/page.tsx — leggere INTERO FILE: va riscritto per mostrare project cards invece del workspace tab - src/app/admin/catalog/page.tsx — pattern admin page con form inline (per /admin/projects/new) - src/app/admin/clients/[id]/quote-actions.ts — per capire come il form usa server actions con redirect </read_first>

**A. Creare src/app/admin/projects/page.tsx**
import { getAllProjectsWithPayments } from "@/lib/admin-queries";
import { ProjectRow } from "@/components/admin/ProjectRow";
import Link from "next/link";

export const revalidate = 0;

export default async function ProjectsPage() {
  const projects = await getAllProjectsWithPayments();

  return (
    <div>
      <div className="mb-6 flex items-center justify-between">
        <h1 className="text-2xl font-bold text-[#1a1a1a]">Progetti</h1>
        <Link
          href="/admin/projects/new"
          className="text-sm bg-[#1A463C] text-white px-4 py-2 rounded-lg hover:bg-[#1A463C]/90 transition-colors"
        >
          + Nuovo Progetto
        </Link>
      </div>

      {projects.length === 0 ? (
        <div className="bg-white rounded-xl border border-[#e5e7eb] p-12 text-center">
          <p className="text-[#71717a]">Nessun progetto ancora. Creane uno dal dettaglio di un cliente.</p>
        </div>
      ) : (
        <div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
          <table className="w-full">
            <thead className="bg-[#f9f9f9] border-b border-[#e5e7eb]">
              <tr>
                <th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Progetto</th>
                <th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Valore</th>
                <th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Acconto</th>
                <th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Saldo</th>
                <th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">Timer</th>
                <th className="text-left py-3 px-4 text-xs font-semibold text-[#71717a] uppercase tracking-wider">/h</th>
              </tr>
            </thead>
            <tbody>
              {projects.map((project) => (
                <ProjectRow key={project.id} project={project} />
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

B. Creare src/app/admin/projects/new/page.tsx

Form che permette di creare un progetto scegliendo il cliente da una select. Il form si sottomette con createProject e redirige al progetto appena creato.

import { getAllClientsWithPayments } from "@/lib/admin-queries";
import { createProject } from "@/app/admin/projects/project-actions";
import { redirect } from "next/navigation";

export const revalidate = 0;

export default async function NewProjectPage() {
  const clients = await getAllClientsWithPayments();
  const activeClients = clients.filter((c) => !c.archived);

  async function handleCreate(fd: FormData) {
    "use server";
    const result = await createProject(fd);
    redirect(`/admin/projects/${result.projectId}`);
  }

  return (
    <div className="max-w-md mx-auto">
      <div className="mb-6">
        <h1 className="text-2xl font-bold text-[#1a1a1a]">Nuovo Progetto</h1>
        <p className="text-sm text-[#71717a] mt-1">Crea un nuovo progetto per un cliente esistente.</p>
      </div>

      <div className="bg-white rounded-xl border border-[#e5e7eb] p-6">
        <form action={handleCreate} className="space-y-4">
          <div>
            <label htmlFor="name" className="block text-sm font-medium text-[#1a1a1a] mb-1">
              Nome Progetto (Brand)
            </label>
            <input
              id="name"
              name="name"
              type="text"
              required
              placeholder="es. Brand Blu"
              className="w-full border border-[#e5e7eb] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20"
            />
          </div>

          <div>
            <label htmlFor="client_id" className="block text-sm font-medium text-[#1a1a1a] mb-1">
              Cliente
            </label>
            <select
              id="client_id"
              name="client_id"
              required
              className="w-full border border-[#e5e7eb] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#1A463C]/20"
            >
              <option value="">Seleziona cliente...</option>
              {activeClients.map((c) => (
                <option key={c.id} value={c.id}>{c.name}</option>
              ))}
            </select>
          </div>

          <div className="flex gap-3 pt-2">
            <button
              type="submit"
              className="flex-1 bg-[#1A463C] text-white py-2 rounded-lg text-sm font-medium hover:bg-[#1A463C]/90 transition-colors"
            >
              Crea Progetto
            </button>
            <a
              href="/admin/projects"
              className="flex-1 text-center border border-[#e5e7eb] py-2 rounded-lg text-sm text-[#71717a] hover:bg-[#f9f9f9] transition-colors"
            >
              Annulla
            </a>
          </div>
        </form>
      </div>
    </div>
  );
}

C. Riscrivere src/app/admin/clients/[id]/page.tsx

Questo file va RISCRITTO per mostrare project cards invece del workspace tab. Leggere il file corrente per capire gli import e adattarli.

Il nuovo file deve:

  1. Chiamare getClientWithProjects(id) invece di getClientFullDetail(id)
  2. Mostrare le cards dei progetti con link a /admin/projects/[id]
  3. Mostrare un bottone "+ Nuovo Progetto" che naviga a /admin/projects/new?client_id=[id]
  4. Mantenere i link di edit e archivio cliente (ClientActions component se esiste, altrimenti link semplici)
import { notFound } from "next/navigation";
import { getClientWithProjects } from "@/lib/admin-queries";
import Link from "next/link";

export const revalidate = 0;

export default async function ClientDetailPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const data = await getClientWithProjects(id);
  if (!data) notFound();

  const { projects, ...client } = data;
  const activeProjects = projects.filter((p) => !p.archived);
  const archivedProjects = projects.filter((p) => p.archived);

  return (
    <div>
      <div className="mb-4">
        <Link href="/admin" className="text-sm text-[#71717a] hover:text-[#1a1a1a]">
           Clienti
        </Link>
      </div>

      <div className="mb-6 flex items-start justify-between gap-4 flex-wrap">
        <div>
          <h1 className="text-2xl font-bold text-[#1a1a1a]">{client.name}</h1>
          <p className="text-sm text-[#71717a]">{client.brand_name}</p>
        </div>
        <div className="flex gap-2">
          <Link
            href={`/admin/projects/new?client_id=${id}`}
            className="text-sm bg-[#1A463C] text-white px-4 py-2 rounded-lg hover:bg-[#1A463C]/90 transition-colors"
          >
            + Nuovo Progetto
          </Link>
          <Link
            href={`/admin/clients/${id}/edit`}
            className="text-sm border border-[#e5e7eb] px-4 py-2 rounded-lg text-[#71717a] hover:bg-[#f9f9f9] transition-colors"
          >
            Modifica Cliente
          </Link>
        </div>
      </div>

      {activeProjects.length === 0 && (
        <div className="bg-white rounded-xl border border-[#e5e7eb] p-12 text-center">
          <p className="text-[#71717a] mb-4">Nessun progetto ancora per questo cliente.</p>
          <Link
            href={`/admin/projects/new?client_id=${id}`}
            className="text-sm text-[#1A463C] hover:underline"
          >
            + Crea il primo progetto
          </Link>
        </div>
      )}

      {activeProjects.length > 0 && (
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          {activeProjects.map((project) => (
            <Link
              key={project.id}
              href={`/admin/projects/${project.id}`}
              className="bg-white border border-[#e5e7eb] rounded-xl p-5 hover:shadow-md hover:border-[#1A463C]/20 transition-all"
            >
              <h3 className="font-bold text-[#1a1a1a] mb-1">{project.name}</h3>
              <p className="text-sm text-[#71717a]">
                {project.accepted_total && parseFloat(project.accepted_total) > 0
                  ? `€${parseFloat(project.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}`
                  : "Preventivo non impostato"}
              </p>
            </Link>
          ))}
        </div>
      )}

      {archivedProjects.length > 0 && (
        <div className="mt-8">
          <p className="text-xs text-[#71717a] font-semibold uppercase tracking-wider mb-3">
            Archiviati ({archivedProjects.length})
          </p>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4 opacity-60">
            {archivedProjects.map((project) => (
              <Link
                key={project.id}
                href={`/admin/projects/${project.id}`}
                className="bg-white border border-[#e5e7eb] rounded-xl p-5 hover:shadow-md transition-all"
              >
                <h3 className="font-bold text-[#1a1a1a] mb-1">{project.name}</h3>
                <p className="text-xs text-[#71717a]">Archiviato</p>
              </Link>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

NOTA: Se il file corrente ha altri import (ClientActions, tabs, ecc.) che non servono più, rimuoverli per evitare TS errors.

D. Aggiornare /admin/projects/new per gestire il query param client_id

Il link "+ Nuovo Progetto" da /admin/clients/[id] passa ?client_id=[id]. Aggiornare la NewProjectPage per pre-selezionare il cliente se il param è presente:

// Aggiungere searchParams alle props:
export default async function NewProjectPage({
  searchParams,
}: {
  searchParams: Promise<{ client_id?: string }>;
}) {
  const { client_id } = await searchParams;
  // ...
  // Nella select, aggiungere defaultValue o usare selected su ogni option:
  // <option key={c.id} value={c.id} selected={c.id === client_id}>{c.name}</option>
}
npm run build 2>&1 | tail -20

<acceptance_criteria> - src/app/admin/projects/page.tsx exists e contains getAllProjectsWithPayments (grep) - src/app/admin/projects/page.tsx contains ProjectRow (grep) - src/app/admin/projects/new/page.tsx exists e contains createProject (grep) - src/app/admin/clients/[id]/page.tsx contains getClientWithProjects (grep) - src/app/admin/clients/[id]/page.tsx contains href={\/admin/projects/${(grep — link alle cards progetto) - src/app/admin/clients/[id]/page.tsx does NOT containgetClientFullDetail(grep — vecchia funzione rimossa) -npm run buildcompleta senza errori TypeScript - Accedendo a /admin/projects (doponpm run dev`) la pagina carica senza 500 error </acceptance_criteria>

/admin/projects funzionale con lista e form creazione; /admin/clients/[id] mostra project cards

Task 3: /admin/clients list — brand names secondari e LTV per cliente (D-12) src/app/admin/page.tsx src/lib/admin-queries.ts src/components/admin/ClientRow.tsx

<read_first> - src/app/admin/page.tsx — leggere INTERAMENTE: struttura della lista clienti, ClientRow usage, tabella HTML - src/components/admin/ClientRow.tsx — leggere come viene mostrato il nome cliente e l'LTV attuale (accepted_total) - src/lib/admin-queries.ts — leggere getAllClientsWithPayments per capire il tipo corrente ClientWithPayments e la Promise.all interna </read_first>

**A. Aggiornare getAllClientsWithPayments in src/lib/admin-queries.ts (D-12)**

La funzione deve restituire anche i brand names dei progetti e il LTV calcolato come somma degli accepted_total di tutti i progetti del cliente.

Estendere il tipo ClientWithPayments (o aggiungere nuovi campi inline) con:

  • projectBrands: string[] — nomi dei progetti non-archiviati del cliente, ordinati per created_at
  • ltv: string — somma degli accepted_total di TUTTI i progetti del cliente (inclusi archiviati)

Modificare getAllClientsWithPayments per aggiungere una query projects alla Promise.all esistente:

// Aggiungere alla Promise.all dentro getAllClientsWithPayments:
db.select({
  client_id: projects.client_id,
  name: projects.name,
  accepted_total: projects.accepted_total,
  archived: projects.archived,
})
  .from(projects)
  .where(inArray(projects.client_id, clientIds)),

Nel map finale, aggiungere il calcolo:

const clientProjects = allProjects.filter((p) => p.client_id === client.id);
const projectBrands = clientProjects
  .filter((p) => !p.archived)
  .map((p) => p.name);
const ltv = clientProjects
  .reduce((sum, p) => sum + parseFloat(p.accepted_total ?? "0"), 0)
  .toFixed(2);
return {
  ...existingClientObject,
  projectBrands,
  ltv,
};

Assicurarsi che projects sia importato da @/db/schema negli import esistenti (da 04-01 è già presente).

B. Aggiornare src/components/admin/ClientRow.tsx — brand names + LTV colonna (D-12)

Leggere ClientRow.tsx interamente. Aggiungere:

  1. Sotto il nome cliente in bold, aggiungere la riga brand secondaria:
{client.projectBrands && client.projectBrands.length > 0 && (
  <p className="text-xs text-[#71717a] mt-0.5">
    {client.projectBrands.join(" | ")}
  </p>
)}
  1. Per la colonna LTV: sostituire client.accepted_total con client.ltv (che è ora la somma dei progetti). Se la colonna LTV non esiste ancora, aggiungere una colonna con €{parseFloat(client.ltv).toLocaleString("it-IT", { minimumFractionDigits: 2 })}.

  2. Aggiornare il tipo prop di ClientRow per includere i nuovi campi:

// Aggiungere ai campi di ClientWithPayments usati da ClientRow:
projectBrands: string[];
ltv: string;

Se ClientRow usa ClientWithPayments importato da admin-queries, il tipo sarà aggiornato automaticamente dalla modifica in A. Verificare che TypeScript non si lamenti.

npm run build 2>&1 | tail -20

<acceptance_criteria> - src/lib/admin-queries.ts contains projectBrands (grep: grep "projectBrands" src/lib/admin-queries.ts) - src/components/admin/ClientRow.tsx contains projectBrands.join (grep) - src/components/admin/ClientRow.tsx contains client.ltv (grep) - npm run build completa senza errori TypeScript - Visitando /admin ogni riga cliente mostra i brand names sotto il nome (es. "Brand Blu | Brand Verde") e la colonna LTV mostra la somma degli accepted_total di tutti i progetti </acceptance_criteria>

Lista /admin/clients mostra brand names secondari sotto nome cliente e LTV calcolato come somma dei progetti — D-12 implementato

<threat_model>

Trust Boundaries

Boundary Description
Admin browser → Server Actions createProject, archiveProject, updateProjectAcceptedTotal chiamati da form con requireAdmin()
Admin → /admin/projects/[id] Link navigazione — il workspace progetto (04-03) avrà il suo guard; questo piano non espone dati sensibili

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-04-05 Elevation of Privilege createProject server action mitigate requireAdmin() all'inizio di ogni server action — verifica sessione Auth.js prima di qualsiasi DB write
T-04-06 Tampering archiveProject / updateProjectAcceptedTotal mitigate requireAdmin() guarda entrambe le actions; projectId viene da path param (non da query string non validata)
T-04-07 Information Disclosure /admin/clients/[id] project cards accept Dati mostrati sono solo nome progetto e accepted_total — nessun dato sensibile (quote_items mai esposti)
T-04-08 Tampering createProject con client_id da form mitigate Action verifica che il client_id esista nel DB prima di inserire — previene inserimento di progetti orfani su client_id inventato
</threat_model>
```bash # 1. NavBar has new links grep "admin/projects\|admin/impostazioni" src/components/admin/NavBar.tsx

2. ProjectRow exists and has formula

grep "totalTrackedSeconds / 3600" src/components/admin/ProjectRow.tsx

3. Server actions have requireAdmin

grep "requireAdmin" src/app/admin/projects/project-actions.ts

4. Client detail uses new query

grep "getClientWithProjects" src/app/admin/clients/[id]/page.tsx

5. Build clean

npm run build

</verification>

<success_criteria>
- /admin/projects mostra tabella vuota (o con dati se il seed ha creato progetti) senza errori
- /admin/projects/new mostra form con select clienti
- /admin/clients/[id] mostra grid cards progetti con bottone "+ Nuovo Progetto"
- Cliccando una card naviga a /admin/projects/[id] (che mostra 404 finché 04-03 non crea la pagina)
- `npm run build` passa senza errori TypeScript
</success_criteria>

<output>
After completion, create `.planning/phases/04-progetti-multi-project/04-02-SUMMARY.md` following the template at `@/Users/simonecavalli/.claude/get-shit-done/templates/summary.md`.

Key items to document:
- Nuovi file creati e loro funzione
- Come viene passato il client_id pre-selezionato nel form nuovo progetto
- Eventuali componenti legacy rimossi da clients/[id]/page.tsx
</output>