Files
clienthub/src/components/admin/AdminSidebar.tsx
T
simone c1cc13a99a 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>
2026-07-11 16:10:14 +02:00

140 lines
4.8 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { signOut } from "next-auth/react";
import {
LayoutDashboard,
Users,
FolderOpen,
Tag,
BookOpen,
Settings,
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 },
{ href: "/admin/preventivi", label: "Preventivi", icon: FileText },
{ href: "/admin/offers", label: "Offerte", icon: Tag },
{ href: "/admin/catalog", label: "Catalogo", icon: BookOpen },
{ href: "/admin/impostazioni", label: "Impostazioni", icon: Settings },
];
export function AdminSidebar({
collapsed = false,
unreadConversations = 0,
}: {
collapsed?: boolean;
unreadConversations?: number;
}) {
const pathname = usePathname();
const isActive = (href: string, exact?: boolean) =>
exact ? pathname === href : pathname === href || pathname.startsWith(href + "/");
// Shared row layout: full width + gap + label when expanded, icon centered
// (no gap, no padding) when collapsed so nothing pushes the icon off-center.
const rowClass = collapsed ? "justify-center px-0" : "gap-3 px-3";
return (
<aside
className={cn(
"shrink-0 min-h-screen bg-[#1A463C] flex flex-col overflow-hidden",
"transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)]",
collapsed ? "w-16" : "w-[200px]"
)}
>
{/* Logo */}
<div
className={cn(
"h-[57px] flex items-center border-b border-white/10",
collapsed ? "justify-center px-0" : "px-5"
)}
>
{!collapsed && (
<span className="font-bold text-white tracking-tight text-sm whitespace-nowrap">
iamcavalli
</span>
)}
</div>
{/* Nav links */}
<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}
href={href}
title={collapsed ? label : undefined}
className={cn(
"flex items-center py-2.5 rounded-lg text-sm transition-all duration-200",
rowClass,
active
? "bg-white/15 text-white font-medium"
: "text-white/65 hover:text-white hover:bg-white/10"
)}
>
<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>
);
})}
</nav>
{/* Theme + Logout */}
<div className="px-2 py-4 border-t border-white/10 flex flex-col gap-2">
<div
className={cn(
"flex items-center py-1",
collapsed ? "justify-center px-0" : "justify-between px-3"
)}
>
{!collapsed && <span className="text-xs text-white/50">Tema</span>}
<ThemeToggle />
</div>
<button
onClick={() => signOut({ callbackUrl: "/admin/login" })}
title={collapsed ? "Esci" : undefined}
className={cn(
"flex items-center py-2.5 w-full rounded-lg text-sm transition-all duration-200",
"text-white/65 hover:text-white hover:bg-white/10",
rowClass
)}
>
<LogOut size={16} strokeWidth={1.8} className="shrink-0" />
{!collapsed && <span className="whitespace-nowrap">Esci</span>}
</button>
</div>
</aside>
);
}