refactor(web): extract ReportResultsPanel and nav icons from monolithic components

Extract ReportResultsPanel (293 lines) from ReportBuilder (1231→1044 lines)
and move 38 inline icon components from AppShell (937→833 lines) to nav-icons.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 08:58:31 +02:00
parent 17f2de5f48
commit d1d33aa810
4 changed files with 1244 additions and 503 deletions
+277 -198
View File
@@ -14,153 +14,49 @@ import { PageTransition } from "./PageTransition.js";
import { NotificationBell } from "../notifications/NotificationBell.js";
import { ChatPanel } from "../assistant/ChatPanel.js";
import { NavProgressBar } from "~/components/ui/NavProgressBar.js";
import {
IconFrame,
DashboardIcon,
ResourcesIcon,
ProjectsIcon,
EstimatesIcon,
AllocationsIcon,
TimelineIcon,
StaffingIcon,
VacationIcon,
RolesIcon,
SkillsIcon,
MarketplaceIcon,
ChargeabilityIcon,
BenchIcon,
ReportBuilderIcon,
GraphIcon,
InsightsIcon,
NotificationsIcon,
BroadcastIcon,
ActivityLogIcon,
AdminIcon,
BlueprintIcon,
ClientsIcon,
CountryIcon,
OrgUnitIcon,
CategoryIcon,
LevelsIcon,
ImportIcon,
CalcRulesIcon,
UsersIcon,
SystemRolesIcon,
SecurityIcon,
SettingsIcon,
WebhooksIcon,
ScenariosIcon,
CollapseIcon,
HamburgerIcon,
CloseIcon,
} from "./nav-icons.js";
const SIDEBAR_COLLAPSED_KEY = "capakraken_sidebar_collapsed";
function IconFrame({ children, isActive }: { children: ReactNode; isActive?: boolean }) {
return (
<span className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border shadow-sm transition-all duration-200 ${
isActive
? "border-[rgb(var(--accent-300))] bg-[rgb(var(--accent-50))] text-[rgb(var(--accent-600))] shadow-[0_0_10px_rgba(var(--accent-500),0.2)] dark:border-[rgb(var(--accent-700))] dark:bg-[rgb(var(--accent-500)/0.15)] dark:text-[rgb(var(--accent-300))]"
: "border-white/60 bg-white/80 text-slate-600 hover:shadow-[0_0_12px_rgba(var(--accent-500),0.15)] dark:border-gray-700/80 dark:bg-gray-800/70 dark:text-gray-400"
}`}>
{children}
</span>
);
}
function DashboardIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 13h6V5H4v8zm10 6h6V5h-6v14zM4 19h6v-2H4v2zm0-4h6v-2H4v2zm10 4h6v-6h-6v6z" /></svg>;
}
function ResourcesIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2m18 0v-2a4 4 0 00-3-3.87M14 3.13a4 4 0 010 7.75M12 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>;
}
function ProjectsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M3 7h18M7 3v4m10-4v4M5 11h14v8H5z" /></svg>;
}
function EstimatesIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M8 7h8M8 11h4m-4 4h8M5 4h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z" /></svg>;
}
function AllocationsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M8 7V3m8 4V3M4 11h16M5 5h14a1 1 0 011 1v13a1 1 0 01-1 1H5a1 1 0 01-1-1V6a1 1 0 011-1z" /></svg>;
}
function TimelineIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 6h16M4 12h10M4 18h7m9-8h-4m4 6h-7" /></svg>;
}
function StaffingIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M12 20l9-5-9-5-9 5 9 5zm0-10l9-5-9-5-9 5 9 5zm0 10v-10" /></svg>;
}
function VacationIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 19c3-4 6-6 8-6s5 2 8 6M7 12c.8-2.5 2.5-4 5-4s4.2 1.5 5 4M12 8V4" /></svg>;
}
function RolesIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M7 7h10v10H7zM4 4h4m8 0h4m-4 16h4M4 20h4" /></svg>;
}
function SkillsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M12 3l2.8 5.7 6.2.9-4.5 4.4 1 6.2L12 17.2 6.5 20.2l1-6.2L3 9.6l6.2-.9L12 3z" /></svg>;
}
function MarketplaceIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M3 3h18l-2 9H5L3 3zm0 0l-1-1m6 16a1 1 0 102 0 1 1 0 00-2 0zm10 0a1 1 0 102 0 1 1 0 00-2 0zM5 12h14" /></svg>;
}
function ChargeabilityIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M5 17l4-4 3 3 7-8M19 19H5V5" /></svg>;
}
function BenchIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M20 7H4a1 1 0 00-1 1v2h18V8a1 1 0 00-1-1zM3 10v4h18v-4M5 14v3m14-3v3M8 17h8" /></svg>;
}
function ReportBuilderIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>;
}
function GraphIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="6" cy="6" r="2.5" strokeWidth={1.8} /><circle cx="18" cy="6" r="2.5" strokeWidth={1.8} /><circle cx="12" cy="18" r="2.5" strokeWidth={1.8} /><path strokeLinecap="round" strokeWidth={1.8} d="M8.5 7.5l2 7M15.5 7.5l-2 7M8.5 6h7" /></svg>;
}
function InsightsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>;
}
function NotificationsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg>;
}
function BroadcastIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z" /></svg>;
}
function ActivityLogIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>;
}
function AdminIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M12 8a4 4 0 100 8 4 4 0 000-8zm8 4l-2.1.7a7.9 7.9 0 01-.6 1.5l1 2-2.1 2.1-2-1a7.9 7.9 0 01-1.5.6L12 20l-1.7-2.1a7.9 7.9 0 01-1.5-.6l-2 1-2.1-2.1 1-2a7.9 7.9 0 01-.6-1.5L4 12l2.1-1.7a7.9 7.9 0 01.6-1.5l-1-2 2.1-2.1 2 1a7.9 7.9 0 011.5-.6L12 4l1.7 2.1a7.9 7.9 0 011.5.6l2-1 2.1 2.1-1 2a7.9 7.9 0 01.6 1.5L20 12z" /></svg>;
}
function BlueprintIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>;
}
function ClientsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>;
}
function CountryIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>;
}
function OrgUnitIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" /></svg>;
}
function CategoryIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /></svg>;
}
function LevelsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M3 4h18M3 8h12M3 12h8M3 16h14M3 20h10" /></svg>;
}
function ImportIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>;
}
function CalcRulesIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>;
}
function UsersIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>;
}
function SystemRolesIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>;
}
function SecurityIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>;
}
function SettingsIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>;
}
function WebhooksIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg>;
}
function ScenariosIcon() {
return <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg>;
}
function CollapseIcon({ collapsed }: { collapsed: boolean }) {
return (
<svg
className={clsx("h-4 w-4 transition-transform duration-200", collapsed && "rotate-180")}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
);
}
function HamburgerIcon() {
return (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
);
}
function CloseIcon() {
return (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
);
}
type NavItem = { href: string; label: string; icon: ReactNode; roles: string[] };
type NavSection = { label: string; collapsed?: boolean; items: NavItem[] };
@@ -168,54 +64,159 @@ const navSections: NavSection[] = [
{
label: "Planning",
items: [
{ href: "/dashboard", label: "Dashboard", icon: <DashboardIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/timeline", label: "Timeline", icon: <TimelineIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/allocations", label: "Allocations", icon: <AllocationsIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{
href: "/dashboard",
label: "Dashboard",
icon: <DashboardIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
},
{
href: "/timeline",
label: "Timeline",
icon: <TimelineIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
},
{
href: "/allocations",
label: "Allocations",
icon: <AllocationsIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{ href: "/staffing", label: "Staffing", icon: <StaffingIcon />, roles: ["ADMIN", "MANAGER"] },
{ href: "/scenarios", label: "Scenarios", icon: <ScenariosIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/notifications", label: "Notifications", icon: <NotificationsIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{
href: "/scenarios",
label: "Scenarios",
icon: <ScenariosIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/notifications",
label: "Notifications",
icon: <NotificationsIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
},
],
},
{
label: "Estimating",
collapsed: true,
items: [
{ href: "/estimates", label: "Estimates", icon: <EstimatesIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/admin/rate-cards", label: "Rate Cards", icon: <EstimatesIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/admin/effort-rules", label: "Effort Rules", icon: <EstimatesIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/admin/experience-multipliers", label: "Exp. Multipliers", icon: <EstimatesIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{
href: "/estimates",
label: "Estimates",
icon: <EstimatesIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
},
{
href: "/admin/rate-cards",
label: "Rate Cards",
icon: <EstimatesIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/admin/effort-rules",
label: "Effort Rules",
icon: <EstimatesIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/admin/experience-multipliers",
label: "Exp. Multipliers",
icon: <EstimatesIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
],
},
{
label: "Resources",
items: [
{ href: "/resources", label: "Resources", icon: <ResourcesIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/bench", label: "Bench", icon: <BenchIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/projects", label: "Projects", icon: <ProjectsIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "VIEWER"] },
{ href: "/roles", label: "Roles", icon: <RolesIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{
href: "/resources",
label: "Resources",
icon: <ResourcesIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/bench",
label: "Bench",
icon: <BenchIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/projects",
label: "Projects",
icon: <ProjectsIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "VIEWER"],
},
{
href: "/roles",
label: "Roles",
icon: <RolesIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
],
},
{
label: "Analytics",
items: [
{ href: "/analytics/skills", label: "Skills Hub", icon: <SkillsIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "VIEWER"] },
{ href: "/reports/chargeability", label: "Chargeability", icon: <ChargeabilityIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/reports/builder", label: "Report Builder", icon: <ReportBuilderIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/analytics/computation-graph", label: "Computation Graph", icon: <GraphIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{ href: "/analytics/insights", label: "AI Insights", icon: <InsightsIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER"] },
{
href: "/analytics/skills",
label: "Skills Hub",
icon: <SkillsIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "VIEWER"],
},
{
href: "/reports/chargeability",
label: "Chargeability",
icon: <ChargeabilityIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/reports/builder",
label: "Report Builder",
icon: <ReportBuilderIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/analytics/computation-graph",
label: "Computation Graph",
icon: <GraphIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
{
href: "/analytics/insights",
label: "AI Insights",
icon: <InsightsIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER"],
},
],
},
{
label: "Time Off",
items: [
{ href: "/vacations/my", label: "My Vacations", icon: <VacationIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{ href: "/vacations", label: "Vacation Mgmt", icon: <VacationIcon />, roles: ["ADMIN", "MANAGER"] },
{
href: "/vacations/my",
label: "My Vacations",
icon: <VacationIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
},
{
href: "/vacations",
label: "Vacation Mgmt",
icon: <VacationIcon />,
roles: ["ADMIN", "MANAGER"],
},
],
},
{
label: "Account",
items: [
{ href: "/account/security", label: "Security", icon: <SecurityIcon />, roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
{
href: "/account/security",
label: "Security",
icon: <SecurityIcon />,
roles: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"],
},
],
},
];
@@ -310,7 +311,13 @@ function NavTooltip({
/* ------------------------------------------------------------------ */
/** Routes that benefit from eager prefetching (loaded while user reads current page). */
const PREFETCH_ROUTES = new Set(["/dashboard", "/timeline", "/projects", "/resources", "/allocations"]);
const PREFETCH_ROUTES = new Set([
"/dashboard",
"/timeline",
"/projects",
"/resources",
"/allocations",
]);
const NavItemLink = memo(function NavItemLink({
href,
@@ -425,14 +432,18 @@ function SidebarContent({
return (
<>
{/* Logo */}
<div className={clsx(
"border-b border-gray-200/80 dark:border-slate-800",
sidebarCollapsed ? "px-3 py-4" : "px-6 py-6",
)}>
<div className={clsx(
"inline-flex items-center rounded-2xl border border-brand-200/70 bg-gradient-to-br from-white to-brand-50 shadow-sm dark:border-white/[0.08] dark:from-[rgb(var(--surface-elevated))] dark:to-[rgb(var(--surface-elevated))]",
sidebarCollapsed ? "p-2" : "gap-3 px-4 py-3",
)}>
<div
className={clsx(
"border-b border-gray-200/80 dark:border-slate-800",
sidebarCollapsed ? "px-3 py-4" : "px-6 py-6",
)}
>
<div
className={clsx(
"inline-flex items-center rounded-2xl border border-brand-200/70 bg-gradient-to-br from-white to-brand-50 shadow-sm dark:border-white/[0.08] dark:from-[rgb(var(--surface-elevated))] dark:to-[rgb(var(--surface-elevated))]",
sidebarCollapsed ? "p-2" : "gap-3 px-4 py-3",
)}
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-brand-600 text-white shadow-lg shadow-brand-600/25">
<DashboardIcon />
</div>
@@ -441,7 +452,9 @@ function SidebarContent({
<h1 className="font-display text-xl font-semibold text-gray-900 dark:text-gray-50">
Capa<span className="text-brand-600">Kraken</span>
</h1>
<p className="text-xs uppercase tracking-[0.18em] text-gray-500 dark:text-gray-400">Resource & Capacity Planning</p>
<p className="text-xs uppercase tracking-[0.18em] text-gray-500 dark:text-gray-400">
Resource & Capacity Planning
</p>
</div>
)}
</div>
@@ -478,7 +491,12 @@ function SidebarContent({
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
)}
</button>
@@ -557,7 +575,9 @@ function SidebarContent({
onClick={() => toggleSection(entry.label)}
className="flex w-full items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium text-gray-500 transition-all hover:bg-gray-100/90 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.05] dark:hover:text-gray-200"
>
<IconFrame><AdminIcon /></IconFrame>
<IconFrame>
<AdminIcon />
</IconFrame>
<span className="flex-1 text-left">{entry.label}</span>
<svg
className={clsx(
@@ -568,7 +588,12 @@ function SidebarContent({
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<AnimatePresence initial={false}>
@@ -632,18 +657,24 @@ function SidebarContent({
</div>
{/* Bottom actions */}
<div className={clsx(
"space-y-1 border-t border-gray-200/80 dark:border-slate-800",
sidebarCollapsed ? "p-2" : "p-4",
)}>
<div
className={clsx(
"space-y-1 border-t border-gray-200/80 dark:border-slate-800",
sidebarCollapsed ? "p-2" : "p-4",
)}
>
<NavTooltip label="Notifications" show={sidebarCollapsed}>
<div className={clsx(
"flex items-center",
sidebarCollapsed ? "justify-center px-2 py-2" : "gap-2 px-3 py-2",
)}>
<div
className={clsx(
"flex items-center",
sidebarCollapsed ? "justify-center px-2 py-2" : "gap-2 px-3 py-2",
)}
>
<NotificationBell />
{!sidebarCollapsed && (
<span className="text-xs uppercase tracking-[0.16em] text-gray-400 dark:text-gray-500">Notifications</span>
<span className="text-xs uppercase tracking-[0.16em] text-gray-400 dark:text-gray-500">
Notifications
</span>
)}
</div>
</NavTooltip>
@@ -658,8 +689,18 @@ function SidebarContent({
)}
>
<IconFrame>
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
<svg
className="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
</IconFrame>
{!sidebarCollapsed && <span>HartBOT</span>}
@@ -676,9 +717,24 @@ function SidebarContent({
)}
>
<IconFrame>
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<svg
className="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</IconFrame>
{!sidebarCollapsed && <span>Preferences</span>}
@@ -695,8 +751,18 @@ function SidebarContent({
)}
>
<IconFrame>
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
<svg
className="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
</IconFrame>
{!sidebarCollapsed && <span>Sign out</span>}
@@ -704,7 +770,10 @@ function SidebarContent({
</NavTooltip>
{/* Collapse toggle */}
<NavTooltip label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} show={sidebarCollapsed}>
<NavTooltip
label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
show={sidebarCollapsed}
>
<button
type="button"
onClick={onToggleCollapse}
@@ -720,7 +789,6 @@ function SidebarContent({
</button>
</NavTooltip>
</div>
</>
);
}
@@ -833,7 +901,13 @@ function MobileSidebar({
/* AppShell (main export) */
/* ------------------------------------------------------------------ */
export function AppShell({ children, userRole = "USER" }: { children: React.ReactNode; userRole?: string }) {
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);
@@ -927,7 +1001,12 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
title="HartBOT"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
</button>
)}