'use client'; /** * Sidebar — Collapsible navigation component with sections. * * Features: * - Expanded (260px) or collapsed (64px icon-only) on desktop (≥1024px) * - Navigation items grouped into collapsible sections: Favorites, Dashboards, Pages * - Section expand/collapse with smooth animation (≤300ms) * - Active item highlighted with accent-colored left border (3-4px) + background * - "Divisi" brand logo and name at the top * - Settings pinned at the bottom with a visual divider * - Mobile hamburger menu + overlay drawer preserved for viewports below 1024px * - Collapse toggle button at the bottom of the sidebar * * Requirements: 1.1–1.10 */ import { useState } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import Link from 'next/link'; import { useAuthStore } from '@/lib/stores/auth-store'; import { useSidebarStore } from '@/lib/stores/sidebar-store'; import { useThemeStore } from '@/lib/stores/theme-store'; import { useLangStore } from '@/lib/stores/lang-store'; import { useTranslation } from '@/lib/hooks/useTranslation'; import type { TranslationKeys } from '@/lib/i18n/translations'; import { getNavItemsForRole, getBottomNavItemsForRole, groupNavItemsBySection, type IconName, type NavItem, type NavSection, } from '@/lib/constants/navigation'; // ── Icon map — simple SVG icons for each nav item ─────────────────────────── function NavIcon({ name, className }: { name: IconName; className?: string }) { const cls = className ?? 'h-5 w-5'; switch (name) { case 'overview': return ( ); case 'works': return ( ); case 'composers': return ( ); case 'payees': return ( ); case 'contracts': return ( ); case 'income': return ( ); case 'transactions': return ( ); case 'costs': return ( ); case 'statements': return ( ); case 'suspense': return ( ); case 'audit': return ( ); case 'settings': return ( ); case 'home': return ( ); case 'analytics': return ( ); case 'songs': return ( ); case 'documents': return ( ); case 'chart-overview': return ( ); case 'top-songs': return ( ); case 'income-groups': return ( ); case 'territories': return ( ); case 'sources': return ( ); default: return ( ); } } // ── Chevron icon for section expand/collapse ───────────────────────────────── function ChevronIcon({ expanded, className }: { expanded: boolean; className?: string }) { return ( ); } // ── Section header labels ──────────────────────────────────────────────────── function getSectionLabels(t: TranslationKeys): Record { return { favorites: t.sidebar.favorites, dashboards: t.sidebar.dashboards, pages: t.sidebar.pages, analytics: t.sidebar.analytics, }; } const SECTION_ORDER: NavSection[] = ['dashboards', 'analytics', 'pages']; // ── Nav label translation helper ───────────────────────────────────────────── function getTranslatedLabel(label: string, t: TranslationKeys): string { const keyMap: Record = { 'Overview': t.nav.overview, 'Works': t.nav.works, 'Composers': t.nav.composers, 'Payees': t.nav.payees, 'Contracts': t.nav.contracts, 'Income': t.nav.income, 'Transactions': t.nav.transactions, 'Costs': t.nav.costs, 'Statements': t.nav.statements, 'Suspense': t.nav.suspense, 'Audit': t.nav.audit, 'Settings': t.nav.settings, 'Inicio': t.nav.inicio, 'Top Songs': t.nav.topSongs, 'Income Groups': t.nav.incomeGroups, 'Territories': t.nav.territories, 'Exploitation Sources': t.nav.exploitationSources, 'Canciones': t.nav.canciones, 'Documentos': t.nav.documentos, }; return keyMap[label] ?? label; } // ── Collapsible nav section ────────────────────────────────────────────────── function NavSectionGroup({ section, items, expanded, onToggle, pathname, onItemClick, collapsed, t, }: { section: NavSection; items: NavItem[]; expanded: boolean; onToggle: () => void; pathname: string; onItemClick?: () => void; collapsed?: boolean; t: TranslationKeys; }) { if (items.length === 0) return null; const sectionLabels = getSectionLabels(t); // Collapsed mode: just show icons, no section headers if (collapsed) { return (
{items.map((item) => { const isActive = pathname === item.href; const translatedLabel = getTranslatedLabel(item.label, t); return ( {isActive && ( )} ); })}
); } return (
{/* Section header */} {/* Collapsible items with smooth animation */}
{items.map((item) => { const isActive = pathname === item.href; const translatedLabel = getTranslatedLabel(item.label, t); return ( {isActive && ( )} {translatedLabel} ); })}
); } // ── Sidebar component ─────────────────────────────────────────────────────── export default function Sidebar() { const pathname = usePathname(); const router = useRouter(); const role = useAuthStore((s) => s.role); const user = useAuthStore((s) => s.user); const logout = useAuthStore((s) => s.logout); const { isMobileOpen, closeMobile, toggleMobile, isCollapsed, toggleCollapse } = useSidebarStore(); const { mode: themeMode, toggle: toggleTheme } = useThemeStore(); const { lang, toggle: toggleLang } = useLangStore(); const t = useTranslation(); // Section expand/collapse state — all default to expanded const [expandedSections, setExpandedSections] = useState>({ favorites: true, dashboards: true, pages: true, analytics: true, }); const toggleSection = (section: NavSection) => { setExpandedSections((prev) => ({ ...prev, [section]: !prev[section], })); }; // Filter nav items based on current role const visibleItems = role ? getNavItemsForRole(role) : []; const bottomItems = role ? getBottomNavItemsForRole(role) : []; // Group items by section const groupedItems = groupNavItemsBySection(visibleItems); return ( <> {/* ─── Desktop sidebar (≥1024px) — collapsible ─────────────────── */} {/* ─── Mobile hamburger button (visible below lg) ───────────────── */} {/* ─── Mobile overlay + drawer (below lg) ───────────────────────── */} {isMobileOpen && ( <> {/* Backdrop */}