公司只有财务和出借收款经理两名办理人,新增限定公司、出借人和借款范围的关联方确认模式。 仅豁免指定公司代表的收款人关联;经办、自审、证据、交易归属、资金占用及 Q6 限制保留。 贯通付款、人工回单、银行对账、补证恢复、晚到补记与追回;电脑和手机显示实际确认身份。 授权版本、付款与对账停用事件及历史决定只增不改,新增迁移 20261009010000_loan_related_approval。 本地合成验证:82 迁移、HTTP 109 组、浏览器 33 组、数据库约束 9 组通过;13 个有效资金守卫变异均捕获。 已有服务端全量 4519 项通过、1 项原有跳过,后续定向 77 项通过;提示收尾另有 908 项合同测试及前端构建通过。 按用户授权提交后对干净候选运行正式 verify-candidate,不沿用旧候选回执。 限制:独立代码复核待完成(inline 模式);真实回单待验。本次仅本地提交,不推送、不部署。 生成关联决定后不能直接降回不理解该模式的旧代码;真实授权、录入、生产核实和付款仍须明确批准。
113 行
24 KiB
TypeScript
113 行
24 KiB
TypeScript
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<string, string> = { proposed: "待复核", approved: "复核通过", rejected: "已驳回", superseded: "已取代", collecting: "补证中", withdrawn: "已撤回", consumed: "已释放", booked: "已登记入账", dismissed: "已确认误报" };
|
||
const categories: Record<string, string> = { bank_terminal: "银行终态或更正书面证据", query_no_result: "指令查询无结果官方 PDF", account_statement: "同期付款账户对账单", exporter_statement: "导出人文件去向说明", late_success: "晚到成功回单", incoming_return: "出借人退款到账回单" };
|
||
const actions: Record<string, string> = { 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<Awaited<ReturnType<typeof readLoanBankResultDetails>> | null>(null);
|
||
const [action, setAction] = useState<string | null>(null); const [target, setTarget] = useState("");
|
||
const [pendingIds, setPendingIds] = useState<string[]>([]);
|
||
const [values, setValues] = useState<Record<string, string>>({}); const [file, setFile] = useState<File | null>(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 = "") => <Button key={`${name}:${id}`} type="button" variant="outline" onClick={() => open(name, id)}>{actionLabel(name)}</Button>;
|
||
const input = (key: string, label: string, type = "text") => <label className="grid gap-2 text-sm" key={key}>{label}<Input required type={type} value={value(key)} onChange={(e) => set(key, e.target.value)} /></label>;
|
||
const select = (key: string, label: string, choices: Array<[string, string]>, required = true) => <label className="grid gap-2 text-sm" key={key}>{label}<select aria-label={label} required={required} className="min-h-11 min-w-0 w-full rounded border px-3" value={value(key)} onChange={(e) => set(key, e.target.value)}><option value="">请选择{required ? "" : "(可不选)"}</option>{choices.map(([id, title]) => <option key={id} value={id}>{title}</option>)}</select></label>;
|
||
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) => <Button type="button" variant="outline" onClick={() => open("download", id)}>证据 {evidences.findIndex((e) => e.id === id) + 1}:{evidences.find((e) => e.id === id)?.originalName ?? "不可用"}</Button>;
|
||
const receiptLink = (id: string) => <Button type="button" variant="outline" onClick={() => open("downloadReceipt", id)}>付款回单 {attachments.findIndex((a) => a.id === id) + 1}{attachments.find((a) => a.id === id)?.archivedAt ? "(已作废,仅核对历史事实)" : ""}</Button>;
|
||
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 <section className="space-y-3 rounded-md border p-3" aria-label="银行结果对账">
|
||
<h3 className="font-semibold">银行结果对账</h3>
|
||
{r.freeze?.frozen ? <p role="status" className="text-sm text-amber-800">该出借人暂停新还款、复核和导出。仍可登记银行事实、对账和追回;所有冻结原因处理完成后才恢复付款。</p> : null}
|
||
{r.freeze?.reasons.filter((x) => x.status === "active").map((x) => <p key={x.id} className="text-sm">待处理:{x.kind === "late_success" ? "晚到成功待确认或补记" : x.kind === "receivable" ? "超额付款待追回" : "其他申请占用冲突"}</p>)}
|
||
{r.bankSubmissions?.legacy ? <p className="text-sm text-amber-800">历史导出明细待核实,不能凭汇总释放额度。</p> : null}
|
||
<p className="text-sm">等待期只是内部谨慎期,不能证明银行未付款。线下核对不能直接解除系统占用。</p>
|
||
{exports.map((e) => <p key={e.id} className="break-words text-sm">导出 {e.sequence} · {formatDisplayDate(e.exportedAt)} · 客户方流水号 {e.customerSerials.join("、")}</p>)}
|
||
{submissions.map((s) => <div key={s.id} className="flex flex-wrap items-center gap-2 text-sm"><span>指令 {s.bankInstructionRef} · {exportLabel(s.exportId)} · {formatDisplayDate(s.submittedAt)} · {s.note} · {s.status === "active" ? "已登记" : "已撤回登记(不代表银行撤销)"}</span>{finance && s.status === "active" && r.status !== "cancelled" ? button("withdrawSubmission", s.id) : null}</div>)}
|
||
{list ? <div className="space-y-2 text-sm"><p>提交清单:{stateLabel(list.status)}{list.stale ? ",已变化,须重新确认复核" : ""}</p>{reviewer && list.status === "proposed" && !list.stale && list.proposedBy !== me?.id ? button("reviewList", list.id) : null}</div> : null}
|
||
<div className="flex flex-wrap gap-2">{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}</div>
|
||
{evidences.map((e, evidenceIndex) => <div key={e.id} className="flex flex-wrap items-center gap-2 text-sm"><span className="break-all">证据 {evidenceIndex + 1} · {categories[e.evidenceKind] ?? "银行证据"}:{e.originalName}{e.voided ? "(已作废)" : ""}</span>{button("download", e.id)}{finance && !e.voided && r.status !== "cancelled" ? button("void", e.id) : null}</div>)}
|
||
{r.terminal?.facts.map((f, i) => <div key={f.id} className="flex flex-wrap items-center gap-2 text-sm"><span>终态事实 {i + 1}:{stateLabel(f.status)} · 指令 {submissionLabel(f.submissionId)} · {f.kind === "failure_receipt" ? "失败回单" : ({ failed: "已失败", revoked: "已撤销", rejected: "已拒绝" }[f.terminalStatus] ?? f.terminalStatus)} · {f.note}</span>{f.evidenceId ? evidenceLink(f.evidenceId) : null}{f.receiptAttachmentId ? receiptLink(f.receiptAttachmentId) : null}{f.resolvesAttachmentIds.length ? <span>了结处理中回单:</span> : null}{f.resolvesAttachmentIds.map((id) => <span key={id}>{receiptLink(id)}</span>)}{reviewer && f.status === "proposed" && f.proposedBy !== me?.id ? button("reviewFact", f.id) : null}</div>)}
|
||
{r.failureAttributions?.map((a) => <div key={a.id} className="space-y-2 text-sm"><p>失败归属:{stateLabel(a.status)} · {a.basisNote}</p><p>{a.exclusionNote}</p><p>归属范围:{a.failureScope === "submission" ? "指定提交" : "本批次提交清单"};覆盖指令:{a.coveredSubmissionIds.map(submissionLabel).join("、")}</p>{a.receiptAttachmentIds.map((id) => <span key={id}>{receiptLink(id)}</span>)}{reviewer && a.status === "proposed" && a.proposedBy !== me?.id ? button("reviewFailure", a.id) : null}</div>)}
|
||
{related ? <p className="text-sm text-amber-800">本模式不支持“人工确认未提交”。无银行终态依据时继续占用。</p> : null}
|
||
{q6 ? <div className="space-y-2 text-sm"><p>人工确认未提交:{stateLabel(q6.status)} · {q6.payload.note} · 覆盖 {formatDisplayDate(q6.payload.coverageFrom)} 至 {formatDisplayDate(q6.payload.coverageThrough)} · 已有 {q6.reviews.filter((x) => x.decision === "approve").length} 人同意{q6.stale ? ";材料已变化,须重新登记及双人复核" : ""}</p><p>覆盖导出:{q6.payload.exportIds.map(exportLabel).join("、")}</p><div className="flex flex-wrap gap-2"><span>查询:</span>{evidenceLink(q6.payload.queryEvidenceId)}<span>对账单:</span>{evidenceLink(q6.payload.statementEvidenceId)}{q6.payload.exporterStatements.map((x, i) => <span key={x.exporterId}>导出人 {i + 1}({exports.filter((e) => e.exportedBy === x.exporterId && q6.payload.exportIds.includes(e.id)).map((e) => exportLabel(e.id)).join("、")}):{evidenceLink(x.evidenceId)}</span>)}</div>{!related && reviewer && q6.status === "proposed" && !q6.stale && q6.proposedBy !== me?.id && !q6.reviews.some((x) => x.reviewedBy === me?.id) ? button("reviewQ6", q6.id) : null}</div> : null}
|
||
{!related && finance && unlinked.length > 0 && r.status !== "cancelled" && (!q6 || q6.stale || q6.status === "rejected") ? button("q6") : null}
|
||
{current ? <div className="space-y-2 text-sm"><p>补证恢复第 {current.sequence} 轮:{stateLabel(current.status)}{current.stale ? ";依据已变化,须重新发起" : ""}</p><div className="flex flex-wrap gap-2">{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}</div></div> : 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 ? <p className="text-sm text-amber-800">原制单文件不得再导入网银。系统不能使已下载文件失效;出现晚到成功应立即登记对账。</p> : null}
|
||
{finance && terminal ? button("event") : null}
|
||
{r.bankResults?.events.map((e, i) => <div key={e.id} className="space-y-2 rounded border p-3 text-sm"><p>{e.kind === "return" ? "追回到账" : "晚到成功"} {i + 1} · ¥{e.amount} · {stateLabel(e.status)}</p>{e.outstandingReturn ? <p>待追回 ¥{e.outstandingReturn}</p> : null}{e.settlements.map((s) => <p key={s.loanId}>银行已付 ¥{s.bankAmount};结清本金 ¥{s.settledAmount};超额 ¥{s.receivableAmount}</p>)}<div className="flex flex-wrap gap-2">{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}</div></div>)}
|
||
<Dialog open={action !== null} onOpenChange={(next) => { if (!next && !mutation.isPending) setAction(null); }}><DialogContent className="max-h-[90dvh] overflow-y-auto"><DialogHeader><DialogTitle>{action ? actionLabel(action) : "银行对账"}</DialogTitle><DialogDescription>{related ? "核对原始银行材料和具体指令。收款人以公司代表身份确认,不能兼任经办;材料不足时保持占用或冻结。人工确认未提交在本模式不可用。" : "核对原始银行材料和具体指令。独立复核人不能兼任相关经办或收款人;材料不足时保持占用或冻结。人工未提交须三类材料齐全并由两名独立人员复核。"}</DialogDescription></DialogHeader>
|
||
<form className="grid gap-4" onSubmit={(e) => { e.preventDefault(); mutation.mutate(); }}>
|
||
{details ? <div className="space-y-2 text-sm" aria-label="已授权查看的银行事实">{details.fields.map((f) => <p key={f.Name} className="break-all">{f.Name}:{f.Value}</p>)}<p>{details.attributionNote}</p><p>{details.exclusionNote}</p></div> : null}
|
||
{mutation.error ? <p role="alert" className="text-sm text-red-700">{resolveErrorMessage(mutation.error)}</p> : null}
|
||
{action === "upload" ? <>{select("category", "证据类别", Object.entries(categories).filter(([key]) => !related || !["query_no_result", "account_statement", "exporter_statement"].includes(key)))}<label className="grid gap-2 text-sm">证据文件<Input required type="file" accept="application/pdf,image/png,image/jpeg" onChange={(e) => setFile(e.target.files?.[0] ?? null)} /></label></> : 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"))}<fieldset className="grid gap-2"><legend className="text-sm">了结的同指令处理中回单(可多选)</legend>{receiptOptions.map(([id, label]) => <label key={id} className="flex min-h-11 items-center gap-2 text-sm"><input type="checkbox" checked={pendingIds.includes(id)} onChange={(e) => setPendingIds((old) => e.target.checked ? [...old, id] : old.filter((x) => x !== id))} />{label}</label>)}</fieldset></> : 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 ?? '') ? <label className="grid gap-2 text-sm">依据与说明<textarea required minLength={10} maxLength={500} className="min-h-24 rounded border px-3 py-2" value={value("note")} onChange={(e) => set("note", e.target.value)} /></label> : null}
|
||
<Button type="submit" disabled={mutation.isPending}>{mutation.isPending ? "处理中…" : "确认"}</Button>
|
||
</form>
|
||
</DialogContent></Dialog>
|
||
</section>;
|
||
}
|