import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { mapApiErrorMessage } from "../api/errorMessages"; import { createBudget, fetchBudgets, archiveBudget, type BudgetListQuery } from "../api/budgets"; import { fetchDepartments } from "../api/departments"; import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { PageHeader } from "../components/common/PageHeader"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; const PAGE_SIZE = 20; function resolveError(error: unknown) { if (isApiError(error)) { return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } if (error instanceof Error) { return error.message; } return "请求失败,请稍后重试。"; } export function BudgetsPage() { const queryClient = useQueryClient(); const { permissions } = useAuth(); const canViewBudget = hasPermission(permissions, "budget.view") || hasPermission(permissions, "budget.manage"); const canManageBudget = hasPermission(permissions, "budget.manage"); const [status, setStatus] = useState<"active" | "archived" | "">("active"); const [departmentId, setDepartmentId] = useState(""); const [page, setPage] = useState(1); const [code, setCode] = useState(""); const [name, setName] = useState(""); const [periodStart, setPeriodStart] = useState(""); const [periodEnd, setPeriodEnd] = useState(""); const [allocatedAmount, setAllocatedAmount] = useState(""); const [warningThresholdPercent, setWarningThresholdPercent] = useState("80"); const [formDepartmentId, setFormDepartmentId] = useState(""); const [error, setError] = useState(""); const queryInput = useMemo( () => ({ status: status || undefined, departmentId: departmentId || undefined, page, pageSize: PAGE_SIZE, }), [status, departmentId, page], ); const departmentsQuery = useQuery({ enabled: canViewBudget, queryKey: ["departments", "budget-filter"], queryFn: () => fetchDepartments(), }); const budgetsQuery = useQuery({ enabled: canViewBudget, queryKey: ["budgets", queryInput], queryFn: () => fetchBudgets(queryInput), }); const createMutation = useMutation({ mutationFn: async () => { return createBudget({ code: code.trim().toUpperCase(), name: name.trim(), departmentId: formDepartmentId || null, periodStart, periodEnd, allocatedAmount: allocatedAmount.trim(), warningThresholdPercent: Number(warningThresholdPercent || 80), }); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["budgets"] }); setCode(""); setName(""); setPeriodStart(""); setPeriodEnd(""); setAllocatedAmount(""); setWarningThresholdPercent("80"); setFormDepartmentId(""); setError(""); }, onError: (mutationError) => { setError(resolveError(mutationError)); }, }); const archiveMutation = useMutation({ mutationFn: async (budgetId: string) => archiveBudget(budgetId), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["budgets"] }); setError(""); }, onError: (mutationError) => { setError(resolveError(mutationError)); }, }); if (!canViewBudget) { return (
所需权限:budget.view 或 budget.manage
); } const totalPages = Math.max(1, Math.ceil((budgetsQuery.data?.total ?? 0) / PAGE_SIZE)); return (
{error ? (

{error}

) : null}

筛选条件

{canManageBudget ? (

新建预算池

setCode(event.target.value)} /> setName(event.target.value)} /> setPeriodStart(event.target.value)} /> setPeriodEnd(event.target.value)} /> setAllocatedAmount(event.target.value)} /> setWarningThresholdPercent(event.target.value)} />
) : null}

预算池列表

编码 名称 状态 期间 额度 预警阈值 操作 {(budgetsQuery.data?.items ?? []).length ? ( (budgetsQuery.data?.items ?? []).map((item) => ( {item.code} {item.name} {item.status} {(item.periodStart ?? "-")} ~ {(item.periodEnd ?? "-")} {item.usedAmount} / {item.allocatedAmount}
剩余 {item.remainingAmount}
{item.warningThresholdPercent}% {canManageBudget && item.status !== "archived" ? ( ) : ( - )}
)) ) : ( 暂无预算池 )}
第 {page} / {totalPages} 页
); }