feat(P20T2-4): 差旅支付轨道卡片与发票绑定前端交互

这个提交包含在:
2026-03-11 06:41:11 +08:00
父节点 dc74d9e53f
当前提交 2553955f28
共修改 9 个文件,包含 977 行新增和 21 行删除
+51 -4
查看文件
@@ -21,6 +21,7 @@ import {
type InvoicePoolStatus,
} from "../api/invoicePool";
import { fetchReimbursements } from "../api/reimbursements";
import { fetchTravelRequestDetail, fetchTravelRequests } from "../api/travelRequests";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { toast } from "sonner";
@@ -30,7 +31,7 @@ import { Button } from "../components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
import { InvoiceUploadCard } from "./invoice-pool/InvoiceUploadCard";
import { InvoiceListCard } from "./invoice-pool/InvoiceListCard";
import { InvoiceBindCard } from "./invoice-pool/InvoiceBindCard";
import { InvoiceBindCard, type InvoiceBindMode } from "./invoice-pool/InvoiceBindCard";
import { InvoiceDetailPanel } from "./invoice-pool/InvoiceDetailPanel";
const PAGE_SIZE = 20;
@@ -43,6 +44,7 @@ export function InvoicePoolPage() {
const canUploadInvoice = hasPermission(permissions, "invoice.upload");
const canVerifyInvoice = hasPermission(permissions, "invoice.verify");
const canProxySubmit = hasPermission(permissions, "expense.proxy_submit");
const canManageTravelPayments = hasPermission(permissions, "travel.payment.manage");
const isAdministration = (me?.roleCodes ?? []).includes("administration");
const showProxyOwnerSelector = canProxySubmit && isAdministration;
const [status, setStatus] = useState<InvoicePoolStatus | "">("");
@@ -51,7 +53,10 @@ export function InvoicePoolPage() {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [uploadOwnerEmployeeId, setUploadOwnerEmployeeId] = useState("");
const [selectedInvoiceId, setSelectedInvoiceId] = useState("");
const [bindMode, setBindMode] = useState<InvoiceBindMode>("reimbursement");
const [selectedReimbursementId, setSelectedReimbursementId] = useState("");
const [selectedTravelRequestId, setSelectedTravelRequestId] = useState("");
const [selectedTravelOrderId, setSelectedTravelOrderId] = useState("");
const [detailInvoiceId, setDetailInvoiceId] = useState("");
const [editOpen, setEditOpen] = useState(false);
const [editSetAvailable, setEditSetAvailable] = useState(false);
@@ -81,6 +86,18 @@ export function InvoicePoolPage() {
queryFn: () => fetchReimbursements({ page: 1, pageSize: 50, scope: "own" }),
});
const travelRequestQuery = useQuery({
enabled: canManageTravelPayments,
queryKey: ["travel-requests", "for-invoice-bind"],
queryFn: () => fetchTravelRequests({ status: "approved", mine: false, page: 1, pageSize: 50 }),
});
const travelRequestDetailQuery = useQuery({
enabled: canManageTravelPayments && bindMode === "travelOrder" && Boolean(selectedTravelRequestId),
queryKey: ["travel-requests", "for-invoice-bind", selectedTravelRequestId],
queryFn: () => fetchTravelRequestDetail(selectedTravelRequestId),
});
const proxyDelegationsQuery = useQuery({
enabled: showProxyOwnerSelector,
queryKey: ["delegations", "mine", "active"],
@@ -132,6 +149,15 @@ export function InvoicePoolPage() {
}
}, [canManageInvoice, ownerScope]);
useEffect(() => {
if (canManageTravelPayments) {
return;
}
setBindMode("reimbursement");
setSelectedTravelRequestId("");
setSelectedTravelOrderId("");
}, [canManageTravelPayments]);
useEffect(() => {
if (!detailInvoiceId && listQuery.data?.items?.length) {
setDetailInvoiceId(listQuery.data.items[0].id);
@@ -169,14 +195,26 @@ export function InvoicePoolPage() {
const bindMutation = useMutation({
mutationFn: async () => {
if (!selectedInvoiceId || !selectedReimbursementId) {
throw new Error("请选择票据与报销单");
if (!selectedInvoiceId) {
throw new Error("请选择票据");
}
return bindInvoice(selectedInvoiceId, selectedReimbursementId);
if (bindMode === "reimbursement") {
if (!selectedReimbursementId) {
throw new Error("请选择报销单");
}
return bindInvoice(selectedInvoiceId, selectedReimbursementId);
}
if (!selectedTravelOrderId) {
throw new Error("请选择差旅订单");
}
return bindInvoice(selectedInvoiceId, { travelOrderId: selectedTravelOrderId });
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
setError("");
setSelectedInvoiceId("");
setSelectedReimbursementId("");
setSelectedTravelOrderId("");
toast.success("绑定成功");
},
onError: (err) => handleError(err, "绑定失败,请稍后重试"),
@@ -380,11 +418,20 @@ export function InvoicePoolPage() {
<InvoiceBindCard
listQuery={listQuery}
reimbursementQuery={reimbursementQuery}
travelRequestQuery={travelRequestQuery}
travelRequestDetailQuery={travelRequestDetailQuery}
bindMutation={bindMutation}
bindMode={bindMode}
setBindMode={setBindMode}
canManageTravelPayments={canManageTravelPayments}
selectedInvoiceId={selectedInvoiceId}
setSelectedInvoiceId={setSelectedInvoiceId}
selectedReimbursementId={selectedReimbursementId}
setSelectedReimbursementId={setSelectedReimbursementId}
selectedTravelRequestId={selectedTravelRequestId}
setSelectedTravelRequestId={setSelectedTravelRequestId}
selectedTravelOrderId={selectedTravelOrderId}
setSelectedTravelOrderId={setSelectedTravelOrderId}
/>
<InvoiceDetailPanel