Files
simone 4403b39e63 docs: task cancellati e date dei pagamenti — STATUS, STATE, design system
STATUS.md: perché la pill sta su "Cancellata" e non su "Fatto" (sono entrambi
barrati, e scambiarli significa credere consegnato ciò che non esiste), perché
`due_date` è nullable, e le due verifiche che restano a mano — il riquadro
"Prossimo pagamento" non compare finché nessuna rata ha una scadenza, e due
`paid_at` storici valgono il primo del mese perché li ha scritti il vecchio
selettore a mese.

DESIGN-SYSTEM.md: la quinta forma, la regola della colonna Kanban che si
nasconde solo da vuota, e la spec del box pagamenti.

STATE.md: due decisioni nuove, e resta sotto le 100 righe accorpando le righe
di tabella della stessa settimana.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:51:51 +02:00

30 KiB
Raw Permalink Blame History

Design System: iamcavalli Admin & Client Portal (v1.0)

"Minimalist Premium / Quiet Luxury" — the aesthetic that replaces the old mixed token/hardcoded styling across ClientHub's admin area. First applied to the Lead Pipeline page (Phase: Pipeline redesign); the tokens and primitives documented here are the base for restyling every other section.

Philosophy

  • Quiet, not loud. Elevation is nearly invisible (shadow opacity ≤ 0.02), borders are hairline, color is used sparingly and only to carry meaning (status, semantics) — never for decoration.
  • Density with air. Generous padding (py-4 px-6 in tables, p-4 in kanban columns) paired with small type (text-xs, text-[11px]) reads as premium rather than cramped.
  • Numbers are monospace. Prices, phone numbers, dates, counts — anything tabular/numeric — use font-mono and right-alignment so columns of digits line up.
  • Dual-theme by construction. Every surface is built from semantic tokens (bg-card, text-muted-foreground, border-border, …), never raw Tailwind palette classes (bg-white, text-slate-900, …) or hex literals. This is what makes dark mode "just work" without a parallel dark stylesheet.
  • Motion is functional. Transitions exist to explain state change (sidebar collapse, hover, drag) — duration-200duration-350, ease-[cubic-bezier(0.4,0,0.2,1)] for the sidebar specifically. No decorative animation.

Typography

  • Sans: Plus Jakarta Sans (weights 300700), loaded via next/font/google as --font-plus-jakarta-sans, mapped to Tailwind's --font-sans in @theme. Used for all UI text.
  • Mono: Geist Mono, --font-geist-mono--font-mono. Used for numeric data cells only (prices, phone numbers, counts, dates in tables).
  • Scale conventions:
    • Page title: text-2xl font-semibold tracking-tight
    • Page subtitle: text-xs text-muted-foreground
    • Table header cells: text-[11px] font-semibold uppercase tracking-wider text-muted-foreground
    • Table body: text-sm
    • Badge/pill label: text-[10px]text-xs font-semibold uppercase tracking-wide

Color Tokens

Raw values live in :root (light) / .dark (dark) in src/app/globals.css and are mapped to Tailwind utilities via @theme inline. Components must consume the mapped utility, never the raw hex or a Tailwind palette shade.

Utility Light Dark Usage
bg-background / text-foreground #ffffff / #1a1a1a #0e1512 / #f2f4f3 Page canvas
bg-card / text-card-foreground #ffffff #131a16 Cards, table, kanban cards, header bar
bg-muted / text-muted-foreground #f9f9f9 / #71717a #171f1b / #9aa39e Subtle backgrounds, secondary text, kanban columns
bg-primary / text-primary-foreground #1A463C #3FA88C Primary actions, sidebar, active nav state
bg-accent #DEF168 #DEF168 Rare highlight accent (lime)
border-border / border-input #e5e7eb #26302b All hairline borders
ring-ring #1A463C #3FA88C Focus rings
bg-destructive #ef4444 #f87171 Destructive actions

