feat: pagina Conversazioni — inbox unificata messaggi clienti

Nuova pagina admin /admin/conversazioni: vista WhatsApp-style (lista
conversazioni a sinistra, thread + risposta a destra) che aggrega i
messaggi di tutti i clienti dalla tabella comments, cross-cliente.

- Migration additiva 0014: clients.admin_last_read_at per tracciare
  letto/non-letto (pallini + badge in sidebar). Applicata a prod.
- conversations-queries.ts: aggregazione entity_id → cliente
  (general/phase/task/deliverable), getConversations /
  getConversationThread / getUnreadConversationsCount.
- Risposte admin salvate come commento "general" (visibili anche nella
  chat cliente e nel CommentsTab della scheda).
- Voce di menu + badge non-letti (AdminSidebar/AdminShell/layout).
- Token semantici per dual light/dark; refresh manuale (no polling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 16:10:14 +02:00
parent a20a9de2d7
commit c1cc13a99a
10 changed files with 994 additions and 10 deletions
+29 -7
View File
@@ -13,12 +13,14 @@ import {
LogOut,
Zap,
FileText,
MessageSquare,
} from "lucide-react";
import ThemeToggle from "@/components/ThemeToggle";
import { cn } from "@/lib/utils";
const NAV_ITEMS = [
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard, exact: true },
{ href: "/admin/conversazioni", label: "Conversazioni", icon: MessageSquare },
{ href: "/admin/pipeline", label: "Pipeline", icon: Zap },
{ href: "/admin/clients", label: "Clienti", icon: Users },
{ href: "/admin/projects", label: "Progetti", icon: FolderOpen },
@@ -28,7 +30,13 @@ const NAV_ITEMS = [
{ href: "/admin/impostazioni", label: "Impostazioni", icon: Settings },
];
export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
export function AdminSidebar({
collapsed = false,
unreadConversations = 0,
}: {
collapsed?: boolean;
unreadConversations?: number;
}) {
const pathname = usePathname();
const isActive = (href: string, exact?: boolean) =>
@@ -64,6 +72,10 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
<nav className="flex-1 px-2 py-4 flex flex-col gap-0.5">
{NAV_ITEMS.map(({ href, label, icon: Icon, exact }) => {
const active = isActive(href, exact);
const badge =
href === "/admin/conversazioni" && unreadConversations > 0
? unreadConversations
: 0;
return (
<Link
key={href}
@@ -77,12 +89,22 @@ export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
: "text-white/65 hover:text-white hover:bg-white/10"
)}
>
<Icon
size={16}
strokeWidth={1.8}
className="shrink-0"
/>
{!collapsed && <span className="whitespace-nowrap">{label}</span>}
<span className="relative shrink-0">
<Icon size={16} strokeWidth={1.8} className="shrink-0" />
{badge > 0 && collapsed && (
<span className="absolute -top-1.5 -right-1.5 w-2 h-2 rounded-full bg-emerald-500 ring-2 ring-[#1A463C]" />
)}
</span>
{!collapsed && (
<span className="flex-1 flex items-center justify-between gap-2 min-w-0">
<span className="whitespace-nowrap">{label}</span>
{badge > 0 && (
<span className="shrink-0 min-w-[18px] h-[18px] px-1.5 inline-flex items-center justify-center rounded-full bg-emerald-500 text-white text-[10px] font-bold">
{badge}
</span>
)}
</span>
)}
</Link>
);
})}