db8e86b749
FMs move to a separate application. Drops Fm/FmPart tables + Repair.fmId column, deletes FM_OPENED/FM_CLOSED PartEvent rows, strips FM enums + webhook events + shared contracts, removes FM routes/services/pages/UI, and collapses dashboard admin ops to Repairs 7d/30d + trend + custody backlog. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import { NavLink } from 'react-router-dom';
|
|
import {
|
|
ArrowRightLeft,
|
|
Boxes,
|
|
ChevronsLeft,
|
|
ChevronsRight,
|
|
Hand,
|
|
LayoutDashboard,
|
|
Layers,
|
|
type LucideIcon,
|
|
MapPinned,
|
|
Package,
|
|
Server,
|
|
Users as UsersIcon,
|
|
Webhook,
|
|
} from 'lucide-react';
|
|
import { cn, Button, Tooltip, TooltipContent, TooltipTrigger } from '@vector/ui';
|
|
import { useAuth } from '../../contexts/AuthContext.js';
|
|
|
|
interface NavItem {
|
|
to: string;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
adminOnly?: boolean;
|
|
}
|
|
|
|
const NAV_ITEMS: NavItem[] = [
|
|
{ to: '/', label: 'Dashboard', icon: LayoutDashboard },
|
|
{ to: '/parts', label: 'Parts', icon: Package },
|
|
{ to: '/part-models', label: 'Part models', icon: Layers },
|
|
{ to: '/locations', label: 'Locations', icon: MapPinned },
|
|
{ to: '/manufacturers', label: 'Manufacturers', icon: Boxes },
|
|
{ to: '/repairs', label: 'Repairs', icon: ArrowRightLeft },
|
|
{ to: '/custody', label: 'My Custody', icon: Hand },
|
|
{ to: '/hosts', label: 'Hosts', icon: Server },
|
|
{ to: '/admin/users', label: 'Users', icon: UsersIcon, adminOnly: true },
|
|
{ to: '/admin/webhooks', label: 'Webhooks', icon: Webhook, adminOnly: true },
|
|
];
|
|
|
|
export interface SidebarProps {
|
|
collapsed: boolean;
|
|
onToggle: () => void;
|
|
}
|
|
|
|
export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
|
const { user } = useAuth();
|
|
const items = NAV_ITEMS.filter((i) => !i.adminOnly || user?.role === 'ADMIN');
|
|
|
|
return (
|
|
<aside
|
|
className={cn(
|
|
'fixed inset-y-0 left-0 z-40 flex flex-col border-r border-border bg-card transition-[width] duration-200',
|
|
collapsed ? 'w-14' : 'w-64',
|
|
)}
|
|
>
|
|
<div className="flex h-13 items-center gap-2 border-b border-border px-3">
|
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-brand text-brand-foreground font-semibold">
|
|
V
|
|
</div>
|
|
{!collapsed && <span className="truncate text-sm font-semibold">Vector</span>}
|
|
</div>
|
|
|
|
<nav className="flex-1 space-y-0.5 p-2">
|
|
{items.map((item) => (
|
|
<NavItemLink key={item.to} item={item} collapsed={collapsed} />
|
|
))}
|
|
</nav>
|
|
|
|
<div className="border-t border-border p-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn('w-full', !collapsed && 'justify-start gap-2 px-2')}
|
|
onClick={onToggle}
|
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
>
|
|
{collapsed ? <ChevronsRight className="h-4 w-4" /> : <ChevronsLeft className="h-4 w-4" />}
|
|
{!collapsed && <span className="text-xs text-muted-foreground">Collapse</span>}
|
|
</Button>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function NavItemLink({ item, collapsed }: { item: NavItem; collapsed: boolean }) {
|
|
const content = (
|
|
<NavLink
|
|
to={item.to}
|
|
end={item.to === '/'}
|
|
className={({ isActive }) =>
|
|
cn(
|
|
'flex items-center gap-3 rounded-md px-2.5 py-2 text-sm transition-colors',
|
|
isActive
|
|
? 'bg-accent text-accent-foreground'
|
|
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
|
|
collapsed && 'justify-center px-0',
|
|
)
|
|
}
|
|
>
|
|
<item.icon className="h-4 w-4 shrink-0" />
|
|
{!collapsed && <span className="truncate">{item.label}</span>}
|
|
</NavLink>
|
|
);
|
|
if (!collapsed) return content;
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
|
<TooltipContent side="right">{item.label}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|