import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage"; import { useEffect } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchNotifications, markNotificationRead, type NotificationItem } from "../api/notifications"; import { fetchPortalHome } from "../api/portal"; import { fetchWorkflowTasks, fetchWorkflowTaskSummary } from "../api/workflow"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission, hasBusinessApproverRole, hasFinanceRole } from "../auth/permissions"; import { businessEntityLabel, compactBusinessReference, joinBusinessMeta } from "../lib/businessDisplay"; import { resolveMobileNotificationTargetLink } from "../lib/notificationLinks"; import { PageHeader } from "../components/common/PageHeader"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { MobileIcon } from "../components/mobile/MobileIcon"; import { Button } from "../components/ui/button"; import { MOBILE_CAPTURE_PERMISSIONS, MOBILE_ASSET_SCAN_PERMISSIONS, MOBILE_HOME_ASSET_SCAN_PERMISSIONS, MOBILE_LEAVE_PERMISSIONS, MOBILE_PURCHASE_PERMISSIONS, MOBILE_REIMBURSEMENT_PERMISSIONS, } from "../features/mobile/navigation"; type MobileAction = { key: string; label: string; meta: string; icon: string; to: string; tone?: "primary" | "warning"; }; function resolveErrorMessage(error: unknown) { return resolveUserErrorMessage(error, "请求失败,请稍后重试。"); } function formatDateTime(value: string | null) { if (!value) { return "-"; } try { return new Date(value).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }); } catch { return value; } } function notificationTypeLabel(type: string) { if (type.startsWith("workflow.")) return "审批事件"; if (type.startsWith("reimbursement.")) return "报销事件"; if (type.startsWith("leave.")) return "请假事件"; if (type.startsWith("purchase_request.")) return "采购事件"; if (type.startsWith("travel_request.")) return "差旅事件"; if (type.startsWith("asset.")) return "资产事件"; return "系统事件"; } function ActionTile({ action }: { action: MobileAction }) { const isPrimary = action.tone === "primary"; const isWarning = action.tone === "warning"; return ( {action.label} {action.meta} ); } export function MobilePortalPage() { const queryClient = useQueryClient(); const navigate = useNavigate(); const location = useLocation(); const { me, permissions } = useAuth(); const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions); const canUseFinance = hasFinanceRole(me?.roleCodes, permissions); const canAccessCapture = hasAnyPermission(permissions, [...MOBILE_CAPTURE_PERMISSIONS]); const canAccessAssetScan = hasAnyPermission(permissions, [...MOBILE_ASSET_SCAN_PERMISSIONS]); const canShowAssetScanOnHome = hasAnyPermission(permissions, [...MOBILE_HOME_ASSET_SCAN_PERMISSIONS]); const canAccessLeave = hasAnyPermission(permissions, [...MOBILE_LEAVE_PERMISSIONS]); const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]); const canAccessReimbursements = hasAnyPermission(permissions, [...MOBILE_REIMBURSEMENT_PERMISSIONS]); const canCreateExpense = hasAnyPermission(permissions, ["expense.create"]); const currentMobileSource = `${location.pathname}${location.search}`; const pendingCountQuery = useQuery({ enabled: canAccessApprovals, queryKey: ["mobile", "workflow", "pending-items"], queryFn: () => fetchWorkflowTasks({ status: "pending", page: 1, pageSize: 3 }), }); const workflowSummaryQuery = useQuery({ enabled: canAccessApprovals, queryKey: ["mobile", "workflow", "summary"], queryFn: () => fetchWorkflowTaskSummary(), }); const notificationsQuery = useQuery({ queryKey: ["mobile", "portal", "notifications"], queryFn: () => fetchNotifications({ page: 1, pageSize: 6 }), }); const portalHomeQuery = useQuery({ enabled: Boolean(me?.id), queryKey: ["mobile", "portal", "home", me?.id], queryFn: ({ signal }) => fetchPortalHome(signal), staleTime: 0, refetchOnMount: true, refetchOnWindowFocus: true, }); useEffect(() => { if (location.hash !== "#finance" || !canUseFinance) { return; } const frameId = window.requestAnimationFrame(() => { const financeSection = document.getElementById("finance"); financeSection?.scrollIntoView({ block: "start", behavior: "smooth" }); financeSection?.focus({ preventScroll: true }); }); return () => window.cancelAnimationFrame(frameId); }, [canUseFinance, location.hash, portalHomeQuery.isError, portalHomeQuery.isLoading]); const markHomeNotificationReadMutation = useMutation({ mutationFn: (notificationId: string) => markNotificationRead(notificationId), onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["mobile", "portal", "notifications"] }), queryClient.invalidateQueries({ queryKey: ["notifications"] }), ]); }, }); const pendingApprovalCount = canAccessApprovals ? workflowSummaryQuery.data?.pendingTotal ?? pendingCountQuery.data?.total ?? 0 : 0; const pendingTasks = pendingCountQuery.data?.items ?? []; const urgentPendingTask = pendingTasks[0] ?? null; const notifications = notificationsQuery.data?.items ?? []; const homeNotifications = notifications .filter((item) => !(canAccessApprovals && item.type.startsWith("workflow.task_"))) .slice(0, 3); const financeSummary = portalHomeQuery.data?.financeSummary ?? null; const businessHandoffs = portalHomeQuery.data?.businessHandoffs; const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled); const financePendingCount = financeSummary?.pendingBatchCount ?? 0; const financeExceptionCount = (businessHandoffs ? 0 : financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0); const shouldShowFinanceCard = canUseFinance && !portalHomeQuery.isLoading && !portalHomeQuery.isError && (businessHandoffs ? financeExceptionCount > 0 : paymentBatchEnabled || financePendingCount > 0 || financeExceptionCount > 0); const financeCardBadge = paymentBatchEnabled ? "复核与付款" : financePendingCount > 0 ? "待付款" : "异常与复核"; async function openHomeNotification(item: NotificationItem) { const targetLink = resolveMobileNotificationTargetLink(item); if (!targetLink) { navigate("/mobile/notifications", { state: { from: currentMobileSource } }); return; } try { if (!item.readAt) { await markHomeNotificationReadMutation.mutateAsync(item.id); } navigate(targetLink, { state: { from: currentMobileSource } }); } catch (error) { toast.error(resolveErrorMessage(error)); } } const actions = [ canCreateExpense && canAccessReimbursements ? { key: "new-reimbursement", label: "新建报销", meta: "填写明细", icon: "request_quote", to: "/mobile/reimbursements?intent=create", tone: "primary", } : null, canAccessCapture ? { key: "capture", label: "上传票据", meta: "上传 PDF", icon: "receipt_long", to: "/mobile/capture", } : null, canAccessLeave ? { key: "leave", label: "请假记录", meta: "企微主办", icon: "event_note", to: "/mobile/leave", } : null, canAccessPurchase ? { key: "purchase", label: "采购", meta: "查看申请", icon: "shopping_cart", to: "/mobile/purchase-requests", } : null, canAccessAssetScan && canShowAssetScanOnHome ? { key: "asset-scan", label: "资产扫码", meta: "扫码盘点", icon: "qr_code_scanner", to: "/mobile/assets/scan", } : null, ].filter((item): item is MobileAction => Boolean(item)).slice(0, 4); return ( 移动工作台} /> {canAccessApprovals && pendingApprovalCount > 0 ? (
待我审批
今日待处理
{pendingApprovalCount}
{workflowSummaryQuery.data?.overdueTotal ? (
超时 {workflowSummaryQuery.data.overdueTotal}
) : null}
{urgentPendingTask ? (
{(() => { const task = urgentPendingTask; const entityType = task.workflowInstance?.entityType; const reference = compactBusinessReference(task.businessSummary?.reference ?? task.workflowInstance?.entityId); return (
{reference ? `${businessEntityLabel(entityType, { noun: true })} ${reference}` : businessEntityLabel(entityType, { noun: true })}
{joinBusinessMeta([task.businessSummary?.applicantName ? `申请人:${task.businessSummary.applicantName}` : null, task.businessSummary?.amount ?? task.businessSummary?.duration, `节点:${task.stepName}`])}
); })()}
) : null}
) : null} {portalHomeQuery.isError ?

业务待办加载失败,请重试。

: null} {!portalHomeQuery.isError && businessHandoffs && businessHandoffs.total > 0 ? (

业务待办

{businessHandoffs.total} 项
{businessHandoffs.items.map((item) => (
{item.title}{item.reference ? ` · ${item.reference}` : ""}

{item.nextStep}

{item.ownerLabel} · 查看处理

))}
) : null} {shouldShowFinanceCard ? (
财务
{financeCardBadge}
{!businessHandoffs ?
待付款
{financePendingCount}
: null}
异常
{financeExceptionCount}
{paymentBatchEnabled ? ( ) : null}
) : null} {actions.length ? (
常用动作
{actions.map((action) => ( ))}
) : null}
通知
{notificationsQuery.isLoading ?

读取中...

: null} {notificationsQuery.isError ? (

{resolveErrorMessage(notificationsQuery.error)}

) : null} {!notificationsQuery.isLoading && !homeNotifications.length ? (

暂无通知。

) : null}
{homeNotifications.map((item) => ( ))}
); }