统一业务台账首屏布局(05-30-fix-payment-ledger-layout-consistency)
这个提交包含在:
+143
-125
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchMyDelegations } from "../api/delegations";
|
||||
@@ -51,6 +52,7 @@ type UploadBatchResult = {
|
||||
|
||||
export function InvoicePoolPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { permissions, me } = useAuth();
|
||||
const canManageInvoice = hasPermission(permissions, "invoice.manage");
|
||||
const canOverrideDuplicate = hasPermission(permissions, "expense.finance.override_duplicate");
|
||||
@@ -71,7 +73,7 @@ export function InvoicePoolPage() {
|
||||
const [selectedReimbursementId, setSelectedReimbursementId] = useState("");
|
||||
const [selectedTravelRequestId, setSelectedTravelRequestId] = useState("");
|
||||
const [selectedTravelOrderId, setSelectedTravelOrderId] = useState("");
|
||||
const [detailInvoiceId, setDetailInvoiceId] = useState("");
|
||||
const [detailInvoiceId, setDetailInvoiceId] = useState(searchParams.get("id") ?? "");
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editSetAvailable, setEditSetAvailable] = useState(false);
|
||||
const [editRemarks, setEditRemarks] = useState("");
|
||||
@@ -203,10 +205,25 @@ export function InvoicePoolPage() {
|
||||
}, [canManageTravelPayments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailInvoiceId && listQuery.data?.items?.length) {
|
||||
setDetailInvoiceId(listQuery.data.items[0].id);
|
||||
const searchInvoiceId = searchParams.get("id") ?? "";
|
||||
if (searchInvoiceId !== detailInvoiceId) {
|
||||
setDetailInvoiceId(searchInvoiceId);
|
||||
}
|
||||
}, [detailInvoiceId, listQuery.data?.items]);
|
||||
}, [detailInvoiceId, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentId = searchParams.get("id") ?? "";
|
||||
if (detailInvoiceId === currentId) {
|
||||
return;
|
||||
}
|
||||
const next = new URLSearchParams(searchParams);
|
||||
if (detailInvoiceId) {
|
||||
next.set("id", detailInvoiceId);
|
||||
} else {
|
||||
next.delete("id");
|
||||
}
|
||||
setSearchParams(next, { replace: true });
|
||||
}, [detailInvoiceId, searchParams, setSearchParams]);
|
||||
|
||||
// --- Helper for error handling ---
|
||||
|
||||
@@ -453,6 +470,13 @@ export function InvoicePoolPage() {
|
||||
detail.ownerEmployeeId === me?.id ||
|
||||
detail.uploadedByEmployeeId === me?.id),
|
||||
);
|
||||
const isInvoiceDetailMode = Boolean(detailInvoiceId);
|
||||
const returnToInvoiceList = () => {
|
||||
setDetailInvoiceId("");
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete("id");
|
||||
setSearchParams(next, { replace: true });
|
||||
};
|
||||
|
||||
// --- Render ---
|
||||
|
||||
@@ -460,11 +484,18 @@ export function InvoicePoolPage() {
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="票据中心"
|
||||
subtitle="优先看识别状态、失败票据和待绑定票据;导出、全范围和财务交接进入台账工具。"
|
||||
actions={
|
||||
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((current) => !current)}>
|
||||
台账工具
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{isInvoiceDetailMode ? (
|
||||
<Button type="button" variant="secondary" onClick={returnToInvoiceList}>
|
||||
返回列表
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((current) => !current)}>
|
||||
台账工具
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -474,14 +505,11 @@ export function InvoicePoolPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{showLedgerTools ? (
|
||||
{!isInvoiceDetailMode && showLedgerTools ? (
|
||||
<div className="finance-command-panel space-y-3" data-testid="invoice-ledger-tools">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">台账工具</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-500">
|
||||
导出按当前状态和范围生成,只用于财务复核、OCR 排障和代账交接;普通员工日常补票据仍从报销详情或移动上传进入。
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -504,124 +532,114 @@ export function InvoicePoolPage() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600">
|
||||
票据原文、预览和下载仍按详情页权限与审计控制处理,列表导出不作为敏感材料分发入口。
|
||||
</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}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
) : 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>
|
||||
{!isInvoiceDetailMode ? (
|
||||
<div className="flex flex-wrap gap-2" data-testid="invoice-pool-summary">
|
||||
{[
|
||||
["全部", invoiceStats.total],
|
||||
["识别中", invoiceStats.pending],
|
||||
["识别失败", invoiceStats.failed],
|
||||
["可绑定", invoiceStats.available],
|
||||
["已绑定", invoiceStats.bound],
|
||||
].map(([label, value]) => (
|
||||
<span key={label} className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-sm text-zinc-700">
|
||||
{label} <span className="font-semibold text-zinc-900">{value}</span>
|
||||
</span>
|
||||
))}
|
||||
</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>
|
||||
) : null}
|
||||
|
||||
<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}
|
||||
/>
|
||||
{!isInvoiceDetailMode ? (
|
||||
<InvoiceListCard
|
||||
listQuery={listQuery}
|
||||
status={status}
|
||||
setStatus={setStatus}
|
||||
ownerScope={ownerScope}
|
||||
setOwnerScope={setOwnerScope}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
totalPages={totalPages}
|
||||
detailInvoiceId={detailInvoiceId}
|
||||
setDetailInvoiceId={setDetailInvoiceId}
|
||||
signedUrlMutation={signedUrlMutation}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户