import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage"; import { useMemo, useState } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { exportDashboardReimbursements } from "../api/exports"; import { fetchDashboardApprovalEfficiency, fetchDashboardOverview, fetchDashboardTopCategories, fetchDashboardTrends, type DashboardQuery, } from "../api/dashboard"; import { fetchDepartments } from "../api/departments"; import { toast } from "sonner"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; import { downloadBlob } from "../lib/download"; function currentDateString() { return new Date().toISOString().slice(0, 10); } function monthStartString() { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`; } function toCurrency(value: string | number | null | undefined) { const numeric = Number(value ?? 0); if (!Number.isFinite(numeric)) { return "0.00"; } return numeric.toFixed(2); } function resolveErrorMessage(error: unknown) { return resolveUserErrorMessage(error, "加载失败,请稍后重试。"); } export function DashboardPage() { const { permissions, me } = useAuth(); const canViewDashboard = hasPermission(permissions, "report.dashboard.view"); const [dateFrom, setDateFrom] = useState(monthStartString()); const [dateTo, setDateTo] = useState(currentDateString()); const [departmentId, setDepartmentId] = useState(""); const queryParams = useMemo( () => ({ dateFrom: dateFrom || undefined, dateTo: dateTo || undefined, departmentId: departmentId || undefined, }), [dateFrom, dateTo, departmentId], ); const departmentsQuery = useQuery({ enabled: canViewDashboard, queryKey: ["departments", "dashboard-filter"], queryFn: () => fetchDepartments(), }); const overviewQuery = useQuery({ enabled: canViewDashboard, queryKey: ["dashboard", "overview", queryParams], queryFn: () => fetchDashboardOverview(queryParams), }); const trendsQuery = useQuery({ enabled: canViewDashboard, queryKey: ["dashboard", "trends", queryParams], queryFn: () => fetchDashboardTrends(queryParams), }); const topCategoriesQuery = useQuery({ enabled: canViewDashboard, queryKey: ["dashboard", "top-categories", queryParams], queryFn: () => fetchDashboardTopCategories({ ...queryParams, limit: 8 }), }); const approvalEfficiencyQuery = useQuery({ enabled: canViewDashboard, queryKey: ["dashboard", "approval-efficiency", queryParams], queryFn: () => fetchDashboardApprovalEfficiency(queryParams), }); const exportMutation = useMutation({ mutationFn: (format: "csv" | "xlsx") => exportDashboardReimbursements({ format, dateFrom: queryParams.dateFrom, dateTo: queryParams.dateTo, departmentId: queryParams.departmentId, }), onSuccess: (response, format) => { downloadBlob(response.blob, response.filename ?? `dashboard-reimbursements.${format}`); toast.success("导出完成"); }, onError: (err) => { toast.error(resolveUserErrorMessage(err, "导出失败")); }, }); if (!canViewDashboard) { return (

当前用户:{me?.name ?? "-"}

所需权限:report.dashboard.view

); } const loading = overviewQuery.isLoading || trendsQuery.isLoading || topCategoriesQuery.isLoading || approvalEfficiencyQuery.isLoading; const firstError = overviewQuery.error ?? trendsQuery.error ?? topCategoriesQuery.error ?? approvalEfficiencyQuery.error ?? departmentsQuery.error ?? exportMutation.error; const trendPoints = trendsQuery.data?.points ?? []; const maxTrendAmount = Math.max( 1, ...trendPoints.map((item) => Number(item.amount ?? 0)).filter((value) => Number.isFinite(value)), ); const topCategories = topCategoriesQuery.data?.items ?? []; const maxCategoryAmount = Math.max( 1, ...topCategories.map((item) => Number(item.amount ?? 0)).filter((value) => Number.isFinite(value)), ); return (
{firstError ? (

{resolveErrorMessage(firstError)}

) : null}
setDateFrom(event.target.value)} />
setDateTo(event.target.value)} />

报销单总数

{loading ? "..." : overviewQuery.data?.totalCount ?? 0}

报销总金额

{loading ? "..." : toCurrency(overviewQuery.data?.totalAmount)}

已付款金额

{loading ? "..." : toCurrency(overviewQuery.data?.paidAmount)}

审批中数量

{loading ? "..." : overviewQuery.data?.reviewingCount ?? 0}

费用趋势

{trendPoints.length ? (
{trendPoints.map((item) => { const amount = Number(item.amount ?? 0); const width = `${Math.max(5, Math.round((amount / maxTrendAmount) * 100))}%`; return (
{item.date} {toCurrency(item.amount)} / {item.count} 笔
); })}
) : (

暂无趋势数据

)}

费用分类 TopN

{topCategories.length ? (
{topCategories.map((item) => { const amount = Number(item.amount ?? 0); const width = `${Math.max(5, Math.round((amount / maxCategoryAmount) * 100))}%`; return (
{item.name} ({item.code}) {toCurrency(item.amount)} / {item.count} 笔
); })}
) : (

暂无分类统计

)}

审批效率

已统计通过单据: {approvalEfficiencyQuery.data?.approvedCount ?? 0}

平均审批耗时(小时): {approvalEfficiencyQuery.data?.averageHours?.toFixed(2) ?? "0.00"}

); }