From ef05d647fe9f1745d383a26ce738a63b936a8ed9 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Fri, 22 May 2026 08:45:10 +0200 Subject: [PATCH] feat(04-02): admin projects list + client detail project cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NavBar: add Progetti (/admin/projects) and Impostazioni links - ProjectRow: project table row with €/h calc (accepted_total / tracked hours) - /admin/projects: table listing all projects with value, payments, timer, €/h - /admin/projects/new: create form with client select + ?client_id pre-selection - project-actions: createProject, archiveProject, unarchiveProject, updateProjectAcceptedTotal - /admin/clients/[id]: rewritten to show project cards grid instead of tabbed workspace - getAllClientsWithPayments: add projectBrands[] and ltv (sum of project accepted_totals) - ClientRow: show project brand names under client name, LTV column replaces Totale Co-Authored-By: Claude Sonnet 4.6 --- src/app/admin/clients/[id]/page.tsx | 111 ++++++++++++---------- src/app/admin/page.tsx | 2 +- src/app/admin/projects/new/page.tsx | 81 ++++++++++++++++ src/app/admin/projects/page.tsx | 49 ++++++++++ src/app/admin/projects/project-actions.ts | 55 +++++++++++ src/components/admin/ClientRow.tsx | 8 +- src/components/admin/NavBar.tsx | 6 ++ src/components/admin/ProjectRow.tsx | 65 +++++++++++++ src/lib/admin-queries.ts | 24 ++++- 9 files changed, 347 insertions(+), 54 deletions(-) create mode 100644 src/app/admin/projects/new/page.tsx create mode 100644 src/app/admin/projects/page.tsx create mode 100644 src/app/admin/projects/project-actions.ts create mode 100644 src/components/admin/ProjectRow.tsx diff --git a/src/app/admin/clients/[id]/page.tsx b/src/app/admin/clients/[id]/page.tsx index 13718a8..95badfa 100644 --- a/src/app/admin/clients/[id]/page.tsx +++ b/src/app/admin/clients/[id]/page.tsx @@ -1,12 +1,5 @@ import { notFound } from "next/navigation"; -import { getClientFullDetail } from "@/lib/admin-queries"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { PhasesTab } from "@/components/admin/tabs/PhasesTab"; -import { PaymentsTab } from "@/components/admin/tabs/PaymentsTab"; -import { DocumentsTab } from "@/components/admin/tabs/DocumentsTab"; -import { CommentsTab } from "@/components/admin/tabs/CommentsTab"; -import { QuoteTab } from "@/components/admin/tabs/QuoteTab"; -import { PhasesViewToggle } from "@/components/admin/kanban/PhasesViewToggle"; +import { getClientWithProjects } from "@/lib/admin-queries"; import { ClientActions } from "@/components/admin/ClientActions"; import Link from "next/link"; @@ -18,10 +11,12 @@ export default async function ClientDetailPage({ params: Promise<{ id: string }>; }) { const { id } = await params; - const detail = await getClientFullDetail(id); - if (!detail) notFound(); + const data = await getClientWithProjects(id); + if (!data) notFound(); - const { client, phases, payments, documents, comments, quoteItems, activeServices } = detail; + const { projects, ...client } = data; + const activeProjects = projects.filter((p) => !p.archived); + const archivedProjects = projects.filter((p) => p.archived); return (
@@ -41,7 +36,13 @@ export default async function ClientDetailPage({ )}
-
+
+ + + Nuovo Progetto +
- - - Fasi & Task - Pagamenti - Documenti - Commenti - Preventivo - + {activeProjects.length === 0 && ( +
+

Nessun progetto ancora per questo cliente.

+ + + Crea il primo progetto + +
+ )} - - } - phases={phases} - clientId={client.id} - /> - - - - - - - - - - - - - -
+ {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

+ + ))} +
+
+ )}
); } \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 69c29e4..e78f9e8 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -48,7 +48,7 @@ export default async function AdminDashboard({ Cliente - Totale + LTV Acconto Saldo Timer diff --git a/src/app/admin/projects/new/page.tsx b/src/app/admin/projects/new/page.tsx new file mode 100644 index 0000000..08a1db4 --- /dev/null +++ b/src/app/admin/projects/new/page.tsx @@ -0,0 +1,81 @@ +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({ + searchParams, +}: { + searchParams: Promise<{ client_id?: string }>; +}) { + const { client_id } = await searchParams; + 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.

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/admin/projects/page.tsx b/src/app/admin/projects/page.tsx new file mode 100644 index 0000000..b79b9f0 --- /dev/null +++ b/src/app/admin/projects/page.tsx @@ -0,0 +1,49 @@ +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) => ( + + ))} + +
ProgettoValoreAccontoSaldoTimer€/h
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/admin/projects/project-actions.ts b/src/app/admin/projects/project-actions.ts new file mode 100644 index 0000000..f509845 --- /dev/null +++ b/src/app/admin/projects/project-actions.ts @@ -0,0 +1,55 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { db } from "@/db"; +import { projects, clients } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { nanoid } from "nanoid"; + +async function requireAdmin() { + const session = await getServerSession(authOptions); + if (!session) throw new Error("Non autorizzato"); +} + +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"); + + 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}`); +} \ No newline at end of file diff --git a/src/components/admin/ClientRow.tsx b/src/components/admin/ClientRow.tsx index 07dcdc1..6cda1ca 100644 --- a/src/components/admin/ClientRow.tsx +++ b/src/components/admin/ClientRow.tsx @@ -22,7 +22,11 @@ export function ClientRow({ client }: { client: ClientWithPayments }) { > {client.name} -

{client.brand_name}

+ {client.projectBrands && client.projectBrands.length > 0 ? ( +

{client.projectBrands.join(" | ")}

+ ) : ( +

{client.brand_name}

+ )} {client.archived && ( Archiviato @@ -30,7 +34,7 @@ export function ClientRow({ client }: { client: ClientWithPayments }) { )} - €{parseFloat(client.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })} + €{parseFloat(client.ltv).toLocaleString("it-IT", { minimumFractionDigits: 2 })} {acconto && ( diff --git a/src/components/admin/NavBar.tsx b/src/components/admin/NavBar.tsx index d42f96a..7162484 100644 --- a/src/components/admin/NavBar.tsx +++ b/src/components/admin/NavBar.tsx @@ -12,12 +12,18 @@ export function NavBar() { Clienti + + Progetti + Statistiche Catalogo + + Impostazioni +