import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { mapApiErrorMessage } from "../api/errorMessages"; import { fetchExpenseTypes } from "../api/expenseTypes"; import { createReimbursementTemplate, deleteReimbursementTemplate, fetchReimbursementTemplates, updateReimbursementTemplate, type ReimbursementTemplate, type ReimbursementTemplateItem, } from "../api/reimbursementTemplates"; import { isApiError } from "../api/types"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; import { PageHeader } from "../components/common/PageHeader"; 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 { Textarea } from "../components/ui/textarea"; type FilterEnabled = "all" | "enabled" | "disabled"; type FormState = { name: string; description: string; sortOrder: string; enabled: boolean; items: Array<{ expenseTypeId: string; description: string; defaultAmount: string; }>; }; 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 emptyForm(expenseTypeId = ""): FormState { return { name: "", description: "", sortOrder: "0", enabled: true, items: [ { expenseTypeId, description: "", defaultAmount: "", }, ], }; } function toForm(item: ReimbursementTemplate): FormState { return { name: item.name, description: item.description ?? "", sortOrder: String(item.sortOrder), enabled: item.enabled, items: item.items.length ? item.items.map((entry) => ({ expenseTypeId: entry.expenseTypeId, description: entry.description, defaultAmount: entry.defaultAmount ?? "", })) : [ { expenseTypeId: "", description: "", defaultAmount: "", }, ], }; } export function ReimbursementTemplatesPage() { const queryClient = useQueryClient(); 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 [error, setError] = useState(""); const templatesQuery = useQuery({ queryKey: ["reimbursement-templates", enabledFilter], queryFn: () => fetchReimbursementTemplates({ enabled: enabledFilter === "all" ? undefined : enabledFilter === "enabled", }), }); const expenseTypesQuery = useQuery({ queryKey: ["expense-types", "reimbursement-templates"], queryFn: () => fetchExpenseTypes({ enabled: true }), }); useEffect(() => { if (!dialogOpen) { return; } if (editingItem) { setForm(toForm(editingItem)); return; } const fallbackExpenseTypeId = expenseTypesQuery.data?.items?.[0]?.id ?? ""; setForm(emptyForm(fallbackExpenseTypeId)); }, [dialogOpen, editingItem, expenseTypesQuery.data?.items]); const expenseTypeMap = useMemo(() => { return new Map((expenseTypesQuery.data?.items ?? []).map((item) => [item.id, item])); }, [expenseTypesQuery.data?.items]); const saveMutation = useMutation({ mutationFn: async () => { const items: ReimbursementTemplateItem[] = form.items .filter((item) => item.expenseTypeId.trim() || item.description.trim() || item.defaultAmount.trim()) .map((item) => ({ expenseTypeId: item.expenseTypeId, description: item.description.trim(), ...(item.defaultAmount.trim() ? { defaultAmount: item.defaultAmount.trim() } : {}), })); const payload = { name: form.name.trim(), description: form.description.trim() || null, sortOrder: Number(form.sortOrder || 0), enabled: form.enabled, items, }; if (editingItem) { return updateReimbursementTemplate(editingItem.id, payload); } return createReimbursementTemplate(payload); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["reimbursement-templates"] }); setDialogOpen(false); setEditingItem(null); setError(""); toast.success(editingItem ? "模板已更新" : "模板已创建"); }, onError: (mutationError) => { const message = resolveErrorMessage(mutationError); setError(message); toast.error(message); }, }); const deleteMutation = useMutation({ mutationFn: (templateId: string) => deleteReimbursementTemplate(templateId), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["reimbursement-templates"] }); setConfirmDelete(null); setError(""); toast.success("模板已停用"); }, onError: (mutationError) => { const message = resolveErrorMessage(mutationError); setError(message); toast.error(message); }, }); const templates = templatesQuery.data?.items ?? []; const canSubmit = form.name.trim().length > 0 && form.items.some((item) => item.expenseTypeId.trim() && item.description.trim()); return (
{ setEditingItem(null); setDialogOpen(true); }} > 新增模板 )} /> {error ? (

{error}

) : null}
{templatesQuery.isLoading ? (
{Array.from({ length: 4 }).map((_, index) => (
))}
) : null} {templatesQuery.isError ? (

{resolveErrorMessage(templatesQuery.error)}

) : null} {!templatesQuery.isLoading && !templatesQuery.isError ? ( templates.length ? (
{templates.map((item) => (

{item.name}

{item.description || "未填写模板说明"}

{item.enabled ? "启用" : "停用"}
预设明细
{item.items.length ? ( item.items.map((entry, index) => (
{expenseTypeMap.get(entry.expenseTypeId)?.name ?? entry.expenseTypeId}
{entry.description}
默认金额:{entry.defaultAmount ? `¥${entry.defaultAmount}` : "未预填"}
)) ) : (
该模板暂未配置预设明细。
)}
排序:{item.sortOrder}
))}
) : (
当前筛选下暂无模板。
) ) : null} { setDialogOpen(open); if (!open) { setEditingItem(null); setError(""); } }} > {editingItem ? "编辑报销模板" : "新增报销模板"} 模板明细会在创建报销时自动生成对应报销项。
setForm((current) => ({ ...current, name: event.target.value }))} placeholder="例如:国内差旅报销" />
setForm((current) => ({ ...current, sortOrder: event.target.value }))} />