feat: dashboard box "Messaggi Clienti" — chat clienti in attesa di risposta

Nuovo MessagesWidget nella colonna 1/3 della dashboard: riusa
getConversations() filtrando le conversazioni unread, mostra le prime 4
con anteprima e deep-link "Rispondi" a /admin/conversazioni?c=<clientId>.
Pill emerald "N Nuovi" + pallino pulsante, empty state quando non c'è nulla
in attesa. Stessa fonte di verità (clients.admin_last_read_at) del badge
sidebar. Aggiornato DESIGN-SYSTEM.md (inventory + note Conversazioni/dashboard)
e il mock design-reference/pagina-dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 16:23:43 +02:00
parent c1cc13a99a
commit a1d8d18902
4 changed files with 147 additions and 12 deletions
+25 -1
View File
@@ -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). | | `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`). | | `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=`. | | `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=<clientId>`. Empty state "Nessun messaggio in attesa ✓". |
**Unified timeline pattern**: activities + transcripts merged into one **Unified timeline pattern**: activities + transcripts merged into one
reverse-chronological "Storico delle Interazioni". Vertical connector = absolute reverse-chronological "Storico delle Interazioni". Vertical connector = absolute
@@ -171,12 +173,34 @@ layout: no in-`<main>` `PageHeader` (sidebar + shell header carry the chrome).
`text-emerald-600 dark:text-emerald-400`. `text-emerald-600 dark:text-emerald-400`.
- **Griglia `lg:grid-cols-3`**: 2/3 col = `ForecastChart` (istogramma flussi 12 - **Griglia `lg:grid-cols-3`**: 2/3 col = `ForecastChart` (istogramma flussi 12
mesi, mese prossimo evidenziato `bg-primary`) + `ClientProfitability`; 1/3 col = 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 - **Year selector** interattivo (`YearSelector`) in una toolbar top-right; governa
i KPI year-scoped + redditività. Il forecast è rolling-12-mesi (non year-scoped). i KPI year-scoped + redditività. Il forecast è rolling-12-mesi (non year-scoped).
- Widget rimossi dal legacy: chart incassi mensili, card "Da incassare"/"Clienti - Widget rimossi dal legacy: chart incassi mensili, card "Da incassare"/"Clienti
acquisiti", barre "Ore per cliente" (sostituite dalla redditività oraria). 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=<clientId>`.
### Client Portal restyle notes ### Client Portal restyle notes
The `/client/[token]` portal was migrated to the same "Quiet Luxury" tokens The `/client/[token]` portal was migrated to the same "Quiet Luxury" tokens
+51 -11
View File
@@ -52,12 +52,22 @@
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg> <svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
</a> </a>
<!-- Voce: Conversazioni (Connessa all'Inbox) -->
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
<div class="flex items-center gap-3">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a.598.598 0 01-.73-.578 5.672 5.672 0 011.02-3.155A8.72 8.72 0 013 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Conversazioni</span>
</div>
<span class="bg-emerald-500 text-white text-[10px] font-bold px-2 py-0.5 rounded-full sidebar-text">2</span>
</a>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200"> <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg> <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
</a> </a>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200"> <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg> <svg class="w-4 h-4 shrink-0 animate-pulse text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
</a> </a>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200"> <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
@@ -65,19 +75,19 @@
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
</a> </a>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200"> <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg> <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Preventivi</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Preventivi</span>
</a> </a>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200"> <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M7 7h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg> <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M7 7h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Offerte</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Offerte</span>
</a> </a>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200"> <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg> <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Catalogo</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Catalogo</span>
</a> </a>
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200"> <a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg> <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Impostazioni</span> <span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Impostazioni</span>
</a> </a>
</nav> </nav>
@@ -175,7 +185,7 @@
<span class="text-xs font-bold text-slate-800 bg-slate-50 px-2.5 py-1 rounded border border-slate-100 font-mono">2026</span> <span class="text-xs font-bold text-slate-800 bg-slate-50 px-2.5 py-1 rounded border border-slate-100 font-mono">2026</span>
</div> </div>
<!-- Istogramma Semplificato (Rappresentazione SVG pulita per stabilità di rendering) --> <!-- Istogramma Semplificato (SVG pulito) -->
<div class="h-48 flex items-end justify-between gap-2 pt-4 border-b border-slate-100"> <div class="h-48 flex items-end justify-between gap-2 pt-4 border-b border-slate-100">
<div class="flex-1 flex flex-col items-center gap-2"> <div class="flex-1 flex flex-col items-center gap-2">
<div class="w-full bg-slate-200/80 rounded-t h-20 hover:bg-brand-dark transition-all" title="Lug: €200"></div> <div class="w-full bg-slate-200/80 rounded-t h-20 hover:bg-brand-dark transition-all" title="Lug: €200"></div>
@@ -220,7 +230,7 @@
</div> </div>
</div> </div>
<!-- Analisi di Profitto Orario Effettivo (Nuovo Upgrade Strategico) --> <!-- Analisi di Profitto Orario Effettivo -->
<div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] p-6"> <div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] p-6">
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-4">Analisi Oraria & Redditività Clienti</h3> <h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-4">Analisi Oraria & Redditività Clienti</h3>
<div class="space-y-4"> <div class="space-y-4">
@@ -251,17 +261,16 @@
</div> </div>
<!-- COLONNA COMPATTA (1/3): Follow-up, Offerte più vendute --> <!-- COLONNA COMPATTA (1/3): Follow-up, Messaggi Clienti, Offerte più vendute -->
<div class="lg:col-span-1 flex flex-col gap-8"> <div class="lg:col-span-1 flex flex-col gap-8">
<!-- Box Operativo: Follow-up --> <!-- Box Operativo 1: Follow-up -->
<div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] p-5"> <div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] p-5">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400">Follow-up Attivi</h3> <h3 class="text-xs font-bold uppercase tracking-wider text-slate-400">Follow-up Attivi</h3>
<span class="text-[10px] font-bold bg-amber-50 text-amber-700 border border-amber-100 px-1.5 py-0.5 rounded-full">2</span> <span class="text-[10px] font-bold bg-amber-50 text-amber-700 border border-amber-100 px-1.5 py-0.5 rounded-full">2</span>
</div> </div>
<!-- Lista Contatti da Richiamare -->
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center justify-between p-3 border border-slate-100 rounded-lg hover:border-slate-200 transition-colors"> <div class="flex items-center justify-between p-3 border border-slate-100 rounded-lg hover:border-slate-200 transition-colors">
<div> <div>
@@ -285,7 +294,38 @@
</div> </div>
</div> </div>
<!-- Distrubuzione Offerte Vendute --> <!-- NUOVO BOX OPERATIVO 2: NOTIFICHE MESSAGGI CLIENTI (Da conversazioni.html) -->
<div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] p-5">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-2">
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400">Messaggi Clienti</h3>
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
</div>
<span class="text-[10px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-100 px-2 py-0.5 rounded-full">2 Nuovi</span>
</div>
<div class="space-y-3">
<!-- Notifica 1 -->
<div class="p-3 bg-slate-50/60 rounded-lg border border-slate-100 flex justify-between items-center gap-3">
<div class="min-w-0 flex-1">
<h4 class="text-xs font-bold text-slate-800">Rossi Inc</h4>
<p class="text-[10px] text-slate-400 truncate">In merito all'analisi competitor della Fase 2...</p>
</div>
<a href="conversazioni.html" class="text-[11px] font-bold text-brand-dark hover:text-brand-darkHover shrink-0">Rispondi ➔</a>
</div>
<!-- Notifica 2 -->
<div class="p-3 bg-slate-50/60 rounded-lg border border-slate-100 flex justify-between items-center gap-3">
<div class="min-w-0 flex-1">
<h4 class="text-xs font-bold text-slate-800">Azienda Srl</h4>
<p class="text-[10px] text-slate-400 truncate">Abbiamo caricato i file di onboarding!</p>
</div>
<a href="conversazioni.html" class="text-[11px] font-bold text-brand-dark hover:text-brand-darkHover shrink-0">Rispondi ➔</a>
</div>
</div>
</div>
<!-- Distribuzione Offerte Vendute -->
<div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] p-5"> <div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] p-5">
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-4">Offerte Più Richieste</h3> <h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-4">Offerte Più Richieste</h3>
<div class="space-y-4"> <div class="space-y-4">
+6
View File
@@ -1,6 +1,7 @@
import { Suspense } from "react"; import { Suspense } from "react";
import { getDashboardStats } from "@/lib/dashboard-queries"; import { getDashboardStats } from "@/lib/dashboard-queries";
import { FollowUpWidget } from "@/components/admin/dashboard/FollowUpWidget"; import { FollowUpWidget } from "@/components/admin/dashboard/FollowUpWidget";
import { MessagesWidget } from "@/components/admin/dashboard/MessagesWidget";
import { import {
getAnalyticsByYear, getAnalyticsByYear,
getTotalTrackedHours, getTotalTrackedHours,
@@ -119,6 +120,11 @@ export default async function AdminDashboard({
> >
<FollowUpWidget /> <FollowUpWidget />
</Suspense> </Suspense>
<Suspense
fallback={<div className="animate-pulse h-40 bg-muted rounded-xl" />}
>
<MessagesWidget />
</Suspense>
<OffersSoldChart data={offersSold} /> <OffersSoldChart data={offersSold} />
</div> </div>
</div> </div>
@@ -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 (
<div className="bg-card rounded-xl border border-border-light shadow-card p-5">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
Messaggi Clienti
</h3>
<p className="text-sm text-muted-foreground mt-3">Nessun messaggio in attesa </p>
</div>
);
}
const visible = unread.slice(0, MAX_ROWS);
return (
<div className="bg-card rounded-xl border border-border-light shadow-card p-5">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
Messaggi Clienti
</h3>
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
</div>
<span className="text-[10px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-100 px-2 py-0.5 rounded-full dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20">
{unread.length} {unread.length === 1 ? "Nuovo" : "Nuovi"}
</span>
</div>
<div className="space-y-3">
{visible.map((conv) => (
<Link
key={conv.clientId}
href={`/admin/conversazioni?c=${conv.clientId}`}
className="flex items-center justify-between gap-3 p-3 border border-border-light rounded-lg hover:border-border transition-colors group"
>
<div className="min-w-0">
<h4 className="text-xs font-bold text-foreground truncate">{conv.brand_name}</h4>
<p className="text-[10px] text-muted-foreground truncate">{conv.lastMessage}</p>
</div>
<span className="text-[11px] font-bold text-primary group-hover:opacity-70 transition-opacity shrink-0 inline-flex items-center gap-0.5">
Rispondi
</span>
</Link>
))}
</div>
{unread.length > MAX_ROWS && (
<Link
href="/admin/conversazioni"
className="block mt-3 pt-3 text-center text-xs font-medium text-muted-foreground hover:text-primary border-t border-border-light transition-colors"
>
Vedi tutti i messaggi
</Link>
)}
</div>
);
}