--- phase: 04-progetti-multi-project plan: "02" type: execute wave: 2 depends_on: - 04-01-PLAN.md files_modified: - 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 autonomous: true requirements: - PROJ-01 - PROJ-03 must_haves: truths: - "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" artifacts: - path: "src/components/admin/NavBar.tsx" provides: "NavBar con link Progetti e Impostazioni" contains: "href=\"/admin/projects\"" - path: "src/components/admin/ProjectRow.tsx" provides: "Riga progetto per la lista /admin/projects" contains: "ProjectWithPayments" - path: "src/app/admin/projects/page.tsx" provides: "Pagina lista tutti i progetti" contains: "getAllProjectsWithPayments" - path: "src/app/admin/projects/new/page.tsx" provides: "Form creazione progetto con selezione cliente" contains: "createProject" - path: "src/app/admin/projects/project-actions.ts" provides: "Server actions: createProject, archiveProject, updateProjectAcceptedTotal" contains: "export async function createProject" - path: "src/app/admin/clients/[id]/page.tsx" provides: "Pagina cliente modificata per mostrare project cards" contains: "getClientWithProjects" key_links: - from: "src/app/admin/projects/page.tsx" to: "src/lib/admin-queries.ts" via: "getAllProjectsWithPayments()" pattern: "getAllProjectsWithPayments" - from: "src/app/admin/clients/[id]/page.tsx" to: "src/lib/admin-queries.ts" via: "getClientWithProjects(id)" pattern: "getClientWithProjects" - from: "src/components/admin/ProjectRow.tsx" to: "src/app/admin/timer-actions.ts" via: "TimerCell with project_id" pattern: "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. @/Users/simonecavalli/.claude/get-shit-done/workflows/execute-plan.md @/Users/simonecavalli/.claude/get-shit-done/templates/summary.md @/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): ```typescript 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; export async function getClientWithProjects(clientId: string): Promise; 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): ```typescript // 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 - 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) **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: ``. **B. Creare src/components/admin/ProjectRow.tsx** Clonare ClientRow.tsx sostituendo: - `ClientWithPayments` → `ProjectWithPayments` (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: `` - 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: ``. Usare gli stessi statusConfig di ClientRow per i badge pagamento. **C. Creare src/app/admin/projects/project-actions.ts** ```typescript "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 { await requireAdmin(); await db.update(projects).set({ archived: true }).where(eq(projects.id, projectId)); revalidatePath("/admin/projects"); } export async function unarchiveProject(projectId: string): Promise { 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 { 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 - 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 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 - 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 **A. Creare src/app/admin/projects/page.tsx** ```typescript 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 (

Progetti

+ Nuovo Progetto
{projects.length === 0 ? (

Nessun progetto ancora. Creane uno dal dettaglio di un cliente.

) : (
{projects.map((project) => ( ))}
Progetto Valore Acconto Saldo Timer €/h
)}
); } ``` **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. ```typescript 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 (

Nuovo Progetto

Crea un nuovo progetto per un cliente esistente.

Annulla
); } ``` **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) ```typescript 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 (
← Clienti

{client.name}

{client.brand_name}

+ Nuovo Progetto Modifica Cliente
{activeProjects.length === 0 && (

Nessun progetto ancora per questo cliente.

+ Crea il primo progetto
)} {activeProjects.length > 0 && (
{activeProjects.map((project) => (

{project.name}

{project.accepted_total && parseFloat(project.accepted_total) > 0 ? `€${parseFloat(project.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}` : "Preventivo non impostato"}

))}
)} {archivedProjects.length > 0 && (

Archiviati ({archivedProjects.length})

{archivedProjects.map((project) => (

{project.name}

Archiviato

))}
)}
); } ``` 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: ```typescript // 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: // } ```
npm run build 2>&1 | tail -20 - 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 contain `getClientFullDetail` (grep — vecchia funzione rimossa) - `npm run build` completa senza errori TypeScript - Accedendo a /admin/projects (dopo `npm run dev`) la pagina carica senza 500 error /admin/projects funzionale con lista e form creazione; /admin/clients/[id] mostra project cards
## 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 | ```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 ``` - /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 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