import { NavLink, useLocation } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { fetchSystemNavigation, type NavigationItem } from "../../api/workspace"; import { useAuth } from "../../auth/AuthContext"; import { hasAnyPermission, hasBusinessApproverRole, hasFinanceRole } from "../../auth/permissions"; import { ADMIN_NAV_ORDER, BACKUP_NAV_ORDER, MAIN_NAV_ORDER, } from "../../features/navigation/schema"; import { isManagementPath, MANAGEMENT_ROOT_PERMISSIONS, } from "../../features/navigation/managementRoutes"; import { cn } from "../../lib/utils"; export type NavItem = { to: string; label: string; icon: string; anyOf?: string[]; }; export const NAV_ITEMS: NavItem[] = [ { to: "/portal", label: "工作台", icon: "home_app_logo" }, { to: "/team", label: "团队", icon: "groups", anyOf: ["report.dashboard.view", "project.view_all", "project.manage", "system.users"] }, { to: "/leave-requests", label: "请假记录", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] }, { to: "/attendance", label: "考勤", icon: "schedule" }, { to: "/reimbursements", label: "报销", icon: "request_quote", anyOf: ["expense.view_own", "expense.view_all"] }, { to: "/assets", label: "资产", icon: "inventory_2", anyOf: ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"] }, { to: "/weekly-reports", label: "周报", icon: "article" }, { to: "/announcements", label: "公告", icon: "campaign" }, { to: "/projects", label: "项目", icon: "workspaces", anyOf: ["project.view_own", "project.view_all", "project.manage"] }, { to: "/system/settings", label: "管理", icon: "settings", anyOf: MANAGEMENT_ROOT_PERMISSIONS }, { to: "/workflow/tasks", label: "待我审批", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] }, { to: "/contracts-registry", label: "合同", icon: "description", anyOf: ["project.view_all", "project.manage", "system.settings"] }, { to: "/purchase-requests", label: "采购", icon: "shopping_cart", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] }, { to: "/travel-requests", label: "差旅申请", icon: "flight", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] }, { to: "/budgets", label: "预算管理", icon: "account_balance", anyOf: ["budget.view", "budget.manage"] }, { to: "/payment-batches", label: "付款", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage", "payment.batch.review"] }, { to: "/ap-operations", label: "应付账款", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage", "payment.batch.review"] }, { to: "/invoice-pool", label: "票据", icon: "receipt_long", anyOf: ["invoice.manage"] }, { to: "/inventory-checks", label: "资产盘点", icon: "inventory", anyOf: ["asset.inventory"] }, { to: "/dashboard", label: "数据看板", icon: "dashboard", anyOf: ["report.dashboard.view"] }, ]; const ACCESSIBLE_LABELS: Record = { "/reimbursements": "我的报销", "/workflow/tasks": "待我审批", "/assets": "资产", }; const PRODUCT_LABELS: Record = { "/workflow/tasks": "待我审批", }; const TEAM_NAVIGATION_ROLE_CODES = ["manager", "ceo", "finance_mgr", "hr", "admin"]; const PAYMENT_REVIEWER_ROLE_CODES = ["manager", "ceo"]; function hasPaymentReviewRole(roleCodes: string[], permissions: string[]) { return roleCodes.some((roleCode) => PAYMENT_REVIEWER_ROLE_CODES.includes(roleCode)) && hasAnyPermission(permissions, ["payment.batch.review"]); } type ResolvedNavItem = { key?: string; href: string; label: string; icon: string; }; function isGroupedActive(item: ResolvedNavItem, pathname: string, permissions: string[]) { if (item.href === "/system/settings") { return isManagementPath(pathname, permissions); } return pathname === item.href || pathname.startsWith(`${item.href}/`); } function renderNavItem(item: ResolvedNavItem, pathname: string, permissions: string[], onNavigate?: () => void) { const accessibleLabel = ACCESSIBLE_LABELS[item.href] ?? item.label; return ( onNavigate?.()} className={({ isActive }) => cn("group nav-link-item", isActive || isGroupedActive(item, pathname, permissions) ? "nav-link-item--active" : "nav-link-item--idle") } > {({ isActive }) => { const active = isActive || isGroupedActive(item, pathname, permissions); return ( <> {item.icon} {item.label} ); }} ); } function renderPrimarySection(items: ResolvedNavItem[], pathname: string, permissions: string[], onNavigate?: () => void) { if (!items.length) { return null; } return (
{items.map((item) => renderNavItem(item, pathname, permissions, onNavigate))}
); } function renderBackupSection(items: ResolvedNavItem[], pathname: string, permissions: string[], onNavigate?: () => void) { if (!items.length) { return null; } return (
more_horiz 更多 expand_more
{items.map((item) => renderNavItem(item, pathname, permissions, onNavigate))}
); } function buildFallbackItems(permissions: string[], roleCodes: string[]) { const paymentPaths = new Set(["/payment-batches", "/ap-operations"]); const financeOnlyPaths = new Set(["/invoice-pool"]); const backupOnlyPaths = new Set(BACKUP_NAV_ORDER); const mapItems = (paths: string[]) => NAV_ITEMS.filter((item) => paths.includes(item.to)) .filter((item) => item.to !== "/workflow/tasks" || hasBusinessApproverRole(roleCodes, permissions)) .filter((item) => item.to !== "/team" || roleCodes.some((roleCode) => TEAM_NAVIGATION_ROLE_CODES.includes(roleCode))) .filter((item) => !paymentPaths.has(item.to) || hasFinanceRole(roleCodes, permissions) || hasPaymentReviewRole(roleCodes, permissions)) .filter((item) => !financeOnlyPaths.has(item.to) || hasFinanceRole(roleCodes, permissions)) .filter((item) => !item.anyOf || hasAnyPermission(permissions, item.anyOf)) .filter((item) => paths === BACKUP_NAV_ORDER || !backupOnlyPaths.has(item.to)) .map((item) => ({ key: item.to, href: item.to, label: item.label, icon: item.icon })); return { main: mapItems(MAIN_NAV_ORDER), admin: mapItems(ADMIN_NAV_ORDER), backup: mapItems(BACKUP_NAV_ORDER), }; } function orderItems(items: ResolvedNavItem[], order: string[]) { const byHref = new Map(); for (const item of items) { if (!byHref.has(item.href)) { byHref.set(item.href, item); } } const used = new Set(); const ordered = order .map((href) => { const item = byHref.get(href); if (item) { used.add(href); } return item; }) .filter((item): item is ResolvedNavItem => Boolean(item)); return [...ordered, ...items.filter((item) => !used.has(item.href))]; } function toResolved(items: NavigationItem[]) { return items.map((item) => ({ key: item.key, href: item.href, label: PRODUCT_LABELS[item.href] ?? item.label, icon: item.icon, })); } function normalizeSections(sections: { main: NavigationItem[]; admin: NavigationItem[]; backup: NavigationItem[] }) { return { main: orderItems(toResolved(sections.main), MAIN_NAV_ORDER), admin: orderItems(toResolved(sections.admin), ADMIN_NAV_ORDER), backup: orderItems(toResolved(sections.backup), BACKUP_NAV_ORDER), }; } function normalizeManagementRoot( sections: { main: ResolvedNavItem[]; admin: ResolvedNavItem[]; backup: ResolvedNavItem[] }, permissions: string[], ) { const canAccessManagement = hasAnyPermission(permissions, MANAGEMENT_ROOT_PERMISSIONS); const withoutManagement = sections.admin.filter((item) => item.href !== "/system/settings"); return { ...sections, admin: canAccessManagement ? [{ key: "/system/settings", href: "/system/settings", label: "管理", icon: "settings" }, ...withoutManagement] : withoutManagement, }; } export function NavLinks({ onNavigate }: { onNavigate?: () => void }) { const { me, permissions } = useAuth(); const location = useLocation(); const navigationQuery = useQuery({ queryKey: ["system-navigation"], queryFn: fetchSystemNavigation, staleTime: 60_000, }); const baseSections = navigationQuery.data?.sections ? normalizeSections(navigationQuery.data.sections) : buildFallbackItems(permissions, me?.roleCodes ?? []); const sections = normalizeManagementRoot(baseSections, permissions); const primaryItems = [...sections.main, ...sections.admin]; return ( ); }