import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchDepartments } from "../api/departments"; import { mapApiErrorMessage } from "../api/errorMessages"; import { fetchExpenseTypes } from "../api/expenseTypes"; import { createExpenseStandard, deleteExpenseStandard, fetchExpenseStandards, updateExpenseStandard, type ExpenseStandard, type ExpenseStandardControlMode, type ExpenseStandardPerUnit, type ExpenseStandardScopeType, } from "../api/expenseStandards"; import { isApiError } from "../api/types"; import { PageHeader } from "../components/common/PageHeader"; import { TableSkeleton } from "../components/common/TableSkeleton"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; import { Button } from "../components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; type FilterEnabled = "all" | "enabled" | "disabled"; type FormState = { expenseTypeId: string; name: string; maxAmount: string; perUnit: ExpenseStandardPerUnit; controlMode: ExpenseStandardControlMode; scopeType: ExpenseStandardScopeType; scopeKey: string; enabled: boolean; note: string; }; const PER_UNIT_OPTIONS: Array<{ value: ExpenseStandardPerUnit; label: string }> = [ { value: "item", label: "每项" }, { value: "day", label: "每天" }, { value: "trip", label: "每次" }, ]; const CONTROL_MODE_OPTIONS: Array<{ value: ExpenseStandardControlMode; label: string }> = [ { value: "warn", label: "预警" }, { value: "block", label: "阻断" }, ]; function emptyForm(expenseTypeId = ""): FormState { return { expenseTypeId, name: "", maxAmount: "", perUnit: "item", controlMode: "warn", scopeType: "global", scopeKey: "", enabled: true, note: "", }; } function toForm(item: ExpenseStandard): FormState { return { expenseTypeId: item.expenseTypeId, name: item.name, maxAmount: item.maxAmount, perUnit: item.perUnit, controlMode: item.controlMode, scopeType: item.scopeType, scopeKey: item.scopeKey ?? "", enabled: item.enabled, note: item.note ?? "", }; } function resolveErrorMessage(error: unknown) { if (isApiError(error)) { return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } if (error instanceof Error) { return error.message; } return "请求失败,请稍后重试"; } function toneClassForControlMode(mode: ExpenseStandardControlMode) { return mode === "block" ? "border-red-200 bg-red-50 text-red-700" : "border-amber-200 bg-amber-50 text-amber-700"; } export function ExpenseStandardsPage() { const queryClient = useQueryClient(); const [error, setError] = useState(""); const [selectedExpenseTypeId, setSelectedExpenseTypeId] = useState(""); const [enabledFilter, setEnabledFilter] = useState("all"); const [dialogOpen, setDialogOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [form, setForm] = useState(emptyForm()); const [confirmDelete, setConfirmDelete] = useState(null); const expenseTypesQuery = useQuery({ queryKey: ["expense-types", "expense-standards"], queryFn: () => fetchExpenseTypes(), }); const departmentsQuery = useQuery({ queryKey: ["departments", "expense-standards", "active"], queryFn: () => fetchDepartments({ includeArchived: false }), }); const standardsQuery = useQuery({ queryKey: ["expense-standards", selectedExpenseTypeId, enabledFilter], queryFn: () => fetchExpenseStandards({ expenseTypeId: selectedExpenseTypeId || undefined, enabled: enabledFilter === "all" ? undefined : enabledFilter === "enabled", }), }); useEffect(() => { if (!dialogOpen) { return; } if (editingItem) { setForm(toForm(editingItem)); return; } const fallbackExpenseTypeId = selectedExpenseTypeId || expenseTypesQuery.data?.items?.[0]?.id || ""; setForm(emptyForm(fallbackExpenseTypeId)); }, [dialogOpen, editingItem, expenseTypesQuery.data?.items, selectedExpenseTypeId]); const saveMutation = useMutation({ mutationFn: async () => { const mutablePayload = { name: form.name.trim(), maxAmount: form.maxAmount.trim(), perUnit: form.perUnit, controlMode: form.controlMode, scopeType: form.scopeType, scopeKey: form.scopeType === "department" ? form.scopeKey || null : null, enabled: form.enabled, note: form.note.trim() || null, }; if (editingItem) { return updateExpenseStandard(editingItem.id, mutablePayload); } return createExpenseStandard({ expenseTypeId: form.expenseTypeId, ...mutablePayload, }); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["expense-standards"] }); setDialogOpen(false); setEditingItem(null); setError(""); toast.success(editingItem ? "标准已更新" : "标准已创建"); }, onError: (error) => { const message = resolveErrorMessage(error); setError(message); toast.error(message); }, }); const deleteMutation = useMutation({ mutationFn: (standardId: string) => deleteExpenseStandard(standardId), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["expense-standards"] }); setConfirmDelete(null); setError(""); toast.success("标准已停用"); }, onError: (error) => { const message = resolveErrorMessage(error); setError(message); toast.error(message); }, }); const items = standardsQuery.data?.items ?? []; const expenseTypeOptions = expenseTypesQuery.data?.items ?? []; const departmentOptions = departmentsQuery.data?.items ?? []; const canSubmit = form.expenseTypeId.trim().length > 0 && form.name.trim().length > 0 && form.maxAmount.trim().length > 0 && (form.scopeType !== "department" || form.scopeKey.trim().length > 0); const selectedExpenseTypeName = useMemo(() => { return expenseTypeOptions.find((item) => item.id === selectedExpenseTypeId)?.name ?? "全部费用类型"; }, [expenseTypeOptions, selectedExpenseTypeId]); return (
{ setEditingItem(null); setDialogOpen(true); }} > 新增标准 } /> {error ? (

{error}

) : null}
当前筛选:{selectedExpenseTypeName} / {enabledFilter === "all" ? "全部状态" : enabledFilter === "enabled" ? "仅启用" : "仅停用"}
{standardsQuery.isLoading ? ( ) : standardsQuery.isError ? (

费用标准加载失败,请刷新页面重试。

) : (
费用类型 标准名称 上限金额 计量单位 管控模式 适用范围 启用状态 操作 {items.length ? ( items.map((item) => (
{item.expenseTypeName ?? item.expenseTypeId}
{item.name}
{item.note || "-"}
¥{item.maxAmount} {PER_UNIT_OPTIONS.find((option) => option.value === item.perUnit)?.label ?? item.perUnit} {item.controlMode === "block" ? "阻断" : "预警"} {item.scopeLabel} {item.enabled ? "启用" : "停用"}
)) ) : ( 当前筛选下暂无费用标准。 )}
)}
{ setDialogOpen(open); if (!open) { setEditingItem(null); setError(""); } }} > {editingItem ? "编辑费用标准" : "新增费用标准"} 维护单条明细的金额上限,以及超标后是预警还是阻断提交。
setForm((current) => ({ ...current, name: event.target.value }))} placeholder="例如:日常交通费上限" />
setForm((current) => ({ ...current, maxAmount: event.target.value }))} placeholder="例如 100.00" />
{form.scopeType === "department" ? (
) : null}
setForm((current) => ({ ...current, note: event.target.value }))} placeholder="可选填写规则说明" />
{ if (!open) { setConfirmDelete(null); } }} title="确认删除该费用标准?" description={confirmDelete ? `删除后会将“${confirmDelete.name}”停用,不会物理移除历史记录。` : undefined} confirmLabel="删除" variant="destructive" onConfirm={async () => { if (confirmDelete) { await deleteMutation.mutateAsync(confirmDelete.id); } }} />
); } export default ExpenseStandardsPage;