import { useEffect, useMemo, useRef, useState } from "react"; import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { createReimbursement, deleteReimbursement, fetchReimbursements, type ReimbursementListItem, type ReimbursementStatus, } from "../api/reimbursements"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; import { Button } from "../components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs"; import { PageHeader } from "../components/common/PageHeader"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay"; import { resolveErrorMessage } from "../lib/resolveErrorMessage"; const STATUS_TABS: Array<{ value: ReimbursementStatus; label: string }> = [ { value: "draft", label: "草稿" }, { value: "reviewing", label: "审批中" }, { value: "approved", label: "已通过" }, { value: "paid", label: "已支付" }, ]; const STATUS_LABEL: Record = { draft: "草稿", reviewing: "审批中", approved: "已通过", paid: "已支付", rejected: "已驳回", cancelled: "已取消", }; function isReimbursementStatus(value: string | null): value is ReimbursementStatus { return ["draft", "reviewing", "approved", "paid", "rejected", "cancelled"].includes(value ?? ""); } function formatCurrency(value: string | null | undefined) { const numberValue = Number(value ?? 0); if (!Number.isFinite(numberValue)) { return "¥0.00"; } return new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY", }).format(numberValue); } function formatDate(value: string | null | undefined) { return value || "未填写日期"; } function ReimbursementCard({ item, canDeleteDraft, currentMobileSource, isDeleting, onDelete, }: { item: ReimbursementListItem; canDeleteDraft: boolean; currentMobileSource: string; isDeleting: boolean; onDelete: (item: ReimbursementListItem) => void; }) { const displayCode = resolveReimbursementDisplayCode(item); return (
{displayCode}
{formatDate(item.reimbursementDate)}
{STATUS_LABEL[item.status]}
金额:{formatCurrency(item.totalAmount)} {" · "} 报销人:{item.owner?.name ?? item.submitter?.name ?? "-"}
{item.description ?

{item.description}

: null} {canDeleteDraft ? (
更多
) : null}
); } export function MobileReimbursementsPage() { const navigate = useNavigate(); const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const { permissions } = useAuth(); const [status, setStatus] = useState(() => { const queryStatus = searchParams.get("status"); return isReimbursementStatus(queryStatus) ? queryStatus : "draft"; }); const [deleteTarget, setDeleteTarget] = useState(null); const createIntentHandledRef = useRef(false); const canCreateExpense = hasPermission(permissions, "expense.create"); const canDeleteExpense = hasPermission(permissions, "expense.delete_own"); const currentMobileSource = useMemo(() => { const params = new URLSearchParams(location.search); params.delete("intent"); params.set("status", status); const query = params.toString(); return `${location.pathname}${query ? `?${query}` : ""}`; }, [location.pathname, location.search, status]); const listQuery = useQuery({ queryKey: ["mobile", "reimbursements", status], queryFn: () => fetchReimbursements({ status, page: 1, pageSize: 20, scope: "own" }), }); const createMutation = useMutation({ mutationFn: () => createReimbursement(), onSuccess: async (response) => { await queryClient.invalidateQueries({ queryKey: ["mobile", "reimbursements"] }); await queryClient.invalidateQueries({ queryKey: ["reimbursements"] }); navigate(`/mobile/reimbursements/${response.id}`, { state: { from: currentMobileSource } }); }, onError: (error) => { toast.error(resolveErrorMessage(error)); }, }); useEffect(() => { const queryStatus = searchParams.get("status"); if (isReimbursementStatus(queryStatus)) { setStatus(queryStatus); } }, [searchParams]); function handleStatusChange(value: string) { if (!isReimbursementStatus(value)) { return; } setStatus(value); const nextParams = new URLSearchParams(searchParams); nextParams.delete("intent"); nextParams.set("status", value); setSearchParams(nextParams, { replace: true }); } useEffect(() => { if (searchParams.get("intent") !== "create" || !canCreateExpense || createIntentHandledRef.current) { return; } createIntentHandledRef.current = true; const nextParams = new URLSearchParams(searchParams); nextParams.delete("intent"); setSearchParams(nextParams, { replace: true }); createMutation.mutate(); }, [canCreateExpense, createMutation, searchParams, setSearchParams]); const deleteMutation = useMutation({ mutationFn: (reimbursementId: string) => deleteReimbursement(reimbursementId), onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["mobile", "reimbursements"] }), queryClient.invalidateQueries({ queryKey: ["reimbursements"] }), ]); setDeleteTarget(null); toast.success("草稿已删除"); }, onError: (error) => { toast.error(resolveErrorMessage(error)); }, }); const items = listQuery.data?.items ?? []; const total = listQuery.data?.total ?? 0; const totalAmount = useMemo( () => items.reduce((sum, item) => sum + Number(item.totalAmount ?? 0), 0), [items], ); return ( createMutation.mutate()} type="button" > {createMutation.isPending ? "创建中..." : "新建报销"} ) : null } />
当前列表
{total}
当前金额
{formatCurrency(String(totalAmount))}
{STATUS_TABS.map((item) => ( {item.label} ))}
{listQuery.isLoading ?
加载中...
: null} {listQuery.isError ? (
{resolveErrorMessage(listQuery.error)}
) : null} {!listQuery.isLoading && !listQuery.isError && !items.length ? (
暂无报销记录
) : null} {items.map((item) => ( ))}
{ if (!open) { setDeleteTarget(null); } }} title="删除草稿?" description="删除后无法恢复,已添加发票会回到可用状态。" variant="destructive" confirmLabel="删除草稿" onConfirm={async () => { if (!deleteTarget) { return; } await deleteMutation.mutateAsync(deleteTarget.id); }} />
); } export default MobileReimbursementsPage;