'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,
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';
const LOGO_URL =
'https://s3.portnimara.com/images/Port%20Nimara%20New%20Logo-Circular%20Frame_250px.png';
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[];
}
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: '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: '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: '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 },
],
},
];
}
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,
onToggleCollapse,
}: {
collapsed: boolean;
portSlug: string | undefined;
portRoles: SidebarProps['portRoles'];
isSuperAdmin: boolean;
hasAdminAccess: boolean;
hasMarinaAccess: boolean;
hasResidentialAccess: boolean;
user?: SidebarProps['user'];
ports?: Port[];
/** 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. */}
{onToggleCollapse && (
)}
{/* Nav */}
{/* User footer - entire row is the trigger for the UserMenu so the
user can click their name/avatar to access Profile / Settings /
port-switcher / sign-out. The same UserMenu component drives the
top-right avatar dropdown, so the menu items stay consistent. */}