feat(detail-page-structure-progress-actions): 统一详情处理进度与操作区
这个提交包含在:
@@ -44,7 +44,7 @@ import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { MobileIcon } from "../components/mobile/MobileIcon";
|
||||
import { MobileDetailHeader } from "../components/mobile/MobileDetailHeader";
|
||||
import { ReimbursementTravelSummary } from "../components/reimbursements/ReimbursementTravelSummary";
|
||||
import { ReimbursementProcessingProgress } from "../components/reimbursements/ReimbursementProcessingProgress";
|
||||
import { ProcessingProgress } from "../components/patterns/ProcessingProgress";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
@@ -944,6 +944,34 @@ export function MobileReimbursementDetailPage() {
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-reimbursement-processing-section">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">处理进度</h2>
|
||||
</div>
|
||||
{canRemindPendingWorkflow && pendingTaskId ? (
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={remindMutation.isPending}
|
||||
onClick={() => remindMutation.mutate(pendingTaskId)}
|
||||
>
|
||||
{remindMutation.isPending ? "催办中..." : "催办"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<ProcessingProgress
|
||||
progress={detail.processingProgress ?? { stages: [] }}
|
||||
ariaLabel="报销处理进度"
|
||||
testId="mobile-reimbursement-processing-progress"
|
||||
targetLinkLabel="查看所在付款批次"
|
||||
mobile
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-reimbursement-items">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-semibold text-zinc-900">报销明细</h2>
|
||||
@@ -1253,28 +1281,6 @@ export function MobileReimbursementDetailPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-reimbursement-processing-section">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">处理进度</h2>
|
||||
</div>
|
||||
{canRemindPendingWorkflow && pendingTaskId ? (
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={remindMutation.isPending}
|
||||
onClick={() => remindMutation.mutate(pendingTaskId)}
|
||||
>
|
||||
{remindMutation.isPending ? "催办中..." : "催办"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<ReimbursementProcessingProgress progress={detail.processingProgress ?? { stages: [] }} mobile />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showSecondaryActions ? (
|
||||
<section className="mobile-card" data-testid="mobile-reimbursement-actions">
|
||||
<h2 className="text-base font-semibold text-zinc-900">操作</h2>
|
||||
|
||||
+188
-143
@@ -24,6 +24,13 @@ import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { HeaderActionGroup, ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import {
|
||||
BusinessDetailActionPanel,
|
||||
BusinessDetailAside,
|
||||
BusinessDetailMain,
|
||||
BusinessDetailWorkspace,
|
||||
} from "../components/patterns/BusinessDetailTemplate";
|
||||
import { ProcessingProgress } from "../components/patterns/ProcessingProgress";
|
||||
import {
|
||||
BusinessListToolbar,
|
||||
BusinessMobileList,
|
||||
@@ -44,6 +51,12 @@ import {
|
||||
DialogTitle,
|
||||
} from "../components/ui/dialog";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../components/ui/dropdown-menu";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
@@ -54,6 +67,14 @@ import { toast } from "sonner";
|
||||
|
||||
const PAYMENT_CHANNEL_OPTIONS: PaymentChannel[] = ["bank", "alipay", "wechat", "cash", "other"];
|
||||
|
||||
type PaymentActionKind =
|
||||
| "review"
|
||||
| "downloadOrder"
|
||||
| "uploadReceipt"
|
||||
| "execute"
|
||||
| "cancel"
|
||||
| "redownloadOrder";
|
||||
|
||||
function entityTypeLabel(value: string) {
|
||||
if (value === "reimbursement") return "报销";
|
||||
if (value === "travel_order") return "差旅订单";
|
||||
@@ -185,7 +206,6 @@ export function PaymentBatchesPage() {
|
||||
canManageBatch &&
|
||||
!!selectedBatch &&
|
||||
["reviewed", "failed"].includes(selectedBatch.status);
|
||||
const canUploadReceipt = canEditReceiptAttachments && receiptAttachmentsQuery.isSuccess;
|
||||
const canReviewSelectedBatch =
|
||||
canReviewBatch &&
|
||||
!!selectedBatch &&
|
||||
@@ -202,6 +222,8 @@ export function PaymentBatchesPage() {
|
||||
["pending", "failed"].includes(item.status) &&
|
||||
(item.paymentChannelOverride ?? selectedBatch.defaultPaymentChannel) === "bank",
|
||||
));
|
||||
const canUploadReceipt =
|
||||
canEditReceiptAttachments && requiresBankReceipt && receiptAttachmentsQuery.isSuccess;
|
||||
const activeReceiptCount = (receiptAttachmentsQuery.data?.items ?? []).filter((item) => !item.archivedAt).length;
|
||||
const needsBankReceipt =
|
||||
canExecuteSelectedBatch && requiresBankReceipt && receiptAttachmentsQuery.isSuccess && activeReceiptCount === 0;
|
||||
@@ -213,6 +235,33 @@ export function PaymentBatchesPage() {
|
||||
!!selectedBatch &&
|
||||
!["executed", "cancelled"].includes(selectedBatch.status);
|
||||
|
||||
const paymentActionModel = useMemo(() => {
|
||||
const primary = canReviewSelectedBatch
|
||||
? "review"
|
||||
: needsBankReceipt
|
||||
? "uploadReceipt"
|
||||
: canRegisterPaymentComplete
|
||||
? "execute"
|
||||
: null;
|
||||
const more: PaymentActionKind[] = [];
|
||||
if (
|
||||
selectedBatch?.status === "reviewed" &&
|
||||
selectedBatch.canExportPaymentOrder &&
|
||||
canExportPaymentOrder
|
||||
) {
|
||||
more.push(selectedBatch.paymentOrderExportCount > 0 ? "redownloadOrder" : "downloadOrder");
|
||||
}
|
||||
if (canCancelSelectedBatch) more.push("cancel");
|
||||
return { primary: primary as PaymentActionKind | null, more };
|
||||
}, [
|
||||
canCancelSelectedBatch,
|
||||
canExportPaymentOrder,
|
||||
canRegisterPaymentComplete,
|
||||
canReviewSelectedBatch,
|
||||
needsBankReceipt,
|
||||
selectedBatch,
|
||||
]);
|
||||
|
||||
const refresh = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["payment-batches"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["payment-batch-candidates"] });
|
||||
@@ -421,6 +470,68 @@ export function PaymentBatchesPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const paymentActionLabel = (kind: PaymentActionKind) => {
|
||||
if (kind === "review") return reviewMutation.isPending ? "复核中..." : "复核通过";
|
||||
if (kind === "downloadOrder") return paymentOrderMutation.isPending ? "生成中..." : "下载银行制单";
|
||||
if (kind === "uploadReceipt") return uploadReceiptMutation.isPending ? "上传中..." : "上传银行回单";
|
||||
if (kind === "execute") return executeMutation.isPending ? "登记中..." : "登记付款完成";
|
||||
if (kind === "redownloadOrder") return "重新下载制单";
|
||||
return "取消批次";
|
||||
};
|
||||
|
||||
const paymentActionDisabled = (kind: PaymentActionKind) => {
|
||||
if (kind === "review") return reviewMutation.isPending;
|
||||
if (["downloadOrder", "redownloadOrder"].includes(kind)) return paymentOrderMutation.isPending;
|
||||
if (kind === "uploadReceipt") return uploadReceiptMutation.isPending;
|
||||
if (kind === "execute") return executeMutation.isPending;
|
||||
return cancelMutation.isPending || !canCancelSelectedBatch;
|
||||
};
|
||||
|
||||
const activatePaymentAction = (kind: PaymentActionKind) => {
|
||||
if (!selectedBatch) return;
|
||||
if (kind === "review" || kind === "execute") {
|
||||
setConfirmPaymentAction({
|
||||
kind: kind === "review" ? "financeReview" : "financePayment",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
highRisk: selectedBatch.highRisk,
|
||||
totalAmount: selectedBatch.totalAmount,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (kind === "uploadReceipt") {
|
||||
receiptInputRef.current?.click();
|
||||
return;
|
||||
}
|
||||
setActionReason("");
|
||||
if (kind === "cancel") {
|
||||
setReasonAction({ kind: "cancel", batchId: selectedBatch.id, batchNo: selectedBatch.batchNo });
|
||||
return;
|
||||
}
|
||||
setReasonAction({
|
||||
kind: "paymentOrder",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
reexport: kind === "redownloadOrder",
|
||||
});
|
||||
};
|
||||
|
||||
const renderPaymentActionButton = (
|
||||
kind: PaymentActionKind,
|
||||
variant: "default" | "secondary" | "destructive" = "default",
|
||||
) => (
|
||||
<Button
|
||||
key={kind}
|
||||
type="button"
|
||||
variant={variant}
|
||||
disabled={paymentActionDisabled(kind)}
|
||||
data-testid={`payment-batch-action-${kind}`}
|
||||
onClick={() => activatePaymentAction(kind)}
|
||||
>
|
||||
{paymentActionLabel(kind)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
{downloadReasonDialog}
|
||||
@@ -543,7 +654,11 @@ export function PaymentBatchesPage() {
|
||||
</Button>
|
||||
</div>
|
||||
) : selectedBatch ? (
|
||||
<div className="space-y-5">
|
||||
<div
|
||||
className={paymentActionModel.primary || paymentActionModel.more.length ? "pb-[calc(var(--mobile-sticky-action-height,78px)+env(safe-area-inset-bottom,0px)+1.25rem)] lg:pb-0" : ""}
|
||||
>
|
||||
<BusinessDetailWorkspace testId="payment-batch-detail-workspace">
|
||||
<BusinessDetailMain>
|
||||
<div className="finance-command-panel space-y-3" data-testid="payment-batch-detail-panel">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
@@ -555,89 +670,7 @@ export function PaymentBatchesPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{canReviewSelectedBatch ? (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={reviewMutation.isPending}
|
||||
onClick={() => setConfirmPaymentAction({
|
||||
kind: "financeReview",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
highRisk: selectedBatch.highRisk,
|
||||
totalAmount: selectedBatch.totalAmount,
|
||||
})}
|
||||
>
|
||||
复核通过
|
||||
</Button>
|
||||
) : null}
|
||||
{selectedBatch.status === "reviewed" && selectedBatch.canExportPaymentOrder && canExportPaymentOrder && selectedBatch.paymentOrderExportCount === 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={paymentOrderMutation.isPending}
|
||||
onClick={() => {
|
||||
setActionReason("");
|
||||
setReasonAction({ kind: "paymentOrder", batchId: selectedBatch.id, batchNo: selectedBatch.batchNo, reexport: false });
|
||||
}}
|
||||
>
|
||||
{paymentOrderMutation.isPending ? "生成中..." : "下载银行制单"}
|
||||
</Button>
|
||||
) : null}
|
||||
{needsBankReceipt ? (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={uploadReceiptMutation.isPending}
|
||||
onClick={() => receiptInputRef.current?.click()}
|
||||
>
|
||||
{uploadReceiptMutation.isPending ? "上传中..." : "上传银行回单"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canRegisterPaymentComplete ? (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={executeMutation.isPending}
|
||||
onClick={() => setConfirmPaymentAction({
|
||||
kind: "financePayment",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
highRisk: selectedBatch.highRisk,
|
||||
totalAmount: selectedBatch.totalAmount,
|
||||
})}
|
||||
>
|
||||
登记付款完成
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageBatch ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={cancelMutation.isPending || !canCancelSelectedBatch}
|
||||
onClick={() => {
|
||||
setActionReason("");
|
||||
setReasonAction({
|
||||
kind: "cancel",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
});
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
) : null}
|
||||
{selectedBatch.status === "reviewed" && selectedBatch.canExportPaymentOrder && canExportPaymentOrder && selectedBatch.paymentOrderExportCount > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={paymentOrderMutation.isPending}
|
||||
onClick={() => {
|
||||
setActionReason("");
|
||||
setReasonAction({ kind: "paymentOrder", batchId: selectedBatch.id, batchNo: selectedBatch.batchNo, reexport: true });
|
||||
}}
|
||||
>
|
||||
重新下载制单
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<StatusBadge status={selectedBatch.status} label={paymentBatchStatusLabel(selectedBatch.status)} />
|
||||
</div>
|
||||
<div className="detail-fact-grid text-sm xl:grid-cols-4">
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">状态</div><div className="detail-fact-card__value">{paymentBatchStatusLabel(selectedBatch.status)}</div></div>
|
||||
@@ -661,59 +694,13 @@ export function PaymentBatchesPage() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="finance-command-panel space-y-3" data-testid="payment-batch-current-processing">
|
||||
<h3 className="text-base font-semibold text-zinc-900">当前处理</h3>
|
||||
<div className="detail-fact-grid text-sm md:grid-cols-2">
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">当前责任</div>
|
||||
<div className="detail-fact-card__value">
|
||||
{selectedBatch.status === "draft"
|
||||
? "负责人(非制单人)"
|
||||
: ["reviewed", "failed"].includes(selectedBatch.status)
|
||||
? "财务"
|
||||
: selectedBatch.status === "executed"
|
||||
? "已完成"
|
||||
: "已结束"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">下一步</div>
|
||||
<div className="detail-fact-card__value">
|
||||
{selectedBatch.status === "draft"
|
||||
? "复核通过"
|
||||
: selectedBatch.status === "reviewed" && requiresBankReceipt && !receiptAttachmentsQuery.isSuccess
|
||||
? "核对银行回单状态"
|
||||
: selectedBatch.status === "reviewed" && needsBankReceipt
|
||||
? selectedBatch.paymentOrderExportCount > 0
|
||||
? "制单已生成,完成付款并上传银行回单"
|
||||
: "线下付款并上传银行回单"
|
||||
: selectedBatch.status === "reviewed"
|
||||
? "登记付款完成"
|
||||
: selectedBatch.status === "failed"
|
||||
? "核对失败原因并重试"
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3 text-sm text-zinc-600">
|
||||
{selectedBatch.status === "draft"
|
||||
? "已提交付款复核,等待负责人处理;制单人不能复核自己的批次。"
|
||||
: selectedBatch.status === "reviewed" && requiresBankReceipt && !receiptAttachmentsQuery.isSuccess
|
||||
? "付款复核已通过,正在核对银行回单状态。"
|
||||
: selectedBatch.status === "reviewed" && needsBankReceipt
|
||||
? selectedBatch.paymentOrderExportCount > 0
|
||||
? "已生成银行制单,等待付款完成并上传回单。"
|
||||
: "付款复核已通过。等待财务线下付款并上传回单。"
|
||||
: selectedBatch.status === "reviewed"
|
||||
? "付款复核已通过。请财务核对付款结果并登记付款完成。"
|
||||
: selectedBatch.status === "executed"
|
||||
? "付款已登记完成。"
|
||||
: selectedBatch.status === "failed"
|
||||
? "付款登记失败,请财务核对失败原因后重试。"
|
||||
: "该付款批次已取消。"}
|
||||
{selectedBatch.highRisk ? ` 批次金额达到 ${formatMoney(selectedBatch.reviewRequirement.thresholdAmount)},处理前需再次核对付款对象、金额和回单,但不增加审批层级。` : ""}
|
||||
{selectedBatch.cancelledReason ? ` 取消原因:${selectedBatch.cancelledReason}` : ""}
|
||||
</div>
|
||||
<div className="finance-command-panel space-y-3" data-testid="payment-batch-processing-panel">
|
||||
<h3 className="text-base font-semibold text-zinc-900">处理进度</h3>
|
||||
<ProcessingProgress
|
||||
progress={selectedBatch.processingProgress}
|
||||
ariaLabel="付款批次处理进度"
|
||||
testId="payment-batch-processing-progress"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="finance-command-panel space-y-4" data-testid="payment-batch-items-panel">
|
||||
@@ -775,9 +762,11 @@ export function PaymentBatchesPage() {
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">银行回单</h3>
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
{selectedBatch.status === "draft"
|
||||
? "付款复核通过后,由财务在线下完成付款并上传银行回单。"
|
||||
: "先在线下银行完成付款,再上传回单;系统不会连接银行自动转账。"}
|
||||
{!requiresBankReceipt
|
||||
? "当前付款渠道不要求银行回单,核对付款结果后可直接登记完成。"
|
||||
: selectedBatch.status === "draft"
|
||||
? "付款复核通过后,由财务在线下完成付款并上传银行回单。"
|
||||
: "先在线下银行完成付款,再上传回单;系统不会连接银行自动转账。"}
|
||||
</p>
|
||||
</div>
|
||||
{canUploadReceipt ? (
|
||||
@@ -866,14 +855,70 @@ export function PaymentBatchesPage() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{receiptAttachmentsQuery.isSuccess && !(receiptAttachmentsQuery.data?.items ?? []).length ? (
|
||||
{requiresBankReceipt && receiptAttachmentsQuery.isSuccess && !(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>
|
||||
</BusinessDetailMain>
|
||||
|
||||
<BusinessDetailAside className="hidden lg:block">
|
||||
<BusinessDetailActionPanel
|
||||
status={<StatusBadge status={selectedBatch.status} label={paymentBatchStatusLabel(selectedBatch.status)} />}
|
||||
primaryAction={paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : undefined}
|
||||
moreActions={paymentActionModel.more.length ? (
|
||||
<details className="detail-disclosure">
|
||||
<summary>更多</summary>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{paymentActionModel.more.map((kind) =>
|
||||
renderPaymentActionButton(kind, kind === "cancel" ? "destructive" : "secondary"),
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
) : undefined}
|
||||
summary={(
|
||||
<>
|
||||
<p>批次:{selectedBatch.batchNo}</p>
|
||||
<p>金额:{formatMoney(selectedBatch.totalAmount)}</p>
|
||||
</>
|
||||
)}
|
||||
testId="payment-batch-action-panel"
|
||||
/>
|
||||
</BusinessDetailAside>
|
||||
</BusinessDetailWorkspace>
|
||||
|
||||
{paymentActionModel.primary || paymentActionModel.more.length ? (
|
||||
<div
|
||||
className="mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur lg:hidden"
|
||||
data-testid="payment-batch-mobile-actions"
|
||||
>
|
||||
<div className="mobile-sticky-action-grid">
|
||||
{paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : <span />}
|
||||
{paymentActionModel.more.length ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="secondary">更多</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{paymentActionModel.more.map((kind) => (
|
||||
<DropdownMenuItem
|
||||
key={kind}
|
||||
disabled={paymentActionDisabled(kind)}
|
||||
className={kind === "cancel" ? "text-red-600 focus:text-red-600" : undefined}
|
||||
onSelect={() => activatePaymentAction(kind)}
|
||||
>
|
||||
{paymentActionLabel(kind)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="finance-command-panel text-sm text-zinc-500">未找到付款批次</div>
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ import { AttachmentDeleteConfirmDialog } from "../components/common/AttachmentDe
|
||||
import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { ReimbursementTravelSummary } from "../components/reimbursements/ReimbursementTravelSummary";
|
||||
import { ReimbursementProcessingProgress } from "../components/reimbursements/ReimbursementProcessingProgress";
|
||||
import { ProcessingProgress } from "../components/patterns/ProcessingProgress";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
@@ -60,6 +60,12 @@ import {
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
|
||||
import {
|
||||
BusinessDetailActionPanel,
|
||||
BusinessDetailAside,
|
||||
BusinessDetailMain,
|
||||
BusinessDetailWorkspace,
|
||||
} from "../components/patterns/BusinessDetailTemplate";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import {
|
||||
@@ -1028,30 +1034,6 @@ export function ReimbursementDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (detail.status === "reviewing") {
|
||||
return (
|
||||
<Button asChild data-testid="reimbursement-primary-action">
|
||||
<a href="#reimbursement-workflow-progress-panel">看审批进度</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (detail.status === "approved") {
|
||||
return (
|
||||
<Button asChild data-testid="reimbursement-primary-action">
|
||||
<a href="#reimbursement-processing-progress-panel">看处理进度</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (detail.status === "paid") {
|
||||
return (
|
||||
<Button asChild data-testid="reimbursement-primary-action">
|
||||
<a href="#reimbursement-processing-progress-panel">查看付款结果</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
@@ -1105,16 +1087,10 @@ export function ReimbursementDetailPage() {
|
||||
<StatusBadge status={detail.status} />
|
||||
</>
|
||||
)}
|
||||
actions={(
|
||||
<div className="flex flex-wrap gap-2" data-testid="reimbursement-first-screen-actions">
|
||||
{primaryAction}
|
||||
{secondaryActions}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mx-auto w-full max-w-5xl space-y-5">
|
||||
<div className="space-y-5">
|
||||
<BusinessDetailWorkspace className="mx-auto w-full max-w-7xl">
|
||||
<BusinessDetailMain>
|
||||
<SectionPanel title="报销概况">
|
||||
<div className="detail-command-panel space-y-4" data-testid="reimbursement-overview">
|
||||
<div>
|
||||
@@ -1227,35 +1203,17 @@ export function ReimbursementDetailPage() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showSecondaryDetail ? (
|
||||
<div className="space-y-4 pt-4" data-testid="reimbursement-secondary-detail">
|
||||
<div className="detail-command-panel">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<h4 className="text-base font-semibold text-zinc-900">更多</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{detail.status !== "paid" ? (
|
||||
<Button asChild variant="secondary">
|
||||
<Link to={`/reimbursements/${detail.id}/print`}>打印 / PDF</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
{canDeleteReimbursement ? (
|
||||
<Button
|
||||
data-testid="reimbursement-delete-draft"
|
||||
disabled={deleteReimbursementMutation.isPending}
|
||||
onClick={() => setConfirmDeleteReimbursement(true)}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
删除草稿
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="处理进度">
|
||||
<ProcessingProgress
|
||||
progress={processingProgress}
|
||||
ariaLabel="报销处理进度"
|
||||
testId="reimbursement-workflow-progress"
|
||||
targetLinkLabel="查看所在付款批次"
|
||||
/>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="报销明细">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4" data-testid="reimbursement-items">
|
||||
@@ -1574,27 +1532,49 @@ export function ReimbursementDetailPage() {
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="处理进度">
|
||||
<div id="reimbursement-processing-progress-panel">
|
||||
<ReimbursementProcessingProgress progress={processingProgress} />
|
||||
</div>
|
||||
</SectionPanel>
|
||||
</div>
|
||||
</BusinessDetailMain>
|
||||
|
||||
<div className="space-y-5">
|
||||
<ReimbursementActions
|
||||
status={detail.status}
|
||||
showSubmitAction={false}
|
||||
actionError={["reviewing", "approved"].includes(detail.status) ? actionError : ""}
|
||||
submitDisabled={blockStandardChecks.length > 0}
|
||||
submitHint=""
|
||||
submitNotice=""
|
||||
isPending={mutation.isPending}
|
||||
onSubmit={() => void handleSubmitAction()}
|
||||
onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BusinessDetailAside>
|
||||
<BusinessDetailActionPanel
|
||||
status={<StatusBadge status={detail.status} />}
|
||||
primaryAction={primaryAction}
|
||||
secondaryActions={secondaryActions}
|
||||
moreActions={showSecondaryDetail ? (
|
||||
<div className="grid gap-2" data-testid="reimbursement-secondary-detail">
|
||||
{detail.status !== "paid" ? (
|
||||
<Button asChild variant="secondary">
|
||||
<Link to={`/reimbursements/${detail.id}/print`}>打印 / PDF</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
{canDeleteReimbursement ? (
|
||||
<Button
|
||||
data-testid="reimbursement-delete-draft"
|
||||
disabled={deleteReimbursementMutation.isPending}
|
||||
onClick={() => setConfirmDeleteReimbursement(true)}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
删除草稿
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
testId="reimbursement-action-panel"
|
||||
>
|
||||
<ReimbursementActions
|
||||
status={detail.status}
|
||||
showSubmitAction={false}
|
||||
actionError={["reviewing", "approved"].includes(detail.status) ? actionError : ""}
|
||||
submitDisabled={blockStandardChecks.length > 0}
|
||||
submitHint=""
|
||||
submitNotice=""
|
||||
isPending={mutation.isPending}
|
||||
onSubmit={() => void handleSubmitAction()}
|
||||
onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })}
|
||||
/>
|
||||
</BusinessDetailActionPanel>
|
||||
</BusinessDetailAside>
|
||||
</BusinessDetailWorkspace>
|
||||
|
||||
<Dialog open={isInvoiceDialogOpen} onOpenChange={handleInvoiceDialogOpenChange}>
|
||||
<DialogContent
|
||||
|
||||
@@ -444,7 +444,7 @@ export function WorkflowTaskDetailPage() {
|
||||
<BusinessDetailAside>
|
||||
<BusinessDetailActionPanel
|
||||
title="处理"
|
||||
className="workflow-action-panel xl:top-20"
|
||||
className="workflow-action-panel lg:top-20"
|
||||
secondaryActions={(
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -37,9 +37,7 @@ export function ReimbursementActions({
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="reimbursement-action-panel" className="card-std space-y-4 p-6">
|
||||
<h3 className="text-base font-semibold text-zinc-900">可执行动作</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{actionError ? (
|
||||
<p
|
||||
className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600"
|
||||
|
||||
在新工单中引用
屏蔽一个用户