diff --git a/design-reference/DESIGN-SYSTEM.md b/design-reference/DESIGN-SYSTEM.md index c5a16f4..df11673 100644 --- a/design-reference/DESIGN-SYSTEM.md +++ b/design-reference/DESIGN-SYSTEM.md @@ -150,6 +150,8 @@ across all future admin pages. | `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 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 ✓". | **Unified timeline pattern**: activities + transcripts merged into one reverse-chronological "Storico delle Interazioni". Vertical connector = absolute @@ -171,12 +173,34 @@ layout: no in-`
` `PageHeader` (sidebar + shell header carry the chrome). `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) + `OffersSoldChart` ("Offerte Più Richieste"). + `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). +### Conversazioni (inbox) notes + +`/admin/conversazioni` è l'inbox unificata dei messaggi clienti (replica +`design-reference/pagina-conversazioni`). Ogni conversazione è **per cliente**: +raccoglie tutti i suoi commenti — `general` + quelli su fasi/task/deliverable — +in un unico thread ordinato, con badge entità per messaggio. + +- **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`). +- **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**: salvata come commento `general` sul cliente e aggiorna + `admin_last_read_at` (mark-read). L'apertura di una conversazione fa + auto-mark-read. Deep-link a un thread specifico via `?c=`. + ### Client Portal restyle notes The `/client/[token]` portal was migrated to the same "Quiet Luxury" tokens diff --git a/design-reference/pagina-dashboard b/design-reference/pagina-dashboard index a06ae0e..8d9db3f 100644 --- a/design-reference/pagina-dashboard +++ b/design-reference/pagina-dashboard @@ -52,12 +52,22 @@ Dashboard + + + +
+ + Conversazioni +
+ 2 +
+ Lead - + Clienti @@ -65,19 +75,19 @@ Progetti - + Preventivi - + Offerte - + Catalogo - + Impostazioni @@ -175,7 +185,7 @@ 2026 - +
@@ -220,7 +230,7 @@
- +

Analisi Oraria & Redditività Clienti

@@ -251,17 +261,16 @@
- +
- +

Follow-up Attivi

2
-
@@ -285,7 +294,38 @@
- + +
+
+
+

Messaggi Clienti

+ +
+ 2 Nuovi +
+ +
+ +
+
+

Rossi Inc

+

In merito all'analisi competitor della Fase 2...

+
+ Rispondi ➔ +
+ + +
+
+

Azienda Srl

+

Abbiamo caricato i file di onboarding!

+
+ Rispondi ➔ +
+
+
+ +

Offerte Più Richieste

diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 7d09676..327c026 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,6 +1,7 @@ import { Suspense } from "react"; import { getDashboardStats } from "@/lib/dashboard-queries"; import { FollowUpWidget } from "@/components/admin/dashboard/FollowUpWidget"; +import { MessagesWidget } from "@/components/admin/dashboard/MessagesWidget"; import { getAnalyticsByYear, getTotalTrackedHours, @@ -119,6 +120,11 @@ export default async function AdminDashboard({ > + } + > + +
diff --git a/src/components/admin/dashboard/MessagesWidget.tsx b/src/components/admin/dashboard/MessagesWidget.tsx new file mode 100644 index 0000000..a74d2a6 --- /dev/null +++ b/src/components/admin/dashboard/MessagesWidget.tsx @@ -0,0 +1,65 @@ +import { getConversations } from "@/lib/conversations-queries"; +import Link from "next/link"; + +const MAX_ROWS = 4; + +export async function MessagesWidget() { + const conversations = await getConversations(); + const unread = conversations.filter((c) => c.unread); + + if (unread.length === 0) { + return ( +
+

+ Messaggi Clienti +

+

Nessun messaggio in attesa ✓

+
+ ); + } + + const visible = unread.slice(0, MAX_ROWS); + + return ( +
+
+
+

+ Messaggi Clienti +

+ +
+ + {unread.length} {unread.length === 1 ? "Nuovo" : "Nuovi"} + +
+ +
+ {visible.map((conv) => ( + +
+

{conv.brand_name}

+

{conv.lastMessage}

+
+ + Rispondi → + + + ))} +
+ + {unread.length > MAX_ROWS && ( + + Vedi tutti i messaggi + + )} +
+ ); +}