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('[data-reimbursement-guard-issue="true"]'); if (!target) return; target.focus({ preventScroll: true }); target.scrollIntoView({ behavior: "smooth", block: "center" }); }); }