R52 A1-A6:完成 AP 运营深度操作
- AP 概览页 Tab 工作台化(待付款/异常付款/风险标记/对账异常) - AP 导出端点支持 CSV/XLSX 与筛选 - 风险标记 resolve/dismiss 支持说明审计留痕 - 支付批次深链、高风险复核提示、对账确认权限收口 - OpenClaw 浏览器级回归全部通过
这个提交包含在:
+1320
-55
文件差异内容过多而无法显示
加载差异
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
cancelPaymentBatch,
|
||||
createPaymentBatch,
|
||||
@@ -35,9 +36,9 @@ function resolveError(error: unknown) {
|
||||
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 [selectedBatchId, setSelectedBatchId] = useState("");
|
||||
const [entityType, setEntityType] = useState<PaymentBatchEntityType>("reimbursement");
|
||||
const [name, setName] = useState("");
|
||||
const [entityIds, setEntityIds] = useState("");
|
||||
@@ -51,10 +52,24 @@ export function PaymentBatchesPage() {
|
||||
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]);
|
||||
|
||||
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?.status === "draft";
|
||||
const canReviewSelectedBatch =
|
||||
canReviewBatch &&
|
||||
@@ -90,7 +105,9 @@ export function PaymentBatchesPage() {
|
||||
}),
|
||||
onSuccess: async ({ item }) => {
|
||||
setError("");
|
||||
setSelectedBatchId(item.id);
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.set("id", item.id);
|
||||
setSearchParams(next, { replace: true });
|
||||
await refresh();
|
||||
toast.success("付款批次已创建");
|
||||
},
|
||||
@@ -257,7 +274,11 @@ export function PaymentBatchesPage() {
|
||||
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={() => setSelectedBatchId(item.id)}
|
||||
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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户