vbox funzionanti

This commit is contained in:
2026-03-19 11:41:10 +01:00
parent 538d6a6bec
commit b7f7c1f7c0
32 changed files with 6043 additions and 262 deletions
+608 -78
View File
@@ -1,3 +1,37 @@
/**
* Sidebar navigazione principale di PecFlow.
*
* Struttura visiva (sidebar espansa):
* ┌────────────────────────────────┐
* │ [PF] PecFlow [◀] │
* ├────────────────────────────────┤
* │ TUTTE LE CASELLE │
* │ 📥 Posta in Arrivo [badge] │
* │ 📤 Posta Inviata │
* │ ⭐ Preferiti │
* │ 📦 Archiviati │
* ├────────────────────────────────┤
* │ LE TUE CASELLE │
* │ ● gmgspa@pec.it [▼] │
* │ ├ 📥 In Arrivo │
* │ ├ 📤 Inviata │
* │ ├ ⭐ Preferiti │
* │ └ 📦 Archiviati │
* ├────────────────────────────────┤
* │ ✉ Nuova PEC │
* ├────────────────────────────────┤
* │ AMMINISTRAZIONE │
* │ 📬 Caselle PEC │
* │ 👥 Utenti │
* │ 🛡 Permessi │
* ├────────────────────────────────┤
* │ [avatar] Nome utente │
* │ Impostazioni | Esci │
* └────────────────────────────────┘
*
* Quando collassata (w-16) mostra solo icone/avatar con tooltip.
*/
import { NavLink } from 'react-router-dom'
import {
Inbox,
@@ -9,38 +43,80 @@ import {
ChevronLeft,
ChevronRight,
Shield,
ChevronDown,
Filter,
Bell,
Star,
Archive,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { useAuth } from '@/hooks/useAuth'
import { useInboxStore } from '@/store/inbox.store'
import { useState } from 'react'
import toast from 'react-hot-toast'
import { useQuery } from '@tanstack/react-query'
import { mailboxesApi } from '@/api/mailboxes.api'
import { virtualBoxesApi } from '@/api/virtual_boxes.api'
import type { MailboxResponse, VirtualBoxResponse } from '@/types/api.types'
interface NavItem {
to: string
label: string
icon: React.ElementType
adminOnly?: boolean
badge?: number
}
const NAV_ITEMS: NavItem[] = [
{ to: '/inbox', label: 'Posta in Arrivo', icon: Inbox },
{ to: '/sent', label: 'Posta Inviata', icon: Send },
{ to: '/compose', label: 'Nuova PEC', icon: MailCheck },
]
const ADMIN_NAV_ITEMS: NavItem[] = [
{ to: '/mailboxes', label: 'Caselle PEC', icon: MailCheck, adminOnly: true },
{ to: '/users', label: 'Utenti', icon: Users, adminOnly: true },
{ to: '/permissions', label: 'Permessi', icon: Shield, adminOnly: true },
]
// ─── Sidebar principale ───────────────────────────────────────────────────────
export function Sidebar() {
const [collapsed, setCollapsed] = useState(false)
/**
* Set degli ID casella che l'utente ha esplicitamente chiuso.
* Tutte le caselle sono espanse per default (nessuno nell'insieme).
*/
const [collapsedMailboxes, setCollapsedMailboxes] = useState<Set<string>>(new Set())
/** Set degli ID virtual box che l'utente ha esplicitamente chiuso. */
const [collapsedVboxes, setCollapsedVboxes] = useState<Set<string>>(new Set())
const { user, isAdmin, logout } = useAuth()
const unreadCount = useInboxStore((s) => s.unreadCount)
// Le caselle PEC vengono caricate qui e condivise via React Query cache
const { data: mailboxesData } = useQuery({
queryKey: ['mailboxes'],
queryFn: () => mailboxesApi.list(),
staleTime: 5 * 60 * 1000,
})
const mailboxes = mailboxesData?.items ?? []
// Virtual Box assegnate all'utente corrente
const { data: myVboxes = [] } = useQuery({
queryKey: ['virtual-boxes', 'my'],
queryFn: () => virtualBoxesApi.myVirtualBoxes(),
staleTime: 5 * 60 * 1000,
})
const isMailboxExpanded = (id: string) => !collapsedMailboxes.has(id)
const toggleMailbox = (id: string) => {
setCollapsedMailboxes((prev) => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next
})
}
const isVboxExpanded = (id: string) => !collapsedVboxes.has(id)
const toggleVbox = (id: string) => {
setCollapsedVboxes((prev) => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next
})
}
const handleLogout = async () => {
try {
await logout()
@@ -53,15 +129,15 @@ export function Sidebar() {
return (
<aside
className={cn(
'flex flex-col h-screen bg-gray-900 text-white transition-all duration-300',
'flex flex-col h-screen bg-gray-900 text-white transition-all duration-300 flex-shrink-0',
collapsed ? 'w-16' : 'w-64',
)}
>
{/* Logo + toggle */}
{/* ── Logo + pulsante collassa ── */}
<div className="flex items-center justify-between p-4 border-b border-gray-700">
{!collapsed && (
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-blue-500 flex items-center justify-center text-white font-bold text-sm">
<div className="h-8 w-8 rounded-lg bg-blue-500 flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
PF
</div>
<span className="font-bold text-lg">PecFlow</span>
@@ -73,54 +149,253 @@ export function Sidebar() {
</div>
)}
<button
onClick={() => setCollapsed(!collapsed)}
onClick={() => setCollapsed((c) => !c)}
className={cn(
'p-1 rounded hover:bg-gray-700 transition-colors text-gray-400',
collapsed && 'mx-auto mt-0',
)}
title={collapsed ? 'Espandi' : 'Comprimi'}
>
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
{collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</button>
</div>
{/* Navigazione principale */}
<nav className="flex-1 overflow-y-auto py-4">
<div className="space-y-1 px-2">
{NAV_ITEMS.map((item) => (
<SidebarLink
key={item.to}
item={item}
collapsed={collapsed}
badge={item.to === '/inbox' ? unreadCount : undefined}
/>
))}
{/* ── Navigazione principale ── */}
<nav className="flex-1 overflow-y-auto py-4 space-y-4">
{/* ── Sezione: Tutte le caselle ── */}
<div>
{!collapsed && (
<p className="px-4 mb-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Tutte le caselle
</p>
)}
<div className="space-y-0.5 px-2">
{/* Posta in Arrivo globale */}
<NavLink
to="/inbox"
end
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
collapsed && 'justify-center px-2',
)
}
title={collapsed ? 'Posta in Arrivo (tutte le caselle)' : undefined}
>
<Inbox className="h-4 w-4 flex-shrink-0" />
{!collapsed && (
<>
<span className="flex-1">Posta in Arrivo</span>
{unreadCount > 0 && (
<span className="inline-flex items-center justify-center h-5 min-w-[20px] px-1 rounded-full bg-blue-500 text-white text-xs font-bold">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</>
)}
{/* Badge compatto in modalità collassata */}
{collapsed && unreadCount > 0 && (
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-red-500 text-white text-[10px] font-bold flex items-center justify-center">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
</NavLink>
{/* Posta Inviata globale */}
<NavLink
to="/sent"
end
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
collapsed && 'justify-center px-2',
)
}
title={collapsed ? 'Posta Inviata (tutte le caselle)' : undefined}
>
<Send className="h-4 w-4 flex-shrink-0" />
{!collapsed && <span>Posta Inviata</span>}
</NavLink>
{/* Preferiti globali */}
<NavLink
to="/starred"
end
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
collapsed && 'justify-center px-2',
)
}
title={collapsed ? 'Preferiti (tutte le caselle)' : undefined}
>
<Star className="h-4 w-4 flex-shrink-0" />
{!collapsed && <span>Preferiti</span>}
</NavLink>
{/* Archiviati globali */}
<NavLink
to="/archived"
end
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
collapsed && 'justify-center px-2',
)
}
title={collapsed ? 'Archiviati (tutte le caselle)' : undefined}
>
<Archive className="h-4 w-4 flex-shrink-0" />
{!collapsed && <span>Archiviati</span>}
</NavLink>
</div>
</div>
{/* Sezione Admin */}
{isAdmin && (
<>
<div className={cn('mt-6 px-4 mb-2', collapsed && 'hidden')}>
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
Amministrazione
</p>
</div>
{!collapsed && <div className="border-t border-gray-700 mx-4 mb-2" />}
<div className="space-y-1 px-2">
{ADMIN_NAV_ITEMS.map((item) => (
<SidebarLink key={item.to} item={item} collapsed={collapsed} />
{/* ── Sezione: Caselle individuali ── */}
{mailboxes.length > 0 && (
<div>
{!collapsed && (
<>
<div className="border-t border-gray-700 mx-4 mb-3" />
<p className="px-4 mb-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Le tue caselle
</p>
</>
)}
{collapsed && <div className="border-t border-gray-700 mx-3 mb-2" />}
<div className="space-y-0.5 px-2">
{mailboxes.map((mailbox) => (
<MailboxNavItem
key={mailbox.id}
mailbox={mailbox}
collapsed={collapsed}
isExpanded={isMailboxExpanded(mailbox.id)}
onToggle={() => toggleMailbox(mailbox.id)}
/>
))}
</div>
</>
</div>
)}
{/* ── Sezione: Le tue Virtual Box ── */}
{myVboxes.length > 0 && (
<div>
{!collapsed && (
<>
<div className="border-t border-gray-700 mx-4 mb-3" />
<p className="px-4 mb-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Le tue virtual box
</p>
</>
)}
{collapsed && <div className="border-t border-gray-700 mx-3 mb-2" />}
<div className="space-y-0.5 px-2">
{myVboxes.map((vbox) => (
<VirtualBoxNavItem
key={vbox.id}
vbox={vbox}
collapsed={collapsed}
isExpanded={isVboxExpanded(vbox.id)}
onToggle={() => toggleVbox(vbox.id)}
/>
))}
</div>
</div>
)}
{/* ── Nuova PEC ── */}
<div>
<div className="border-t border-gray-700 mx-4 mb-3" />
<div className="px-2">
<NavLink
to="/compose"
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
collapsed && 'justify-center px-2',
)
}
title={collapsed ? 'Nuova PEC' : undefined}
>
<MailCheck className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span>Nuova PEC</span>}
</NavLink>
</div>
</div>
{/* ── Sezione Amministrazione ── */}
{isAdmin && (
<div>
{!collapsed && (
<>
<div className="border-t border-gray-700 mx-4 mb-3" />
<p className="px-4 mb-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Amministrazione
</p>
</>
)}
{collapsed && <div className="border-t border-gray-700 mx-3 mb-2" />}
<div className="space-y-0.5 px-2">
{([
{ to: '/mailboxes', label: 'Caselle PEC', icon: MailCheck },
{ to: '/users', label: 'Utenti', icon: Users },
{ to: '/permissions', label: 'Permessi', icon: Shield },
{ to: '/virtual-boxes', label: 'Virtual Box', icon: Filter },
{ to: '/notifications', label: 'Notifiche', icon: Bell },
] as const).map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
collapsed && 'justify-center px-2',
)
}
title={collapsed ? item.label : undefined}
>
<item.icon className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span>{item.label}</span>}
</NavLink>
))}
</div>
</div>
)}
</nav>
{/* Profilo utente + logout */}
{/* ── Profilo utente + logout ── */}
<div className="border-t border-gray-700 p-3">
{!collapsed ? (
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium flex-shrink-0">
{user?.full_name?.[0]?.toUpperCase() || 'U'}
{user?.full_name?.[0]?.toUpperCase() ?? 'U'}
</div>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{user?.full_name}</p>
@@ -158,40 +433,295 @@ export function Sidebar() {
)
}
interface SidebarLinkProps {
item: NavItem
// ─── Voce di casella PEC nel sidebar ─────────────────────────────────────────
interface MailboxNavItemProps {
mailbox: MailboxResponse
collapsed: boolean
badge?: number
isExpanded: boolean
onToggle: () => void
}
function SidebarLink({ item, collapsed, badge }: SidebarLinkProps) {
const Icon = item.icon
/** Colore del pallino di stato casella */
function statusDot(status: MailboxResponse['status']): string {
switch (status) {
case 'active':
return 'bg-green-500'
case 'paused':
return 'bg-yellow-400'
case 'error':
return 'bg-red-500'
case 'deleted':
return 'bg-gray-600'
default:
return 'bg-gray-500'
}
}
function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle }: MailboxNavItemProps) {
const displayName = mailbox.display_name || mailbox.email_address
const initial = displayName[0]?.toUpperCase() ?? '?'
const dotClass = statusDot(mailbox.status)
/* ── Modalità compressa: solo avatar/iniziale → link diretto all'inbox ── */
if (collapsed) {
return (
<NavLink
to={`/mailbox/${mailbox.id}/inbox`}
className={({ isActive }) =>
cn(
'relative flex justify-center items-center w-full px-2 py-2 rounded-lg transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
)
}
title={displayName}
>
<div className="relative">
<div className="h-6 w-6 rounded-full bg-gray-600 flex items-center justify-center text-xs font-semibold">
{initial}
</div>
<span
className={cn(
'absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border border-gray-900',
dotClass,
)}
/>
</div>
</NavLink>
)
}
/* ── Modalità espansa: sezione espandibile con In Arrivo, Inviata, Preferiti, Archiviati ── */
return (
<NavLink
to={item.to}
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
collapsed && 'justify-center px-2',
)
}
title={collapsed ? item.label : undefined}
>
<Icon className="h-5 w-5 flex-shrink-0" />
{!collapsed && (
<>
<span className="flex-1">{item.label}</span>
{badge !== undefined && badge > 0 && (
<span className="inline-flex items-center justify-center h-5 w-5 rounded-full bg-blue-500 text-white text-xs font-bold">
{badge > 99 ? '99+' : badge}
</span>
<div>
{/* Intestazione casella (espandi/comprimi) */}
<button
onClick={onToggle}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white transition-colors group"
>
{/* Avatar + status dot */}
<div className="relative flex-shrink-0">
<div className="h-5 w-5 rounded-full bg-gray-600 flex items-center justify-center text-xs font-semibold text-white">
{initial}
</div>
<span
className={cn(
'absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border border-gray-900',
dotClass,
)}
/>
</div>
{/* Nome / email */}
<span className="flex-1 text-left truncate text-xs leading-tight">
{displayName}
</span>
{/* Chevron espandi/comprimi */}
<ChevronDown
className={cn(
'h-3.5 w-3.5 text-gray-500 transition-transform flex-shrink-0',
isExpanded && 'rotate-180',
)}
</>
/>
</button>
{/* Sub-voci: In Arrivo, Inviata, Preferiti, Archiviati */}
{isExpanded && (
<div className="ml-4 mt-0.5 mb-1 space-y-0.5 border-l border-gray-700 pl-3">
<NavLink
to={`/mailbox/${mailbox.id}/inbox`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Inbox className="h-3.5 w-3.5 flex-shrink-0" />
<span>In Arrivo</span>
</NavLink>
<NavLink
to={`/mailbox/${mailbox.id}/sent`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Send className="h-3.5 w-3.5 flex-shrink-0" />
<span>Inviata</span>
</NavLink>
<NavLink
to={`/mailbox/${mailbox.id}/starred`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Star className="h-3.5 w-3.5 flex-shrink-0" />
<span>Preferiti</span>
</NavLink>
<NavLink
to={`/mailbox/${mailbox.id}/archived`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-blue-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Archive className="h-3.5 w-3.5 flex-shrink-0" />
<span>Archiviati</span>
</NavLink>
</div>
)}
</NavLink>
</div>
)
}
// ─── Voce di Virtual Box nel sidebar ─────────────────────────────────────────
interface VirtualBoxNavItemProps {
vbox: VirtualBoxResponse
collapsed: boolean
isExpanded: boolean
onToggle: () => void
}
function VirtualBoxNavItem({ vbox, collapsed, isExpanded, onToggle }: VirtualBoxNavItemProps) {
const displayName = vbox.label || vbox.name
const initial = displayName[0]?.toUpperCase() ?? '?'
/* ── Modalità compressa: solo icona filtro → link diretto all'inbox ── */
if (collapsed) {
return (
<NavLink
to={`/virtual-box/${vbox.id}/inbox`}
className={({ isActive }) =>
cn(
'relative flex justify-center items-center w-full px-2 py-2 rounded-lg transition-colors',
isActive
? 'bg-purple-600 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
)
}
title={displayName}
>
<div className="h-6 w-6 rounded-full bg-purple-800 flex items-center justify-center text-xs font-semibold">
{initial}
</div>
</NavLink>
)
}
/* ── Modalità espansa: sezione espandibile ── */
return (
<div>
{/* Intestazione VBox (espandi/comprimi) */}
<button
onClick={onToggle}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white transition-colors"
>
{/* Icona VBox */}
<div className="h-5 w-5 rounded-full bg-purple-800 flex items-center justify-center text-xs font-semibold text-white flex-shrink-0">
{initial}
</div>
{/* Nome */}
<span className="flex-1 text-left truncate text-xs leading-tight">
{displayName}
</span>
{/* Chevron espandi/comprimi */}
<ChevronDown
className={cn(
'h-3.5 w-3.5 text-gray-500 transition-transform flex-shrink-0',
isExpanded && 'rotate-180',
)}
/>
</button>
{/* Sub-voci: In Arrivo, Inviata, Preferiti, Archiviati */}
{isExpanded && (
<div className="ml-4 mt-0.5 mb-1 space-y-0.5 border-l border-purple-800/40 pl-3">
<NavLink
to={`/virtual-box/${vbox.id}/inbox`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-purple-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Inbox className="h-3.5 w-3.5 flex-shrink-0" />
<span>In Arrivo</span>
</NavLink>
<NavLink
to={`/virtual-box/${vbox.id}/sent`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-purple-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Send className="h-3.5 w-3.5 flex-shrink-0" />
<span>Inviata</span>
</NavLink>
<NavLink
to={`/virtual-box/${vbox.id}/starred`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-purple-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Star className="h-3.5 w-3.5 flex-shrink-0" />
<span>Preferiti</span>
</NavLink>
<NavLink
to={`/virtual-box/${vbox.id}/archived`}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
isActive
? 'bg-purple-600 text-white'
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
)
}
>
<Archive className="h-3.5 w-3.5 flex-shrink-0" />
<span>Archiviati</span>
</NavLink>
</div>
)}
</div>
)
}