收口报销详情返回与金额展示(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { ArrowLeft, ChevronRight } from "lucide-react";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchExpenseTypes } from "../api/expenseTypes";
|
||||
@@ -72,7 +72,12 @@ import {
|
||||
import { useInvoiceOcrTracker } from "../hooks/useInvoiceOcrTracker";
|
||||
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
|
||||
import { attachmentRequiresDownloadReason, type AttachmentGovernanceFields } from "../lib/attachmentGovernance";
|
||||
import { formatDisplayDate, formatDisplayDateTime } from "../lib/displayFormat";
|
||||
import { formatDisplayDate, formatDisplayDateTime, formatMoney } from "../lib/displayFormat";
|
||||
import {
|
||||
buildPcDetailSource,
|
||||
resolvePcDetailSource,
|
||||
type PcDetailNavigationState,
|
||||
} from "../lib/pcDetailNavigation";
|
||||
import { TEST_IDS } from "../lib/testIds";
|
||||
import { PDF_FILE_ACCEPT, rejectedPdfMessage, splitPdfFiles } from "../lib/pdfFiles";
|
||||
import { ReimbursementActions } from "./reimbursement-detail/ReimbursementActions";
|
||||
@@ -100,6 +105,7 @@ function buildActionErrorMessage(error: unknown, fallbackMessage: string) {
|
||||
|
||||
export function ReimbursementDetailPage() {
|
||||
const { reimbursementId = "" } = useParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { permissions, me, accessToken } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -129,6 +135,10 @@ export function ReimbursementDetailPage() {
|
||||
amount: "",
|
||||
description: "",
|
||||
});
|
||||
const currentDetailSource = buildPcDetailSource(location.pathname, location.search);
|
||||
const navigationState = location.state as PcDetailNavigationState | null;
|
||||
const backSource = resolvePcDetailSource(navigationState?.from, currentDetailSource) ?? "/reimbursements";
|
||||
const backLabel = backSource.startsWith("/workflow/tasks/") ? "返回审批详情" : "返回报销列表";
|
||||
const runGuarded = useUnsavedChangesGuard(
|
||||
Boolean(itemEditor) ||
|
||||
selectedInvoiceFiles.length > 0 ||
|
||||
@@ -749,7 +759,7 @@ export function ReimbursementDetailPage() {
|
||||
<div className="card-std space-y-3 p-5">
|
||||
<p className="text-sm text-red-600">{message}</p>
|
||||
<Button asChild variant="secondary">
|
||||
<Link to="/reimbursements">返回报销列表</Link>
|
||||
<Link replace to={backSource}>{backLabel}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -1042,13 +1052,10 @@ export function ReimbursementDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild data-testid="reimbursement-primary-action">
|
||||
<Link to="/reimbursements">返回列表</Link>
|
||||
</Button>
|
||||
);
|
||||
return null;
|
||||
})();
|
||||
|
||||
const hasMoreActions = detail.status !== "paid" || canDeleteReimbursement;
|
||||
const secondaryActions = [
|
||||
detail.status === "reviewing" && canRemindPendingWorkflow && pendingTaskId ? (
|
||||
<Button
|
||||
@@ -1061,29 +1068,36 @@ export function ReimbursementDetailPage() {
|
||||
>
|
||||
{remindMutation.isPending ? "催办中..." : "催办审批"}
|
||||
</Button>
|
||||
) : detail.status === "paid" ? (
|
||||
) : null,
|
||||
detail.status === "paid" ? (
|
||||
<Button key="print" asChild variant="secondary">
|
||||
<Link to={`/reimbursements/${detail.id}/print`}>打印 / PDF</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button key="back" asChild variant="secondary" data-testid="reimbursement-back">
|
||||
<Link to="/reimbursements">报销列表</Link>
|
||||
) : null,
|
||||
hasMoreActions ? (
|
||||
<Button
|
||||
key="more"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="reimbursement-more-toggle"
|
||||
onClick={() => setShowSecondaryDetail((value) => !value)}
|
||||
>
|
||||
{showSecondaryDetail ? "收起更多" : "更多"}
|
||||
</Button>
|
||||
),
|
||||
<Button
|
||||
key="more"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="reimbursement-more-toggle"
|
||||
onClick={() => setShowSecondaryDetail((value) => !value)}
|
||||
>
|
||||
{showSecondaryDetail ? "收起更多" : "更多"}
|
||||
</Button>,
|
||||
) : null,
|
||||
];
|
||||
|
||||
return (
|
||||
<PageScaffold>
|
||||
{downloadReasonDialog}
|
||||
<div>
|
||||
<Button asChild className="min-h-11 px-2" data-testid="reimbursement-back" variant="ghost">
|
||||
<Link replace to={backSource}>
|
||||
<ArrowLeft aria-hidden="true" className="size-4" />
|
||||
{backLabel}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<EntityOverviewHero
|
||||
title={`报销详情 ${displayCode}`}
|
||||
meta={(
|
||||
@@ -1112,7 +1126,7 @@ export function ReimbursementDetailPage() {
|
||||
<div className="detail-fact-grid xl:grid-cols-3" data-testid="reimbursement-summary">
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">金额</div>
|
||||
<div className="detail-fact-card__value">¥{detail.totalAmount ?? "-"}</div>
|
||||
<div className="detail-fact-card__value">{formatMoney(detail.totalAmount)}</div>
|
||||
</div>
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">当前处理</div>
|
||||
@@ -1224,9 +1238,6 @@ export function ReimbursementDetailPage() {
|
||||
<Link to={`/reimbursements/${detail.id}/print`}>打印 / PDF</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button asChild variant="secondary" data-testid="reimbursement-secondary-back">
|
||||
<Link to="/reimbursements">报销列表</Link>
|
||||
</Button>
|
||||
{canDeleteReimbursement ? (
|
||||
<Button
|
||||
data-testid="reimbursement-delete-draft"
|
||||
@@ -1307,7 +1318,7 @@ export function ReimbursementDetailPage() {
|
||||
</TableCell>
|
||||
<TableCell>{item.expenseDate ?? "-"}</TableCell>
|
||||
<TableCell>
|
||||
<div>{item.amount}</div>
|
||||
<div>{formatMoney(item.amount)}</div>
|
||||
{precheckStandardChecks
|
||||
.filter((check) => check.itemId === item.id)
|
||||
.map((check) => (
|
||||
@@ -1315,8 +1326,8 @@ export function ReimbursementDetailPage() {
|
||||
key={check.standardId}
|
||||
className={`mt-1 text-xs ${check.exceeded ? (check.controlMode === "block" ? "text-red-600" : "text-amber-600") : "text-zinc-500"}`}
|
||||
>
|
||||
标准 ¥{check.maxAmount}/{check.perUnit}
|
||||
{check.exceeded ? `,超出 ¥${check.exceededAmount}` : ""}
|
||||
标准 {formatMoney(check.maxAmount)}/{check.perUnit}
|
||||
{check.exceeded ? `,超出 ${formatMoney(check.exceededAmount)}` : ""}
|
||||
</div>
|
||||
))}
|
||||
</TableCell>
|
||||
@@ -1453,7 +1464,7 @@ export function ReimbursementDetailPage() {
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-medium text-zinc-900">{formatInvoiceDisplayTitle(invoice)}</span>
|
||||
<span className="mt-1 block text-xs text-zinc-500">
|
||||
{[resolveInvoicePartyText(invoice), invoice.invoiceDate ? formatDisplayDate(invoice.invoiceDate) : "", `¥${invoice.totalAmount ?? "-"}`].filter(Boolean).join(" · ")}
|
||||
{[resolveInvoicePartyText(invoice), invoice.invoiceDate ? formatDisplayDate(invoice.invoiceDate) : "", formatMoney(invoice.totalAmount)].filter(Boolean).join(" · ")}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-zinc-500">明细:{resolveLinkedItemLabel(invoice.linkedItemId)}</span>
|
||||
{invoiceHint ? <span className="mt-1 block text-xs text-zinc-500">{invoiceHint}</span> : null}
|
||||
@@ -1645,7 +1656,7 @@ export function ReimbursementDetailPage() {
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium text-zinc-900">{formatInvoiceDisplayTitle(item)}</div>
|
||||
<div className="text-zinc-600">
|
||||
{[resolveInvoicePartyText(item), item.invoiceDate ? formatDisplayDate(item.invoiceDate) : "", `金额 ¥${item.totalAmount ?? "-"}`].filter(Boolean).join(" · ")}
|
||||
{[resolveInvoicePartyText(item), item.invoiceDate ? formatDisplayDate(item.invoiceDate) : "", `金额 ${formatMoney(item.totalAmount)}`].filter(Boolean).join(" · ")}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
原始文件:{item.originalName || "未记录原始文件"} · 上传时间 {formatDisplayDateTime(item.createdAt)}
|
||||
@@ -1769,7 +1780,7 @@ export function ReimbursementDetailPage() {
|
||||
<div key={`${item.itemId}-${item.standardId}`} className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-amber-700">
|
||||
<div className="font-medium">{item.expenseTypeName}</div>
|
||||
<div>
|
||||
当前 ¥{item.amount} / 标准 ¥{item.maxAmount}/{item.perUnit} / 超出 ¥{item.exceededAmount}
|
||||
当前 {formatMoney(item.amount)} / 标准 {formatMoney(item.maxAmount)}/{item.perUnit} / 超出 {formatMoney(item.exceededAmount)}
|
||||
</div>
|
||||
<div>{item.standardName}</div>
|
||||
</div>
|
||||
|
||||
在新工单中引用
屏蔽一个用户