import { Link, NavLink, Outlet } from 'react-router-dom' import { ChevronDown, Circle, type LucideIcon } from 'lucide-react' import * as Icons from 'lucide-react' import { useState } from 'react' import { useAuth } from '@/contexts/AuthContext' import { TopBar } from '@/components/TopBar' import type { MenuNode } from '@/types/menu' import { cn } from '@/lib/cn' function getIcon(name: string | null): LucideIcon { if (!name) return Circle const candidate = (Icons as unknown as Record)[name] return candidate ?? Circle } // Map contract type code → ContractType int enum value (mirrors // Domain/Contracts/ContractType.cs for URL filter). const TYPE_CODE_TO_INT: Record = { ThauPhu: 1, GiaoKhoan: 2, NhaCungCap: 3, DichVu: 4, MuaBan: 5, NguyenTacNcc: 6, NguyenTacDv: 7, } // Resolve menu key → route. Static map for top-level items, pattern for // Ct__ sub-menu entries. function resolvePath(key: string): string | null { const staticMap: Record = { Dashboard: '/dashboard', Suppliers: '/master/suppliers', Projects: '/master/projects', Departments: '/master/departments', Contracts: '/contracts', Forms: '/forms', Reports: '/reports', Users: '/system/users', Roles: '/system/roles', Permissions: '/system/permissions', Workflows: '/system/workflows', } if (staticMap[key]) return staticMap[key] const match = key.match(/^Ct_([^_]+)_(List|Create|Pending)$/) if (match) { const [, code, action] = match const typeInt = TYPE_CODE_TO_INT[code] if (!typeInt) return null if (action === 'List') return `/contracts?type=${typeInt}` if (action === 'Create') return `/contracts/new?type=${typeInt}` if (action === 'Pending') return `/contracts?type=${typeInt}&pendingMe=1` } // Workflow admin per ContractType: Wf_ → /system/workflows/ const wfMatch = key.match(/^Wf_(.+)$/) if (wfMatch) { const code = wfMatch[1] if (TYPE_CODE_TO_INT[code]) return `/system/workflows/${code}` } return null } // Admin side: hide the per-ContractType contract submenu (Ct_*) — that's a // user-app concern. Keep Wf_* workflow-admin leaves. function isAdminHidden(key: string): boolean { return key.startsWith('Ct_') } function filterForAdmin(nodes: MenuNode[]): MenuNode[] { return nodes .filter(n => !isAdminHidden(n.key)) .map(n => ({ ...n, children: filterForAdmin(n.children) })) } function MenuNodeRenderer({ node, depth = 0 }: { node: MenuNode; depth?: number }) { const hasChildren = node.children.length > 0 if (hasChildren) return return } function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) { // Top-level groups expanded by default, nested ones collapsed to reduce noise const [open, setOpen] = useState(depth === 0) const Icon = getIcon(node.icon) const isTopLevel = depth === 0 return (
{open && (
{node.children.map(c => ( ))}
)}
) } function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) { const Icon = getIcon(node.icon) const path = resolvePath(node.key) if (!path) return null const isDeep = depth >= 2 return ( cn( 'flex items-center gap-2.5 rounded-md transition', isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium', isActive ? 'bg-brand-50 text-brand-700' : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', ) } > {node.label} ) } export function Layout() { const { menu } = useAuth() return (
) }