c1cc13a99a
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>
140 lines
4.8 KiB
TypeScript
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>
|
|
);
|
|
}
|