diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d247d32 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,31 @@ +{ + "permissions": { + "allow": [ + "mcp__plugin_claude-mem_mcp-search__get_observations", + "Bash(rtk tsc *)", + "Bash(rtk git *)", + "Bash(rtk grep *)", + "Read(//Users/simonecavalli/.claude/get-shit-done/references/**)", + "Skill(gsd-execute-phase)", + "Skill(gsd-execute-phase:*)", + "Skill(gsd-plan-phase)", + "Skill(gsd-plan-phase:*)", + "Skill(gsd-progress)", + "Skill(gsd-progress:*)", + "Skill(gsd-complete-milestone)", + "Skill(gsd-complete-milestone:*)", + "Read(//Users/simonecavalli/Downloads/**)" + ] + }, + "enabledPlugins": { + "impeccable@impeccable": true + }, + "extraKnownMarketplaces": { + "impeccable": { + "source": { + "source": "github", + "repo": "pbakaus/impeccable" + } + } + } +} diff --git a/design-reference/DESIGN-SYSTEM.md b/design-reference/DESIGN-SYSTEM.md index df11673..10ebe20 100644 --- a/design-reference/DESIGN-SYSTEM.md +++ b/design-reference/DESIGN-SYSTEM.md @@ -152,6 +152,8 @@ across all future admin pages. | `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 bolle messaggio (admin a destra `bg-primary`, cliente a sinistra), badge entità per messaggio (Generale/Fase/Task/Deliverable) e form di risposta. Auto-mark-read all'apertura. | | `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=`. 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 @@ -201,6 +203,23 @@ in un unico thread ordinato, con badge entità per messaggio. `admin_last_read_at` (mark-read). L'apertura di una conversazione fa auto-mark-read. Deep-link a un thread specifico via `?c=`. +### Clienti (lista) notes + +`/admin/clients` migrata a "Quiet Luxury" (replica `design-reference/pagina-clienti`). +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/…`) 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". + ### Client Portal restyle notes The `/client/[token]` portal was migrated to the same "Quiet Luxury" tokens diff --git a/src/app/admin/clients/page.tsx b/src/app/admin/clients/page.tsx index fce98f0..397bb0a 100644 --- a/src/app/admin/clients/page.tsx +++ b/src/app/admin/clients/page.tsx @@ -16,10 +16,10 @@ export default async function AdminClientsPage({ const clients = await getAllClientsWithPayments(showArchived); const clientiAction = ( -
+
{showArchived ? "Nascondi archiviati" : "Mostra archiviati"} @@ -31,37 +31,48 @@ export default async function AdminClientsPage({ return (
- + {clients.length === 0 ? ( -
+

{showArchived ? "Nessun cliente archiviato." : "Nessun cliente ancora."}

{!showArchived && (

- + Crea il primo cliente

)}
) : ( -
- - - - - - - - - - - - {clients.map((client) => ( - - ))} - -
ClienteLTVImporto incassatoImporto da saldareLink
+
+
+ + + + + + + + + + + + {clients.map((client) => ( + + ))} + +
ClienteLTVImporto incassatoImporto da saldareLink profilo
+
+
+ Mostrando {clients.length}{" "} + {clients.length === 1 ? "cliente" : "clienti"} + {showArchived ? "" : clients.length === 1 ? " attivo" : " attivi"} +
)}
diff --git a/src/components/admin/ClientRow.tsx b/src/components/admin/ClientRow.tsx index aa8ff77..42b06a0 100644 --- a/src/components/admin/ClientRow.tsx +++ b/src/components/admin/ClientRow.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import type { ClientWithPayments } from "@/lib/admin-queries"; +import { CopyLinkButton } from "@/components/admin/CopyLinkButton"; function fmt(amount: number) { return `€${amount.toLocaleString("it-IT", { minimumFractionDigits: 2 })}`; @@ -14,53 +15,60 @@ export function ClientRow({ client }: { client: ClientWithPayments }) { .filter((p) => p.status !== "saldato") .reduce((sum, p) => sum + parseFloat(p.amount || "0"), 0); + const slug = client.slug || client.token; + const path = `/client/${slug}`; + return ( - - + + {client.name} {client.projectBrands && client.projectBrands.length > 0 ? ( -

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

+

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

) : ( -

{client.brand_name}

+ client.brand_name && ( +

{client.brand_name}

+ ) )} {client.archived && ( - + Archiviato )} - + {fmt(parseFloat(client.ltv))} - + {incassato > 0 ? ( - {fmt(incassato)} + + {fmt(incassato)} + ) : ( - + )} - - {daSaldare > 0 ? ( - {fmt(daSaldare)} - ) : ( - - )} + + {daSaldare > 0 ? fmt(daSaldare) : } - - - /client/{(client.slug || client.token).slice(0, 12)}… - + + ); -} \ No newline at end of file +} diff --git a/src/components/admin/CopyLinkButton.tsx b/src/components/admin/CopyLinkButton.tsx new file mode 100644 index 0000000..e212c37 --- /dev/null +++ b/src/components/admin/CopyLinkButton.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useState } from "react"; + +export function CopyLinkButton({ path, title = "Copia link" }: { path: string; title?: string }) { + const [copied, setCopied] = useState(false); + + function handleCopy() { + const url = typeof window !== "undefined" ? `${window.location.origin}${path}` : path; + navigator.clipboard?.writeText(url).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }); + } + + return ( + + ); +}