文件
hyb-oa/client/src/pages/PaymentBatchesPage.tsx
T

675 行
30 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import {
cancelPaymentBatch,
createPaymentBatch,
deletePaymentBatchReceiptAttachment,
downloadPaymentBatchReceiptAttachment,
executePaymentBatch,
fetchPaymentBatchReceiptAttachments,
fetchPaymentBatches,
reviewPaymentBatch,
updatePaymentBatchItem,
uploadPaymentBatchReceiptAttachments,
type PaymentBatch,
type PaymentBatchReceiptAttachment,
type PaymentBatchEntityType,
type PaymentChannel,
} from "../api/paymentBatches";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { StatusBadge } from "../components/common/StatusBadge";
import { DateTextInput } from "../components/common/DateTextInput";
import { downloadBlob } from "../lib/download";
import { formatDisplayDate, formatDisplayDateTime } from "../lib/displayFormat";
import { toast } from "sonner";
const PAYMENT_CHANNEL_OPTIONS: PaymentChannel[] = ["bank", "alipay", "wechat", "cash", "other"];
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function batchStatusLabel(value: string) {
if (value === "draft") return "草稿";
if (value === "reviewed") return "已复核";
if (value === "executed") return "已执行";
if (value === "failed") return "执行失败";
if (value === "cancelled") return "已取消";
return value || "-";
}
function entityTypeLabel(value: string) {
if (value === "reimbursement") return "报销";
if (value === "travel_order") return "差旅订单";
return value || "-";
}
function paymentChannelLabel(value: PaymentChannel | string | null | undefined) {
if (value === "bank") return "银行转账";
if (value === "alipay") return "支付宝";
if (value === "wechat") return "微信";
if (value === "cash") return "现金";
if (value === "other") return "其他";
return value || "-";
}
function paymentItemStatusLabel(value: string) {
if (value === "pending") return "待付款";
if (value === "paid") return "已支付";
if (value === "executed") return "已执行";
if (value === "failed") return "付款失败";
if (value === "cancelled") return "已取消";
if (value === "skipped") return "已跳过";
return value || "-";
}
function reviewNextStepLabel(value: PaymentBatch["reviewRequirement"]["nextStep"]) {
if (value === "first_review") return "等待首轮复核";
if (value === "second_review") return "等待二次复核";
return "可执行付款标记";
}
function reviewerDisplayName(value: string | null | undefined) {
if (!value) return "";
return UUID_PATTERN.test(value) ? "已记录" : value;
}
function formatFileSize(value: number) {
if (!Number.isFinite(value) || value <= 0) return "-";
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${Math.ceil(value / 1024)} KB`;
return `${(value / 1024 / 1024).toFixed(1)} MB`;
}
function resolveError(error: unknown) {
return isApiError(error)
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
: error instanceof Error
? error.message
: "操作失败";
}
export function PaymentBatchesPage() {
const queryClient = useQueryClient();
const { permissions } = useAuth();
const [searchParams, setSearchParams] = useSearchParams();
const canManageBatch = hasPermission(permissions, "payment.batch.manage");
const canReviewBatch = hasPermission(permissions, "risk.review.manage");
const [entityType, setEntityType] = useState<PaymentBatchEntityType>("reimbursement");
const [name, setName] = useState("");
const [entityIds, setEntityIds] = useState("");
const [defaultPaymentDate, setDefaultPaymentDate] = useState("");
const [defaultPaymentChannel, setDefaultPaymentChannel] = useState<PaymentChannel | "">("");
const [voucherPrefix, setVoucherPrefix] = useState("");
const receiptInputRef = useRef<HTMLInputElement | null>(null);
const [selectedReceiptFiles, setSelectedReceiptFiles] = useState<File[]>([]);
const [error, setError] = useState("");
const [confirmPaymentAction, setConfirmPaymentAction] = useState<{
kind: "financeReview" | "financePayment" | "cancel";
batchId: string;
batchNo: string;
} | null>(null);
const listQuery = useQuery({
queryKey: ["payment-batches"],
queryFn: () => fetchPaymentBatches(),
});
const selectedBatchId = searchParams.get("id") ?? "";
const selectedBatch = useMemo<PaymentBatch | null>(() => {
const items = listQuery.data?.items ?? [];
return items.find((item) => item.id === selectedBatchId) ?? items[0] ?? null;
}, [listQuery.data?.items, selectedBatchId]);
const receiptAttachmentsQuery = useQuery({
queryKey: ["payment-batch-receipts", selectedBatch?.id],
enabled: Boolean(selectedBatch?.id),
queryFn: () => fetchPaymentBatchReceiptAttachments(selectedBatch!.id),
});
useEffect(() => {
if (!selectedBatch) {
return;
}
if (selectedBatch.id === selectedBatchId) {
return;
}
const next = new URLSearchParams(searchParams);
next.set("id", selectedBatch.id);
setSearchParams(next, { replace: true });
}, [searchParams, selectedBatch, selectedBatchId, setSearchParams]);
const canEditSelectedBatch =
canManageBatch &&
!!selectedBatch &&
["draft", "reviewed", "failed"].includes(selectedBatch.status);
const canEditReceiptAttachments =
canManageBatch &&
!!selectedBatch &&
!["executed", "cancelled"].includes(selectedBatch.status);
const canReviewSelectedBatch =
canReviewBatch &&
!!selectedBatch &&
(
(selectedBatch.status === "draft" && selectedBatch.reviewRequirement.nextStep === "first_review") ||
(selectedBatch.status === "reviewed" && selectedBatch.reviewRequirement.nextStep === "second_review")
);
const canExecuteSelectedBatch =
canManageBatch &&
!!selectedBatch &&
["reviewed", "failed"].includes(selectedBatch.status) &&
selectedBatch.reviewRequirement.nextStep === "ready_for_execute";
const canCancelSelectedBatch =
canManageBatch &&
!!selectedBatch &&
!["executed", "cancelled"].includes(selectedBatch.status);
const refresh = async () => {
await queryClient.invalidateQueries({ queryKey: ["payment-batches"] });
await queryClient.invalidateQueries({ queryKey: ["payment-batch-receipts"] });
await queryClient.invalidateQueries({ queryKey: ["ap-operations"] });
};
const createMutation = useMutation({
mutationFn: async () =>
createPaymentBatch({
entityType,
name: name.trim() || `${entityType}-batch`,
entityIds: entityIds.split(/[\s,]+/).map((item) => item.trim()).filter(Boolean),
defaultPaymentDate: defaultPaymentDate || undefined,
defaultPaymentChannel: defaultPaymentChannel || undefined,
voucherPrefix: voucherPrefix || undefined,
}),
onSuccess: async ({ item }) => {
setError("");
const next = new URLSearchParams(searchParams);
next.set("id", item.id);
setSearchParams(next, { replace: true });
await refresh();
toast.success("付款批次已创建");
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
const reviewMutation = useMutation({
mutationFn: async (batchId: string) => {
const batch = listQuery.data?.items.find((item) => item.id === batchId);
const resolveRiskFlagIds = (batch?.items ?? []).flatMap((item) => item.openRiskFlags.map((flag) => flag.id));
return reviewPaymentBatch(batchId, { resolveRiskFlagIds });
},
onSuccess: async ({ item }) => {
setError("");
await refresh();
toast.success(item.reviewRequirement.nextStep === "ready_for_execute" ? "付款批次复核完成" : "付款批次已完成首轮复核");
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
const executeMutation = useMutation({
mutationFn: executePaymentBatch,
onSuccess: async () => {
setError("");
await refresh();
toast.success("付款批次已执行");
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
const cancelMutation = useMutation({
mutationFn: cancelPaymentBatch,
onSuccess: async () => {
setError("");
await refresh();
toast.success("付款批次已取消");
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
const updateItemMutation = useMutation({
mutationFn: async (payload: { batchId: string; itemId: string; paymentVoucherNoOverride?: string | null; paymentDateOverride?: string | null; paymentChannelOverride?: PaymentChannel | null }) =>
updatePaymentBatchItem(payload.batchId, payload.itemId, {
paymentVoucherNoOverride: payload.paymentVoucherNoOverride,
paymentDateOverride: payload.paymentDateOverride,
paymentChannelOverride: payload.paymentChannelOverride,
}),
onSuccess: async () => {
setError("");
await refresh();
toast.success("批次项已更新");
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
const uploadReceiptMutation = useMutation({
mutationFn: async () => {
if (!selectedBatch) {
throw new Error("请先选择付款批次");
}
if (!selectedReceiptFiles.length) {
throw new Error("请选择银行回单附件");
}
return uploadPaymentBatchReceiptAttachments(selectedBatch.id, selectedReceiptFiles);
},
onSuccess: async () => {
setError("");
setSelectedReceiptFiles([]);
if (receiptInputRef.current) {
receiptInputRef.current.value = "";
}
await refresh();
toast.success("银行回单附件已上传");
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
const downloadReceiptMutation = useMutation({
mutationFn: async (attachment: PaymentBatchReceiptAttachment) => {
if (!selectedBatch) {
throw new Error("请先选择付款批次");
}
const response = await downloadPaymentBatchReceiptAttachment(selectedBatch.id, attachment.id);
return { response, attachment };
},
onSuccess: ({ response, attachment }) => {
downloadBlob(response.blob, response.filename ?? attachment.originalName);
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
const deleteReceiptMutation = useMutation({
mutationFn: async (attachmentId: string) => {
if (!selectedBatch) {
throw new Error("请先选择付款批次");
}
return deletePaymentBatchReceiptAttachment(selectedBatch.id, attachmentId);
},
onSuccess: async () => {
setError("");
await refresh();
toast.success("银行回单附件已删除");
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
},
});
return (
<ListWorkspace>
<WorkspaceHeader title="付款批次" subtitle="线下银行付款后的 OA 复核、回单留存和已付款标记;高风险批次达到 5 万元后需完成二次复核。" />
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null}
<div className="grid gap-6 xl:grid-cols-[360px,1fr]">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">新建批次</h3>
{!canManageBatch ? (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
当前账号仅可查看批次并执行复核,创建与编辑入口仅对付款批次管理员开放。
</div>
) : null}
<div className="space-y-2">
<Label>实体类型</Label>
<select
className="select-std"
value={entityType}
disabled={!canManageBatch}
onChange={(event) => setEntityType(event.target.value as PaymentBatchEntityType)}
>
<option value="reimbursement">报销付款</option>
<option value="travel_order">差旅订单付款</option>
</select>
</div>
<div className="space-y-2">
<Label>批次名称</Label>
<Input
value={name}
disabled={!canManageBatch}
onChange={(event) => setName(event.target.value)}
placeholder="例如:2026Q2-财务付款批次-01"
/>
</div>
<div className="space-y-2">
<Label>待付款单据 ID</Label>
<textarea
className="min-h-28 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm disabled:bg-zinc-50"
value={entityIds}
disabled={!canManageBatch}
onChange={(event) => setEntityIds(event.target.value)}
placeholder="输入报销或差旅待付款单据 ID,多个用逗号或换行分隔"
/>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-2">
<Label>默认付款日期</Label>
<DateTextInput
value={defaultPaymentDate}
disabled={!canManageBatch}
onChange={(event) => setDefaultPaymentDate(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label>默认渠道</Label>
<select
className="select-std"
value={defaultPaymentChannel}
disabled={!canManageBatch}
onChange={(event) => setDefaultPaymentChannel((event.target.value as PaymentChannel | "") || "") }
>
<option value="">不设置</option>
{PAYMENT_CHANNEL_OPTIONS.map((item) => <option key={item} value={item}>{paymentChannelLabel(item)}</option>)}
</select>
</div>
</div>
<div className="space-y-2">
<Label>凭证前缀</Label>
<Input
value={voucherPrefix}
disabled={!canManageBatch}
onChange={(event) => setVoucherPrefix(event.target.value)}
placeholder="例如 PBX"
/>
</div>
<Button type="button" disabled={createMutation.isPending || !canManageBatch} onClick={() => createMutation.mutate()}>
{createMutation.isPending ? "创建中..." : "创建批次"}
</Button>
</div>
<div className="space-y-6">
<div className="card-std p-4 md:p-5">
<h3 className="mb-3 text-base font-semibold text-zinc-900">批次列表</h3>
<div className="space-y-2">
{(listQuery.data?.items ?? []).map((item) => (
<button
key={item.id}
type="button"
className={selectedBatch?.id === item.id ? "w-full rounded-lg border border-primary bg-primary/5 px-3 py-3 text-left" : "w-full rounded-lg border border-zinc-200 px-3 py-3 text-left hover:bg-zinc-50"}
onClick={() => {
const next = new URLSearchParams(searchParams);
next.set("id", item.id);
setSearchParams(next, { replace: true });
}}
>
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.batchNo}</div>
<StatusBadge status={item.status} label={batchStatusLabel(item.status)} />
</div>
<div className="mt-1 text-sm text-zinc-600">{item.name}</div>
<div className="mt-1 text-xs text-zinc-500">
金额 {item.totalAmount}
{item.highRisk ? " · 高风险" : ""}
{` · 复核 ${item.reviewRequirement.completedReviewCount}/${item.reviewRequirement.requiredReviewCount}`}
</div>
</button>
))}
{!listQuery.data?.items?.length ? <div className="text-sm text-zinc-500">暂无付款批次</div> : null}
</div>
</div>
{selectedBatch ? (
<div className="space-y-6">
<div className="card-std space-y-3 p-4 md:p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-xs text-zinc-500">{selectedBatch.batchNo}</div>
<div className="flex flex-wrap items-center gap-2">
<div className="text-lg font-semibold text-zinc-900">{selectedBatch.name}</div>
{selectedBatch.highRisk ? (
<span className="rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700">高风险</span>
) : null}
</div>
</div>
<div className="flex flex-wrap gap-2">
{canReviewBatch ? (
<Button
type="button"
variant="secondary"
disabled={reviewMutation.isPending || !canReviewSelectedBatch}
onClick={() => setConfirmPaymentAction({
kind: "financeReview",
batchId: selectedBatch.id,
batchNo: selectedBatch.batchNo,
})}
>
{selectedBatch.reviewRequirement.nextStep === "second_review" ? "二次复核" : "复核"}
</Button>
) : null}
{canManageBatch ? (
<Button
type="button"
disabled={executeMutation.isPending || !canExecuteSelectedBatch}
onClick={() => setConfirmPaymentAction({
kind: "financePayment",
batchId: selectedBatch.id,
batchNo: selectedBatch.batchNo,
})}
>
执行
</Button>
) : null}
{canManageBatch ? (
<Button
type="button"
variant="secondary"
disabled={cancelMutation.isPending || !canCancelSelectedBatch}
onClick={() => setConfirmPaymentAction({
kind: "cancel",
batchId: selectedBatch.id,
batchNo: selectedBatch.batchNo,
})}
>
取消
</Button>
) : null}
</div>
</div>
<div className="grid gap-3 text-sm md:grid-cols-3 xl:grid-cols-6">
<div>状态:{batchStatusLabel(selectedBatch.status)}</div>
<div>实体:{entityTypeLabel(selectedBatch.entityType)}</div>
<div>总金额:{selectedBatch.totalAmount}</div>
<div>默认日期:{formatDisplayDate(selectedBatch.defaultPaymentDate)}</div>
<div>默认渠道:{paymentChannelLabel(selectedBatch.defaultPaymentChannel)}</div>
<div>复核进度:{selectedBatch.reviewRequirement.completedReviewCount}/{selectedBatch.reviewRequirement.requiredReviewCount}</div>
</div>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3 text-sm text-zinc-600">
当前要求:{selectedBatch.reviewRequirement.requiredReviewCount === 2 ? `批次金额达到 ${selectedBatch.reviewRequirement.thresholdAmount},需两轮复核` : "单轮复核后可执行"}。
下一步:{reviewNextStepLabel(selectedBatch.reviewRequirement.nextStep)}。
{selectedBatch.reviewedBy ? ` 首轮复核人:${reviewerDisplayName(selectedBatch.reviewedBy)}` : ""}
{selectedBatch.secondaryReviewedBy ? ` 二次复核人:${reviewerDisplayName(selectedBatch.secondaryReviewedBy)}` : ""}
</div>
</div>
<div className="card-std space-y-4 p-4 md:p-5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-900">银行回单附件</h3>
<p className="mt-1 text-sm text-zinc-500">先在网银完成对公付款,再上传银行回单并执行 OA 已付款标记。</p>
</div>
{canManageBatch ? (
<div className="flex flex-wrap items-center gap-2">
<input
ref={receiptInputRef}
type="file"
className="hidden"
accept=".pdf,image/jpeg,image/png,image/webp"
multiple
disabled={!canEditReceiptAttachments || uploadReceiptMutation.isPending}
onChange={(event) => setSelectedReceiptFiles(Array.from(event.target.files ?? []))}
/>
<Button
type="button"
variant="secondary"
disabled={!canEditReceiptAttachments || uploadReceiptMutation.isPending}
onClick={() => receiptInputRef.current?.click()}
>
选择回单
</Button>
<Button
type="button"
disabled={!canEditReceiptAttachments || uploadReceiptMutation.isPending || selectedReceiptFiles.length === 0}
onClick={() => uploadReceiptMutation.mutate()}
>
{uploadReceiptMutation.isPending ? "上传中..." : "上传"}
</Button>
</div>
) : null}
</div>
{selectedReceiptFiles.length ? (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
待上传:{selectedReceiptFiles.map((file) => file.name).join("、")}
</div>
) : null}
{receiptAttachmentsQuery.isError ? (
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
{resolveError(receiptAttachmentsQuery.error)}
</div>
) : null}
<div className="space-y-2">
{(receiptAttachmentsQuery.data?.items ?? []).map((attachment) => (
<div key={attachment.id} className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-zinc-900">{attachment.originalName}</div>
<div className="mt-1 text-xs text-zinc-500">
{attachment.mimeType} · {formatFileSize(attachment.fileSize)} · {formatDisplayDateTime(attachment.createdAt)}
{attachment.uploadedBy ? ` · ${attachment.uploadedBy.name}` : ""}
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
size="sm"
variant="secondary"
disabled={downloadReceiptMutation.isPending}
onClick={() => downloadReceiptMutation.mutate(attachment)}
>
下载
</Button>
{canManageBatch ? (
<Button
type="button"
size="sm"
variant="secondary"
disabled={!canEditReceiptAttachments || deleteReceiptMutation.isPending}
onClick={() => deleteReceiptMutation.mutate(attachment.id)}
>
删除
</Button>
) : null}
</div>
</div>
))}
{!receiptAttachmentsQuery.isLoading && !(receiptAttachmentsQuery.data?.items ?? []).length ? (
<div className="rounded-lg border border-zinc-200 px-3 py-3 text-sm text-zinc-500">
暂无银行回单附件;默认银行渠道执行前需要上传至少一份回单。
</div>
) : null}
</div>
</div>
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">批次项</h3>
<div className="space-y-4">
{selectedBatch.items.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 p-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="font-medium text-zinc-900">{item.entityCode}</div>
<div className="text-sm text-zinc-600">{item.entityTitle}</div>
</div>
<div className="text-sm text-zinc-600">{item.amount} {item.currencyCode} · {paymentItemStatusLabel(item.status)}</div>
</div>
<div className="mt-3 grid gap-3 md:grid-cols-4">
<Input
defaultValue={item.paymentVoucherNoOverride ?? ""}
placeholder="凭证号"
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentVoucherNoOverride: event.target.value || null })}
/>
<DateTextInput
defaultValue={item.paymentDateOverride ?? ""}
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentDateOverride: event.target.value || null })}
/>
<select
className="select-std"
defaultValue={item.paymentChannelOverride ?? ""}
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
onChange={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentChannelOverride: (event.target.value as PaymentChannel | "") || null })}
>
<option value="">默认渠道</option>
{PAYMENT_CHANNEL_OPTIONS.map((option) => <option key={option} value={option}>{paymentChannelLabel(option)}</option>)}
</select>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
{item.failureMessage ? `失败原因:${item.failureMessage}` : "付款异常:无"}
</div>
</div>
<RiskFlagsCard items={item.openRiskFlags} title="关联开放风险" />
</div>
))}
</div>
</div>
</div>
) : null}
</div>
</div>
<ActionConfirmDialog
open={Boolean(confirmPaymentAction)}
onOpenChange={(open) => {
if (!open) setConfirmPaymentAction(null);
}}
kind={confirmPaymentAction?.kind ?? "financePayment"}
targetName={confirmPaymentAction?.batchNo}
description={
confirmPaymentAction?.kind === "financeReview"
? "确认后将写入付款批次复核记录;高风险批次仍需满足双人复核要求。"
: confirmPaymentAction?.kind === "cancel"
? "取消后该付款批次不能继续执行,历史记录会保留。"
: "确认后将执行付款批次并影响付款状态,请确认批次、金额和复核要求均已核对。"
}
onConfirm={async () => {
if (!confirmPaymentAction) return;
if (confirmPaymentAction.kind === "financeReview") reviewMutation.mutate(confirmPaymentAction.batchId);
if (confirmPaymentAction.kind === "financePayment") executeMutation.mutate(confirmPaymentAction.batchId);
if (confirmPaymentAction.kind === "cancel") cancelMutation.mutate(confirmPaymentAction.batchId);
}}
/>
</ListWorkspace>
);
}