608 行
23 KiB
TypeScript
608 行
23 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { isApiError } from "../api/types";
|
|
import { mapApiErrorMessage } from "../api/errorMessages";
|
|
import { fetchMyDelegations } from "../api/delegations";
|
|
import { exportInvoicePool } from "../api/exports";
|
|
import {
|
|
archiveInvoice,
|
|
bindInvoice,
|
|
fetchInvoiceVerifications,
|
|
fetchInvoicePoolDetail,
|
|
fetchInvoicePoolList,
|
|
fetchInvoiceSignedUrl,
|
|
overrideInvoiceDuplicate,
|
|
patchInvoice,
|
|
retryInvoiceVerification,
|
|
retryInvoiceOcr,
|
|
triggerInvoiceVerification,
|
|
unbindInvoice,
|
|
uploadInvoice,
|
|
type InvoicePoolStatus,
|
|
type UploadInvoiceResponse,
|
|
} 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";
|
|
import { downloadBlob } from "../lib/download";
|
|
import { formatInvoiceOcrErrorMessage } from "../lib/invoiceOcr";
|
|
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
|
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, type InvoiceBindMode } from "./invoice-pool/InvoiceBindCard";
|
|
import { InvoiceDetailPanel } from "./invoice-pool/InvoiceDetailPanel";
|
|
|
|
const PAGE_SIZE = 20;
|
|
|
|
type UploadBatchProgress = {
|
|
completed: number;
|
|
total: number;
|
|
currentFileName: string;
|
|
};
|
|
|
|
type UploadBatchResult = {
|
|
successes: Array<{ file: File; data: UploadInvoiceResponse }>;
|
|
failures: Array<{ file: File; message: string }>;
|
|
};
|
|
|
|
export function InvoicePoolPage() {
|
|
const queryClient = useQueryClient();
|
|
const { permissions, me } = useAuth();
|
|
const canManageInvoice = hasPermission(permissions, "invoice.manage");
|
|
const canOverrideDuplicate = hasPermission(permissions, "expense.finance.override_duplicate");
|
|
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 | "">("");
|
|
const [ownerScope, setOwnerScope] = useState<"own" | "all">("own");
|
|
const [page, setPage] = useState(1);
|
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
const [uploadProgress, setUploadProgress] = useState<UploadBatchProgress | 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);
|
|
const [editRemarks, setEditRemarks] = useState("");
|
|
const [editInvoiceCode, setEditInvoiceCode] = useState("");
|
|
const [editInvoiceNumber, setEditInvoiceNumber] = useState("");
|
|
const [editInvoiceDate, setEditInvoiceDate] = useState("");
|
|
const [editSellerName, setEditSellerName] = useState("");
|
|
const [editBuyerName, setEditBuyerName] = useState("");
|
|
const [editAmount, setEditAmount] = useState("");
|
|
const [editTaxAmount, setEditTaxAmount] = useState("");
|
|
const [editTotalAmount, setEditTotalAmount] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [confirmAction, setConfirmAction] = useState<{ type: string; invoiceId: string } | null>(null);
|
|
const previousDetailStatusRef = useRef<InvoicePoolStatus | null>(null);
|
|
|
|
// --- Queries ---
|
|
|
|
const listQuery = useQuery({
|
|
queryKey: ["invoice-pool", "list", status, ownerScope, page],
|
|
queryFn: () =>
|
|
fetchInvoicePoolList({
|
|
status: status || undefined,
|
|
ownerScope,
|
|
page,
|
|
pageSize: PAGE_SIZE,
|
|
}),
|
|
refetchInterval: (query) => {
|
|
const items = query.state.data?.items ?? [];
|
|
return items.some((item) => item.status === "pending_ocr") ? 3_000 : false;
|
|
},
|
|
});
|
|
|
|
const reimbursementQuery = useQuery({
|
|
queryKey: ["reimbursements", "for-bind"],
|
|
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"],
|
|
queryFn: () => fetchMyDelegations("active"),
|
|
});
|
|
|
|
const detailQuery = useQuery({
|
|
enabled: Boolean(detailInvoiceId),
|
|
queryKey: ["invoice-pool", "detail", detailInvoiceId],
|
|
queryFn: () => fetchInvoicePoolDetail(detailInvoiceId),
|
|
refetchInterval: (query) => {
|
|
const statusValue = query.state.data?.status;
|
|
if (statusValue === "pending_ocr") {
|
|
return 3_000;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
const verificationQuery = useQuery({
|
|
enabled: Boolean(detailInvoiceId) && canVerifyInvoice,
|
|
queryKey: ["invoice-pool", "verification", detailInvoiceId],
|
|
queryFn: () => fetchInvoiceVerifications(detailInvoiceId, 20),
|
|
refetchInterval: (query) => {
|
|
const items = query.state.data?.items ?? [];
|
|
const hasPending = items.some((item) => item.status === "pending");
|
|
return hasPending ? 3_000 : false;
|
|
},
|
|
});
|
|
|
|
// --- Side effects ---
|
|
|
|
useEffect(() => {
|
|
if (!detailQuery.data || editOpen) {
|
|
return;
|
|
}
|
|
|
|
setEditSetAvailable(false);
|
|
setEditRemarks(detailQuery.data.remarks ?? "");
|
|
setEditInvoiceCode(detailQuery.data.invoiceCode ?? "");
|
|
setEditInvoiceNumber(detailQuery.data.invoiceNumber ?? "");
|
|
setEditInvoiceDate(detailQuery.data.invoiceDate ?? "");
|
|
setEditSellerName(detailQuery.data.sellerName ?? "");
|
|
setEditBuyerName(detailQuery.data.buyerName ?? "");
|
|
setEditAmount(detailQuery.data.amount ?? "");
|
|
setEditTaxAmount(detailQuery.data.taxAmount ?? "");
|
|
setEditTotalAmount(detailQuery.data.totalAmount ?? "");
|
|
}, [detailQuery.data, editOpen]);
|
|
|
|
useEffect(() => {
|
|
const currentStatus = detailQuery.data?.status ?? null;
|
|
const previousStatus = previousDetailStatusRef.current;
|
|
|
|
if (previousStatus === "pending_ocr" && currentStatus && currentStatus !== "pending_ocr") {
|
|
void queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
|
if (currentStatus === "available") {
|
|
toast.success("票据识别已完成,可继续添加到业务单据或财务复核");
|
|
} else if (currentStatus === "ocr_failed") {
|
|
toast.error(`票据识别未完成:${formatInvoiceOcrErrorMessage(detailQuery.data?.ocrLastError)}`);
|
|
}
|
|
}
|
|
|
|
previousDetailStatusRef.current = currentStatus;
|
|
}, [detailQuery.data?.ocrLastError, detailQuery.data?.status, queryClient]);
|
|
|
|
useEffect(() => {
|
|
if (!canManageInvoice && ownerScope === "all") {
|
|
setOwnerScope("own");
|
|
}
|
|
}, [canManageInvoice, ownerScope]);
|
|
|
|
useEffect(() => {
|
|
if (canManageTravelPayments) {
|
|
return;
|
|
}
|
|
setBindMode("reimbursement");
|
|
setSelectedTravelRequestId("");
|
|
setSelectedTravelOrderId("");
|
|
}, [canManageTravelPayments]);
|
|
|
|
useEffect(() => {
|
|
if (!detailInvoiceId && listQuery.data?.items?.length) {
|
|
setDetailInvoiceId(listQuery.data.items[0].id);
|
|
}
|
|
}, [detailInvoiceId, listQuery.data?.items]);
|
|
|
|
// --- Helper for error handling ---
|
|
|
|
const toUserError = (err: unknown, fallback: string) => {
|
|
return isApiError(err)
|
|
? `${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`
|
|
: err instanceof Error ? err.message : fallback;
|
|
};
|
|
|
|
const handleError = (err: unknown, fallback: string) => {
|
|
const msg = toUserError(err, fallback);
|
|
setError(msg);
|
|
toast.error(msg);
|
|
};
|
|
|
|
// --- Mutations ---
|
|
|
|
const uploadMutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (!selectedFiles.length) {
|
|
throw new Error("请选择文件");
|
|
}
|
|
|
|
const files = [...selectedFiles];
|
|
const result: UploadBatchResult = { successes: [], failures: [] };
|
|
setUploadProgress({ completed: 0, total: files.length, currentFileName: files[0]?.name ?? "" });
|
|
|
|
for (const [index, file] of files.entries()) {
|
|
setUploadProgress({ completed: index, total: files.length, currentFileName: file.name });
|
|
try {
|
|
const data = await uploadInvoice(file, "vat", uploadOwnerEmployeeId || undefined);
|
|
if (data.uploadAction === "blocked" || !data.invoicePoolId) {
|
|
result.failures.push({ file, message: data.unavailableMessage ?? "该发票不可添加" });
|
|
} else {
|
|
result.successes.push({ file, data });
|
|
}
|
|
} catch (err) {
|
|
result.failures.push({ file, message: toUserError(err, "上传失败,请稍后重试") });
|
|
}
|
|
setUploadProgress({ completed: index + 1, total: files.length, currentFileName: file.name });
|
|
}
|
|
|
|
return result;
|
|
},
|
|
onSuccess: async (result) => {
|
|
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
|
const lastSuccess = result.successes.at(-1);
|
|
if (lastSuccess?.data.invoicePoolId) {
|
|
setDetailInvoiceId(lastSuccess.data.invoicePoolId);
|
|
}
|
|
setSelectedFiles(result.failures.map((item) => item.file));
|
|
setUploadProgress(null);
|
|
|
|
if (result.failures.length) {
|
|
const message = result.failures.map((item) => `${item.file.name}: ${item.message}`).join(";");
|
|
setError(message);
|
|
if (result.successes.length) {
|
|
toast.error(`已上传 ${result.successes.length} 个,${result.failures.length} 个失败`);
|
|
} else {
|
|
toast.error("上传失败,请检查文件或稍后重试");
|
|
}
|
|
return;
|
|
}
|
|
|
|
setError("");
|
|
const hasPending = result.successes.some((item) => item.data.status === "pending_ocr");
|
|
toast.success(hasPending ? `已上传 ${result.successes.length} 个,识别中` : `已上传 ${result.successes.length} 个,可直接处理`);
|
|
},
|
|
onSettled: () => setUploadProgress(null),
|
|
onError: (err) => handleError(err, "上传失败,请稍后重试"),
|
|
});
|
|
|
|
const bindMutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (!selectedInvoiceId) {
|
|
throw new Error("请选择票据");
|
|
}
|
|
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, "绑定失败,请稍后重试"),
|
|
});
|
|
|
|
const signedUrlMutation = useMutation({
|
|
mutationFn: async (invoiceId: string) => fetchInvoiceSignedUrl(invoiceId),
|
|
onSuccess: (data) => {
|
|
window.open(data.signedUrl, "_blank", "noopener,noreferrer");
|
|
},
|
|
onError: (err) => {
|
|
if (isApiError(err)) {
|
|
setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`);
|
|
} else {
|
|
setError("预览链接获取失败");
|
|
}
|
|
},
|
|
});
|
|
|
|
const patchMutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (!detailInvoiceId) {
|
|
throw new Error("请选择一条票据");
|
|
}
|
|
|
|
return patchInvoice(detailInvoiceId, {
|
|
...(editSetAvailable ? { status: "available" } : {}),
|
|
remarks: editRemarks.trim() ? editRemarks.trim() : null,
|
|
invoiceCode: editInvoiceCode.trim() ? editInvoiceCode.trim() : null,
|
|
invoiceNumber: editInvoiceNumber.trim() ? editInvoiceNumber.trim() : null,
|
|
invoiceDate: editInvoiceDate ? editInvoiceDate : null,
|
|
sellerName: editSellerName.trim() ? editSellerName.trim() : null,
|
|
buyerName: editBuyerName.trim() ? editBuyerName.trim() : null,
|
|
amount: editAmount.trim() ? editAmount.trim() : null,
|
|
taxAmount: editTaxAmount.trim() ? editTaxAmount.trim() : null,
|
|
totalAmount: editTotalAmount.trim() ? editTotalAmount.trim() : null,
|
|
});
|
|
},
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
|
setEditOpen(false);
|
|
setError("");
|
|
toast.success("修正已保存");
|
|
},
|
|
onError: (err) => handleError(err, "保存失败,请稍后重试"),
|
|
});
|
|
|
|
const archiveMutation = useMutation({
|
|
mutationFn: async (invoiceId: string) => archiveInvoice(invoiceId),
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
|
setError("");
|
|
toast.success("已归档");
|
|
},
|
|
onError: (err) => handleError(err, "归档失败,请稍后重试"),
|
|
});
|
|
|
|
const unbindMutation = useMutation({
|
|
mutationFn: async (invoiceId: string) => unbindInvoice(invoiceId),
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
|
setError("");
|
|
toast.success("已解绑");
|
|
},
|
|
onError: (err) => handleError(err, "解绑失败,请稍后重试"),
|
|
});
|
|
|
|
const retryOcrMutation = useMutation({
|
|
mutationFn: async (invoiceId: string) => retryInvoiceOcr(invoiceId),
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
|
setError("");
|
|
toast.success("已开始重新识别");
|
|
},
|
|
onError: (err) => handleError(err, "重新识别失败,请稍后再试"),
|
|
});
|
|
|
|
const triggerVerificationMutation = useMutation({
|
|
mutationFn: async (invoiceId: string) => triggerInvoiceVerification(invoiceId),
|
|
onSuccess: async () => {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: ["invoice-pool"] }),
|
|
queryClient.invalidateQueries({ queryKey: ["invoice-pool", "verification"] }),
|
|
]);
|
|
setError("");
|
|
toast.success("发票核验已提交");
|
|
},
|
|
onError: (err) => handleError(err, "提交发票核验失败,请稍后重试"),
|
|
});
|
|
|
|
const retryVerificationMutation = useMutation({
|
|
mutationFn: async (invoiceId: string) => retryInvoiceVerification(invoiceId),
|
|
onSuccess: async () => {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: ["invoice-pool"] }),
|
|
queryClient.invalidateQueries({ queryKey: ["invoice-pool", "verification"] }),
|
|
]);
|
|
setError("");
|
|
toast.success("发票核验已重新提交");
|
|
},
|
|
onError: (err) => handleError(err, "重新提交发票核验失败,请稍后重试"),
|
|
});
|
|
|
|
const overrideDuplicateMutation = useMutation({
|
|
mutationFn: async (params: { invoiceId: string; refFormNumber: string; reason: string }) =>
|
|
overrideInvoiceDuplicate(params.invoiceId, {
|
|
refFormNumber: params.refFormNumber,
|
|
reason: params.reason,
|
|
}),
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
|
setError("");
|
|
toast.success("判重例外已生效");
|
|
},
|
|
onError: (err) => handleError(err, "判重例外失败,请稍后重试"),
|
|
});
|
|
|
|
const exportMutation = useMutation({
|
|
mutationFn: (format: "csv" | "xlsx") =>
|
|
exportInvoicePool({
|
|
format,
|
|
status: status || undefined,
|
|
ownerScope,
|
|
}),
|
|
onSuccess: (response, format) => {
|
|
downloadBlob(response.blob, response.filename ?? `invoice-pool.${format}`);
|
|
setError("");
|
|
toast.success("导出完成");
|
|
},
|
|
onError: (err) => handleError(err, "导出失败,请稍后重试"),
|
|
});
|
|
|
|
// --- Derived state ---
|
|
|
|
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
|
const detail = detailQuery.data;
|
|
const invoiceStats = useMemo(() => {
|
|
const items = listQuery.data?.items ?? [];
|
|
return {
|
|
total: listQuery.data?.total ?? 0,
|
|
pending: items.filter((item) => item.status === "pending_ocr").length,
|
|
failed: items.filter((item) => item.status === "ocr_failed").length,
|
|
available: items.filter((item) => item.status === "available").length,
|
|
bound: items.filter((item) => item.status === "bound").length,
|
|
};
|
|
}, [listQuery.data?.items, listQuery.data?.total]);
|
|
const canEditDetail = Boolean(
|
|
detail &&
|
|
(canManageInvoice ||
|
|
detail.ownerEmployeeId === me?.id ||
|
|
detail.uploadedByEmployeeId === me?.id),
|
|
);
|
|
|
|
// --- Render ---
|
|
|
|
return (
|
|
<ListWorkspace>
|
|
<WorkspaceHeader
|
|
title="票据中心"
|
|
subtitle="上传、识别、绑定和异常复核集中处理。"
|
|
actions={
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
data-testid="invoice-export"
|
|
className="btn-secondary"
|
|
type="button"
|
|
disabled={exportMutation.isPending}
|
|
>
|
|
{exportMutation.isPending ? "导出中..." : "导出"}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem data-testid="invoice-export-csv" onSelect={() => exportMutation.mutate("csv")}>
|
|
导出 CSV
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem data-testid="invoice-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
|
|
导出 Excel
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
}
|
|
/>
|
|
|
|
{error ? (
|
|
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
|
|
<div className="finance-status-strip" data-testid="invoice-pool-summary">
|
|
<div className="finance-metric-card">
|
|
<div className="finance-metric-card__label">票据总数</div>
|
|
<div className="finance-metric-card__value">{invoiceStats.total}</div>
|
|
</div>
|
|
<div className="finance-metric-card">
|
|
<div className="finance-metric-card__label">识别中</div>
|
|
<div className="finance-metric-card__value">{invoiceStats.pending}</div>
|
|
</div>
|
|
<div className="finance-metric-card">
|
|
<div className="finance-metric-card__label">识别失败</div>
|
|
<div className="finance-metric-card__value">{invoiceStats.failed}</div>
|
|
</div>
|
|
<div className="finance-metric-card">
|
|
<div className="finance-metric-card__label">可绑定</div>
|
|
<div className="finance-metric-card__value">{invoiceStats.available}</div>
|
|
</div>
|
|
<div className="finance-metric-card">
|
|
<div className="finance-metric-card__label">已绑定</div>
|
|
<div className="finance-metric-card__value">{invoiceStats.bound}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<InvoiceUploadCard
|
|
selectedFiles={selectedFiles}
|
|
setSelectedFiles={setSelectedFiles}
|
|
isUploading={uploadMutation.isPending}
|
|
uploadProgressLabel={
|
|
uploadProgress
|
|
? `上传中 ${uploadProgress.completed}/${uploadProgress.total}`
|
|
: "上传中..."
|
|
}
|
|
onUpload={() => uploadMutation.mutate()}
|
|
showProxyOwnerSelector={showProxyOwnerSelector}
|
|
proxyDelegationsQuery={proxyDelegationsQuery}
|
|
uploadOwnerEmployeeId={uploadOwnerEmployeeId}
|
|
setUploadOwnerEmployeeId={setUploadOwnerEmployeeId}
|
|
/>
|
|
|
|
<InvoiceListCard
|
|
listQuery={listQuery}
|
|
status={status}
|
|
setStatus={setStatus}
|
|
ownerScope={ownerScope}
|
|
setOwnerScope={setOwnerScope}
|
|
page={page}
|
|
setPage={setPage}
|
|
totalPages={totalPages}
|
|
detailInvoiceId={detailInvoiceId}
|
|
setDetailInvoiceId={setDetailInvoiceId}
|
|
signedUrlMutation={signedUrlMutation}
|
|
/>
|
|
|
|
<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
|
|
detail={detail}
|
|
detailQuery={detailQuery}
|
|
canEditDetail={canEditDetail}
|
|
canVerifyInvoice={canVerifyInvoice}
|
|
canUploadInvoice={canUploadInvoice}
|
|
canOverrideDuplicate={canOverrideDuplicate}
|
|
patchMutation={patchMutation}
|
|
archiveMutation={archiveMutation}
|
|
unbindMutation={unbindMutation}
|
|
retryOcrMutation={retryOcrMutation}
|
|
triggerVerificationMutation={triggerVerificationMutation}
|
|
retryVerificationMutation={retryVerificationMutation}
|
|
overrideDuplicateMutation={overrideDuplicateMutation}
|
|
verificationQuery={verificationQuery}
|
|
editOpen={editOpen}
|
|
setEditOpen={setEditOpen}
|
|
editSetAvailable={editSetAvailable}
|
|
setEditSetAvailable={setEditSetAvailable}
|
|
editRemarks={editRemarks}
|
|
setEditRemarks={setEditRemarks}
|
|
editInvoiceCode={editInvoiceCode}
|
|
setEditInvoiceCode={setEditInvoiceCode}
|
|
editInvoiceNumber={editInvoiceNumber}
|
|
setEditInvoiceNumber={setEditInvoiceNumber}
|
|
editInvoiceDate={editInvoiceDate}
|
|
setEditInvoiceDate={setEditInvoiceDate}
|
|
editSellerName={editSellerName}
|
|
setEditSellerName={setEditSellerName}
|
|
editBuyerName={editBuyerName}
|
|
setEditBuyerName={setEditBuyerName}
|
|
editAmount={editAmount}
|
|
setEditAmount={setEditAmount}
|
|
editTaxAmount={editTaxAmount}
|
|
setEditTaxAmount={setEditTaxAmount}
|
|
editTotalAmount={editTotalAmount}
|
|
setEditTotalAmount={setEditTotalAmount}
|
|
confirmAction={confirmAction}
|
|
setConfirmAction={setConfirmAction}
|
|
/>
|
|
</ListWorkspace>
|
|
);
|
|
}
|