HEL-123: 第4批——公司端剩余6页 + 对应弹窗换原型壳

- upload 流水导入: 多步流程(选文件→解析→分sheet审核→确认)完整保留, 内联卡片化 + 批次详情弹窗 #modal-batch
- manual 手工记录: 两栏布局 + 撤回确认弹窗 #withdraw-modal
- flows 流水管理: 公司端数据集 + 待确认/已确认状态口径 + 详情弹窗
- reconcile 往来确认: 候选匹配/匹配度/断档提示 + 未选候选禁用态 + 单边全部匹配空态
- accounts 银行账户: 表格 + 账户明细弹窗 + 新账户登记弹窗
- notifications 通知: tabs/已读/空态/角标
- 跨批依赖: 往来确认完成后回写工作台待办区(#workspacePendingStatus/待办行/账期流程条)

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
leefer
2026-08-25 11:03:17 +08:00
co-authored by multica-agent
parent 307f9ba76f
commit 2fe06b74ae
2 changed files with 1209 additions and 194 deletions
+500 -142
View File
@@ -667,38 +667,65 @@ function initPairQueries() {
$("#traceModal")?.addEventListener("click", (event) => { if (event.target === event.currentTarget) event.currentTarget.classList.remove("open"); });
}
function manualStatusMeta(status) {
if (status === "已确认") return { cls: "pill-success", label: "已通过" };
if (status === "已驳回") return { cls: "pill-danger", label: "已驳回" };
return { cls: "pill-info", label: "待审核" };
}
function renderCompanyManualRecords() {
const tbody = $("#manualRecordRows");
if (!tbody) return;
$$('[data-stored-record]', tbody).forEach((row) => row.remove());
tbody.replaceChildren();
const records = readStoredRecords(storageKeys.manual).filter((record) => record.company === "A公司");
[...records].reverse().forEach((record) => {
const row = document.createElement("tr");
row.dataset.storedRecord = record.id;
const identity = document.createElement("td");
const id = document.createElement("strong"); id.textContent = record.id;
const date = document.createElement("small"); date.textContent = record.transactionDate;
identity.append(id, date);
const direction = document.createElement("td");
const directionName = document.createElement("strong"); directionName.textContent = record.direction;
const subject = document.createElement("small"); subject.textContent = record.subject;
direction.append(directionName, subject);
const source = document.createElement("td"); source.textContent = record.sourceAccount;
const date = document.createElement("td"); date.className = "num"; date.textContent = record.transactionDate || "—";
const direction = document.createElement("td"); direction.textContent = record.direction || "—";
const counterparty = document.createElement("td");
const counterpartyName = document.createElement("strong"); counterpartyName.textContent = record.counterparty;
const counterpartyType = document.createElement("small"); counterpartyType.textContent = record.counterpartyType;
const counterpartyName = document.createElement("span"); counterpartyName.className = "cell-main"; counterpartyName.textContent = record.counterparty;
const counterpartyType = document.createElement("span"); counterpartyType.className = "cell-sub"; counterpartyType.textContent = record.counterpartyType || "";
counterparty.append(counterpartyName, counterpartyType);
const summary = document.createElement("td"); summary.textContent = record.summary;
const amount = document.createElement("td"); amount.className = "number"; amount.textContent = formatCurrency(record.amount);
const subject = document.createElement("td"); subject.innerHTML = `<span class="tag">${record.subject || "—"}</span>`;
const isIn = record.direction === "收款";
const amount = document.createElement("td");
amount.className = `num-col ${isIn ? "amt-in" : "amt-out"}`;
amount.textContent = `${isIn ? "+" : "-"}¥ ${formatCurrency(record.amount)}`;
const summary = document.createElement("td"); summary.className = "wrap"; summary.textContent = record.summary || "—";
const statusMeta = manualStatusMeta(record.status);
const statusCell = document.createElement("td");
const status = recordStatus(record.status);
const badge = document.createElement("span"); badge.className = `status ${status.className}`; badge.textContent = status.label;
statusCell.append(badge);
row.append(identity, direction, source, counterparty, summary, amount, statusCell);
statusCell.innerHTML = `<span class="pill ${statusMeta.cls}">${statusMeta.label}</span>`;
const action = document.createElement("td");
if (record.status === "待总账复核") {
action.innerHTML = '<button type="button" class="btn btn-sm btn-danger" data-action="withdraw">撤回</button>';
} else {
action.innerHTML = '<span class="muted">—</span>';
}
row.append(date, direction, counterparty, subject, amount, summary, statusCell, action);
tbody.append(row);
});
const pending = records.filter((record) => record.status === "待总账复核").length + 1;
if ($("#manualPendingStatus")) $("#manualPendingStatus").textContent = `${pending} 笔待总账复核`;
updateManualCounts();
}
function updateManualCounts() {
const records = readStoredRecords(storageKeys.manual).filter((record) => record.company === "A公司");
const pending = records.filter((record) => record.status === "待总账复核").length;
const pendingEl = $("#manualPendingStatus");
if (pendingEl) pendingEl.textContent = pending;
const foot = $("#manualFoot");
if (foot) foot.textContent = `${records.length} 条 · 待复核 ${pending}`;
const empty = $("#manualEmpty");
if (empty) empty.hidden = records.length > 0;
}
function fillAccountSelects(accounts) {
@@ -716,38 +743,49 @@ function fillAccountSelects(accounts) {
});
}
function companyAccountMeta(status) {
if (status === "active") return { status: { cls: "pill-success", label: "启用" }, audit: { cls: "pill-success", label: "已审核" } };
if (status === "returned") return { status: { cls: "pill-danger", label: "已退回" }, audit: { cls: "pill-danger", label: "已退回" } };
if (status === "disabled") return { status: { cls: "pill-muted", label: "停用" }, audit: { cls: "pill-success", label: "已审核" } };
return { status: { cls: "pill-info", label: "待启用" }, audit: { cls: "pill-info", label: "待审核" } };
}
function renderCompanyAccounts(accounts) {
const directory = $("#accountDirectory");
if (directory) {
directory.replaceChildren(...accounts.map((account) => {
const article = document.createElement("article");
const header = document.createElement("header");
const mark = document.createElement("span"); mark.className = "bank-mark"; mark.textContent = account.bank_name.slice(0, 1);
const identity = document.createElement("div");
const name = document.createElement("strong"); name.textContent = account.bank_name;
const meta = document.createElement("small"); meta.textContent = `${account.account_type} · 尾号 ${accountTail(account.account_number_masked)}`;
identity.append(name, meta);
const status = recordStatus(accountStatusLabel(account.status));
const badge = document.createElement("em"); badge.className = `status ${status.className}`; badge.textContent = status.label;
header.append(mark, identity, badge);
const details = document.createElement("dl");
const rows = [
["申请启用", account.effective_from || "待审核确定"],
["流水覆盖", account.usable ? "尚未上传" : "不参与计算"],
["提交时间", String(account.created_at || "").slice(0, 10) || "—"],
];
if (account.status === "returned" && account.review_reason) rows.push(["退回原因", account.review_reason]);
if (account.status === "disabled" && account.effective_to) rows.push(["停用日期", account.effective_to]);
rows.forEach(([term, value]) => {
const wrapper = document.createElement("div");
const dt = document.createElement("dt"); dt.textContent = term;
const dd = document.createElement("dd"); dd.textContent = value;
wrapper.append(dt, dd); details.append(wrapper);
});
article.append(header, details);
return article;
state.accounts = accounts;
const tbody = $("#account-tbody");
if (tbody) {
tbody.replaceChildren(...accounts.map((account) => {
const row = document.createElement("tr");
row.dataset.accountId = account.id;
const identity = document.createElement("td");
const name = document.createElement("span"); name.className = "cell-main"; name.textContent = account.bank_name;
const tail = document.createElement("span"); tail.className = "cell-sub"; tail.textContent = `尾号 ${accountTail(account.account_number_masked)}`;
identity.append(name, tail);
const type = document.createElement("td"); type.innerHTML = `<span class="tag">${account.account_type || "—"}</span>`;
const meta = companyAccountMeta(account.status);
const statusCell = document.createElement("td"); statusCell.innerHTML = `<span class="pill ${meta.status.cls}">${meta.status.label}</span>`;
const coverage = document.createElement("td");
coverage.className = "num muted";
coverage.textContent = account.usable ? "尚未上传" : "—";
const auditCell = document.createElement("td"); auditCell.innerHTML = `<span class="pill ${meta.audit.cls}">${meta.audit.label}</span>`;
const action = document.createElement("td"); action.innerHTML = '<button type="button" class="btn btn-sm" data-account-view>查看明细</button>';
row.append(identity, type, statusCell, coverage, auditCell, action);
return row;
}));
}
const count = $("#account-count");
if (count) count.textContent = accounts.length;
const foot = $("#account-foot");
if (foot) foot.textContent = `${accounts.length} 个账户`;
const empty = $("#account-empty");
if (empty) empty.hidden = accounts.length > 0;
fillAccountSelects(accounts);
}
@@ -1406,13 +1444,34 @@ const FLOW_DEMO = [
{ date: "2026-07-25", company: "金牛贸易", bank: "农行", account: "2208", acctLabel: "农行 · 尾号 2208", dir: "付", dirPill: "pill-danger", peer: "安阳钢铁集团有限责任公司", summary: "螺纹钢采购款(7 月)", serial: "ABC202607250819673", status: "未归集", statusPill: "pill-muted", amount: "5,620,000.00", amtClass: "amt-out", detail: { bank: "农业银行", account: "河南金牛贸易有限公司 · 基本户 1606 3301 0400 0220 8", time: "2026-07-25 10:19:05", peer: "安阳钢铁集团有限责任公司", peerAcct: "工行安阳分行 1706 0211 0900 4428 17", amount: "¥ 5,620,000.00(付)", status: "未归集", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外螺纹钢采购付款,不参与集团内部往来归集。" } },
];
const COMPANY_FLOWS = [
{ date: "2026-07-02", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "收", dirPill: "pill-success", peer: "河南金牛置业有限公司", summary: "煤炭采购款(2026 年 6 月供煤合同结算)", serial: "ICBC2026070200185347", status: "已归集", statusPill: "pill-success", amount: "3,200,000.00", amtClass: "amt-in", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-02 15:47:22", peer: "河南金牛置业有限公司", peerAcct: "中行郑州郑东新区支行 2546 0387 0200 8821", amount: "¥ 3,200,000.00(收)", status: "已归集", pair: "金牛煤业 ↔ 金牛置业", subject: "应收(煤业侧)", batch: "JH-202607-014", note: "与置业侧中行尾号 8821 账户付款流水已双向匹配。" } },
{ date: "2026-07-03", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "山西晋城王坡煤矿有限责任公司", summary: "原料煤采购预付款", serial: "ICBC2026070300221091", status: "未归集 · 外部", statusPill: "pill-muted", amount: "860,000.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-03 10:26:31", peer: "山西晋城王坡煤矿有限责任公司", peerAcct: "工行晋城分行 1702 0218 0902 6641 20", amount: "¥ 860,000.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对方为集团外供应商,不参与内部往来归集,仅作银行流水留档。" } },
{ date: "2026-07-06", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "收", dirPill: "pill-success", peer: "河南神火运销有限公司", summary: "动力煤销售货款(7 月第一批)", serial: "BOC2026070600772018", status: "未归集 · 外部", statusPill: "pill-muted", amount: "1,246,800.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-06 09:58:44", peer: "河南神火运销有限公司", peerAcct: "工行永城分行 1702 0218 0902 9075 63", amount: "¥ 1,246,800.00(收)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外动力煤销售回款,不参与集团内部往来归集。" } },
{ date: "2026-07-08", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "河南金牛物流有限公司", summary: "6 月煤炭公路运输费结算", serial: "ICBC2026070800311276", status: "已归集", statusPill: "pill-success", amount: "486,500.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-08 11:15:09", peer: "河南金牛物流有限公司", peerAcct: "建行郑州经开区支行 4105 0167 8080 5562", amount: "¥ 486,500.00(付)", status: "已归集", pair: "金牛煤业 ↔ 金牛物流", subject: "应付(煤业侧)", batch: "JH-202607-014", note: "与物流侧建行收款流水已双向匹配,运单 42 张随附。" } },
{ date: "2026-07-10", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "付", dirPill: "pill-danger", peer: "河南金牛贸易有限公司", summary: "选煤设备配件代购款", serial: "BOC2026071000819455", status: "待确认", statusPill: "pill-warn", amount: "214,700.00", amtClass: "amt-out", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-10 14:08:52", peer: "河南金牛贸易有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 214,700.00(付)", status: "待确认", pair: "金牛煤业 ↔ 金牛贸易", subject: "其他应付(待复核)", batch: "JH-202607-021", note: "贸易侧已确认收款,科目待双方复核(应付 / 其他应付)。" } },
{ date: "2026-07-13", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "国网河南省电力公司新密市供电公司", summary: "7 月工业电费", serial: "ICBC2026071300458820", status: "未归集 · 外部", statusPill: "pill-muted", amount: "1,528,300.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-13 10:19:05", peer: "国网河南省电力公司新密市供电公司", peerAcct: "工行新密支行 1706 0211 0900 4428 17", amount: "¥ 1,528,300.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外电费支出,不参与集团内部往来归集。" } },
{ date: "2026-07-15", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "收", dirPill: "pill-success", peer: "河南金牛新能源有限公司", summary: "场区租赁费返还(二季度)", serial: "BOC2026071500923314", status: "单边", statusPill: "pill-danger", amount: "95,000.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-15 11:06:33", peer: "河南金牛新能源有限公司", peerAcct: "工行郑州分行 1704 0512 0900 2266 49", amount: "¥ 95,000.00(收)", status: "单边", pair: "金牛煤业 ↔ 金牛新能源", subject: "其他应收(煤业侧)", batch: "—(待归集)", note: "新能源侧尚未提报对应付款流水,形成单边。" } },
{ date: "2026-07-18", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "国家税务总局新密市税务局", summary: "增值税及附加税费(6 月属期)", serial: "ICBC2026071800506639", status: "未归集 · 外部", statusPill: "pill-muted", amount: "2,073,450.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-18 13:29:40", peer: "国家税务总局新密市税务局", peerAcct: "国库专户", amount: "¥ 2,073,450.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "税费缴库,不参与集团内部往来归集。" } },
{ date: "2026-07-21", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "收", dirPill: "pill-success", peer: "永城煤电控股集团有限公司", summary: "块煤销售款(年度长协第 7 批)", serial: "BOC2026072101054472", status: "未归集 · 外部", statusPill: "pill-muted", amount: "3,864,000.00", amtClass: "amt-in", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-21 09:35:12", peer: "永城煤电控股集团有限公司", peerAcct: "工行永城分行 1702 0218 0902 3477 12", amount: "¥ 3,864,000.00(收)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "对外块煤销售回款,不参与集团内部往来归集。" } },
{ date: "2026-07-24", company: "金牛煤业", bank: "交通银行", account: "7710", acctLabel: "交行 · 尾号 7710", dir: "收", dirPill: "pill-success", peer: "河南金牛置业有限公司", summary: "临时往来款归还", serial: "BCM2026072400088116", status: "待确认", statusPill: "pill-warn", amount: "1,500,000.00", amtClass: "amt-in", detail: { bank: "交通银行", account: "河南金牛煤业有限公司 · 一般户 4110 6120 0181 0077 10(账户待审核)", time: "2026-07-24 14:52:26", peer: "河南金牛置业有限公司", peerAcct: "中行郑州郑东新区支行 2546 0387 0200 8821", amount: "¥ 1,500,000.00(收)", status: "待确认", pair: "金牛煤业 ↔ 金牛置业", subject: "其他应收(煤业侧)", batch: "JH-202607-021", note: "交行 7710 账户尚处待审核,归集结果以账户审核通过后为准。" } },
{ date: "2026-07-27", company: "金牛煤业", bank: "工商银行", account: "3305", acctLabel: "工行 · 尾号 3305", dir: "付", dirPill: "pill-danger", peer: "煤业职工工资代发(2026 年 7 月)", summary: "7 月职工工资及奖金代发,共 612 人", serial: "ICBC2026072700582241", status: "未归集 · 外部", statusPill: "pill-muted", amount: "2,416,780.00", amtClass: "amt-out", detail: { bank: "工商银行", account: "河南金牛煤业有限公司 · 基本户 1702 0231 0900 8133 05", time: "2026-07-27 10:11:57", peer: "煤业职工工资代发(2026 年 7 月)", peerAcct: "代发专户", amount: "¥ 2,416,780.00(付)", status: "未归集 · 外部", pair: "—(外部单位交易)", subject: "—", batch: "—", note: "工资代发,不参与集团内部往来归集。" } },
{ date: "2026-07-30", company: "金牛煤业", bank: "中国银行", account: "9916", acctLabel: "中行 · 尾号 9916", dir: "付", dirPill: "pill-danger", peer: "河南金牛农业科技发展有限公司", summary: "临时周转借款(约定 8 月归还)", serial: "BOC2026073001187903", status: "单边", statusPill: "pill-danger", amount: "800,000.00", amtClass: "amt-out", detail: { bank: "中国银行", account: "河南金牛煤业有限公司 · 一般户 2546 0387 1100 9916", time: "2026-07-30 16:32:08", peer: "河南金牛农业科技发展有限公司", peerAcct: "农行郑州金水支行 1606 3301 0400 0220 8", amount: "¥ 800,000.00(付)", status: "单边", pair: "金牛煤业 ↔ 金牛农业", subject: "其他应收(煤业侧)", batch: "—(待归集)", note: "农业 7 月未提交流水,暂无对方侧证据。" } },
];
function currentFlowData() {
return portal === "admin" ? FLOW_DEMO : COMPANY_FLOWS;
}
function renderFlowRows() {
const tbody = $("#flowTable tbody");
if (!tbody) return;
tbody.innerHTML = FLOW_DEMO.map((row, index) => {
return `<tr class="clickable" data-flow-idx="${index}" data-company="${row.company}" data-bank="${row.bank}" data-account="${row.account}" data-date="${row.date}">
const data = currentFlowData();
tbody.innerHTML = data.map((row, index) => {
const companyCell = portal === "admin" ? `<td class="cell-main">${row.company}</td>` : "";
return `<tr class="clickable" data-flow-idx="${index}" data-company="${row.company}" data-bank="${row.bank}" data-account="${row.account}" data-date="${row.date}" data-dir="${row.dir}">
<td class="num">${row.date}</td>
<td class="cell-main">${row.company}</td>
${companyCell}
<td>${row.acctLabel}</td>
<td><span class="pill ${row.dirPill}">${row.dir}</span></td>
<td class="wrap">${row.peer}</td>
@@ -1423,13 +1482,21 @@ function renderFlowRows() {
</tr>`;
}).join("");
const count = $("#flowCount");
if (count) count.textContent = `${FLOW_DEMO.length} · 本页 1-${FLOW_DEMO.length}`;
if (count) count.textContent = `${data.length}`;
const empty = $("#flowEmpty");
if (empty) empty.hidden = data.length > 0;
const sum = $("#flowSum");
if (sum) {
let inflow = 0, outflow = 0;
data.forEach((row) => { if (row.dir === "收") inflow += Number(row.amount.replace(/,/g, "")); else outflow += Number(row.amount.replace(/,/g, "")); });
sum.innerHTML = data.length ? `收 <span class="amt-in">+¥ ${formatCurrency(inflow)}</span> · 付 <span class="amt-out">-¥ ${formatCurrency(outflow)}</span>` : "";
}
}
function openFlowDetail(index) {
const modal = $("#tx-modal");
if (!modal) return;
const row = FLOW_DEMO[index];
const row = currentFlowData()[index];
if (!row) return;
$("#tx-modal-sub").textContent = `${row.company} · ${row.acctLabel} · ${row.date}`;
const d = row.detail;
@@ -1477,6 +1544,18 @@ function filterFlows() {
if (!row.hidden) count += 1;
});
$("#flowCount").textContent = count === 0 ? "共 0 条 · 无匹配记录" : `${count} 笔 · 本页 1-${count}`;
const empty = $("#flowEmpty");
if (empty) empty.hidden = count > 0;
const sum = $("#flowSum");
if (sum) {
let inflow = 0, outflow = 0;
$$("tbody tr", table).forEach((row) => {
if (row.hidden) return;
const amount = Number(($("td:last-child", row)?.textContent || "").replace(/[^\d.]/g, ""));
if (row.dataset.dir === "收") inflow += amount; else outflow += amount;
});
sum.innerHTML = count ? `收 <span class="amt-in">+¥ ${formatCurrency(inflow)}</span> · 付 <span class="amt-out">-¥ ${formatCurrency(outflow)}</span>` : "";
}
showToast("查询完成", `当前显示 ${count} 笔流水`);
}
@@ -1488,13 +1567,14 @@ function exportFlows() {
const cells = $$('td', row).map((cell) => cell.innerText.replace(/\n/g, " ").trim());
const imp = `IMP-DEMO-${String(index + 1).padStart(3, "0")}`;
const loc = `Sheet1!R${index + 8}`;
const companyName = state.me?.company_name || "本公司";
if (portal === "admin") return [...cells.slice(0, 9), imp, loc];
return [cells[0], "A公司", cells[1], ...cells.slice(2), imp, loc];
return [cells[0], companyName, cells[1], ...cells.slice(2), imp, loc];
});
const csv = [headers, ...records].map((record) => record.map((value) => `"${String(value ?? "").replace(/"/g, '""')}"`).join(",")).join("\r\n");
const link = document.createElement("a");
link.href = URL.createObjectURL(new Blob(["\ufeff", csv], { type: "text/csv;charset=utf-8" }));
link.download = `${portal === "admin" ? "集团" : "A公司"}银行流水_202607.csv`;
link.download = `${portal === "admin" ? "集团" : (state.me?.company_name || "公司")}银行流水_202607.csv`;
link.click();
URL.revokeObjectURL(link.href);
showToast("导出已生成", `${rows.length} 笔,已保留银行标识与源行定位`);
@@ -1503,14 +1583,14 @@ function exportFlows() {
function initFlowTools() {
$("#applyFlowFilters")?.addEventListener("click", filterFlows);
$("#exportFlows")?.addEventListener("click", exportFlows);
if (portal !== "admin") return;
renderFlowRows();
$("#resetFlowFilters")?.addEventListener("click", () => {
$("#flowCompany").value = "全部公司";
const company = $("#flowCompany");
if (company) company.value = "全部公司";
$("#flowBank").value = "全部银行";
$("#flowAccount").value = "全部账户";
$("#flowStart").value = "2026-07-01";
$("#flowEnd").value = "2026-07-31";
$("#flowEnd").value = portal === "admin" ? "2026-07-31" : "2026-08-20";
$("#flowKeyword").value = "";
filterFlows();
});
@@ -1592,8 +1672,10 @@ function renderParseResult(result, parsed) {
const sheets = Array.isArray(result.sheets) ? result.sheets : [];
const duplicated = result.status === "duplicate";
const opaque = duplicated && !sheets.length;
panel.classList.toggle("is-exception", !parsed);
$("use", panel).setAttribute("href", parsed ? "icons.svg#circle-check" : "icons.svg#circle-alert");
panel.classList.toggle("info", parsed);
panel.classList.toggle("warn", !parsed);
const parseIcon = $("use", panel);
if (parseIcon) parseIcon.setAttribute("href", parsed ? "icons.svg#circle-check" : "icons.svg#circle-alert");
$("#parseTitle").textContent = duplicated ? "文件已导入过" : parsed ? "文件解析完成" : "未识别到银行模板";
const pendingCount = sheets.filter((s) => s.outcome === "parsed" && s.review_status === "pending").length;
const exceptionCount = sheets.filter((s) => s.outcome === "exception").length;
@@ -1643,20 +1725,17 @@ function renderSheetList(sheets, batchId) {
}
function buildSheetItem(sheet, batchId) {
const item = document.createElement("article");
item.className = "sheet-item";
if (sheet.review_status === "pending") item.classList.add("is-pending");
const item = document.createElement("div");
item.className = "list-row";
const meta = sheetStatusMeta(sheet);
const head = document.createElement("div");
head.className = "sheet-item-head";
const name = document.createElement("strong");
name.textContent = sheet.sheet_name;
const badge = document.createElement("em");
badge.className = `status ${meta.className}`;
badge.textContent = meta.label;
head.append(name, badge);
const details = document.createElement("p");
details.className = "sheet-item-meta";
const main = document.createElement("div");
main.className = "lr-main";
const title = document.createElement("div");
title.className = "lr-title";
title.textContent = sheet.sheet_name;
main.append(title);
const details = document.createElement("div");
details.className = "lr-sub";
if (sheet.outcome === "parsed" && sheet.bank) {
const period = sheet.period_start ? ` · ${sheet.period_start}${sheet.period_end}` : "";
details.textContent = `${sheet.bank} · ${sheet.transactions} 条明细${period}`;
@@ -1668,28 +1747,34 @@ function buildSheetItem(sheet, batchId) {
if (sheet.review_reason) {
details.textContent += ` · 原因:${sheet.review_reason}`;
}
const body = document.createElement("div");
body.append(head, details);
main.append(details);
const badge = document.createElement("span");
badge.className = `pill ${pillClass(meta.className)}`;
badge.textContent = meta.label;
item.append(main, badge);
const actions = document.createElement("div");
actions.className = "sheet-item-actions";
if (sheet.review_status === "pending") {
const actions = document.createElement("div");
actions.className = "lr-side";
actions.style.cssText = "display:flex;gap:6px;flex:none;";
if (sheet.outcome === "parsed") {
const confirmButton = document.createElement("button");
confirmButton.type = "button";
confirmButton.className = "text-button";
confirmButton.className = "btn btn-sm btn-primary";
confirmButton.textContent = "确认";
confirmButton.addEventListener("click", () => sheetReviewAction(batchId, sheet.sheet_name, "confirm"));
actions.append(confirmButton);
}
const ignoreButton = document.createElement("button");
ignoreButton.type = "button";
ignoreButton.className = "text-button";
ignoreButton.className = "btn btn-sm";
ignoreButton.textContent = "忽略";
ignoreButton.addEventListener("click", () => sheetReviewAction(batchId, sheet.sheet_name, "ignore"));
actions.append(ignoreButton);
item.append(actions);
}
item.append(body, actions);
return item;
}
@@ -1743,7 +1828,7 @@ async function confirmImport() {
.filter((s) => s.outcome === "parsed" && s.review_status === "pending")
.map((s) => s.sheet_name);
if (!pending.length) {
$("#uploadDialog").close();
resetUpload();
await loadImportBatches();
showView("upload");
return;
@@ -1770,37 +1855,37 @@ async function confirmImport() {
button.disabled = false;
if (Array.isArray(outcome.sheets)) renderSheetList(outcome.sheets, batchId);
await loadImportBatches();
$("#uploadDialog").close();
resetUpload();
showView("upload");
showToast("流水已确认", `${outcome.updated.length} 个工作表已确认;未确认的工作表不参与计算`);
}
function submitImportException() {
$("#uploadDialog").close();
resetUpload();
showView("upload");
showToast("解析异常未入账", `${state.selectedFile?.name || "该文件"} 不会进入匹配与计算,请核对模板后重新导出`);
}
function renderBatchRow(batch) {
const row = document.createElement("tr");
row.dataset.batchId = batch.id;
const idCell = document.createElement("td");
const id = document.createElement("strong");
id.textContent = `IMP-${String(batch.id).padStart(6, "0")}`;
const file = document.createElement("small");
file.textContent = batch.original_filename || "";
const id = document.createElement("span"); id.className = "cell-main num"; id.textContent = `IMP-${String(batch.id).padStart(6, "0")}`;
const file = document.createElement("span"); file.className = "cell-sub"; file.textContent = batch.original_filename || "";
idCell.append(id, file);
const bank = document.createElement("td");
bank.textContent = batch.bank_name || "—";
const period = document.createElement("td");
period.className = "num";
period.textContent = batch.period_start && batch.period_end
? `${batch.period_start}${batch.period_end}`
? `${batch.period_start} ~ ${batch.period_end}`
: "—";
const count = document.createElement("td");
count.className = "number";
count.textContent = `${batch.confirmed_transactions ?? 0}`;
count.className = "num-col";
count.textContent = `${batch.confirmed_transactions ?? 0}`;
const coverage = document.createElement("td");
const coverageStatus = batch.status === "exception"
@@ -1810,18 +1895,26 @@ function renderBatchRow(batch) {
: batch.confirmed_sheets > 0
? { className: "success", label: "已确认" }
: { className: "neutral", label: "待处理" };
coverage.innerHTML = `<span class="status ${coverageStatus.className}">${coverageStatus.label}</span>`;
coverage.innerHTML = `<span class="pill ${pillClass(coverageStatus.className)}">${coverageStatus.label}</span>`;
const parseState = document.createElement("td");
const statusParts = [];
if (batch.exception_sheets > 0) statusParts.push(`${batch.exception_sheets} 个异常`);
if (batch.ignored_sheets > 0) statusParts.push(`${batch.ignored_sheets} 个忽略`);
parseState.textContent = statusParts.length ? statusParts.join("、") : "解析成功";
if (statusParts.length) {
parseState.innerHTML = `<span class="pill pill-warn">${statusParts.join("、")}</span>`;
} else {
parseState.innerHTML = '<span class="pill pill-success">解析成功</span>';
}
const time = document.createElement("td");
time.className = "meta";
time.textContent = String(batch.created_at || "").slice(0, 16).replace("T", " ");
row.append(idCell, bank, period, count, coverage, parseState, time);
const action = document.createElement("td");
action.innerHTML = '<button type="button" class="btn btn-sm" data-batch-view>查看</button>';
row.append(idCell, bank, period, count, coverage, parseState, time, action);
return row;
}
@@ -1835,17 +1928,257 @@ async function loadImportBatches() {
}
const result = await response?.json().catch(() => ({}));
const batches = Array.isArray(result?.batches) ? result.batches : [];
if (batches.length) tbody.replaceChildren(...batches.map(renderBatchRow));
state.batches = batches;
tbody.replaceChildren(...batches.map(renderBatchRow));
const foot = $("#importFoot");
if (foot) foot.textContent = batches.length ? `${batches.length} 个批次` : "暂无批次";
}
function refreshWorkspacePending() {
const card = $("#workspaceTodos");
if (!card) return;
const count = $$(".list-row", card).length;
const status = $("#workspacePendingStatus");
if (status) status.textContent = `${count} 项待处理`;
}
function initReconcile() {
const matchCards = $$("[data-match-card]");
const subjectRows = $$("[data-subject-row]");
if (!matchCards.length && !subjectRows.length) return;
const noticeTitle = $("#notice-title");
const noticeBody = $("#notice-body");
const notice = $("#blocking-notice");
const matchedSummary = $("#matched-summary");
const matchedList = $("#matched-list");
const matchStack = $("#match-stack");
let pendingMatch = matchCards.length;
let pendingSubject = subjectRows.length;
function refreshCounts() {
const countMatch = $("#count-match");
const countSubject = $("#count-subject");
if (countMatch) countMatch.textContent = pendingMatch;
if (countSubject) countSubject.textContent = pendingSubject;
const total = pendingMatch + pendingSubject;
const badge = $('.side-nav a[data-view="reconcile"] .nav-badge');
if (badge) {
badge.textContent = total;
badge.style.display = total ? "" : "none";
}
if (noticeTitle) {
noticeTitle.textContent = total ? `${total} 项待确认,是 7 月结账的阻断项` : "全部确认完成";
}
if (noticeBody) {
noticeBody.textContent = total
? `含单边流水匹配 ${pendingMatch} 项、科目确认 ${pendingSubject} 项。请于 2026-08-29(7 月顺延结账日)前处理完毕,否则集团无法对贵公司执行 7 月结账。`
: "本公司 2026-07 账期已具备结账条件,集团将于 08-29 统一执行结账。";
}
if (notice) {
notice.classList.toggle("warn", total > 0);
notice.classList.toggle("success", total === 0);
}
if (pendingMatch === 0) $('[data-task-type="match"]', $("#workspaceTodos"))?.remove();
const matchRow = $('[data-task-type="match"]', $("#workspaceTodos"));
if (matchRow) {
const title = $(".lr-title", matchRow);
if (title) title.textContent = `处理 ${pendingMatch} 笔单边流水确认`;
}
const subjectRow = $('[data-task-type="subject"]', $("#workspaceTodos"));
if (subjectRow) {
const title = $(".lr-title", subjectRow);
if (title) title.textContent = `确认 ${pendingSubject} 笔其他应收科目`;
}
const flowState = $("#workspaceConfirmState");
if (flowState) flowState.textContent = total ? `待处理 ${total}` : "已完成";
const flowMeta = $("#workspaceConfirmMeta");
if (flowMeta) flowMeta.textContent = total ? `单边流水 ${pendingMatch} 笔 · 科目确认 ${pendingSubject}` : "已全部确认,等待集团结账";
refreshWorkspacePending();
}
matchCards.forEach((card) => {
const confirmButton = $("[data-match-confirm]", card);
const radios = $$('input[type="radio"]', card);
radios.forEach((radio) => radio.addEventListener("change", () => { if (confirmButton) confirmButton.disabled = false; }));
confirmButton?.addEventListener("click", () => {
const summaryText = confirmButton.dataset.matchSummary || "已确认匹配";
const row = document.createElement("div");
row.className = "list-row";
const main = document.createElement("div");
main.className = "lr-main";
const title = document.createElement("div");
title.className = "lr-title";
title.textContent = summaryText;
main.append(title);
const pill = document.createElement("span");
pill.className = "pill pill-success lr-side";
pill.textContent = "已匹配";
row.append(main, pill);
matchedList.append(row);
matchedSummary.style.display = "";
card.remove();
pendingMatch -= 1;
if (pendingMatch === 0) {
const empty = document.createElement("div");
empty.className = "empty";
empty.innerHTML = '<div class="e-title">单边流水已全部匹配</div><div>确认结果将同步至集团审核中心复核</div>';
matchStack.append(empty);
}
refreshCounts();
showToast("匹配已确认", "待办状态、操作人、时间和依据已同步更新");
});
});
$$(".subject-confirm-btn").forEach((button) => {
button.addEventListener("click", () => {
const row = button.closest("[data-subject-row]");
const select = $("select", row);
const subject = select.value;
select.disabled = true;
button.disabled = true;
button.textContent = "已确认";
const statusCell = $(".subject-status", row);
statusCell.innerHTML = `<span class="pill pill-success">已确认 · ${subject}</span>`;
pendingSubject -= 1;
refreshCounts();
showToast("科目已确认", "待办状态、操作人、时间和依据已同步更新");
});
});
const tabMatch = $("#tab-match");
const tabSubject = $("#tab-subject");
const panelMatch = $("#panel-match");
const panelSubject = $("#panel-subject");
function switchTab(which) {
tabMatch?.classList.toggle("active", which === "match");
tabSubject?.classList.toggle("active", which === "subject");
tabMatch?.setAttribute("aria-pressed", String(which === "match"));
tabSubject?.setAttribute("aria-pressed", String(which === "subject"));
if (panelMatch) panelMatch.style.display = which === "match" ? "" : "none";
if (panelSubject) panelSubject.style.display = which === "subject" ? "" : "none";
}
tabMatch?.addEventListener("click", () => switchTab("match"));
tabSubject?.addEventListener("click", () => switchTab("subject"));
refreshCounts();
}
function initNotifications() {
const tabs = $("#notice-tabs");
const list = $("#notice-list");
if (!tabs || !list) return;
const rows = $$(".list-row", list);
const emptyBox = $("#notice-empty");
let currentFilter = "all";
const STATUS_PILL = { unread: "pill-danger", doing: "pill-warn", done: "pill-success" };
const STATUS_LABEL = { unread: "未读", doing: "处理中", done: "已完成" };
function counts() {
const c = { all: rows.length, unread: 0, doing: 0, done: 0 };
rows.forEach((row) => { c[row.getAttribute("data-status")] += 1; });
return c;
}
function refreshCounts() {
const c = counts();
$("#count-all").textContent = c.all;
$("#count-unread").textContent = c.unread;
$("#count-doing").textContent = c.doing;
$("#count-done").textContent = c.done;
const navBadge = $('.side-nav a[data-view="notifications"] .nav-badge');
if (navBadge) {
navBadge.textContent = c.unread;
navBadge.style.display = c.unread ? "" : "none";
}
}
function applyFilter() {
let visible = 0;
rows.forEach((row) => {
const show = currentFilter === "all" || row.getAttribute("data-status") === currentFilter;
row.style.display = show ? "" : "none";
if (show) visible += 1;
});
emptyBox.style.display = visible === 0 ? "" : "none";
}
function setStatus(row, status) {
row.setAttribute("data-status", status);
const pill = $(".pill", row);
if (pill) {
pill.className = `pill ${STATUS_PILL[status]}`;
pill.textContent = STATUS_LABEL[status];
}
}
tabs.addEventListener("click", (event) => {
const btn = event.target.closest("button[data-filter]");
if (!btn) return;
$$("button", tabs).forEach((b) => { b.classList.remove("active"); b.setAttribute("aria-pressed", "false"); });
btn.classList.add("active");
btn.setAttribute("aria-pressed", "true");
currentFilter = btn.dataset.filter;
applyFilter();
});
list.addEventListener("click", (event) => {
const btn = event.target.closest(".btn-mark-read");
if (!btn) return;
const row = btn.closest(".list-row");
setStatus(row, "done");
btn.remove();
refreshCounts();
applyFilter();
});
$("#mark-all-read")?.addEventListener("click", () => {
rows.forEach((row) => {
if (row.getAttribute("data-status") !== "unread") return;
setStatus(row, "done");
$(".btn-mark-read", row)?.remove();
});
refreshCounts();
applyFilter();
showToast("通知已全部标为已读");
});
refreshCounts();
applyFilter();
}
function openAccountDetail(account) {
const meta = companyAccountMeta(account.status);
$("#ad-title").textContent = `${account.bank_name || ""} · 尾号 ${accountTail(account.account_number_masked)}`;
$("#ad-sub").textContent = "本公司 · 登记账户明细";
$("#ad-bank").textContent = account.bank_name || "—";
$("#ad-tail").textContent = `尾号 ${accountTail(account.account_number_masked)}`;
$("#ad-type").textContent = account.account_type || "—";
$("#ad-branch").textContent = account.account_name || "—";
$("#ad-reg").textContent = String(account.created_at || "").slice(0, 10) || "—";
$("#ad-status").textContent = meta.status.label;
$("#ad-audit").textContent = meta.audit.label;
$("#ad-purpose").textContent = "—";
$("#ad-effective").textContent = account.effective_from || "待审核确定";
$("#ad-range").textContent = account.usable ? "尚未上传" : "—";
$("#ad-reason").textContent = account.status === "returned" && account.review_reason ? account.review_reason : "—";
$("#modal-account-detail")?.classList.add("open");
}
function initCompany() {
renderCompanyManualRecords();
loadCompanyAccounts();
loadImportBatches();
const uploadDialog = $("#uploadDialog");
$$('[data-open-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.showModal()));
$$('[data-close-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.close()));
uploadDialog?.addEventListener("close", resetUpload);
function openModal(id) { $("#" + id)?.classList.add("open"); }
function closeModal(id) { $("#" + id)?.classList.remove("open"); }
$$("[data-close]").forEach((el) => el.addEventListener("click", () => closeModal(el.dataset.close)));
$$(".modal-backdrop").forEach((bd) => bd.addEventListener("click", (e) => { if (e.target === bd) bd.classList.remove("open"); }));
document.addEventListener("keydown", (e) => { if (e.key === "Escape") $$(".modal-backdrop.open").forEach((m) => m.classList.remove("open")); });
$$('[data-open-upload]').forEach((button) => button.addEventListener("click", () => showView("upload")));
// ── 流水导入:多步流程(选文件 → 解析 → 分 sheet 审核 → 确认) ──
$("#accountSelect")?.addEventListener("change", updateParseButton);
$("#fileInput")?.addEventListener("change", (event) => acceptFile(event.target.files[0]));
$("#removeFile")?.addEventListener("click", () => {
@@ -1858,8 +2191,10 @@ function initCompany() {
});
const dropzone = $("#dropzone");
if (dropzone) {
["dragenter", "dragover"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.add("is-dragging"); }));
["dragleave", "drop"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.remove("is-dragging"); }));
["dragenter", "dragover"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.add("dragover"); }));
["dragleave", "drop"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.remove("dragover"); }));
dropzone.addEventListener("click", () => $("#fileInput").click());
dropzone.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); $("#fileInput").click(); } });
dropzone.addEventListener("drop", (event) => acceptFile(event.dataTransfer.files[0]));
}
$("#uploadForm")?.addEventListener("submit", async (event) => {
@@ -1871,7 +2206,7 @@ function initCompany() {
if (parsed && stage === "confirm") {
await confirmImport();
} else if (parsed) {
$("#uploadDialog").close();
resetUpload();
showView("upload");
await loadImportBatches();
} else {
@@ -1884,42 +2219,25 @@ function initCompany() {
await parseFile();
});
function finishReview(button, disposition) {
const item = button.closest("article");
const type = item.dataset.reviewType;
const selectedMatch = $('input[name="matchCandidate"]:checked', item)?.value;
const selectedSubject = $("select", item)?.value;
const detail = type === "match" ? (selectedMatch || "转为匹配异常") : selectedSubject;
const history = $("#reviewHistory");
const record = document.createElement("p");
record.textContent = `${new Date().toLocaleString("zh-CN", { hour12: false })} · 牛女士 · ${disposition} · ${detail}`;
history.append(record);
history.hidden = false;
item.remove();
if (type === "match") $("#matchPendingCount").textContent = "0 笔";
else $("#subjectPendingCount").textContent = "0 笔";
const remaining = $$("#reviewList > article").length;
const badge = $('.side-nav a[data-view="reconcile"] .nav-badge');
if (badge) badge.textContent = remaining;
$(`.cashier-tasks [data-task-type="${type}"]`)?.remove();
const workspaceRemaining = $$(".cashier-tasks > article").length;
$("#workspacePendingStatus").textContent = `${workspaceRemaining} 项待处理`;
const workspaceBadge = $('.side-nav a[data-view="workspace"] .nav-badge');
if (workspaceBadge) workspaceBadge.textContent = workspaceRemaining;
showToast("处理结果已记录", "待办状态、操作人、时间和依据已同步更新");
}
$$('[data-resolve]').forEach((button) => button.addEventListener("click", () => finishReview(button, "确认")));
$$('[data-reject]').forEach((button) => button.addEventListener("click", () => finishReview(button, "转异常")));
$("#markAllRead")?.addEventListener("click", () => {
$$("#companyNotifications .is-unread").forEach((item) => {
item.classList.remove("is-unread");
const status = $(".status", item);
status.className = "status neutral";
status.textContent = "已读";
});
showToast("通知已全部标为已读");
// ── 导入批次详情弹窗 ──
$("#importRows")?.addEventListener("click", (event) => {
const btn = event.target.closest("[data-batch-view]");
if (!btn) return;
const tr = btn.closest("tr");
const batch = (state.batches || []).find((b) => String(b.id) === String(tr?.dataset.batchId));
if (!batch) return;
$("#mb-sub").textContent = `IMP-${String(batch.id).padStart(6, "0")} · ${batch.original_filename || ""}`;
$("#mb-id").textContent = `IMP-${String(batch.id).padStart(6, "0")}`;
$("#mb-bank").textContent = batch.bank_name || "—";
$("#mb-period").textContent = batch.period_start && batch.period_end ? `${batch.period_start} ~ ${batch.period_end}` : "—";
$("#mb-count").textContent = `${batch.confirmed_transactions ?? 0}`;
$("#mb-cover").textContent = batch.status === "exception" ? "未导入" : batch.pending_sheets > 0 ? `${batch.pending_sheets} 个待确认` : batch.confirmed_sheets > 0 ? "已确认" : "待处理";
$("#mb-parse").textContent = batch.exception_sheets > 0 ? `${batch.exception_sheets} 个异常` : batch.ignored_sheets > 0 ? `${batch.ignored_sheets} 个忽略` : "解析成功";
$("#mb-time").textContent = String(batch.created_at || "").slice(0, 16).replace("T", " ");
openModal("modal-batch");
});
// ── 手工记录:提交 + 撤回 ──
$("#manualEntryForm")?.addEventListener("submit", (event) => {
event.preventDefault();
const form = event.currentTarget;
@@ -1955,14 +2273,54 @@ function initCompany() {
showToast("手工记录已提交", "总账复核前不会纳入公司间往来计算");
});
const accountDialog = $("#accountDialog");
$("#openAccountDialog")?.addEventListener("click", () => accountDialog.showModal());
$$('[data-close-account]').forEach((button) => button.addEventListener("click", () => accountDialog.close()));
let manualWithdrawRow = null;
$("#manualRecordRows")?.addEventListener("click", (event) => {
const btn = event.target.closest("button[data-action='withdraw']");
if (!btn) return;
const tr = btn.closest("tr");
manualWithdrawRow = tr;
const cells = tr.cells;
$("#wd-date").textContent = cells[0]?.textContent.trim() || "—";
$("#wd-peer").textContent = cells[2]?.textContent.trim() || "—";
$("#wd-amount").textContent = cells[4]?.textContent.trim() || "—";
$("#wd-summary").textContent = cells[5]?.textContent.trim() || "—";
openModal("withdraw-modal");
});
$("#wd-confirm")?.addEventListener("click", () => {
if (manualWithdrawRow) {
const id = manualWithdrawRow.dataset.storedRecord;
if (id) {
const records = readStoredRecords(storageKeys.manual).filter((r) => r.id !== id);
writeStoredRecords(storageKeys.manual, records);
} else {
manualWithdrawRow.remove();
}
manualWithdrawRow = null;
}
closeModal("withdraw-modal");
renderCompanyManualRecords();
showToast("手工记录已撤回", "该记录已从审核队列中移除,需重新登记提交");
});
// ── 往来确认 ──
initReconcile();
// ── 通知 ──
initNotifications();
// ── 银行账户 ──
$("#openAccountDialog")?.addEventListener("click", () => openModal("accountDialog"));
$("#account-tbody")?.addEventListener("click", (event) => {
const btn = event.target.closest("[data-account-view]");
if (!btn) return;
const tr = btn.closest("tr");
const account = (state.accounts || []).find((a) => String(a.id) === String(tr?.dataset.accountId));
if (!account) return;
openAccountDetail(account);
});
$("#accountForm")?.addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
// The server binds the account to the session company and normalizes the
// number; duplicates come back as 409.
const response = await fetch("/api/company/accounts", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -1982,7 +2340,7 @@ function initCompany() {
showToast("账户登记失败", result?.message || "请稍后重试");
return;
}
accountDialog.close();
closeModal("accountDialog");
event.currentTarget.reset();
await loadCompanyAccounts();
showToast("银行账户已提交登记", "复核通过前不能上传流水,也不参与账户识别和覆盖计算");