feat(报销): 收敛只读内容与审批上下文 [09-02-reimbursement-readonly-content-consistency]
这个提交包含在:
@@ -60,7 +60,7 @@ import { Textarea } from "../components/ui/textarea";
|
||||
import { PDF_FILE_ACCEPT, rejectedPdfMessage, splitPdfFiles } from "../lib/pdfFiles";
|
||||
import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
import { buildReimbursementApprovalEvidence } from "../lib/reimbursementApprovalProjection";
|
||||
import { buildReimbursementWorkspaceHeader, buildReimbursementWorkspaceIssueAnchors } from "../lib/reimbursementWorkspace";
|
||||
import { buildReimbursementWorkspaceModel } from "../lib/reimbursementWorkspace";
|
||||
import {
|
||||
formatInvoiceDisplayTitle,
|
||||
resolveInvoicePartyText,
|
||||
@@ -909,11 +909,7 @@ export function MobileReimbursementDetailPage() {
|
||||
budgetCheckStatus: "not_applicable",
|
||||
parseStatus: "ready",
|
||||
});
|
||||
const workspaceModel = {
|
||||
header: buildReimbursementWorkspaceHeader(detail),
|
||||
evidence: workspaceEvidence,
|
||||
issueAnchors: buildReimbursementWorkspaceIssueAnchors(workspaceEvidence),
|
||||
};
|
||||
const workspaceModel = buildReimbursementWorkspaceModel(detail, workspaceEvidence);
|
||||
const workspaceCapabilities = {
|
||||
onOpenInvoice: (invoice: { id: string }) => openInvoiceMutation.mutate(invoice.id),
|
||||
onOpenAttachment: (attachment: { id: string }) => {
|
||||
|
||||
@@ -84,7 +84,7 @@ import {
|
||||
} from "./reimbursement-detail/ReimbursementItemEditor";
|
||||
import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
import { buildReimbursementApprovalEvidence } from "../lib/reimbursementApprovalProjection";
|
||||
import { buildReimbursementWorkspaceHeader, buildReimbursementWorkspaceIssueAnchors } from "../lib/reimbursementWorkspace";
|
||||
import { buildReimbursementWorkspaceModel } from "../lib/reimbursementWorkspace";
|
||||
import {
|
||||
focusFirstInvoiceGuardIssue,
|
||||
parseReimbursementInvoiceGuardIssues,
|
||||
@@ -1042,11 +1042,7 @@ export function ReimbursementDetailPage() {
|
||||
budgetCheckStatus: "not_applicable",
|
||||
parseStatus: "ready",
|
||||
});
|
||||
const workspaceModel = {
|
||||
header: buildReimbursementWorkspaceHeader(detail),
|
||||
evidence: workspaceEvidence,
|
||||
issueAnchors: buildReimbursementWorkspaceIssueAnchors(workspaceEvidence),
|
||||
};
|
||||
const workspaceModel = buildReimbursementWorkspaceModel(detail, workspaceEvidence);
|
||||
const workspaceCapabilities = {
|
||||
onOpenInvoice: (invoice: { id: string }) => openInvoiceMutation.mutate(invoice.id),
|
||||
onOpenAttachment: (attachment: { id: string }) => {
|
||||
|
||||
@@ -36,10 +36,7 @@ import { approvalRoleLabel, workflowActionLabel } from "../../lib/approvalDispla
|
||||
import { attachmentRequiresDownloadReason, type AttachmentGovernanceFields } from "../../lib/attachmentGovernance";
|
||||
import { formatDisplayDateTime } from "../../lib/displayFormat";
|
||||
import { buildReimbursementApprovalEvidence } from "../../lib/reimbursementApprovalProjection";
|
||||
import {
|
||||
buildReimbursementWorkspaceHeader,
|
||||
buildReimbursementWorkspaceIssueAnchors,
|
||||
} from "../../lib/reimbursementWorkspace";
|
||||
import { buildReimbursementWorkspaceModel, reimbursementAmountIntegrityMessage } from "../../lib/reimbursementWorkspace";
|
||||
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
||||
import { isWecomWebView } from "../../lib/runtimeContext";
|
||||
import { getWorkflowConfirmCopy, type WorkflowConfirmAction } from "../../lib/workflowActionConfirm";
|
||||
@@ -121,16 +118,17 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
||||
}, [reimbursementDetailQuery.data, snapshot]);
|
||||
const model = useMemo(() => {
|
||||
if (!reimbursementDetailQuery.data || !evidence) return null;
|
||||
return {
|
||||
header: buildReimbursementWorkspaceHeader(reimbursementDetailQuery.data),
|
||||
evidence,
|
||||
issueAnchors: buildReimbursementWorkspaceIssueAnchors(evidence, {
|
||||
snapshotInvalid: snapshot?.parseStatus === "invalid",
|
||||
snapshotMissing: snapshot?.budgetCheckStatus === "missing",
|
||||
}),
|
||||
};
|
||||
return buildReimbursementWorkspaceModel(reimbursementDetailQuery.data, evidence, {
|
||||
snapshotInvalid: snapshot?.parseStatus === "invalid",
|
||||
snapshotMissing: snapshot?.budgetCheckStatus === "missing",
|
||||
});
|
||||
}, [evidence, reimbursementDetailQuery.data, snapshot?.budgetCheckStatus, snapshot?.parseStatus]);
|
||||
const decisionReady = Boolean(online && model && snapshot?.parseStatus === "ready");
|
||||
const decisionReady = Boolean(
|
||||
online
|
||||
&& model
|
||||
&& model.amountSummary.integrity === "ready"
|
||||
&& snapshot?.parseStatus === "ready",
|
||||
);
|
||||
const allowedActions = useMemo(() => new Set(detail.allowedActions), [detail.allowedActions]);
|
||||
const canTransfer = allowedActions.has("transfer") && hasPermission(permissions, "expense.transfer");
|
||||
const approverQuery = useQuery({
|
||||
@@ -176,7 +174,13 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
||||
|
||||
const actionMutation = useMutation({
|
||||
mutationFn: async (action: "approve" | "reject" | "return" | "transfer") => {
|
||||
if (!decisionReady) throw new Error("完整报销内容或提交时检查尚未就绪,暂时不能处理");
|
||||
if (!decisionReady) {
|
||||
throw new Error(
|
||||
model
|
||||
? reimbursementAmountIntegrityMessage(model.amountSummary.integrity) ?? "完整报销内容或提交时检查尚未就绪,暂时不能处理"
|
||||
: "完整报销内容或提交时检查尚未就绪,暂时不能处理",
|
||||
);
|
||||
}
|
||||
if (action === "approve") return approveWorkflowTask(taskId);
|
||||
if (action === "reject") return rejectWorkflowTask(taskId, rejectComment.trim());
|
||||
if (action === "return") return returnWorkflowTask(taskId, returnComment.trim());
|
||||
@@ -205,7 +209,11 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
||||
|
||||
function requestAction(action: WorkflowConfirmAction) {
|
||||
if (!decisionReady) {
|
||||
setActionError(online ? "完整报销内容或提交时检查尚未就绪,请重试加载后再决定。" : "报销审批需联网核对完整报销内容后才能处理。");
|
||||
setActionError(!online
|
||||
? "报销审批需联网核对完整报销内容后才能处理。"
|
||||
: model
|
||||
? reimbursementAmountIntegrityMessage(model.amountSummary.integrity) ?? "完整报销内容或提交时检查尚未就绪,请重试加载后再决定。"
|
||||
: "完整报销内容或提交时检查尚未就绪,请重试加载后再决定。");
|
||||
return;
|
||||
}
|
||||
const comment = action === "reject" ? rejectComment : action === "return" ? returnComment : "";
|
||||
@@ -322,7 +330,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
||||
);
|
||||
const timeline = (
|
||||
<SectionPanel title="处理历史">
|
||||
<div className="detail-timeline">
|
||||
<div className="detail-timeline" data-testid="reimbursement-workflow-history">
|
||||
{detail.history.length ? detail.history.map((item) => (
|
||||
<div className="detail-timeline-item" key={item.id}>
|
||||
<div className="font-medium text-zinc-900">{workflowActionLabel(item.action)} · {item.actorName || item.actorEmployeeNo}</div>
|
||||
@@ -433,8 +441,8 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
||||
backAction: <Button asChild variant="ghost"><Link replace to={backTarget}>返回审批列表</Link></Button>,
|
||||
beforeEvidence: detail.task.status !== "pending" ? <p className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-800">当前显示报销单最新版本;审批意见与提交检查保留历史语义。</p> : null,
|
||||
afterEvidence: snapshot?.budgetCheckStatus === "missing" ? <p className="text-sm text-zinc-500" id="reimbursement-workspace-snapshot">该次提交未留存预算快照。</p> : null,
|
||||
timeline,
|
||||
actionPanel,
|
||||
asideContext: timeline,
|
||||
}}
|
||||
/>
|
||||
{dialogs}
|
||||
|
||||
在新工单中引用
屏蔽一个用户