import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useAuth } from "../../auth/AuthContext"; import { saveLoanAction, uploadLoanBankEvidence, downloadLoanBankEvidence, readLoanBankResultDetails, downloadLoanReconciliationReceipt, type LoanRepayment } from "../../api/companyLoans"; import { type PaymentBatchReceiptAttachment } from "../../api/paymentBatches"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "../ui/dialog"; import { resolveErrorMessage } from "../../lib/resolveErrorMessage"; import { downloadBlob } from "../../lib/download"; import { formatDisplayDate } from "../../lib/displayFormat"; const states: Record = { proposed: "待复核", approved: "复核通过", rejected: "已驳回", superseded: "已取代", collecting: "补证中", withdrawn: "已撤回", consumed: "已释放", booked: "已登记入账", dismissed: "已确认误报" }; const categories: Record = { bank_terminal: "银行终态或更正书面证据", query_no_result: "指令查询无结果官方 PDF", account_statement: "同期付款账户对账单", exporter_statement: "导出人文件去向说明", late_success: "晚到成功回单", incoming_return: "出借人退款到账回单" }; const actions: Record = { downloadReceipt: "查看对应付款回单", details: "查看录入的银行事实", upload: "上传银行证据", submission: "登记银行提交", withdrawSubmission: "撤回提交登记", attest: "确认提交清单完整", reviewList: "独立复核提交清单", terminal: "登记银行终态事实", reviewFact: "独立复核终态事实", confirm: "确认银行未付款", failureAttribution: "登记失败回单归属", reviewFailure: "复核失败归属", q6: "登记人工未提交", reviewQ6: "特别复核人工未提交", recovery: "发起补证恢复", submitRecovery: "送审补证恢复", reviewRecovery: "复核补证恢复", withdrawRecovery: "撤回补证恢复", cancel: "重验依据并取消释放", event: "登记晚到成功", refund: "登记追回到账", reviewEvent: "独立复核银行事实", apply: "登记补记或追回", download: "查看银行证据", void: "作废银行证据" }; export function LoanBankReconciliation({ repayment: r, attachments }: { repayment: LoanRepayment; attachments: PaymentBatchReceiptAttachment[] }) { const { me, permissions } = useAuth(); const cache = useQueryClient(); const related = r.approvalMode === "related_party"; const finance = related ? r.actionCapabilities?.canRecordFacts === true : !!me?.roleCodes.some((role) => ["finance", "finance_mgr"].includes(role)) && permissions.includes("loan.manage"); const reviewer = related ? r.actionCapabilities?.canReviewReconciliation === true : !!me?.roleCodes.some((role) => ["manager", "ceo"].includes(role)) && permissions.includes("payment.batch.review"); const actionLabel = (name: string) => related ? actions[name].replaceAll("独立复核", "公司代表确认").replaceAll("复核", "公司代表确认") : actions[name]; const stateLabel = (status: string) => related && ["proposed", "approved"].includes(status) ? status === "proposed" ? "待公司代表确认" : "公司代表已确认" : states[status]; const [details, setDetails] = useState> | null>(null); const [action, setAction] = useState(null); const [target, setTarget] = useState(""); const [pendingIds, setPendingIds] = useState([]); const [values, setValues] = useState>({}); const [file, setFile] = useState(null); const value = (key: string) => values[key] ?? ""; const set = (key: string, next: string) => setValues((old) => ({ ...old, [key]: next })); const base = `/payment-batches/${r.paymentBatchId}`; const exports = r.bankSubmissions?.exports ?? []; const submissions = r.bankSubmissions?.submissions ?? []; const evidences = r.terminal?.evidences ?? []; const current = r.releaseRecovery?.current; const q6 = r.notSubmitted?.history[0]; const list = r.bankSubmissions?.attestation; const unlinked = exports.filter((e) => !submissions.some((s) => s.status === "active" && s.exportId === e.id)); const mutation = useMutation({ mutationFn: async () => { const note = value("note"); const decision = value("decision") || "approve"; const reviewBody = { decision, reason: note }; if (action === "details") setDetails(await readLoanBankResultDetails(r.paymentBatchId, target, note)); if (action === "upload") { if (!file) throw new Error("请选择证据文件"); await uploadLoanBankEvidence(r.paymentBatchId, value("category") || "bank_terminal", file); } if (action === "download") { const result = await downloadLoanBankEvidence(r.paymentBatchId, target, note); downloadBlob(result.blob, result.filename ?? "银行证据"); } if (action === "downloadReceipt") { const result = await downloadLoanReconciliationReceipt(r.paymentBatchId, target, note); downloadBlob(result.blob, result.filename ?? "付款回单"); } if (action === "void") await saveLoanAction(`${base}/loan-terminal-evidences/${target}/void`, { reason: note }); if (action === "submission") await saveLoanAction(`${base}/loan-bank-submissions`, { channel: value("export") ? "online_banking_import" : "manual_entry", ...(value("export") ? { exportId: value("export") } : {}), bankInstructionRef: value("reference"), submittedAt: new Date(value("date")).toISOString(), note }); if (action === "withdrawSubmission") await saveLoanAction(`${base}/loan-bank-submissions/${target}/withdraw`, { reason: note }); if (action === "attest") await saveLoanAction(`${base}/loan-submission-attestation`, { note }); if (action === "reviewList") await saveLoanAction(`${base}/loan-submission-attestation/${target}/review`, reviewBody); if (action === "terminal") await saveLoanAction(`${base}/loan-terminal-facts`, { submissionId: value("submission"), kind: value("receipt") ? "failure_receipt" : "termination_evidence", ...(value("receipt") ? { receiptAttachmentId: value("receipt") } : { evidenceId: value("evidence"), terminalStatus: value("terminalStatus") || "failed" }), resolvesAttachmentIds: pendingIds, note }); if (action === "reviewFact") await saveLoanAction(`${base}/loan-terminal-facts/${target}/review`, reviewBody); if (action === "confirm") await saveLoanAction(`${base}/loan-not-paid/review`, {}); if (action === "failureAttribution") await saveLoanAction(`${base}/loan-receipt-attribution`, { kind: "failure", attachmentId: value("receipt"), ...(value("submission") ? { submissionId: value("submission") } : {}), basisKind: "bank_instruction_ref", basisNote: note, exclusionNote: value("exclusion") }); if (action === "reviewFailure") await saveLoanAction(`${base}/loan-receipt-attribution/${target}/review`, reviewBody); if (action === "q6") await saveLoanAction(`${base}/loan-not-submitted`, { exportIds: unlinked.map((e) => e.id), queryEvidenceId: value("query"), statementEvidenceId: value("statement"), exporterStatements: [...new Set(unlinked.map((e) => e.exportedBy))].map((id) => ({ exporterId: id, evidenceId: value(`exporter:${id}`) })), coverageFrom: new Date(value("from")).toISOString(), coverageThrough: new Date(value("through")).toISOString(), note }); if (action === "reviewQ6") await saveLoanAction(`${base}/loan-not-submitted/${target}/review`, reviewBody); if (action === "recovery") await saveLoanAction(`${base}/loan-release-recoveries`, { requestKey: crypto.randomUUID(), reason: note }); if (action === "submitRecovery") await saveLoanAction(`${base}/loan-release-recoveries/${target}/submit`, {}); if (action === "reviewRecovery") await saveLoanAction(`${base}/loan-release-recoveries/${target}/review`, reviewBody); if (action === "withdrawRecovery") await saveLoanAction(`${base}/loan-release-recoveries/${target}/withdraw`, { reason: note }); if (action === "cancel") await saveLoanAction(`${base}/cancel`, { reason: note, ...(current ? { recoveryId: current.id, requestKey: crypto.randomUUID() } : {}) }); if (action === "event" || action === "refund") await saveLoanAction(`${base}/loan-bank-results`, { kind: action === "refund" ? "return" : "late_success", ...(action === "refund" ? { parentId: target } : {}), evidenceId: value("evidence"), requestKey: crypto.randomUUID(), attributionNote: note, exclusionNote: value("exclusion"), fields: [{ Name: "付款账号", Value: value("payer") }, { Name: "收款账号", Value: value("payee") }, { Name: "金额", Value: value("amount") }, { Name: "币种", Value: "CNY" }, { Name: "交易状态", Value: "交易成功" }, { Name: "流水号", Value: value("reference") }] }); if (action === "reviewEvent") await saveLoanAction(`${base}/loan-bank-results/${target}/review`, { ...reviewBody, ...(decision === "reject" && value("evidence") ? { correctionEvidenceId: value("evidence"), correctionKind: value("correction") || "misassociated" } : {}) }); if (action === "apply") await saveLoanAction(`${base}/loan-bank-results/${target}/apply`, {}); }, onSuccess: async () => { if (action === "details") return; setAction(null); await Promise.all(["payment-batches", "company-loans", "loan-repayments"].map((key) => cache.invalidateQueries({ queryKey: [key] }))); } }); function open(name: string, id = "") { setAction(name); setTarget(id); setValues({}); setPendingIds([]); setDetails(null); setFile(null); mutation.reset(); } const button = (name: string, id = "") => ; const input = (key: string, label: string, type = "text") => ; const select = (key: string, label: string, choices: Array<[string, string]>, required = true) => ; const evidenceSelect = (key: string, label: string, kind: string, required = true) => select(key, label, evidences.filter((e) => !e.voided && e.evidenceKind === kind).map((e) => [e.id, e.originalName]), required); const exportLabel = (id: string | null) => id ? `第 ${exports.find((e) => e.id === id)?.sequence ?? "未知"} 次导出` : "系统外手工提交"; const submissionLabel = (id: string) => submissions.find((s) => s.id === id)?.bankInstructionRef ?? "提交记录已不可用"; const evidenceLink = (id: string) => ; const receiptLink = (id: string) => ; const receiptOptions: Array<[string, string]> = attachments.map((a, i) => [a.id, `回单 ${i + 1}${a.archivedAt ? "(已作废的历史事实)" : ""}`]); const terminal = ["confirmed_failed", "confirmed_not_paid", "confirmed_not_submitted"].includes(r.bankResultState); return

银行结果对账

{r.freeze?.frozen ?

该出借人暂停新还款、复核和导出。仍可登记银行事实、对账和追回;所有冻结原因处理完成后才恢复付款。

: null} {r.freeze?.reasons.filter((x) => x.status === "active").map((x) =>

待处理:{x.kind === "late_success" ? "晚到成功待确认或补记" : x.kind === "receivable" ? "超额付款待追回" : "其他申请占用冲突"}

)} {r.bankSubmissions?.legacy ?

历史导出明细待核实,不能凭汇总释放额度。

: null}

等待期只是内部谨慎期,不能证明银行未付款。线下核对不能直接解除系统占用。

{exports.map((e) =>

导出 {e.sequence} · {formatDisplayDate(e.exportedAt)} · 客户方流水号 {e.customerSerials.join("、")}

)} {submissions.map((s) =>
指令 {s.bankInstructionRef} · {exportLabel(s.exportId)} · {formatDisplayDate(s.submittedAt)} · {s.note} · {s.status === "active" ? "已登记" : "已撤回登记(不代表银行撤销)"}{finance && s.status === "active" && r.status !== "cancelled" ? button("withdrawSubmission", s.id) : null}
)} {list ?

提交清单:{stateLabel(list.status)}{list.stale ? ",已变化,须重新确认复核" : ""}

{reviewer && list.status === "proposed" && !list.stale && list.proposedBy !== me?.id ? button("reviewList", list.id) : null}
: null}
{finance && ["reviewed", "failed", "cancelled"].includes(r.status) ? button("upload") : null}{finance && r.status !== "cancelled" ? <>{button("submission")}{button("attest")}{button("terminal")}{button("failureAttribution")} : null}{reviewer && r.bankResultState === "unknown" ? button("confirm") : null}
{evidences.map((e, evidenceIndex) =>
证据 {evidenceIndex + 1} · {categories[e.evidenceKind] ?? "银行证据"}:{e.originalName}{e.voided ? "(已作废)" : ""}{button("download", e.id)}{finance && !e.voided && r.status !== "cancelled" ? button("void", e.id) : null}
)} {r.terminal?.facts.map((f, i) =>
终态事实 {i + 1}:{stateLabel(f.status)} · 指令 {submissionLabel(f.submissionId)} · {f.kind === "failure_receipt" ? "失败回单" : ({ failed: "已失败", revoked: "已撤销", rejected: "已拒绝" }[f.terminalStatus] ?? f.terminalStatus)} · {f.note}{f.evidenceId ? evidenceLink(f.evidenceId) : null}{f.receiptAttachmentId ? receiptLink(f.receiptAttachmentId) : null}{f.resolvesAttachmentIds.length ? 了结处理中回单: : null}{f.resolvesAttachmentIds.map((id) => {receiptLink(id)})}{reviewer && f.status === "proposed" && f.proposedBy !== me?.id ? button("reviewFact", f.id) : null}
)} {r.failureAttributions?.map((a) =>

失败归属:{stateLabel(a.status)} · {a.basisNote}

{a.exclusionNote}

归属范围:{a.failureScope === "submission" ? "指定提交" : "本批次提交清单"};覆盖指令:{a.coveredSubmissionIds.map(submissionLabel).join("、")}

{a.receiptAttachmentIds.map((id) => {receiptLink(id)})}{reviewer && a.status === "proposed" && a.proposedBy !== me?.id ? button("reviewFailure", a.id) : null}
)} {related ?

本模式不支持“人工确认未提交”。无银行终态依据时继续占用。

: null} {q6 ?

人工确认未提交:{stateLabel(q6.status)} · {q6.payload.note} · 覆盖 {formatDisplayDate(q6.payload.coverageFrom)} 至 {formatDisplayDate(q6.payload.coverageThrough)} · 已有 {q6.reviews.filter((x) => x.decision === "approve").length} 人同意{q6.stale ? ";材料已变化,须重新登记及双人复核" : ""}

覆盖导出:{q6.payload.exportIds.map(exportLabel).join("、")}

查询:{evidenceLink(q6.payload.queryEvidenceId)}对账单:{evidenceLink(q6.payload.statementEvidenceId)}{q6.payload.exporterStatements.map((x, i) => 导出人 {i + 1}({exports.filter((e) => e.exportedBy === x.exporterId && q6.payload.exportIds.includes(e.id)).map((e) => exportLabel(e.id)).join("、")}):{evidenceLink(x.evidenceId)})}
{!related && reviewer && q6.status === "proposed" && !q6.stale && q6.proposedBy !== me?.id && !q6.reviews.some((x) => x.reviewedBy === me?.id) ? button("reviewQ6", q6.id) : null}
: null} {!related && finance && unlinked.length > 0 && r.status !== "cancelled" && (!q6 || q6.stale || q6.status === "rejected") ? button("q6") : null} {current ?

补证恢复第 {current.sequence} 轮:{stateLabel(current.status)}{current.stale ? ";依据已变化,须重新发起" : ""}

{finance && current.status === "collecting" ? button("submitRecovery", current.id) : null}{finance && ["collecting", "proposed"].includes(current.status) ? button("withdrawRecovery", current.id) : null}{reviewer && current.status === "proposed" && !current.stale && r.releaseRecovery?.history[0]?.startedBy !== me?.id ? button("reviewRecovery", current.id) : null}
: null} {finance && ["confirmed_failed", "confirmed_not_paid"].includes(r.bankResultState) && r.status !== "cancelled" && (!current || current.stale || ["withdrawn", "rejected"].includes(current.status)) ? button("recovery") : null} {finance && (!related || r.actionCapabilities?.canRelease) && terminal && r.status !== "cancelled" && !r.bankResults?.events.some((e) => e.kind === "late_success" && ["proposed", "approved"].includes(e.status)) ? button("cancel") : null} {terminal ?

原制单文件不得再导入网银。系统不能使已下载文件失效;出现晚到成功应立即登记对账。

: null} {finance && terminal ? button("event") : null} {r.bankResults?.events.map((e, i) =>

{e.kind === "return" ? "追回到账" : "晚到成功"} {i + 1} · ¥{e.amount} · {stateLabel(e.status)}

{e.outstandingReturn ?

待追回 ¥{e.outstandingReturn}

: null}{e.settlements.map((s) =>

银行已付 ¥{s.bankAmount};结清本金 ¥{s.settledAmount};超额 ¥{s.receivableAmount}

)}
{button("download", e.evidenceId)}{button("details", e.id)}{reviewer && e.status === "proposed" && e.proposedBy !== me?.id ? button("reviewEvent", e.id) : null}{finance && e.status === "approved" ? button("apply", e.id) : null}{finance && e.status === "booked" && Number(e.outstandingReturn) > 0 ? button("refund", e.id) : null}
)} { if (!next && !mutation.isPending) setAction(null); }}>{action ? actionLabel(action) : "银行对账"}{related ? "核对原始银行材料和具体指令。收款人以公司代表身份确认,不能兼任经办;材料不足时保持占用或冻结。人工确认未提交在本模式不可用。" : "核对原始银行材料和具体指令。独立复核人不能兼任相关经办或收款人;材料不足时保持占用或冻结。人工未提交须三类材料齐全并由两名独立人员复核。"}
{ e.preventDefault(); mutation.mutate(); }}> {details ?
{details.fields.map((f) =>

{f.Name}:{f.Value}

)}

{details.attributionNote}

{details.exclusionNote}

: null} {mutation.error ?

{resolveErrorMessage(mutation.error)}

: null} {action === "upload" ? <>{select("category", "证据类别", Object.entries(categories).filter(([key]) => !related || !["query_no_result", "account_statement", "exporter_statement"].includes(key)))} : null} {action === "submission" ? <>{select("export", "使用的导出文件(手工录入可不选)", exports.map((e) => [e.id, `第 ${e.sequence} 次导出`]), false)}{input("reference", "银行指令号")}{input("date", "实际提交时间", "datetime-local")} : null} {action === "terminal" || action === "failureAttribution" ? <>{select("submission", "对应银行提交", submissions.filter((s) => s.status === "active").map((s) => [s.id, s.bankInstructionRef]), action === "terminal")}{select("receipt", "失败回单(终态书面证据可不选)", receiptOptions, action === "failureAttribution")}{action === "terminal" ? <>{evidenceSelect("evidence", "银行终态书面证据", "bank_terminal", !value("receipt"))}{select("terminalStatus", "银行终态", [["failed", "已失败,不会再执行"], ["revoked", "已撤销,不会再执行"], ["rejected", "已拒绝,不会再执行"]], !value("receipt"))}
了结的同指令处理中回单(可多选){receiptOptions.map(([id, label]) => )}
: input("exclusion", "排除其他候选批次的依据")} : null} {action === "q6" ? <>{evidenceSelect("query", "查询无结果官方 PDF", "query_no_result")}{evidenceSelect("statement", "同期对账单", "account_statement")}{[...new Set(unlinked.map((e) => e.exportedBy))].map((id, i) => evidenceSelect(`exporter:${id}`, `导出人 ${i + 1} 的文件去向说明`, "exporter_statement"))}{input("from", "查询与对账单覆盖开始", "datetime-local")}{input("through", "查询与对账单覆盖截至(须含登记日)", "datetime-local")} : null} {action?.startsWith("review") ? select("decision", "复核结论", [["approve", "依据充分,同意"], ["reject", "不同意/有依据确认误报"]]) : null} {action === "reviewEvent" && value("decision") === "reject" ? <>{evidenceSelect("evidence", "误关联或重复登记的更正证据(晚到成功必填)", "bank_terminal", false)}{select("correction", "关闭原因", [["misassociated", "误关联"], ["duplicate", "重复登记"]], false)} : null} {action === "event" || action === "refund" ? <>{evidenceSelect("evidence", "对应的成功银行回单", action === "refund" ? "incoming_return" : "late_success")}{input("payer", "回单付款账号")}{input("payee", "回单收款账号")}{input("amount", "回单人民币金额")}{input("reference", "回单流水号")}{input("exclusion", "为什么不属于其他候选批次或交易")} : null} {!['upload', 'submitRecovery', 'confirm', 'apply'].includes(action ?? '') ?