Sidebar exception: the sidebar stays brand green bg-[#1A463C] in both themes (it is not a themed surface — it's the constant brand anchor).

Status/semantic colors (lead stages, badges) use Tailwind's default palette (blue/purple/amber/orange/emerald/red) directly, each paired with an explicit dark: variant for legibility on dark surfaces — see StatusBadge.

Elevation & Radius

  • --radius: 0.5rem (base) is wired into @theme as --radius-lg (= --radius) and --radius-xl (= --radius + 0.25rem), so rounded-lg / rounded-xl utilities resolve consistently off one token.
  • --shadow-card: 0 4px 20px rgba(0,0,0,0.01)shadow-card utility. Default resting elevation for cards, table containers, kanban cards.
  • --shadow-card-hover: 0 8px 30px rgba(0,0,0,0.02)shadow-card-hover utility. Applied on hover for interactive cards.

Layout Conventions

  • Table container: bg-card rounded-xl border border-border shadow-card overflow-hidden.
  • Table header row: bg-muted/50 border-b border-border text-[11px] uppercase tracking-wider text-muted-foreground.
  • Table body rows: py-4 px-6, divide-y divide-border, hover:bg-muted/40 transition-colors.
  • Kanban column: bg-muted/60 rounded-xl border border-border p-4.
  • Kanban card: bg-card rounded-lg border border-border shadow-sm hover:border-primary/30 hover:shadow-card-hover transition-all.
  • Page header: title text-2xl font-semibold tracking-tight text-foreground, subtitle text-xs text-muted-foreground mt-1, primary action button bg-primary text-primary-foreground hover:bg-primary/90.

Color/Class Migration Map (dual-theme principle)

When restyling any surface, replace hardcoded classes with token utilities:

Old (hardcoded) New (token)
bg-white bg-card
bg-slate-50 / bg-gray-50 bg-muted
border-slate-100 / border-slate-200 border-border
text-slate-900 / text-[#1a1a1a] text-foreground
text-slate-500 / text-slate-400 / text-[#71717a] text-muted-foreground
bg-brand-dark / text-[#1A463C] (as action color) bg-primary / text-primary

Motion

  • Sidebar collapse: transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)] on the <aside>; label/logo text use whitespace-nowrap transition-opacity duration-200 and fade to opacity-0 immediately on collapse, but only fade back in ~150ms after the width transition starts on expand (avoids text reflow/wrap during the animation).
  • Hover states: transition-colors duration-150duration-200 on rows, buttons, nav links.
  • Kanban drag: dragged card renders in a DragOverlay with rotate-1 shadow-xl; drop target column highlights via border-primary bg-primary/5.

UX Rules

  1. Numeric/tabular data is always monospace and right-aligned in tables.
  2. Every interactive control has a visible focus state (focus:ring-1 focus:ring-ring focus:border-primary pattern) for keyboard accessibility.
  3. Status is always communicated redundantly — color + text label — never color alone.
  4. Destructive actions use destructive tokens, never ad-hoc red.
  5. Empty states are muted, centered, and brief ("Nessun lead trovato").
  6. Sidebar collapse state persists across page loads via localStorage (iamcavalli:sidebar) and is restored before paint where possible to avoid layout jump.

Component Inventory

Primitives extracted from the Lead Pipeline redesign, intended for reuse across all future admin pages.

Component Path Purpose
AdminShell src/components/admin/AdminShell.tsx Client shell holding sidebar-collapsed state (persisted to localStorage), renders sidebar + top header + <main>. Wraps all /admin/* pages.
AdminSidebar src/components/admin/AdminSidebar.tsx Collapsible brand sidebar (w-64w-20), controlled by AdminShell. Nav items, theme toggle, sign-out.
StatusBadge src/components/ui/StatusBadge.tsx Rounded-full status pill with a color map per lead stage, each with a dark: variant. Replaces the old scattered STAGE_COLOR maps.
SearchInput src/components/ui/SearchInput.tsx Search-icon input, rounded-lg border-border focus:ring-primary.
SegmentedToggle src/components/ui/SegmentedToggle.tsx Generic Lista/Kanban-style segmented control (bg-muted p-1 rounded-lg, active option bg-card shadow-sm).
PageHeader src/components/admin/PageHeader.tsx Page title (text-2xl tracking-tight) + subtitle + action slot, token-based.
LeadTable src/components/admin/leads/LeadTable.tsx Luxury table restyle: bg-card rounded-xl shadow-card, uppercase muted headers, hover:bg-muted/40 rows, uses StatusBadge.
LeadsKanbanBoard src/components/admin/leads/LeadsKanbanBoard.tsx Kanban restyle: bg-muted/60 columns with count pills, bg-card cards with hover:border-primary/30. Uses @dnd-kit, keeps all 6 lead stages.
LeadsViewToggle src/components/admin/leads/LeadsViewToggle.tsx Wires SearchInput + SegmentedToggle together with the table/kanban views.
MilestoneStepper src/components/client/MilestoneStepper.tsx Client-portal horizontal milestone stepper: one node per real phase, connecting line filled by global_progress_pct. Done = filled bg-primary ✓, active = border-primary outline, upcoming = bg-muted. Layout compatto (py-3, due sole righe di testo: Step N + stato in linea, titolo fase sotto) — mock: design-reference/Client-Portal-Progress-Bar.
LeadDetail src/components/admin/leads/LeadDetail.tsx Lead detail page (/admin/pipeline/[id]): bespoke header (name + inline StatusBadge + action bar), asymmetric lg:grid-cols-3 layout (1/3 profile card w/ icon-chip rows + tags, 2/3 Note/Follow-up + unified timeline).
ClientProfitability src/components/admin/dashboard/ClientProfitability.tsx Dashboard widget: valore orario reale per cliente (getClientProfitability → contrattualizzato ÷ ore tracciate) con badge margine (Ottimo bg-primary/10 text-primary / In Linea bg-muted).
YearSelector src/components/admin/YearSelector.tsx Selettore anno restilizzato a pill (rounded-full border-border bg-card shadow-card), frecce ←/→, valore font-mono. Guida i dati year-scoped della dashboard via ?year=.
ConversationsView src/components/admin/conversazioni/ConversationsView.tsx Inbox messaggi clienti (/admin/conversazioni) a due pannelli: lista conversazioni filtrabile (search su nome/brand/ultimo messaggio, pallino unread) + thread attivo con tab per canale (Generale + una per fase, pallino accent sul tab non letto) e bolle messaggio (admin a destra bg-primary, cliente a sinistra). Badge entità solo sui messaggi storici di task/deliverable. La risposta va sull'entità del canale attivo. Auto-mark-read all'apertura, su snapshot di adminLastReadAt.
ChatPanel src/components/client/ChatPanel.tsx Chat del portale cliente: FAB h-14 w-14 fisso in basso a destra con pallino unread, drawer laterale fixed inset-y-0 right-0 bg-card shadow-2xl che in expanded diventa full-screen (Maximize2/Minimize2, Esc chiude prima l'espansione poi il pannello). Tab canale scrollabili orizzontalmente (no-scrollbar), invio e modifica ottimistici, polling GET /api/client/chat ogni 20s. La firma di chi risponde arriva da settings via ChatData, non è più cablata.
MessageRow src/components/client/ChatPanel.tsx Una riga del feed, componente a sé perché la modifica ha bisogno di stato per messaggio (bozza, salvataggio, errore). Divider di giornata, raggruppamento dei messaggi consecutivi dello stesso autore entro GROUP_WINDOW_MS, badge entità per lo storico di task/deliverable, «(modificato)» inline dopo il corpo.
Avatar (chat) src/components/client/ChatPanel.tsx Cerchio h-9 w-9 con immagine da URL esterno e fallback al monogramma se src manca o il caricamento fallisce (onError). <img> nudo e non next/image: l'URL lo incolla l'utente in Impostazioni e non è noto in fase di build, quindi non si può dichiarare l'host in next.config.
ChatProvider src/components/client/ChatProvider.tsx Context del solo stato UI (aperto/espanso/canale attivo) — i messaggi restano dentro ChatPanel, unico a fare polling e a tenere la copia optimistic. Il canale sta nel context perché lo decide anche chi è fuori dal pannello: openChat(phaseId) dalla bolla di una PhaseCard apre la chat già sulla fase giusta.
MessagesWidget src/components/admin/dashboard/MessagesWidget.tsx Widget dashboard "Messaggi Clienti": chat clienti in attesa di risposta (getConversations() filtrato su unread), pill emerald "N Nuovi" + pallino pulsante, righe con anteprima e link Rispondi → deep-link a /admin/conversazioni?c=<clientId>. Empty state "Nessun messaggio in attesa ✓".
CopyLinkButton src/components/admin/CopyLinkButton.tsx Icon button che copia un URL assoluto (window.location.origin + path) negli appunti; swap icona → check emerald per 1.5s con tooltip "Copiato!". Usato nella lista Clienti per il link profilo pubblico.
ClientRow src/components/admin/ClientRow.tsx Riga tabella Clienti restilizzata Quiet Luxury: nome link → dettaglio + sottotitolo brand/progetti, colonne numeriche font-mono right-aligned (LTV, incassato come pill emerald, da saldare), cella "Link profilo" con pill mono troncata (apre portale in new tab) + CopyLinkButton.

Unified timeline pattern: activities + transcripts merged into one reverse-chronological "Storico delle Interazioni". Vertical connector = absolute w-px bg-border at left-[19px] behind w-10 h-10 rounded-full nodes. Transcript node is highlighted (bg-primary text-primary-foreground) and expandable (line-clamp-2 → "Mostra tutto/meno"); activity nodes are muted (bg-muted border-border). Dates are font-mono — activities relative (formatDistanceToNow), transcripts absolute (d MMMM yyyy).

Admin Dashboard restyle notes

The /admin dashboard was migrated to "Quiet Luxury" (applied 2026-07-11), replicating design-reference/pagina-dashboard.html. It is a condensed single-screen layout: no in-<main> PageHeader (sidebar + shell header carry the chrome).

  • KPI strip: 4 bg-card metric cards (Clienti Attivi · Contrattualizzato · Incassato Reale · Ore Tracciate). Value text-2xl font-bold font-mono, label text-[10px] uppercase text-muted-foreground. Positive delta uses text-emerald-600 dark:text-emerald-400.
  • Griglia lg:grid-cols-3: 2/3 col = ForecastChart (istogramma flussi 12 mesi, mese prossimo evidenziato bg-primary) + ClientProfitability; 1/3 col = FollowUpWidget (count pill amber) + MessagesWidget ("Messaggi Clienti", pill emerald "N Nuovi" + pallino pulsante, chat clienti in attesa di risposta) + OffersSoldChart ("Offerte Più Richieste").
  • Year selector interattivo (YearSelector) in una toolbar top-right; governa i KPI year-scoped + redditività. Il forecast è rolling-12-mesi (non year-scoped).
  • Widget rimossi dal legacy: chart incassi mensili, card "Da incassare"/"Clienti acquisiti", barre "Ore per cliente" (sostituite dalla redditività oraria).

Chat a canali (portale + inbox) notes

I messaggi vivono in canali: "Generale" più uno per fase. Il canale non è una colonna — comments resta polimorfica (entity_type + entity_id) — ma una chiave derivata, e la derivazione sta in un posto solo: src/lib/chat-channels.ts, funzioni pure senza accesso al DB, importate da entrambe le sponde.

"Generale"   -> clients.id
una fase     -> phases.id
task         -> la chiave della fase proprietaria (tasks.phase_id)
deliverable  -> la chiave della fase del task proprietario

Perché in un modulo condiviso: se cliente e admin calcolassero il canale ognuno per conto suo, una risposta atterrerebbe in un tab diverso da quello in cui è stata scritta la domanda. Task e deliverable non sono più scrivibili da nessuna UI, ma lo storico esiste e rientra nel canale della fase, conservando il nome dell'entità come badge sul messaggio.

Il letto/non-letto è asimmetrico fra le due sponde, di proposito:

  • Cliente → tabella client_channel_reads (migration 0021), una riga per (client_id, channel_key) con upsert ON CONFLICT … DO UPDATE. Serve una riga per canale perché un singolo client_last_read_at segnerebbe letti tutti i canali all'apertura della chat, e il pallino sul singolo tab — l'unica cosa che dice al cliente dove guardare — perderebbe senso.
  • Admin → resta su clients.admin_last_read_at (0014): lì la lettura è già a livello di conversazione, e il pallino per canale si deriva da quel timestamp. Nessuna tabella nuova.

/admin/conversazioni (replica design-reference/pagina-conversazioni.html) resta a due pannelli, con i canali come tab dentro il thread attivo.

  • Fonte unread: clients.admin_last_read_at confrontato con l'ultimo messaggio autore client. Una conversazione è non letta se esiste un messaggio cliente dopo admin_last_read_at (o se è NULL). Query in src/lib/conversations-queries.ts (getConversations, getConversationThread, getUnreadConversationsCount). Il thread porta con sé adminLastReadAt come snapshot: congelato al caricamento, così l'auto-mark-read all'apertura non cancella i pallini per canale sotto gli occhi di chi sta leggendo.
  • Badge sidebar: voce "Conversazioni" (AdminSidebar) con pill emerald del numero di clienti con non letti (pallino verde se collapsed), alimentato da getUnreadConversationsCount() in src/app/admin/layout.tsx. Stessa fonte di verità del MessagesWidget in dashboard.
  • Reply: scritta sull'entità del canale attivo (cliente per "Generale", fase per gli altri) — non più sempre general — e aggiorna admin_last_read_at. Deep-link a un thread via ?c=<clientId>.

Indice: comments non ha mai avuto un indice, nemmeno su entity_id, dal giorno 0. La 0021 aggiunge (entity_id, created_at), che è esattamente il modo in cui il feed si legge.

Pattern «Modifica» sui messaggi (portale e inbox admin, stessa forma): pulsante testuale piccolo, non un'icona, coerente con i già presenti Riprova/Elimina. Si rivela al passaggio del mouse ma deve restare raggiungibile senza puntatore:

opacity-0 transition-opacity group-hover:opacity-100
focus-visible:opacity-100 [@media(hover:none)]:opacity-100

L'ultima clausola non è un dettaglio — senza, il pulsante è irraggiungibile da telefono, ed è da telefono che il cliente apre il portale. In modifica la bolla diventa una textarea: Invio salva, Esc annulla, più i due pulsanti espliciti. Nell'inbox admin i controlli stanno fuori dalla bolla: sopra bg-primary un testo tenue non si legge. Il pulsante nascosto non è una difesa — chi può modificare lo ridecide il server.

Clienti (lista) notes

/admin/clients migrata a "Quiet Luxury" (replica design-reference/pagina-clienti.html). Tabella bg-card rounded-xl border-border shadow-card con header uppercase muted e righe py-4 px-6 / divide-y divide-border / hover:bg-muted/40.

  • Colonne: Cliente (nome link → /admin/clients/[id] + sottotitolo brand/progetti) a sinistra; LTV, Importo incassato, Importo da saldare, Link profilo tutte right-aligned. Valori monetari font-mono; "incassato" reso come pill emerald (bg-emerald-50 text-emerald-700 + variante dark), zero → muted.
  • Link profilo: pill mono troncata (/client/<slug|token>…) che apre il portale in new tab + CopyLinkButton (copia URL assoluto).
  • Header actions: link testuale "Mostra/Nascondi archiviati" (?archived=1) + Button primario "Nuovo cliente". Footer tabella "Mostrando N clienti attivi" (singolare/plurale gestiti).
  • Le righe archiviate restano opacity-60 con pill "Archiviato".

Offerte (lista) notes

/admin/offers migrata a "Quiet Luxury" (replica design-reference/pagina-offerte.html). Griglia responsive di card bg-card rounded-xl border-border p-6 shadow-card (hover:shadow-card-hover), md:grid-cols-2 lg:grid-cols-3.

  • Header: PageHeader "Offerte" + sottotitolo, azione "+ Nuova Offerta" (Button primario) che espande un form inline (Input nome + "Crea").
  • Filtri: pill categoria rettangolari (rounded-lg px-4 py-2) — attiva bg-primary text-primary-foreground, inattiva border-border bg-card text-muted-foreground hover:bg-muted. "Tutti" + categoryOptions dinamiche. A destra checkbox "Mostra offerte archiviate".
  • Card: nome interno + descrizione muted + badge categoria colorato per tipo (Retainer→blue, Entry→purple, Signature→amber, tutti con variante dark) via helper categoryBadgeClass. Le archiviate sono opacity-60 con pill "Archiviata".
  • Tabella tier: border-border rounded-lg, header uppercase muted (Tier · Servizi · Pubblico), righe font-mono divide-y divide-border; solo i tier con servicesTotal > 0 o public_price valorizzato; "Pubblico" assente → .

Catalogo (griglia servizi) notes

/admin/catalog migrata a "Quiet Luxury" (replica design-reference/pagina-catalogo.html). È una griglia editabile in stile foglio di calcolo: ServiceTable in un contenitore bg-card rounded-xl border-border shadow-card overflow-hidden, header uppercase muted (text-[11px] font-semibold uppercase tracking-wider text-muted-foreground, bg-muted/50), righe hover:bg-muted/40 / divide via border-border.

  • Celle inline: nome/descrizione/prezzo/stato usano EditableCell; fase usa OptionSelect; offerta/pacchetto usano OptionMultiSelect. I badge fase/offerta/ pacchetto sono colorati deterministicamente via getOptionColor (hash → palette a 7 colori) — nessun colore semantico fisso, coerente col sistema tag del resto dell'app.
  • Colonne: riordinabili via drag (GripVertical on hover) e ordinabili sulle colonne sortable (ArrowUp/Down in text-primary); ordine persistito in localStorage (catalog_col_order).
  • Quick-add: ultima riga bg-muted/40 con input inline (+ Aggiungi servizio, prezzo, fase datalist) — Invio ↵ crea il servizio. Servizi disattivati raggruppati sotto un header bg-muted/40 e resi opacity-50.
  • Search: SearchInput primitivo (non più input+icona inline) su nome/categoria/fase/ tag/pacchetto. Footer tabella "Visualizzazione di N servizi" (singolare/plurale).
  • Nota primitivi: in questa pass OptionSelect ed EditableCell (accent-primary) 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.html). 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/<slug> 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".

Progetti (lista) notes

/admin/projects migrata a "Quiet Luxury" (replica design-reference/pagina-progetti.html). Stesso contenitore tabella di Preventivi (bg-card rounded-xl border-border shadow-card, header bg-muted/50 uppercase muted, righe hover:bg-muted/40 + divide-y divide-border).

  • Header: PageHeader "Progetti" + sottotitolo (redditività oraria), azione Button "Nuovo Progetto" (+, link a /admin/projects/new).
  • Colonne: Progetto (link → dettaglio + cliente muted, badge "Archiviato" se archiviato), Valore Totale (text-right font-mono), Acconto (badge centrato), Saldo (badge centrato), Timer (TimerCell centrato), Redditività €/H (text-right).
  • Badge pagamento (ProjectRow): pill rounded-full text-[10px] uppercase border dual light/dark — saldato→emerald, da_saldare→rose, inviata→amber; prefissati "Acconto:"/"Saldo:". Pagamento assente → muted.
  • TimerCell: ora una pill rounded-full border font-mono — idle neutro (bg-muted, play triangle su bg-foreground/10), running emerald (bg-emerald-50/60 dark emerald-950/30, pausa su bg-emerald-500), contatore live. Stessa pill riusata nel TimerTab del dettaglio progetto.
  • Redditività: null (0h tracciate) → muted; >0 → emerald bold con dot bg-emerald-500; =0 → mono muted. Footer "Mostrando N di N progetti attivi".

Impostazioni notes

/admin/impostazioni migrata a "Quiet Luxury" (replica design-reference/pagina-impostazioni.html). Pagina a sezioni impilate (space-y-8), ogni sezione è una card rounded-xl border-border-light bg-card shadow-card p-6.

  • Header: PageHeader "Impostazioni" + sottotitolo ("Configura le tariffe target e gestisci le tassonomie globali del sistema").
  • Titoli sezione: text-xs font-bold uppercase tracking-wider text-foreground, sottotitolo text-xs text-tertiary. Le sezioni tassonomia hanno un'icona (Tag/BookOpen) in un box h-9 w-9 rounded-lg border-border-light bg-muted text-muted-foreground.
  • Analytics Profittabilità: form server-action con input type=number in un gruppo bordato focus-within:border-primary focus-within:ring-1 ring-primary, prefisso /suffisso /h muted assoluti, valore font-mono; bottone "Salva" bg-primary text-primary-foreground.
  • Griglie tassonomia (TaxonomyManager): border-t border-border-light pt-4 + grid gap-6 — Offerte md:grid-cols-2, Catalogo md:grid-cols-2 lg:grid-cols-3.
  • PoolManager (box campo): rounded-xl border-border-light bg-muted/40 p-5 flex flex-col justify-between. Testata = label uppercase muted + badge conteggio mono (bg-card border-border-light text-tertiary, sempre visibile anche a 0). Valori = chip bg-card border-border/60 text-foreground shadow-sm con X (hover destructive → cascade delete confermata via window.confirm). Vuoto → "Nessun valore inserito" italic muted. Aggiunta = input full-width bg-card border-border con Enter (no bottone +); duplicati bloccati con errore inline text-destructive.
  • Footer: nota text-xs text-tertiary sulla sincronizzazione bidirezionale dei valori.

Client Portal restyle notes

The /client/[token] portal was migrated to the same "Quiet Luxury" tokens (applied 2026-07-10). Key conventions specific to the client-facing surface:

  • Header: bg-card sticky bar, three zones — iamcavalli | Client Portal (left), brand name centered, "Area Riservata Protetta" emerald pill (right).
  • Status pills (phases, payments) use soft tints, not solid fills: emerald-50/700 (done/saldato), amber-50/700 (in corso/da saldare), bg-muted/text-muted-foreground (upcoming) — each with a dark: variant.
  • Phase cards (PhaseCard): rounded-xl border-border-light bg-card shadow-card, progress bar colored per status (emerald/amber/bg-border).
  • Stato dei task (TaskStatusIndicator): le cinque forme si distinguono senza coloretodo cerchio vuoto border-border, in_progress cerchio + punto ambra, in_review cerchio + spunta vuota violetta, done cerchio pieno
    • spunta emerald, cancelled cerchio spento + X. La spunta compare a lavoro finito e si riempie a lavoro confermato; la X è l'unica forma che non è un passo avanti. La pill di testo compare su in_progress, in_review e cancelled; todo e done si leggono dalla forma e dal titolo. done e cancelled sono entrambi barrati, ed è proprio per questo che cancelled porta la pill: la differenza fra consegnato e tolto dal lavoro è troppo importante per affidarla al solo disegno dentro il cerchio. Colori come le altre pill del portale (soft tint -50/700 + variante dark:, tinta muted per la cancellata), tutti raccolti in TASK_STATUS_VISUALS e mai riscritti nei componenti: le stesse tinte colorano le colonne del Kanban cliente e la legenda, che si genera da TASK_STATUSES. L'icona è aria-hidden e lo stato viaggia in uno sr-only, così lo screen reader lo annuncia anche su quelli senza pill. La colonna «Cancellate» del Kanban cliente è l'unica che si nasconde da vuota (le altre quattro si tengono la larghezza) — mai se è piena: nessun task deve sparire dalla board.
  • Box pagamenti (PaymentStatus): sopra le righe, un riquadro «Prossimo pagamento» con data lunga e conto alla rovescia in parole («tra 12 giorni», «domani», «oggi»); se la scadenza è passata diventa rosso e dice «Pagamento scaduto — scaduto da N giorni». Ogni riga porta la sua data sotto l'etichetta: «Scade il…» se aperta, «Pagato il…» se saldata, nulla se in gestionale la data non c'è — il portale non inventa scadenze. Nessun importo per riga (LOCKED #2): le date non sono cifre. I giorni si contano in src/lib/payment-dates.ts sui giorni civili a Roma, non sugli istanti.
  • Sidebar cards (OffersSection, PaymentStatus, DocumentsSection, NotesSection, TranscriptsSection) all follow rounded-xl border-border-light bg-card shadow-card.
  • Offer card (OffersSection): due sole righe di importo — «Valore dell'offerta» (override admin o somma dei servizi; nascosta a 0) e «Canone mensile» / «Investimento finale». Niente accordion «Cosa è compreso»: la lista servizi non arriva più al client, e il componente non è più "use client".
  • ClientDashboard takes an embedded prop: when rendered inside the multi-project tabs wrapper (page.tsx), it drops its own header/footer/canvas so the page-level chrome isn't duplicated.