"use client";
import type { ReactNode } from "react";
import { signOut } from "next-auth/react";
import Link from "next/link";
import type { Route } from "next";
import { usePathname } from "next/navigation";
import { clsx } from "clsx";
import { memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { PreferencesModal } from "./PreferencesModal.js";
import { ThemeProvider } from "./ThemeProvider.js";
import { PageTransition } from "./PageTransition.js";
import { NotificationBell } from "../notifications/NotificationBell.js";
import { ChatPanel } from "../assistant/ChatPanel.js";
import { NavProgressBar } from "~/components/ui/NavProgressBar.js";
const SIDEBAR_COLLAPSED_KEY = "capakraken_sidebar_collapsed";
function IconFrame({ children }: { children: ReactNode }) {
return (
{children}
);
}
function DashboardIcon() {
return ;
}
function ResourcesIcon() {
return ;
}
function ProjectsIcon() {
return ;
}
function EstimatesIcon() {
return ;
}
function AllocationsIcon() {
return ;
}
function TimelineIcon() {
return ;
}
function StaffingIcon() {
return ;
}
function VacationIcon() {
return ;
}
function RolesIcon() {
return ;
}
function SkillsIcon() {
return ;
}
function MarketplaceIcon() {
return ;
}
function ChargeabilityIcon() {
return ;
}
function ReportBuilderIcon() {
return ;
}
function GraphIcon() {
return ;
}
function InsightsIcon() {
return ;
}
function NotificationsIcon() {
return ;
}
function BroadcastIcon() {
return ;
}
function ActivityLogIcon() {
return ;
}
function AdminIcon() {
return ;
}
function BlueprintIcon() {
return ;
}
function ClientsIcon() {
return ;
}
function CountryIcon() {
return ;
}
function OrgUnitIcon() {
return ;
}
function CategoryIcon() {
return ;
}
function LevelsIcon() {
return ;
}
function ImportIcon() {
return ;
}
function CalcRulesIcon() {
return ;
}
function UsersIcon() {
return ;
}
function SystemRolesIcon() {
return ;
}
function SecurityIcon() {
return ;
}
function SettingsIcon() {
return ;
}
function WebhooksIcon() {
return ;
}
function CollapseIcon({ collapsed }: { collapsed: boolean }) {
return (
);
}
function HamburgerIcon() {
return (
);
}
function CloseIcon() {
return (
);
}
type NavItem = { href: string; label: string; icon: ReactNode; roles: string[] };
type NavSection = { label: string; collapsed?: boolean; items: NavItem[] };
const navSections: NavSection[] = [
{
label: "Planning",
items: [
{ href: "/dashboard", label: "Dashboard", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/timeline", label: "Timeline", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/allocations", label: "Allocations", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/staffing", label: "Staffing", icon: , roles: ["ADMIN", "MANAGER"] },
{ href: "/notifications", label: "Notifications", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
],
},
{
label: "Estimating",
collapsed: true,
items: [
{ href: "/estimates", label: "Estimates", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/admin/rate-cards", label: "Rate Cards", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/admin/effort-rules", label: "Effort Rules", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/admin/experience-multipliers", label: "Exp. Multipliers", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
],
},
{
label: "Resources",
items: [
{ href: "/resources", label: "Resources", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/projects", label: "Projects", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "VIEWER"] },
{ href: "/roles", label: "Roles", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
],
},
{
label: "Analytics",
items: [
{ href: "/analytics/skills", label: "Skills Hub", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "VIEWER"] },
{ href: "/reports/chargeability", label: "Chargeability", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/reports/builder", label: "Report Builder", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/analytics/computation-graph", label: "Computation Graph", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/analytics/insights", label: "AI Insights", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
],
},
{
label: "Time Off",
items: [
{ href: "/vacations/my", label: "My Vacations", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/vacations", label: "Vacation Mgmt", icon: , roles: ["ADMIN", "MANAGER"] },
],
},
{
label: "Account",
items: [
{ href: "/account/security", label: "Security", icon: , roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
],
},
];
type AdminNavItem = { href: string; label: string; icon: ReactNode };
type AdminSubGroup = { label: string; collapsed: boolean; items: AdminNavItem[] };
type AdminEntry = AdminNavItem | AdminSubGroup;
function isSubGroup(entry: AdminEntry): entry is AdminSubGroup {
return "items" in entry;
}
const adminNavEntries: AdminEntry[] = [
{ href: "/admin/blueprints", label: "Blueprints", icon: },
{ href: "/admin/clients", label: "Clients", icon: },
{
label: "ACN-Orga",
collapsed: true,
items: [
{ href: "/admin/countries", label: "Countries", icon: },
{ href: "/admin/org-units", label: "Org Units", icon: },
{ href: "/admin/utilization-categories", label: "Util. Categories", icon: },
{ href: "/admin/management-levels", label: "Mgmt Levels", icon: },
{ href: "/admin/imports", label: "Data Import", icon: },
],
},
{ href: "/admin/calculation-rules", label: "Calc. Rules", icon: },
{ href: "/admin/vacations", label: "Vacations & Holidays", icon: },
{ href: "/admin/users", label: "Users", icon: },
{ href: "/admin/system-roles", label: "System Roles", icon: },
{ href: "/admin/settings", label: "Settings", icon: },
{ href: "/admin/notifications", label: "Broadcasts", icon: },
{ href: "/admin/webhooks", label: "Webhooks", icon: },
{ href: "/admin/activity-log", label: "Activity Log", icon: },
];
/**
* Collect every href registered in the sidebar so that the active-check
* can determine whether a more-specific sibling matches the current path.
*/
const ALL_NAV_HREFS: string[] = (() => {
const hrefs: string[] = [];
for (const section of navSections) {
for (const item of section.items) hrefs.push(item.href);
}
for (const entry of adminNavEntries) {
if (isSubGroup(entry)) {
for (const item of entry.items) hrefs.push(item.href);
} else {
hrefs.push(entry.href);
}
}
return hrefs;
})();
function isNavItemActive(pathname: string, href: string): boolean {
if (pathname === href) return true;
if (!pathname.startsWith(href + "/")) return false;
const hasMoreSpecificSibling = ALL_NAV_HREFS.some(
(other) => other !== href && other.length > href.length && pathname.startsWith(other),
);
return !hasMoreSpecificSibling;
}
/* ------------------------------------------------------------------ */
/* Tooltip wrapper — shows label on hover when sidebar is collapsed */
/* ------------------------------------------------------------------ */
function NavTooltip({
label,
show,
children,
}: {
label: string;
show: boolean;
children: ReactNode;
}) {
if (!show) return <>{children}>;
return (
);
}
/* ------------------------------------------------------------------ */
/* Memoized nav item — prevents re-render of inactive items */
/* ------------------------------------------------------------------ */
/** Routes that benefit from eager prefetching (loaded while user reads current page). */
const PREFETCH_ROUTES = new Set(["/dashboard", "/timeline", "/projects", "/resources", "/allocations"]);
const NavItemLink = memo(function NavItemLink({
href,
label,
icon,
isActive,
collapsed,
onClick,
}: {
href: string;
label: string;
icon: ReactNode;
isActive: boolean;
collapsed: boolean;
onClick?: (() => void) | undefined;
}) {
const linkProps = {
...(onClick ? { onClick } : {}),
...(PREFETCH_ROUTES.has(href) ? { prefetch: true as const } : {}),
};
return (
{isActive && (
)}
{icon}
{!collapsed && {label}}
);
});
/* ------------------------------------------------------------------ */
/* Sidebar component */
/* ------------------------------------------------------------------ */
function SidebarContent({
userRole,
onChatOpen,
sidebarCollapsed,
onToggleCollapse,
onNavClick,
onPrefsOpen,
}: {
userRole: string;
onChatOpen: () => void;
sidebarCollapsed: boolean;
onToggleCollapse: () => void;
onNavClick?: () => void;
onPrefsOpen: () => void;
}) {
const pathname = usePathname();
const activeHrefSet = useMemo(() => {
const set = new Set();
for (const href of ALL_NAV_HREFS) {
if (isNavItemActive(pathname, href)) set.add(href);
}
return set;
}, [pathname]);
const visibleSections = navSections
.map((section) => ({
...section,
items: section.items.filter((item) => item.roles.includes(userRole)),
}))
.filter((section) => section.items.length > 0);
const showAdmin = userRole === "ADMIN";
const showManagerSection = userRole === "ADMIN" || userRole === "MANAGER";
const [collapsedSections, setCollapsedSections] = useState>(() => {
const initial: Record = {};
for (const section of visibleSections) {
if (section.collapsed) {
const hasActiveRoute = section.items.some((item) => activeHrefSet.has(item.href));
initial[section.label] = !hasActiveRoute;
}
}
for (const entry of adminNavEntries) {
if (isSubGroup(entry) && entry.collapsed) {
const hasActiveRoute = entry.items.some((item) => activeHrefSet.has(item.href));
initial[entry.label] = !hasActiveRoute;
}
}
return initial;
});
const toggleSection = (label: string) => {
setCollapsedSections((prev) => ({ ...prev, [label]: !prev[label] }));
};
const handleLinkClick = () => {
onNavClick?.();
};
return (
<>
{/* Logo */}
{!sidebarCollapsed && (
CapaKraken
Resource & Capacity Planning
)}
{/* Nav links */}
{visibleSections.map((section, idx) => {
const isCollapsed = collapsedSections[section.label] ?? false;
const isCollapsible = section.collapsed === true;
return (
0 ? "mt-2" : ""}>
{/* Section header — hidden when sidebar collapsed */}
{!sidebarCollapsed && (
)}
{/* Collapsed sidebar: show a thin divider between sections instead of header */}
{sidebarCollapsed && idx > 0 && (
)}
{(!isCollapsed || sidebarCollapsed) && (
{section.items.map((item) => (
))}
)}
);
})}
{showManagerSection && showAdmin && (
{!sidebarCollapsed && (
Admin
)}
{sidebarCollapsed && (
)}
{adminNavEntries.map((entry) => {
if (isSubGroup(entry)) {
const subCollapsed = collapsedSections[entry.label] ?? false;
if (sidebarCollapsed) {
// In collapsed mode, show sub-group items directly as icon-only
return entry.items.map((item) => (
));
}
return (
{!subCollapsed && (
{entry.items.map((item) => {
const isActive = activeHrefSet.has(item.href);
return (
{isActive && (
)}
{item.label}
);
})}
)}
);
}
return (
);
})}
)}
{/* Bottom actions */}
{!sidebarCollapsed && (
Notifications
)}
{/* Collapse toggle */}
>
);
}
/* ------------------------------------------------------------------ */
/* Desktop sidebar wrapper */
/* ------------------------------------------------------------------ */
function DesktopSidebar({
userRole,
onChatOpen,
sidebarCollapsed,
onToggleCollapse,
onPrefsOpen,
}: {
userRole: string;
onChatOpen: () => void;
sidebarCollapsed: boolean;
onToggleCollapse: () => void;
onPrefsOpen: () => void;
}) {
return (
);
}
/* ------------------------------------------------------------------ */
/* Mobile sidebar overlay */
/* ------------------------------------------------------------------ */
function MobileSidebar({
open,
onClose,
userRole,
onChatOpen,
onPrefsOpen,
}: {
open: boolean;
onClose: () => void;
userRole: string;
onChatOpen: () => void;
onPrefsOpen: () => void;
}) {
return (
{open && (
<>
{/* Backdrop */}
{/* Slide-in sidebar */}
{/* Close button */}
{
onChatOpen();
onClose();
}}
sidebarCollapsed={false}
onToggleCollapse={() => {}}
onNavClick={onClose}
onPrefsOpen={() => {
onPrefsOpen();
onClose();
}}
/>
>
)}
);
}
/* ------------------------------------------------------------------ */
/* AppShell (main export) */
/* ------------------------------------------------------------------ */
export function AppShell({ children, userRole = "USER" }: { children: React.ReactNode; userRole?: string }) {
const [chatOpen, setChatOpen] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [prefsOpen, setPrefsOpen] = useState(false);
const pathname = usePathname();
const contentRef = useRef(null);
// Read collapsed state from localStorage on mount (avoid SSR hydration mismatch)
useEffect(() => {
try {
const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY);
if (stored === "true") setSidebarCollapsed(true);
} catch {
// localStorage unavailable — ignore
}
}, []);
// Persist collapsed state
const handleToggleCollapse = useCallback(() => {
setSidebarCollapsed((prev) => {
const next = !prev;
try {
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(next));
} catch {
// ignore
}
return next;
});
}, []);
// Scroll to top on route change
useEffect(() => {
contentRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}, [pathname]);
// Close mobile sidebar on route change
useEffect(() => {
setMobileOpen(false);
}, [pathname]);
return (
{/* Desktop sidebar */}
setChatOpen(true)}
sidebarCollapsed={sidebarCollapsed}
onToggleCollapse={handleToggleCollapse}
onPrefsOpen={() => setPrefsOpen(true)}
/>
{/* Mobile sidebar overlay */}
setMobileOpen(false)}
userRole={userRole}
onChatOpen={() => setChatOpen(true)}
onPrefsOpen={() => setPrefsOpen(true)}
/>
{/* Main content area */}
{/* Mobile hamburger */}
CapaKraken
{children}
{chatOpen && setChatOpen(false)} />}
{/* Floating chat FAB */}
{!chatOpen && (
)}
{prefsOpen && setPrefsOpen(false)} />}
);
}