'use client'; import { useState } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { LayoutDashboard, Users, Bookmark, Anchor, Ship, Building2, Receipt, FileText, Inbox, Camera, Globe, Settings, Shield, ScrollText, RefreshCw, Home, ChevronLeft, ChevronRight, ChevronDown, ChevronUp, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { formatRole } from '@/lib/constants'; import { useUIStore } from '@/stores/ui-store'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { UserMenu } from '@/components/layout/user-menu'; import { useUmamiActive } from '@/components/website-analytics/use-website-analytics'; import type { UserPortRole } from '@/lib/db/schema/users'; import type { Role } from '@/lib/db/schema/users'; import type { Port } from '@/lib/db/schema/ports'; interface SidebarProps { portRoles: (UserPortRole & { port: { id: string; slug: string; name: string }; role: Role })[]; isSuperAdmin?: boolean; user?: { name: string; email: string }; /** Ports the user has access to. Drives the footer port switcher. */ ports?: Port[]; /** Per-port logo URLs resolved server-side in the dashboard layout. * The sidebar header swaps to the current port's logo via the UI * store's `currentPortId`. Null entries render the wordmark fallback. */ portLogoUrls?: Record; } interface NavItem { href: string; label: string; icon: React.ElementType; exact?: boolean; } interface NavSection { title: string; items: NavItem[]; adminRequired?: boolean; /** When true, only render if the user has marina-side access. */ marinaRequired?: boolean; /** When true, only render if the user has residential-side access. */ residentialRequired?: boolean; /** When true, only render if Umami analytics is wired up for the port. */ umamiRequired?: boolean; } function buildNavSections(portSlug: string | undefined): NavSection[] { const base = portSlug ? `/${portSlug}` : ''; return [ { title: 'Main', marinaRequired: true, items: [ { href: `${base}/dashboard`, label: 'Dashboard', icon: LayoutDashboard }, { href: `${base}/clients`, label: 'Clients', icon: Users }, { href: `${base}/yachts`, label: 'Yachts', icon: Ship }, { href: `${base}/companies`, label: 'Companies', icon: Building2 }, { href: `${base}/interests`, label: 'Interests', icon: Bookmark }, { href: `${base}/berths`, label: 'Berths', icon: Anchor }, ], }, { title: 'Residential', residentialRequired: true, items: [ { href: `${base}/residential/clients`, label: 'Residential Clients', icon: Home, }, { href: `${base}/residential/interests`, label: 'Residential Interests', icon: Bookmark, }, ], }, { title: 'Insights', marinaRequired: true, umamiRequired: true, items: [ // Marketing / Umami integration. Distinct from the main dashboard // (which is sales-focused) so the audience and the metrics don't // compete for visual real estate. Whole section is hidden when // Umami isn't wired up — see SidebarContent. { href: `${base}/website-analytics`, label: 'Website analytics', icon: Globe, }, ], }, { title: 'Documents', marinaRequired: true, items: [{ href: `${base}/documents`, label: 'Documents', icon: FileText }], }, { title: 'Financial', marinaRequired: true, items: [ { href: `${base}/expenses`, label: 'Expenses', icon: Receipt }, // Invoices nav entry removed — the expense-to-PDF flow is the // only invoicing surface now (employee expense reports). The // standalone /invoices route still exists for any back-compat // links but is no longer surfaced in nav. // Dedicated explainer page covers "Add to Home Screen" install // + walkthrough; the mobile-only scanner UI itself lives at /scan // and is reached via the install or the explainer page button. { href: `${base}/invoices/upload-receipts`, label: 'How to upload receipts', icon: Camera, }, ], }, { title: 'Communication', marinaRequired: true, items: [ // Email tab removed: deferred building a full inbox/threading // feature (would require Google OAuth + scope review + IMAP // syncing infra). This entry routes to the merged // Alerts + Reminders surface (2026-05-11) — explicit name so // reps don't mistake it for an email inbox. { href: `${base}/inbox`, label: 'Alerts & Reminders', icon: Inbox }, ], }, { title: 'Admin', adminRequired: true, items: [ { href: `${base}/settings`, label: 'Settings', icon: Settings }, { href: `${base}/admin`, label: 'Administration', icon: Shield }, // F14: audit log page existed but had no nav link. { href: `${base}/admin/audit`, label: 'Audit Log', icon: ScrollText }, // #67 Phase 5: surfaces berths flipped manually without a backing // interest so reps can run the catch-up wizard. { href: `${base}/admin/berths/reconcile`, label: 'Reconcile berths', icon: RefreshCw, }, ], }, ]; } function NavItemLink({ item, collapsed, active, }: { item: NavItem; collapsed: boolean; active: boolean; }) { const content = ( {active && !collapsed && ( )} {!collapsed && {item.label}} ); if (collapsed) { return ( {content} {item.label} ); } return content; } function SidebarContent({ collapsed, portSlug, portRoles, isSuperAdmin, hasAdminAccess, hasMarinaAccess, hasResidentialAccess, user, ports, currentPort, currentLogoUrl, onToggleCollapse, }: { collapsed: boolean; portSlug: string | undefined; portRoles: SidebarProps['portRoles']; isSuperAdmin: boolean; hasAdminAccess: boolean; hasMarinaAccess: boolean; hasResidentialAccess: boolean; user?: SidebarProps['user']; ports?: Port[]; currentPort: Port | null; currentLogoUrl: string | null; /** When provided, renders the collapse toggle row above the user footer (desktop). */ onToggleCollapse?: () => void; }) { const pathname = usePathname(); const [adminExpanded, setAdminExpanded] = useState(true); const sections = buildNavSections(portSlug); const umami = useUmamiActive('today'); const umamiConfigured = !umami.isLoading && umami.data?.notConfigured !== true; // Small label under the user identity when the user has access to more // than one port — disambiguates which port is currently active without // pulling the port name back into the breadcrumbs. const showPortLabel = !!ports && ports.length > 1; const currentPortName = showPortLabel ? (ports.find((p) => p.slug === portSlug)?.name ?? null) : null; // Pre-compute every nav href the sidebar offers across all sections so // the active-state check can do longest-prefix-match. Without this, // /invoices/upload-receipts would highlight both "Invoices" and "How to // upload receipts" - every ancestor prefix matches. const allHrefs = sections.flatMap((s) => s.items.map((i) => i.href)); function isActive(href: string, exact?: boolean): boolean { if (exact) return pathname === href; if (!pathname.startsWith(href)) return false; // Active only if no other sidebar item has a strictly more-specific // prefix match. That makes the highlight follow the URL into the // deepest registered route (e.g. /invoices/upload-receipts wins over // /invoices when both are sidebar items). return !allHrefs.some((other) => other.length > href.length && pathname.startsWith(other)); } // Sidebar header showcases the Port Nimara logo as the visual anchor of // the brand. Collapse toggle is a small overlay button so it doesn't // compete with the logo for attention. return (
{/* Brand header - logo centered. Soft hairline below echoes the inter-section separators in the nav so the eye reads the logo as a distinct top-row, not a floating element. */}
{currentLogoUrl ? ( {currentPort?.name ) : (
{currentPort?.name ?? 'CRM'}
)} {onToggleCollapse && ( )}
{/* Nav */} {/* User footer - entire row is the trigger for the UserMenu so the user can click their name/avatar to access Settings / port-switcher / sign-out. The same UserMenu component drives the top-right avatar dropdown, so the menu items stay consistent. */}
{collapsed ? ( {(user?.name ?? 'U').slice(0, 1).toUpperCase()} } /> ) : ( {(user?.name ?? 'U').slice(0, 1).toUpperCase()}

{user?.name ?? 'User'}

{isSuperAdmin ? 'Super Admin' : formatRole(portRoles[0]?.role?.name)} {currentPortName && (

{currentPortName}

)}
} /> )}
); } export function Sidebar({ portRoles, isSuperAdmin = false, user, ports, portLogoUrls, }: SidebarProps) { // Sidebar collapse removed — design preference is the always-expanded // form. Forcibly false; the store flag stays for backwards-compat with // any code still reading it. const sidebarCollapsed = false; const currentPortSlug = useUIStore((s) => s.currentPortSlug); const currentPortId = useUIStore((s) => s.currentPortId); const currentPort = ports?.find((p) => p.id === currentPortId) ?? ports?.[0] ?? null; const currentLogoUrl = currentPortId ? (portLogoUrls?.[currentPortId] ?? null) : null; // Super admins see every section regardless of role rows. const hasAdminAccess = isSuperAdmin || portRoles.some( (pr) => pr.role?.permissions?.admin?.manage_users || pr.role?.permissions?.admin?.manage_settings, ); const hasMarinaAccess = isSuperAdmin || portRoles.some((pr) => pr.role?.permissions?.clients?.view); const hasResidentialAccess = isSuperAdmin || portRoles.some((pr) => pr.residentialAccess || pr.role?.permissions?.residential_clients?.view); return ( ); }