修复报销票据门禁提示与 OCR 降级(08-30-b2-submit-error-ocr-fallback)
这个提交包含在:
@@ -0,0 +1,148 @@
|
||||
import { isApiError } from "../api/types";
|
||||
|
||||
export type InvoiceGuardAmountUnavailableItem = {
|
||||
itemId: string;
|
||||
invoiceIds: string[];
|
||||
};
|
||||
|
||||
export type InvoiceGuardAmountExceededItem = {
|
||||
itemId: string;
|
||||
declaredAmount: string;
|
||||
invoiceTotal: string;
|
||||
};
|
||||
|
||||
export type ReimbursementInvoiceGuardIssues = {
|
||||
unlinkedInvoiceCount: number;
|
||||
unlinkedInvoiceIds: string[];
|
||||
missingInvoiceItemIds: string[];
|
||||
amountUnavailableItems: InvoiceGuardAmountUnavailableItem[];
|
||||
amountExceededItems: InvoiceGuardAmountExceededItem[];
|
||||
};
|
||||
|
||||
function readStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === "string" && item.length > 0)
|
||||
: [];
|
||||
}
|
||||
|
||||
function readNonNegativeInteger(value: unknown): number {
|
||||
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
||||
}
|
||||
|
||||
export function parseReimbursementInvoiceGuardIssues(
|
||||
error: unknown,
|
||||
): ReimbursementInvoiceGuardIssues | null {
|
||||
if (!isApiError(error) || error.details?.items !== "invoice_item_guard_failed") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const amountUnavailableItems = Array.isArray(error.details.amountUnavailableItems)
|
||||
? error.details.amountUnavailableItems.flatMap((value) => {
|
||||
if (!value || typeof value !== "object") return [];
|
||||
const itemId = "itemId" in value && typeof value.itemId === "string" ? value.itemId : "";
|
||||
if (!itemId) return [];
|
||||
return [{
|
||||
itemId,
|
||||
invoiceIds: readStringArray("invoiceIds" in value ? value.invoiceIds : undefined),
|
||||
}];
|
||||
})
|
||||
: [];
|
||||
|
||||
const amountExceededItems = Array.isArray(error.details.amountExceededItems)
|
||||
? error.details.amountExceededItems.flatMap((value) => {
|
||||
if (!value || typeof value !== "object") return [];
|
||||
const itemId = "itemId" in value && typeof value.itemId === "string" ? value.itemId : "";
|
||||
const declaredAmount = "declaredAmount" in value && typeof value.declaredAmount === "string"
|
||||
? value.declaredAmount
|
||||
: "";
|
||||
const invoiceTotal = "invoiceTotal" in value && typeof value.invoiceTotal === "string"
|
||||
? value.invoiceTotal
|
||||
: "";
|
||||
if (!itemId || !declaredAmount || !invoiceTotal) return [];
|
||||
return [{ itemId, declaredAmount, invoiceTotal }];
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
unlinkedInvoiceCount: readNonNegativeInteger(error.details.unlinkedInvoiceCount),
|
||||
unlinkedInvoiceIds: readStringArray(error.details.unlinkedInvoiceIds),
|
||||
missingInvoiceItemIds: readStringArray(error.details.missingInvoiceItemIds),
|
||||
amountUnavailableItems,
|
||||
amountExceededItems,
|
||||
};
|
||||
}
|
||||
|
||||
export function invoiceGuardIssueCount(issues: ReimbursementInvoiceGuardIssues): number {
|
||||
return issues.unlinkedInvoiceCount
|
||||
+ issues.missingInvoiceItemIds.length
|
||||
+ issues.amountUnavailableItems.length
|
||||
+ issues.amountExceededItems.length;
|
||||
}
|
||||
|
||||
export function resolveInvoiceGuardIssuesAfterLink(
|
||||
issues: ReimbursementInvoiceGuardIssues | null,
|
||||
invoiceId: string,
|
||||
linkedItemId: string,
|
||||
): ReimbursementInvoiceGuardIssues | null {
|
||||
if (!issues) return null;
|
||||
const locatedInvoice = issues.unlinkedInvoiceIds.includes(invoiceId);
|
||||
const unlinkedInvoiceIds = issues.unlinkedInvoiceIds.filter((id) => id !== invoiceId);
|
||||
const shouldDecreaseCount = locatedInvoice || issues.unlinkedInvoiceIds.length === 0;
|
||||
const next = {
|
||||
...issues,
|
||||
unlinkedInvoiceCount: shouldDecreaseCount
|
||||
? Math.max(0, issues.unlinkedInvoiceCount - 1)
|
||||
: issues.unlinkedInvoiceCount,
|
||||
unlinkedInvoiceIds,
|
||||
missingInvoiceItemIds: issues.missingInvoiceItemIds.filter((id) => id !== linkedItemId),
|
||||
};
|
||||
return invoiceGuardIssueCount(next) > 0 ? next : null;
|
||||
}
|
||||
|
||||
export function invoiceGuardItemMessages(
|
||||
issues: ReimbursementInvoiceGuardIssues | null,
|
||||
itemId: string,
|
||||
): string[] {
|
||||
if (!issues) return [];
|
||||
const messages: string[] = [];
|
||||
if (issues.missingInvoiceItemIds.includes(itemId)) {
|
||||
messages.push("尚未关联正式发票,请到“报销材料”添加发票并选择本明细。");
|
||||
}
|
||||
if (issues.amountUnavailableItems.some((item) => item.itemId === itemId)) {
|
||||
messages.push("关联发票的票面金额未识别,请重试识别或联系财务处理。");
|
||||
}
|
||||
const exceeded = issues.amountExceededItems.find((item) => item.itemId === itemId);
|
||||
if (exceeded) {
|
||||
messages.push(
|
||||
`申报金额 ¥${exceeded.declaredAmount} 高于关联发票合计 ¥${exceeded.invoiceTotal},请补充发票或调整申报金额。`,
|
||||
);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
export function invoiceGuardInvoiceMessages(
|
||||
issues: ReimbursementInvoiceGuardIssues | null,
|
||||
invoiceId: string,
|
||||
linkedItemId: string | null,
|
||||
): string[] {
|
||||
if (!issues) return [];
|
||||
const messages: string[] = [];
|
||||
const isUnlinked = issues.unlinkedInvoiceIds.includes(invoiceId)
|
||||
|| (issues.unlinkedInvoiceIds.length === 0 && issues.unlinkedInvoiceCount > 0 && !linkedItemId);
|
||||
if (isUnlinked) {
|
||||
messages.push("这张发票还没有选择报销明细,请在下方完成关联。");
|
||||
}
|
||||
if (issues.amountUnavailableItems.some((item) => item.invoiceIds.includes(invoiceId))) {
|
||||
messages.push("这张发票未识别出票面金额,请重试识别或联系财务处理。");
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
export function focusFirstInvoiceGuardIssue() {
|
||||
window.requestAnimationFrame(() => {
|
||||
const target = document.querySelector<HTMLElement>('[data-reimbursement-guard-issue="true"]');
|
||||
if (!target) return;
|
||||
target.focus({ preventScroll: true });
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
});
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户