diff --git a/client/src/api/employees.ts b/client/src/api/employees.ts index bba5d8ed..165fed59 100644 --- a/client/src/api/employees.ts +++ b/client/src/api/employees.ts @@ -24,6 +24,79 @@ export type EmployeeListResponse = { items: EmployeeSummary[]; }; +export type EmployeeLifecycleEvent = { + id: string; + employeeId: string; + eventType: "onboard" | "regularize" | "transfer" | "offboard"; + effectiveDate: string | null; + payloadJson: Record | null; + createdAt: string | null; + createdBy: string | null; +}; + +export type EmployeeChecklistTask = { + id: string; + employeeId: string; + scene: "onboarding" | "offboarding" | "transfer"; + taskType: + | "account_setup" + | "asset_issue" + | "policy_ack" + | "asset_return" + | "access_revoke" + | "expense_clearance" + | "handover_note"; + title: string; + status: "pending" | "completed" | "skipped"; + dueDate: string | null; + completedAt: string | null; + completedBy: string | null; + payloadJson: Record | null; + createdAt: string | null; +}; + +export type EmployeeProfile = { + employee: { + id: string; + employeeNo: string; + name: string; + phone: string; + email: string; + status: EmployeeStatus; + employmentStatus: "onboarding" | "probation" | "active" | "offboarding" | "inactive"; + departmentId: string; + title: string; + directManagerId: string | null; + hireDate: string | null; + probationEndDate: string | null; + lastWorkingDate: string | null; + workLocation: string | null; + emergencyContactName: string | null; + emergencyContactPhone: string | null; + notes: string | null; + archivedAt: string | null; + roleCodes: string[]; + department: { id: string; name: string } | null; + directManager: { id: string; employeeNo: string; name: string } | null; + }; + lifecycle: { + events: EmployeeLifecycleEvent[]; + checklistTasks: EmployeeChecklistTask[]; + blockers: { + openAssetCount: number; + pendingWorkflowTaskCount: number; + canCompleteOffboarding: boolean; + }; + }; + assignedAssets: Array<{ + id: string; + assetNo: string; + name: string; + status: string; + location: string | null; + }>; +}; + export async function fetchEmployees(params: { page: number; pageSize: number; @@ -39,3 +112,90 @@ export async function fetchEmployees(params: { return apiRequestData(`/api/employees?${search.toString()}`); } +export async function fetchEmployeeProfile(employeeId: string) { + return apiRequestData(`/api/employees/${employeeId}/profile`); +} + +export async function patchEmployeeProfile( + employeeId: string, + payload: Partial<{ + departmentId: string; + directManagerId: string | null; + title: string; + probationEndDate: string | null; + lastWorkingDate: string | null; + workLocation: string | null; + emergencyContactName: string | null; + emergencyContactPhone: string | null; + notes: string | null; + }>, +) { + return apiRequestData(`/api/employees/${employeeId}/profile`, { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +export async function startEmployeeOnboarding( + employeeId: string, + payload: { effectiveDate?: string; probationEndDate?: string | null; notes?: string | null } = {}, +) { + return apiRequestData(`/api/employees/${employeeId}/onboarding/start`, { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function regularizeEmployee( + employeeId: string, + payload: { effectiveDate?: string; notes?: string | null } = {}, +) { + return apiRequestData(`/api/employees/${employeeId}/regularize`, { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function transferEmployee( + employeeId: string, + payload: { + effectiveDate?: string; + departmentId?: string; + directManagerId?: string | null; + title?: string; + notes?: string | null; + } = {}, +) { + return apiRequestData(`/api/employees/${employeeId}/transfer`, { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function startEmployeeOffboarding( + employeeId: string, + payload: { effectiveDate?: string; lastWorkingDate?: string | null; notes?: string | null } = {}, +) { + return apiRequestData(`/api/employees/${employeeId}/offboarding/start`, { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function fetchEmployeeLifecycle(employeeId: string) { + return apiRequestData(`/api/employees/${employeeId}/lifecycle`); +} + +export async function completeEmployeeChecklistTask(employeeId: string, taskId: string) { + return apiRequestData(`/api/employees/${employeeId}/checklist-tasks/${taskId}/complete`, { + method: "POST", + body: JSON.stringify({}), + }); +} + +export async function skipEmployeeChecklistTask(employeeId: string, taskId: string) { + return apiRequestData(`/api/employees/${employeeId}/checklist-tasks/${taskId}/skip`, { + method: "POST", + body: JSON.stringify({}), + }); +} diff --git a/client/src/api/featureFlags.ts b/client/src/api/featureFlags.ts index 129d237c..00ceb521 100644 --- a/client/src/api/featureFlags.ts +++ b/client/src/api/featureFlags.ts @@ -1,6 +1,6 @@ import { apiRequestData } from "./http"; -export type FeatureFlagKey = "erp_sync" | "invoice_verification" | "leave_holiday_calendar" | "budget_precheck_v1" | "payment_batch_v1" | "portal_home_v1" | "global_search_v1" | "announcements_v1" | "workflow_definition_admin_v1" | "cutover_console_v1"; +export type FeatureFlagKey = "erp_sync" | "invoice_verification" | "leave_holiday_calendar" | "budget_precheck_v1" | "payment_batch_v1" | "portal_home_v1" | "global_search_v1" | "announcements_v1" | "workflow_definition_admin_v1" | "cutover_console_v1" | "presence_board_v1" | "weekly_report_v1" | "employee_lifecycle_v1" | "todo_board_v1"; export type FeatureFlagOverrideMode = "inherit" | "force_on" | "force_off"; export type FeatureFlagRolloutType = "global" | "department_allowlist"; diff --git a/client/src/api/portal.ts b/client/src/api/portal.ts index c8b79422..e37259ca 100644 --- a/client/src/api/portal.ts +++ b/client/src/api/portal.ts @@ -1,6 +1,11 @@ import { apiRequestData } from "./http"; export type PortalHomePayload = { + workspace: { + id: string | null; + persisted: boolean; + profile: "small_team_internal"; + }; greeting: { name: string }; cards: Array<{ key: string; title: string; value: string | number; href: string; severity?: "normal" | "warning" | "high" }>; pendingTasks: { count: number; href: string }; @@ -8,6 +13,22 @@ export type PortalHomePayload = { riskSummary: { openCount: number; href: string } | null; announcements: Array<{ id: string; title: string; summary: string; href: string | null }>; quickLinks: Array<{ key: string; title: string; href: string }>; + todayPresence: { date: string; status: string | null; label: string; note: string | null; href: string } | null; + teamPresence: + | ({ date: string; href: string } & { + office: number; + remote: number; + out: number; + travel: number; + leave: number; + unspecified: number; + total: number; + }) + | null; + weeklyReport: { weekStart: string; status: "draft" | "submitted"; needsHelp: boolean; href: string } | null; + weeklySummary: { weekStart: string; submittedCount: number; missingCount: number; needsHelpCount: number; href: string } | null; + todoSummary: { openCount: number; highPriorityCount: number; href: string } | null; + peopleLifecycle: { onboardingCount: number; offboardingCount: number; assetReturnPendingCount: number; href: string } | null; }; export async function fetchPortalHome() { diff --git a/client/src/api/presence.ts b/client/src/api/presence.ts new file mode 100644 index 00000000..b76026c2 --- /dev/null +++ b/client/src/api/presence.ts @@ -0,0 +1,67 @@ +import { apiRequestData } from "./http"; + +export type PresenceStatus = "office" | "remote" | "out" | "travel" | "leave"; +export type PresenceTimeRange = "full_day" | "morning" | "afternoon"; +export type PresenceSource = "manual" | "leave_sync"; + +export type PresenceItem = { + id: string; + employeeId: string; + date: string; + status: PresenceStatus; + timeRange: PresenceTimeRange; + note: string | null; + source: PresenceSource; + updatedAt: string | null; +}; + +export type PresenceEmployee = { + id: string; + employeeNo: string; + name: string; + title: string; + employmentStatus: string; + department: { id: string; name: string } | null; +}; + +export type TeamPresenceItem = { + employee: PresenceEmployee; + presence: PresenceItem | null; +}; + +export type PresenceSummary = { + office: number; + remote: number; + out: number; + travel: number; + leave: number; + unspecified: number; + total: number; +}; + +export async function fetchMyPresence(date?: string) { + const suffix = date ? `?date=${encodeURIComponent(date)}` : ""; + return apiRequestData<{ item: PresenceItem | null; date: string }>(`/api/presence/me${suffix}`); +} + +export async function upsertMyPresence(payload: { + date?: string; + status: Extract; + timeRange?: PresenceTimeRange; + note?: string | null; +}) { + return apiRequestData<{ item: PresenceItem }>("/api/presence/me", { + method: "PUT", + body: JSON.stringify(payload), + }); +} + +export async function fetchTeamPresence(date?: string) { + const suffix = date ? `?date=${encodeURIComponent(date)}` : ""; + return apiRequestData<{ date: string; summary: PresenceSummary; items: TeamPresenceItem[] }>(`/api/presence/team${suffix}`); +} + +export async function fetchPresenceSummary(date?: string) { + const suffix = date ? `?date=${encodeURIComponent(date)}` : ""; + return apiRequestData<{ date: string; summary: PresenceSummary }>(`/api/presence/summary${suffix}`); +} diff --git a/client/src/api/search.ts b/client/src/api/search.ts index b00db011..174a538a 100644 --- a/client/src/api/search.ts +++ b/client/src/api/search.ts @@ -1,7 +1,16 @@ import { apiRequestData } from "./http"; export type GlobalSearchHit = { - entityType: "reimbursement" | "leave" | "purchase_request" | "travel_request" | "asset"; + entityType: + | "employee" + | "announcement" + | "reimbursement" + | "leave" + | "purchase_request" + | "travel_request" + | "asset" + | "weekly_report" + | "todo"; entityId: string; title: string; subtitle: string | null; diff --git a/client/src/api/todos.ts b/client/src/api/todos.ts new file mode 100644 index 00000000..d0791261 --- /dev/null +++ b/client/src/api/todos.ts @@ -0,0 +1,79 @@ +import { apiRequestData } from "./http"; + +export type TodoPriority = "low" | "normal" | "high"; +export type TodoStatus = "todo" | "doing" | "done" | "cancelled"; +export type TodoSource = "manual" | "weekly_report"; + +export type TodoItem = { + id: string; + title: string; + description: string | null; + assigneeEmployeeId: string; + createdBy: string; + dueDate: string | null; + priority: TodoPriority; + status: TodoStatus; + source: TodoSource; + createdAt: string | null; + updatedAt: string | null; + completedAt: string | null; + assignee: { id: string; employeeNo: string; name: string } | null; + creator: { id: string; employeeNo: string; name: string } | null; +}; + +export async function fetchTodos(params: { + status?: TodoStatus; + scope?: "mine" | "team"; + assigneeId?: string; + q?: string; +} = {}) { + const search = new URLSearchParams(); + if (params.status) search.set("status", params.status); + if (params.scope) search.set("scope", params.scope); + if (params.assigneeId) search.set("assigneeId", params.assigneeId); + if (params.q) search.set("q", params.q); + const suffix = search.toString() ? `?${search.toString()}` : ""; + return apiRequestData<{ items: TodoItem[] }>(`/api/todos${suffix}`); +} + +export async function createTodo(payload: { + title: string; + description?: string | null; + assigneeEmployeeId?: string; + dueDate?: string | null; + priority?: TodoPriority; + source?: TodoSource; +}) { + return apiRequestData<{ item: TodoItem }>("/api/todos", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function patchTodo(id: string, payload: Partial<{ + title: string; + description: string | null; + assigneeEmployeeId: string; + dueDate: string | null; + priority: TodoPriority; + status: TodoStatus; +}>) { + return apiRequestData<{ item: TodoItem }>(`/api/todos/${id}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +export async function completeTodo(id: string) { + return apiRequestData<{ item: TodoItem }>(`/api/todos/${id}/complete`, { + method: "POST", + body: JSON.stringify({}), + }); +} + +export async function cancelTodo(id: string) { + return apiRequestData<{ item: TodoItem }>(`/api/todos/${id}/cancel`, { + method: "POST", + body: JSON.stringify({}), + }); +} diff --git a/client/src/api/weeklyReports.ts b/client/src/api/weeklyReports.ts new file mode 100644 index 00000000..a6ecff5e --- /dev/null +++ b/client/src/api/weeklyReports.ts @@ -0,0 +1,68 @@ +import { apiRequestData } from "./http"; + +export type WeeklyReport = { + id: string; + employeeId: string; + weekStart: string; + status: "draft" | "submitted"; + highlightsText: string; + blockersText: string; + nextWeekText: string; + needsHelp: boolean; + submittedAt: string | null; + createdAt: string | null; + updatedAt: string | null; +}; + +export type WeeklyReportTeamItem = { + employee: { + id: string; + employeeNo: string; + name: string; + title: string; + department: { id: string; name: string } | null; + }; + report: WeeklyReport | null; +}; + +export type WeeklyReportTeamSummary = { + totalEmployees: number; + submittedCount: number; + draftCount: number; + missingCount: number; + needsHelpCount: number; +}; + +export async function fetchMyWeeklyReport(weekStart?: string) { + const suffix = weekStart ? `?weekStart=${encodeURIComponent(weekStart)}` : ""; + return apiRequestData<{ item: WeeklyReport }>(`/api/weekly-reports/me${suffix}`); +} + +export async function updateMyWeeklyReport(payload: { + weekStart?: string; + highlightsText?: string; + blockersText?: string; + nextWeekText?: string; + needsHelp?: boolean; +}) { + return apiRequestData<{ item: WeeklyReport }>("/api/weekly-reports/me", { + method: "PUT", + body: JSON.stringify(payload), + }); +} + +export async function submitMyWeeklyReport(payload: { weekStart?: string } = {}) { + return apiRequestData<{ item: WeeklyReport }>("/api/weekly-reports/me/submit", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function fetchTeamWeeklyReports(weekStart?: string) { + const suffix = weekStart ? `?weekStart=${encodeURIComponent(weekStart)}` : ""; + return apiRequestData<{ + weekStart: string; + summary: WeeklyReportTeamSummary; + items: WeeklyReportTeamItem[]; + }>(`/api/weekly-reports/team${suffix}`); +} diff --git a/client/src/api/workspace.ts b/client/src/api/workspace.ts new file mode 100644 index 00000000..89e582db --- /dev/null +++ b/client/src/api/workspace.ts @@ -0,0 +1,62 @@ +import { apiRequestData } from "./http"; + +export type WorkspaceModuleVisibilityMode = "main" | "admin" | "hidden"; + +export type WorkspaceSettings = { + id: string | null; + persisted: boolean; + profile: "small_team_internal"; + orgMode: "single_company"; + primaryEntryMode: "password"; + notificationStrategy: "in_app"; + devicePriority: "desktop"; + moduleVisibility: Record; + createdAt: string | null; + updatedAt: string | null; + updatedBy: string | null; + defaults?: Record; +}; + +export type NavigationItem = { + key: string; + label: string; + icon: string; + href: string; + placement: WorkspaceModuleVisibilityMode; + permissionCodes: string[]; + featureFlagKey: string | null; +}; + +export type RolePreset = { + key: string; + label: string; + roleCodes: string[]; + description: string; +}; + +export type NavigationPayload = { + workspace: WorkspaceSettings; + sections: { + main: NavigationItem[]; + admin: NavigationItem[]; + backup: NavigationItem[]; + }; + recommendedRolePresets: RolePreset[]; +}; + +export async function fetchWorkspaceSettings() { + return apiRequestData("/api/system/workspace"); +} + +export async function updateWorkspaceSettings(payload: { + moduleVisibility: Record; +}) { + return apiRequestData<{ item: WorkspaceSettings }>("/api/system/workspace", { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +export async function fetchSystemNavigation() { + return apiRequestData("/api/system/navigation"); +} diff --git a/client/src/components/GlobalSearchDialog.tsx b/client/src/components/GlobalSearchDialog.tsx index 8d3e67f0..ab7c5f7d 100644 --- a/client/src/components/GlobalSearchDialog.tsx +++ b/client/src/components/GlobalSearchDialog.tsx @@ -8,6 +8,19 @@ import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"; +function entityLabel(entityType: string) { + if (entityType === "employee") return "人员"; + if (entityType === "announcement") return "公告"; + if (entityType === "weekly_report") return "周报"; + if (entityType === "todo") return "待办"; + if (entityType === "reimbursement") return "报销"; + if (entityType === "leave") return "请假"; + if (entityType === "purchase_request") return "采购"; + if (entityType === "travel_request") return "差旅"; + if (entityType === "asset") return "资产"; + return entityType; +} + export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { const navigate = useNavigate(); const [keyword, setKeyword] = useState(""); @@ -19,7 +32,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp }); const message = useMemo(() => { - if (!trimmed) return "输入编号、标题或关键字开始搜索。"; + if (!trimmed) return "输入姓名、公告、资产、请假、报销、周报或待办关键字开始搜索。"; if (trimmed.length < 2) return "至少输入 2 个字符。"; if (searchQuery.isError) { return isApiError(searchQuery.error) @@ -38,7 +51,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
setKeyword(event.target.value)} /> @@ -61,7 +74,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp
{item.matchedBy}
{item.subtitle ?
{item.subtitle}
: null} -
{item.entityType} · {item.status ?? "-"}
+
{entityLabel(item.entityType)} · {item.status ?? "-"}
))} diff --git a/client/src/components/navigation/NavLinks.tsx b/client/src/components/navigation/NavLinks.tsx index 847f34f9..f3704aa2 100644 --- a/client/src/components/navigation/NavLinks.tsx +++ b/client/src/components/navigation/NavLinks.tsx @@ -1,4 +1,6 @@ import { NavLink } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { fetchSystemNavigation } from "../../api/workspace"; import { useAuth } from "../../auth/AuthContext"; import { hasAnyPermission } from "../../auth/permissions"; import { cn } from "../../lib/utils"; @@ -12,183 +14,57 @@ export type NavItem = { export const NAV_ITEMS: NavItem[] = [ { to: "/portal", label: "Portal", icon: "home_app_logo" }, - { to: "/dashboard", label: "仪表盘", icon: "dashboard" }, - { - to: "/reimbursements", - label: "我的报销", - icon: "request_quote", - anyOf: ["expense.view_own", "expense.view_all"], - }, - { - to: "/leave-requests", - label: "请假申请", - icon: "event_available", - anyOf: ["leave.view_own", "leave.view_all"], - }, - { - 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.create", "travel.view_own", "travel.view_all", "travel.manage", "travel.payment.manage", "integration.manage"], - }, - { - to: "/workflow/tasks", - label: "待我处理", - icon: "fact_check", - anyOf: ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"], - }, - { - to: "/payment-batches", - label: "支付批次", - icon: "payments", - anyOf: ["payment.batch.manage", "risk.review.manage"], - }, - { - to: "/ap-operations", - label: "AP 运营", - icon: "account_balance_wallet", - anyOf: ["payment.batch.manage", "risk.review.manage"], - }, - { - to: "/assets", - label: "资产台账", - icon: "inventory_2", - anyOf: ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"], - }, - { - to: "/inventory-checks", - label: "资产盘点", - icon: "fact_check", - anyOf: ["asset.inventory"], - }, - { - to: "/invoice-pool", - label: "票据中心", - icon: "receipt_long", - anyOf: ["invoice.upload", "invoice.view_own", "invoice.manage"], - }, - { - to: "/mobile/capture", - label: "移动拍照", - icon: "document_scanner", - anyOf: ["invoice.upload", "invoice.manage"], - }, - { - to: "/mobile/assets/scan", - label: "资产扫码", - icon: "qr_code_scanner", - anyOf: ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"], - }, - { - to: "/mobile/approvals", - label: "移动审批", - icon: "mobile_friendly", - anyOf: ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"], - }, - { - to: "/budgets", - label: "预算管理", - icon: "account_balance", - anyOf: ["budget.view", "budget.manage"], - }, - { - to: "/audit-logs", - label: "审计工作台", - icon: "manage_search", - anyOf: ["system.audit.view"], - }, - { - to: "/delegations", - label: "委派管理", - icon: "swap_horiz", - anyOf: ["expense.proxy_manage"], - }, - { - to: "/system/users", - label: "用户管理", - icon: "group", - anyOf: ["system.users"], - }, - { - to: "/system/departments", - label: "部门管理", - icon: "account_tree", - anyOf: ["system.users"], - }, - { - to: "/system/master-data", - label: "主数据", - icon: "dataset", - anyOf: ["masterdata.view", "masterdata.manage"], - }, - { - to: "/system/notification-channels", - label: "通知渠道", - icon: "notifications_active", - anyOf: ["notification.channel.manage"], - }, - { - to: "/system/integrations/erp", - label: "ERP 集成", - icon: "hub", - anyOf: ["integration.manage"], - }, - { - to: "/system/approval-rules", - label: "审批规则", - icon: "rule", - anyOf: ["system.settings"], - }, - { - to: "/system/workflow-definitions", - label: "流程定义", - icon: "schema", - anyOf: ["system.settings"], - }, - { - to: "/system/announcements", - label: "公告管理", - icon: "campaign", - anyOf: ["system.settings"], - }, - { - to: "/system/cutover", - label: "切换控制台", - icon: "swap_calls", - anyOf: ["system.settings"], - }, - { - to: "/system/feature-flags", - label: "功能开关", - icon: "toggle_on", - anyOf: ["system.settings"], - }, + { to: "/team", label: "团队", icon: "groups" }, + { to: "/leave-requests", label: "请假", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] }, + { 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: "/todos", label: "待办", icon: "check_circle" }, + { to: "/announcements", label: "公告", icon: "campaign" }, + { to: "/system/workspace", label: "工作区设置", icon: "tune", anyOf: ["system.settings"] }, + { to: "/people", label: "人员管理", icon: "group", anyOf: ["system.users", "report.dashboard.view"] }, + { to: "/system/workflow-definitions", label: "流程定义", icon: "schema", anyOf: ["system.settings"] }, + { to: "/system/settings", label: "系统设置", icon: "settings", anyOf: ["system.settings", "system.users", "masterdata.view", "masterdata.manage", "notification.channel.manage", "integration.manage"] }, + { to: "/system/feature-flags", label: "功能开关", icon: "toggle_on", anyOf: ["system.settings"] }, + { to: "/audit-logs", label: "审计日志", icon: "manage_search", anyOf: ["system.audit.view"] }, + { to: "/workflow/tasks", label: "审批待办", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"] }, + { 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.create", "travel.view_own", "travel.view_all", "travel.manage", "travel.payment.manage", "integration.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"] }, + { to: "/ap-operations", label: "AP 运营", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage"] }, + { to: "/invoice-pool", label: "票据中心", icon: "receipt_long", anyOf: ["invoice.upload", "invoice.view_own", "invoice.manage"] }, + { to: "/inventory-checks", label: "资产盘点", icon: "inventory", anyOf: ["asset.inventory"] }, + { to: "/system/cutover", label: "切换控制台", icon: "swap_calls", anyOf: ["system.settings"] }, + { to: "/system/integrations/erp", label: "ERP 集成", icon: "hub", anyOf: ["integration.manage"] }, + { to: "/dashboard", label: "仪表盘", icon: "dashboard", anyOf: ["report.dashboard.view"] }, { to: "/settings/profile", label: "个人设置", icon: "manage_accounts" }, ]; -export function NavLinks({ onNavigate }: { onNavigate?: () => void }) { - const { permissions } = useAuth(); +const LEGACY_ACCESSIBLE_LABELS: Record = { + "/reimbursements": "我的报销", + "/workflow/tasks": "待我处理", + "/assets": "资产台账", +}; - const visibleItems = NAV_ITEMS.filter((item) => { - if (!item.anyOf) { - return true; - } - - return hasAnyPermission(permissions, item.anyOf); - }); +function renderSection( + title: string, + items: Array<{ key?: string; href: string; label: string; icon: string }>, + onNavigate?: () => void, +) { + if (!items.length) { + return null; + } return ( -