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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user