import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { mapApiErrorMessage } from "../api/errorMessages"; import { fetchPortalHome, type PortalHomePayload } from "../api/portal"; import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission, isManagerRole } from "../auth/permissions"; import { ErrorBoundary } from "../components/common/ErrorBoundary"; import { WelcomeDialog } from "../components/onboarding/WelcomeDialog"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { Button } from "../components/ui/button"; import { clearWelcomeDialogFlag, shouldShowWelcomeDialog, } from "../lib/onboardingGuide"; type RoleCapabilities = NonNullable; type WorkItem = { key: string; title: string; reason: string; meta: string; href: string; actionLabel: string; severity: "normal" | "warning" | "high"; icon: string; priority: number; count: number; }; type EmptyAction = { key: string; label: string; href: string; variant?: "primary" | "secondary" | "ghost"; }; type QuickEntry = { key: string; label: string; href: string; actionLabel: string; icon: string; }; const LEAVE_PORTAL_PERMISSIONS = ["leave.view_own", "leave.view_all", "leave.create", "leave.submit"]; const EXPENSE_PORTAL_PERMISSIONS = ["expense.view_own", "expense.view_all", "expense.create", "expense.proxy_manage"]; const PROCUREMENT_PORTAL_PERMISSIONS = ["procurement.view_own", "procurement.view_all", "procurement.create", "procurement.submit"]; const TRAVEL_PORTAL_PERMISSIONS = ["travel.view_own", "travel.view_all", "travel.create", "travel.submit"]; const APPROVAL_PORTAL_PERMISSIONS = ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"]; const FINANCE_PORTAL_PERMISSIONS = ["payment.batch.manage", "risk.review.manage", "expense.finance.pay"]; const SYSTEM_PORTAL_PERMISSIONS = ["system.users", "system.settings", "system.audit.view", "notification.channel.manage", "payment.account.read", "payment.account.manage"]; const CONTRACT_PORTAL_PERMISSIONS = ["project.view_all", "project.manage", "system.settings"]; const PROJECT_PORTAL_PERMISSIONS = ["project.view_own", "project.view_all", "project.manage"]; const ASSET_PORTAL_PERMISSIONS = ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]; const INVOICE_PORTAL_PERMISSIONS = ["invoice.manage"]; const SEVERITY_RANK = { high: 3, warning: 2, normal: 1, } as const; function buildFallbackRoleCapabilities(permissions: string[], roleCodes: string[]): RoleCapabilities { const hasAdminRole = roleCodes.includes("admin") || hasAnyPermission(permissions, ["*"]); const hasBusinessApproverRole = roleCodes.some((roleCode) => ["manager", "ceo", "finance", "finance_mgr", "hr"].includes(roleCode), ); const hasFinanceRole = roleCodes.some((roleCode) => ["finance", "finance_mgr"].includes(roleCode)); return { canSubmitApplications: hasAnyPermission(permissions, [ ...LEAVE_PORTAL_PERMISSIONS, ...EXPENSE_PORTAL_PERMISSIONS, ...PROCUREMENT_PORTAL_PERMISSIONS, ...TRAVEL_PORTAL_PERMISSIONS, ]), canApproveTasks: hasBusinessApproverRole && hasAnyPermission(permissions, APPROVAL_PORTAL_PERMISSIONS), canUseFinance: hasFinanceRole && hasAnyPermission(permissions, FINANCE_PORTAL_PERMISSIONS), canManageSystem: hasAdminRole || hasAnyPermission(permissions, SYSTEM_PORTAL_PERMISSIONS), canViewTeam: hasBusinessApproverRole || hasAnyPermission(permissions, ["report.dashboard.view", "system.users", "project.manage"]), isAdminOnly: hasAdminRole && !hasBusinessApproverRole, }; } function sortWorkItems(items: WorkItem[]) { return [...items].sort((left, right) => { const severityCompare = SEVERITY_RANK[right.severity] - SEVERITY_RANK[left.severity]; if (severityCompare !== 0) { return severityCompare; } return right.priority - left.priority; }); } function uniqueWorkItems(items: WorkItem[]) { const deduped = new Map(); for (const item of items) { if (!deduped.has(item.key)) { deduped.set(item.key, item); } } return sortWorkItems(Array.from(deduped.values())); } function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: RoleCapabilities) { const items: WorkItem[] = []; if (payload.pendingTasks.count > 0) { items.push({ key: "workflow-pending", title: `待我处理 ${payload.pendingTasks.count} 条`, reason: "审批、确认或流程处理正在等你。", meta: "流程待办", href: payload.pendingTasks.href, actionLabel: "去处理", severity: "warning", icon: "fact_check", priority: 100, count: payload.pendingTasks.count, }); } const openTodoCount = payload.todoSummary?.openCount ?? 0; if (openTodoCount > 0) { const highPriorityCount = payload.todoSummary?.highPriorityCount ?? 0; items.push({ key: "todo-open", title: `待办事项 ${openTodoCount} 条`, reason: highPriorityCount > 0 ? `其中 ${highPriorityCount} 条是高优先级,需要先确认。` : "这些事项已分配给你,需要继续推进。", meta: highPriorityCount > 0 ? "高优先级" : "个人待办", href: payload.todoSummary?.href ?? "/todos", actionLabel: "查看待办", severity: highPriorityCount > 0 ? "high" : "normal", icon: "check_circle", priority: 90, count: openTodoCount, }); } if (payload.weeklyReport?.status === "draft") { items.push({ key: "weekly-draft", title: "本周周报待填写", reason: "需要补充本周进展、风险和下周计划。", meta: "本周", href: payload.weeklyReport.href, actionLabel: "填写周报", severity: payload.weeklyReport.needsHelp ? "high" : "warning", icon: "article", priority: 72, count: 1, }); } if (roleCapabilities.canUseFinance && payload.financeSummary) { if (payload.financeSummary.failedItemCount > 0) { items.push({ key: "finance-failed-payment", title: `异常付款 ${payload.financeSummary.failedItemCount} 项`, reason: "付款失败或异常需要财务确认原因。", meta: "财务异常", href: payload.financeSummary.href, actionLabel: "处理异常", severity: "high", icon: "priority_high", priority: 96, count: payload.financeSummary.failedItemCount, }); } if (payload.financeSummary.riskCount > 0) { items.push({ key: "finance-risk-review", title: `待复核风险 ${payload.financeSummary.riskCount} 项`, reason: "付款或报销风险需要复核后再推进。", meta: "风险复核", href: payload.financeSummary.href, actionLabel: "去复核", severity: "high", icon: "shield_person", priority: 94, count: payload.financeSummary.riskCount, }); } if (payload.financeSummary.pendingBatchCount > 0) { items.push({ key: "finance-pending-batch", title: `待付款批次 ${payload.financeSummary.pendingBatchCount} 个`, reason: "需要继续确认、复核或提交付款。", meta: "付款", href: payload.financeSummary.href, actionLabel: "处理付款", severity: "warning", icon: "payments", priority: 86, count: payload.financeSummary.pendingBatchCount, }); } } if (payload.projectSummary?.highPriorityTaskCount) { items.push({ key: "project-high-priority", title: `高优先级项目任务 ${payload.projectSummary.highPriorityTaskCount} 项`, reason: "项目动作需要今天确认下一步。", meta: "项目", href: payload.projectSummary.href, actionLabel: "查看项目", severity: "warning", icon: "workspaces", priority: 64, count: payload.projectSummary.highPriorityTaskCount, }); } if (payload.projectSummary?.followUpDueTodayCount) { items.push({ key: "project-follow-up", title: `今日项目跟进 ${payload.projectSummary.followUpDueTodayCount} 项`, reason: "客户、交付或验收动作今天需要跟进。", meta: "今日", href: payload.projectSummary.href, actionLabel: "去跟进", severity: "warning", icon: "event_upcoming", priority: 62, count: payload.projectSummary.followUpDueTodayCount, }); } return uniqueWorkItems(items).slice(0, 8); } function buildProgressItems(payload: PortalHomePayload) { const items: WorkItem[] = []; if (payload.weeklyReport?.status === "submitted") { items.push({ key: "weekly-submitted", title: "本周周报已提交", reason: "本周进展已记录,可进入周报页查看。", meta: "已提交", href: payload.weeklyReport.href, actionLabel: "查看", severity: "normal", icon: "done_all", priority: 30, count: 1, }); } return uniqueWorkItems(items).slice(0, 3); } function buildTeamBlockers(payload: PortalHomePayload, roleCapabilities: RoleCapabilities) { if (!roleCapabilities.canViewTeam && !roleCapabilities.canManageSystem) { return []; } const items: WorkItem[] = []; if ((payload.weeklySummary?.missingCount ?? 0) > 0) { items.push({ key: "team-weekly-missing", title: `${payload.weeklySummary?.missingCount ?? 0} 人未交周报`, reason: "负责人需要提醒或确认是否有阻塞。", meta: "团队", href: payload.weeklySummary?.href ?? "/weekly-reports", actionLabel: "查看周报", severity: "warning", icon: "article", priority: 78, count: payload.weeklySummary?.missingCount ?? 0, }); } if ((payload.weeklySummary?.needsHelpCount ?? 0) > 0) { items.push({ key: "team-weekly-help", title: `${payload.weeklySummary?.needsHelpCount ?? 0} 人标记需要协助`, reason: "需要负责人确认问题和下一步支持。", meta: "协助", href: payload.weeklySummary?.href ?? "/weekly-reports", actionLabel: "查看详情", severity: "high", icon: "support_agent", priority: 82, count: payload.weeklySummary?.needsHelpCount ?? 0, }); } if ((payload.teamPresence?.unspecified ?? 0) > 0) { items.push({ key: "team-presence-missing", title: `${payload.teamPresence?.unspecified ?? 0} 人未更新团队状态`, reason: "今日在岗、外出或请假状态还不完整。", meta: "状态", href: payload.teamPresence?.href ?? "/team", actionLabel: "查看团队", severity: "warning", icon: "groups", priority: 58, count: payload.teamPresence?.unspecified ?? 0, }); } if ((payload.projectSummary?.overdueMilestonesCount ?? 0) > 0) { items.push({ key: "team-project-overdue", title: `${payload.projectSummary?.overdueMilestonesCount ?? 0} 个里程碑已逾期`, reason: "项目负责人需要调整计划或补充说明。", meta: "项目", href: payload.projectSummary?.href ?? "/projects", actionLabel: "查看项目", severity: "warning", icon: "flag", priority: 74, count: payload.projectSummary?.overdueMilestonesCount ?? 0, }); } if ((payload.projectSummary?.openRiskCount ?? 0) > 0) { items.push({ key: "team-project-risk", title: `${payload.projectSummary?.openRiskCount ?? 0} 个项目风险未关闭`, reason: "需要确认风险责任人、应对措施和截止时间。", meta: "风险", href: payload.projectSummary?.href ?? "/projects", actionLabel: "处理风险", severity: "high", icon: "warning", priority: 76, count: payload.projectSummary?.openRiskCount ?? 0, }); } if ((payload.peopleLifecycle?.offboardingCount ?? 0) > 0 || (payload.peopleLifecycle?.assetReturnPendingCount ?? 0) > 0) { items.push({ key: "team-offboarding-assets", title: "离职和资产交接待收口", reason: `离职 ${payload.peopleLifecycle?.offboardingCount ?? 0} 条,资产 ${payload.peopleLifecycle?.assetReturnPendingCount ?? 0} 项。`, meta: "交接", href: payload.peopleLifecycle?.href ?? "/people", actionLabel: "查看人员", severity: "warning", icon: "inventory_2", priority: 60, count: (payload.peopleLifecycle?.offboardingCount ?? 0) + (payload.peopleLifecycle?.assetReturnPendingCount ?? 0), }); } return uniqueWorkItems(items).slice(0, 3); } function buildManagementReminders( payload: PortalHomePayload, roleCapabilities: RoleCapabilities, canSetupWorkspace: boolean, ) { if (!roleCapabilities.canManageSystem) { return []; } const items: WorkItem[] = []; if (!payload.workspace.persisted && canSetupWorkspace) { items.push({ key: "workspace-setup", title: "工作区设置未完成", reason: "需要确认基础入口、通知策略和可见模块。", meta: "配置", href: "/system/workspace", actionLabel: "去设置", severity: "warning", icon: "settings", priority: 88, count: 1, }); } const inactiveLoginCount = payload.adoptionSummary ? Math.max(0, payload.adoptionSummary.activeEmployeeCount - payload.adoptionSummary.login7dCount) : 0; if (inactiveLoginCount > 0) { items.push({ key: "trial-login-missing", title: `近 7 天 ${inactiveLoginCount} 人未登录`, reason: "试运行覆盖率不足,需要确认账号或提醒到人。", meta: "试运行", href: "/system/workspace", actionLabel: "查看配置", severity: "warning", icon: "manage_accounts", priority: 52, count: inactiveLoginCount, }); } return uniqueWorkItems(items).slice(0, 3); } function buildEmptyActions(permissions: string[], roleCapabilities: RoleCapabilities) { const actions: EmptyAction[] = []; if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)) { actions.push({ key: "new-expense", label: "新建报销", href: "/reimbursements", variant: "primary" }); actions.push({ key: "my-progress", label: "查看进度", href: "/reimbursements", variant: "secondary" }); } if (roleCapabilities.canManageSystem) { actions.push({ key: "manage-settings", label: "管理设置", href: "/system/workspace", variant: actions.length ? "ghost" : "secondary" }); } return actions.slice(0, roleCapabilities.canManageSystem && actions.length > 2 ? 3 : 2); } function buildQuickEntries(permissions: string[], roleCapabilities: RoleCapabilities) { const entries: QuickEntry[] = []; if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)) { entries.push({ key: "reimbursements", label: "报销", href: "/reimbursements", actionLabel: hasAnyPermission(permissions, ["expense.create", "expense.proxy_manage"]) ? "新建报销" : "查看报销", icon: "request_quote", }); } if (hasAnyPermission(permissions, PROCUREMENT_PORTAL_PERMISSIONS)) { entries.push({ key: "purchase", label: "采购", href: "/purchase-requests", actionLabel: hasAnyPermission(permissions, ["procurement.create", "procurement.submit"]) ? "新建采购" : "查看采购", icon: "shopping_cart", }); } if (hasAnyPermission(permissions, CONTRACT_PORTAL_PERMISSIONS)) { entries.push({ key: "contracts", label: "合同", href: "/contracts-registry", actionLabel: "查看合同", icon: "description", }); } if (hasAnyPermission(permissions, PROJECT_PORTAL_PERMISSIONS)) { entries.push({ key: "projects", label: "项目", href: "/projects", actionLabel: "查看项目", icon: "workspaces", }); } if (hasAnyPermission(permissions, ASSET_PORTAL_PERMISSIONS)) { entries.push({ key: "assets", label: "资产", href: "/assets", actionLabel: "查看资产", icon: "inventory_2", }); } if (roleCapabilities.canUseFinance) { entries.push({ key: "payments", label: "付款", href: "/payment-batches", actionLabel: "处理付款", icon: "payments", }); } if (hasAnyPermission(permissions, INVOICE_PORTAL_PERMISSIONS)) { entries.push({ key: "invoices", label: "票据", href: "/invoice-pool", actionLabel: "查看票据", icon: "receipt_long", }); } return entries.slice(0, 6); } function sumWorkItemCount(items: WorkItem[]) { return items.reduce((total, item) => total + Math.max(1, item.count), 0); } function SummaryStrip({ userName, actionCount, progressCount, blockerCount, reminderCount, }: { userName: string; actionCount: number; progressCount: number; blockerCount: number; reminderCount: number; }) { const attentionCount = blockerCount + reminderCount; const summaryParts = [`待处理 ${actionCount}`]; if (progressCount > 0) summaryParts.push(`进度 ${progressCount}`); if (attentionCount > 0) summaryParts.push(`提醒 ${attentionCount}`); const summaryText = summaryParts.join(" · "); return (
{summaryText}
{userName} · 当前工作区
); } function WorkItemRow({ item }: { item: WorkItem }) { const severityClass = item.severity === "high" ? "portal-task-row portal-task-row--high" : item.severity === "warning" ? "portal-task-row portal-task-row--warning" : "portal-task-row"; return ( {item.title} {item.meta} {item.actionLabel} ); } function WorkItemList({ items }: { items: WorkItem[] }) { return (
{items.map((item) => ( ))}
); } function QuickEntryPanel({ entries }: { entries: QuickEntry[] }) { if (!entries.length) { return null; } return (
{entries.map((entry) => ( {entry.label} ))}
); } function TodayActionPanel({ items, emptyActions, }: { items: WorkItem[]; emptyActions: EmptyAction[]; }) { return ( {items.length ? ( ) : (
暂无待处理
{emptyActions.length ? (
{emptyActions.map((action) => ( ))}
) : null}
)}
); } function ConditionalWorkPanel({ title, items, }: { title: string; items: WorkItem[]; }) { if (!items.length) { return null; } return ( ); } export function PortalHomePage() { const { me, permissions } = useAuth(); const [showWelcome, setShowWelcome] = useState(false); const portalQuery = useQuery({ queryKey: ["portal-home"], queryFn: () => fetchPortalHome() }); const canSetupWorkspace = hasAnyPermission(permissions, ["system.settings"]); useEffect(() => { setShowWelcome(shouldShowWelcomeDialog()); }, []); if (portalQuery.isLoading) { return
加载工作台中...
; } if (portalQuery.isError || !portalQuery.data) { const message = isApiError(portalQuery.error) ? `${portalQuery.error.code}: ${mapApiErrorMessage(portalQuery.error.code, portalQuery.error.details, portalQuery.error.message)}` : "工作台加载失败"; return
{message}
; } const payload = portalQuery.data; const roleCapabilities = payload.roleCapabilities ?? buildFallbackRoleCapabilities(permissions, me?.roleCodes ?? []); const isManagerView = roleCapabilities.canViewTeam || roleCapabilities.canApproveTasks || roleCapabilities.canUseFinance || roleCapabilities.canManageSystem || isManagerRole(permissions); const todayActionItems = buildTodayActionItems(payload, roleCapabilities); const quickEntries = buildQuickEntries(permissions, roleCapabilities); const progressItems = buildProgressItems(payload); const teamBlockers = isManagerView ? buildTeamBlockers(payload, roleCapabilities) : []; const managementReminders = buildManagementReminders(payload, roleCapabilities, canSetupWorkspace); const emptyActions = buildEmptyActions(permissions, roleCapabilities); const actionCount = sumWorkItemCount(todayActionItems); const progressCount = sumWorkItemCount(progressItems); const blockerCount = sumWorkItemCount(teamBlockers); const reminderCount = sumWorkItemCount(managementReminders); function handleCloseWelcome() { clearWelcomeDialogFlag(); setShowWelcome(false); } return (
); }