From 4aae2e0d0f304bd81f5f5a4a533709aa5ceff1ea Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sun, 17 May 2026 11:43:49 +0200 Subject: [PATCH] feat(03-02): catalog page + ServiceTable + ServiceForm + NavBar link - Create src/app/admin/catalog/page.tsx: server component, fetches all services, renders ServiceForm + ServiceTable - Create src/components/admin/catalog/ServiceForm.tsx: add-new-service form with open/collapse toggle - Create src/components/admin/catalog/ServiceTable.tsx: per-row inline edit (DocumentRow pattern), Attivo/Disattivato badges, opacity-50 for inactive - Modify src/components/admin/NavBar.tsx: add Catalogo link after Statistiche - TypeScript clean, npm run build compiled successfully --- src/app/admin/catalog/page.tsx | 29 ++++ src/components/admin/NavBar.tsx | 3 + src/components/admin/catalog/ServiceForm.tsx | 94 ++++++++++ src/components/admin/catalog/ServiceTable.tsx | 162 ++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 src/app/admin/catalog/page.tsx create mode 100644 src/components/admin/catalog/ServiceForm.tsx create mode 100644 src/components/admin/catalog/ServiceTable.tsx diff --git a/src/app/admin/catalog/page.tsx b/src/app/admin/catalog/page.tsx new file mode 100644 index 0000000..0206e20 --- /dev/null +++ b/src/app/admin/catalog/page.tsx @@ -0,0 +1,29 @@ +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 ( +
+
+

Catalogo Servizi

+
+ +
+ +
+ + {services.length === 0 ? ( +

+ Nessun servizio nel catalogo. Aggiungi il primo servizio qui sopra. +

+ ) : ( + + )} +
+ ); +} diff --git a/src/components/admin/NavBar.tsx b/src/components/admin/NavBar.tsx index 687e534..d42f96a 100644 --- a/src/components/admin/NavBar.tsx +++ b/src/components/admin/NavBar.tsx @@ -15,6 +15,9 @@ export function NavBar() { Statistiche + + Catalogo + + ); + } + + return ( +
+

Nuovo servizio

+
+
+ + +
+
+ + +
+
+ + +
+ {error &&

{error}

} +
+ + +
+
+
+ ); +} diff --git a/src/components/admin/catalog/ServiceTable.tsx b/src/components/admin/catalog/ServiceTable.tsx new file mode 100644 index 0000000..e418034 --- /dev/null +++ b/src/components/admin/catalog/ServiceTable.tsx @@ -0,0 +1,162 @@ +"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(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 ( + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ {error &&

{error}

} +
+ + +
+
+ + + ); + } + + return ( + + {service.name} + + {service.description ?? "—"} + + + €{parseFloat(service.unit_price).toLocaleString("it-IT", { minimumFractionDigits: 2 })} + + + {service.active ? ( + + Attivo + + ) : ( + + Disattivato + + )} + + +
+ + +
+ + + ); +} + +export function ServiceTable({ services }: { services: ServiceCatalog[] }) { + return ( +
+ + + + + + + + + + + + {services.map((s) => ( + + ))} + +
NomeDescrizionePrezzoStato
+
+ ); +}