feat(报销): 收敛只读内容与审批上下文 [09-02-reimbursement-readonly-content-consistency]

这个提交包含在:
2026-09-02 21:13:08 +08:00
父节点 dda625b15f
当前提交 e48514d542
共修改 20 个文件,包含 740 行新增和 47 行删除
+71 -4
查看文件
@@ -7,7 +7,6 @@ export type ReimbursementWorkspaceHeaderModel = {
displayCode: string;
ownerName: string;
reimbursementDate: string | null;
totalAmount: string | null;
description: string | null;
};
@@ -17,8 +16,14 @@ export type ReimbursementWorkspaceIssueAnchor = {
severity: "blocking" | "review" | "info";
};
export type ReimbursementAmountSummary = {
authoritativeTotalAmount: string | null;
integrity: "ready" | "mismatch" | "unavailable";
};
export type ReimbursementWorkspaceModel = {
header: ReimbursementWorkspaceHeaderModel;
amountSummary: ReimbursementAmountSummary;
evidence: ReimbursementApprovalEvidenceProjection;
issueAnchors: ReimbursementWorkspaceIssueAnchor[];
};
@@ -31,27 +36,69 @@ export type ReimbursementWorkspaceSlots = {
afterEvidence?: ReactNode;
timeline?: ReactNode;
actionPanel?: ReactNode;
asideContext?: ReactNode;
mobileActions?: ReactNode;
transientFeedback?: ReactNode;
};
export function buildReimbursementWorkspaceHeader(
detail: Pick<ReimbursementDetail, "formNumber" | "owner" | "reimbursementDate" | "totalAmount" | "description" | "id">,
detail: Pick<ReimbursementDetail, "formNumber" | "owner" | "reimbursementDate" | "description" | "id">,
): ReimbursementWorkspaceHeaderModel {
return {
displayCode: resolveReimbursementDisplayCode(detail),
ownerName: detail.owner?.name ?? "-",
reimbursementDate: detail.reimbursementDate,
totalAmount: detail.totalAmount,
description: detail.description,
};
}
export function parseMoneyToMinorUnits(value: string | null | undefined): bigint | null {
if (typeof value !== "string" || !/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/.test(value)) return null;
const [whole, fraction = ""] = value.split(".");
return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, "0"));
}
export function buildReimbursementAmountSummary(
authoritativeTotalAmount: string | null,
items: Array<{ amount: string }>,
): ReimbursementAmountSummary {
const authoritativeMinorUnits = parseMoneyToMinorUnits(authoritativeTotalAmount);
const itemMinorUnits = items.map((item) => parseMoneyToMinorUnits(item.amount));
if (authoritativeMinorUnits === null || itemMinorUnits.some((value) => value === null)) {
return { authoritativeTotalAmount, integrity: "unavailable" };
}
const derivedMinorUnits = itemMinorUnits.reduce<bigint>((sum, value) => sum + (value ?? 0n), 0n);
return {
authoritativeTotalAmount,
integrity: authoritativeMinorUnits === derivedMinorUnits ? "ready" : "mismatch",
};
}
export function reimbursementAmountIntegrityMessage(integrity: ReimbursementAmountSummary["integrity"]) {
if (integrity === "mismatch") return "报销总额与明细汇总不一致,请联系管理员";
if (integrity === "unavailable") return "明细金额无法完整核对,请联系管理员";
return null;
}
export function buildReimbursementWorkspaceIssueAnchors(
evidence: ReimbursementApprovalEvidenceProjection,
options: { snapshotMissing?: boolean; snapshotInvalid?: boolean } = {},
options: {
amountIntegrity?: ReimbursementAmountSummary["integrity"];
snapshotMissing?: boolean;
snapshotInvalid?: boolean;
} = {},
): ReimbursementWorkspaceIssueAnchor[] {
const anchors: ReimbursementWorkspaceIssueAnchor[] = [];
const amountIntegrityMessage = options.amountIntegrity
? reimbursementAmountIntegrityMessage(options.amountIntegrity)
: null;
if (amountIntegrityMessage) {
anchors.push({
id: "reimbursement-workspace-amount-summary",
label: amountIntegrityMessage,
severity: "blocking",
});
}
if (options.snapshotInvalid) {
anchors.push({ id: "reimbursement-workspace-snapshot", label: "提交记录无法读取", severity: "blocking" });
} else if (options.snapshotMissing) {
@@ -94,3 +141,23 @@ export function buildReimbursementWorkspaceIssueAnchors(
}
return anchors;
}
export function buildReimbursementWorkspaceModel(
detail: Pick<
ReimbursementDetail,
"formNumber" | "owner" | "reimbursementDate" | "totalAmount" | "description" | "id"
>,
evidence: ReimbursementApprovalEvidenceProjection,
options: { snapshotMissing?: boolean; snapshotInvalid?: boolean } = {},
): ReimbursementWorkspaceModel {
const amountSummary = buildReimbursementAmountSummary(detail.totalAmount, evidence.items);
return {
header: buildReimbursementWorkspaceHeader(detail),
amountSummary,
evidence,
issueAnchors: buildReimbursementWorkspaceIssueAnchors(evidence, {
...options,
amountIntegrity: amountSummary.integrity,
}),
};
}