From dd4ae425427b16fb417b3fc5ab0fd4d796298f14 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sun, 12 Jul 2026 11:53:41 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20pagina=20Preventivi=20redesign=20Quiet?= =?UTF-8?q?=20Luxury=20=E2=80=94=20tabella=20tokenizzata=20+=20badge=20sta?= =?UTF-8?q?to=20dual-mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- design-reference/DESIGN-SYSTEM.md | 19 ++++ src/app/admin/preventivi/page.tsx | 172 ++++++++++++++++++------------ 2 files changed, 124 insertions(+), 67 deletions(-) diff --git a/design-reference/DESIGN-SYSTEM.md b/design-reference/DESIGN-SYSTEM.md index 3540dc0..fef7eb5 100644 --- a/design-reference/DESIGN-SYSTEM.md +++ b/design-reference/DESIGN-SYSTEM.md @@ -263,6 +263,25 @@ righe `hover:bg-muted/40` / `divide` via `border-border`. sono stati tokenizzati — erano rimasti con hex hardcoded mentre `OptionMultiSelect` era già token-based. Il fix vale per tutte le pagine che li usano (Offerte, Lead). +### Preventivi (lista) notes + +`/admin/preventivi` migrata a "Quiet Luxury" (replica `design-reference/pagina-preventivi`). +Tabella in contenitore `bg-card rounded-xl border-border shadow-card overflow-hidden`, header +uppercase muted (`bg-muted/50`, `text-[11px] font-semibold uppercase tracking-wider`), righe +`hover:bg-muted/40` con `divide-y divide-border`. + +- **Header**: `PageHeader` "Preventivi" + sottotitolo, azione `Button` "Genera preventivo" + (`+`, link a `/admin/preventivi/genera`). +- **Colonne**: Titolo (link → dettaglio, `font-semibold hover:text-primary`), Lead/Cliente, + Offerta (muted), Stato (badge centrato), Data (`font-mono text-xs`), Azione. +- **Badge stato**: pill rounded-full `text-[10px] uppercase border` con dual light/dark — + Bozza→amber, Pubblicato→sky, Accettato→emerald, Rifiutato→red (stesso linguaggio di + `StatusBadge`; label ITA mappate localmente perché gli stati proposal ≠ lead stages). +- **Azione**: solo per `published`/`accepted` link "Apri →" (`ArrowRight` con group-hover + translate) al preventivo pubblico `/preventivo/` in new tab; altrimenti `—` muted. +- **Empty state**: `border-dashed border-border rounded-xl`, `FileText` muted + CTA + "Genera il primo preventivo →". Footer "Mostrando N di N preventivi generati". + ### Client Portal restyle notes The `/client/[token]` portal was migrated to the same "Quiet Luxury" tokens diff --git a/src/app/admin/preventivi/page.tsx b/src/app/admin/preventivi/page.tsx index 07507b8..78a4ea0 100644 --- a/src/app/admin/preventivi/page.tsx +++ b/src/app/admin/preventivi/page.tsx @@ -1,28 +1,44 @@ import Link from "next/link"; import { listProposals } from "@/lib/proposal/queries"; -import { FileText, Plus } from "lucide-react"; +import { FileText, Plus, ArrowRight } from "lucide-react"; import { PageHeader } from "@/components/admin/PageHeader"; +import { Button } from "@/components/ui/button"; export const revalidate = 0; -const STATE_LABELS: Record = { - draft: { label: "Bozza", className: "bg-yellow-100 text-yellow-800" }, - published: { label: "Pubblicato", className: "bg-blue-100 text-blue-800" }, - accepted: { label: "Accettato", className: "bg-green-100 text-green-800" }, - rejected: { label: "Rifiutato", className: "bg-red-100 text-red-800" }, +const STATE_STYLES: Record = { + draft: { + label: "Bozza", + className: + "bg-amber-50 text-amber-600 border-amber-100 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-900", + }, + published: { + label: "Pubblicato", + className: + "bg-sky-50 text-sky-700 border-sky-100 dark:bg-sky-950/40 dark:text-sky-300 dark:border-sky-900", + }, + accepted: { + label: "Accettato", + className: + "bg-emerald-50 text-emerald-600 border-emerald-100 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900", + }, + rejected: { + label: "Rifiutato", + className: + "bg-red-50 text-red-600 border-red-100 dark:bg-red-950/40 dark:text-red-300 dark:border-red-900", + }, }; export default async function PreventiviPage() { const proposals = await listProposals(); const preventiviAction = ( - - - Genera preventivo - + ); return ( @@ -33,69 +49,91 @@ export default async function PreventiviPage() { action={preventiviAction} /> - {/* Lista */} {proposals.length === 0 ? ( -
- -

Nessun preventivo ancora.

+
+ +

Nessun preventivo ancora.

Genera il primo preventivo →
) : ( -
- - - - - - - - - - - - {proposals.map((p) => { - const stateInfo = STATE_LABELS[p.state] ?? { label: p.state, className: "bg-gray-100 text-gray-700" }; - const subject = p.clientName ?? p.leadName ?? "—"; - return ( - - - - - - - + + + + + + + ); + })} + +
TitoloLead / ClienteOffertaStatoData -
- - {p.title ?? "Senza titolo"} - - {subject}{p.offerName} - - {stateInfo.label} - - - {p.createdAt.toLocaleDateString("it-IT")} - - {p.state === "published" || p.state === "accepted" ? ( - +
+ + + + + + + + + + + + + {proposals.map((p) => { + const stateInfo = + STATE_STYLES[p.state] ?? { + label: p.state, + className: "bg-muted text-muted-foreground border-border", + }; + const subject = p.clientName ?? p.leadName ?? "—"; + const isPublic = p.state === "published" || p.state === "accepted"; + return ( + + - - ); - })} - -
TitoloLead / ClienteOffertaStatoDataAzione
+ - Apri → - - ) : null} -
+ {p.title ?? "Senza titolo"} + +
{subject}{p.offerName} + + {stateInfo.label} + + + {p.createdAt.toLocaleDateString("it-IT")} + + {isPublic ? ( + + Apri + + + ) : ( + + )} +
+
+ +
+ Mostrando {proposals.length} di {proposals.length}{" "} + {proposals.length === 1 ? "preventivo generato" : "preventivi generati"} +
)}