diff --git a/web/app.js b/web/app.js index b0ca75f..d80d472 100644 --- a/web/app.js +++ b/web/app.js @@ -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 = `${record.subject || "—"}`; + + 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 = `${statusMeta.label}`; + + const action = document.createElement("td"); + if (record.status === "待总账复核") { + action.innerHTML = ''; + } else { + action.innerHTML = ''; + } + + 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 = `${account.account_type || "—"}`; + + const meta = companyAccountMeta(account.status); + const statusCell = document.createElement("td"); statusCell.innerHTML = `${meta.status.label}`; + + const coverage = document.createElement("td"); + coverage.className = "num muted"; + coverage.textContent = account.usable ? "尚未上传" : "—"; + + const auditCell = document.createElement("td"); auditCell.innerHTML = `${meta.audit.label}`; + + const action = document.createElement("td"); action.innerHTML = ''; + + 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 ` + const data = currentFlowData(); + tbody.innerHTML = data.map((row, index) => { + const companyCell = portal === "admin" ? `${row.company}` : ""; + return ` ${row.date} - ${row.company} + ${companyCell} ${row.acctLabel} ${row.dir} ${row.peer} @@ -1423,13 +1482,21 @@ function renderFlowRows() { `; }).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 ? `收 +¥ ${formatCurrency(inflow)} · 付 -¥ ${formatCurrency(outflow)}` : ""; + } } 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 ? `收 +¥ ${formatCurrency(inflow)} · 付 -¥ ${formatCurrency(outflow)}` : ""; + } 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 = `${coverageStatus.label}`; + coverage.innerHTML = `${coverageStatus.label}`; 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 = `${statusParts.join("、")}`; + } else { + parseState.innerHTML = '解析成功'; + } 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 = ''; + + 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 = '
单边流水已全部匹配
确认结果将同步至集团审核中心复核
'; + 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 = `已确认 · ${subject}`; + 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("银行账户已提交登记", "复核通过前不能上传流水,也不参与账户识别和覆盖计算"); diff --git a/web/company.html b/web/company.html index f292de8..e4fe33e 100644 --- a/web/company.html +++ b/web/company.html @@ -23,10 +23,10 @@ 流水导入 手工记录 流水管理 - 往来确认2 + 往来确认5 银行账户 - 通知2 + 通知3
@@ -80,8 +80,8 @@
03往来确认
-
待处理 5 笔
-
单边流水 3 笔 · 科目确认 2 笔
+
待处理 5 笔
+
单边流水 3 笔 · 科目确认 2 笔
04总账复核
@@ -98,12 +98,12 @@
-
+
本月待办本月任务 13/18 · 按处理顺序排列,前 3 项阻断结账 4 项待处理
-
+
阻断
处理 3 笔单边流水确认
@@ -111,7 +111,7 @@
-
+
阻断
确认 2 笔其他应收科目
@@ -233,73 +233,730 @@
-

流水导入

上传 A公司银行账户流水,系统按表头识别银行模板

-
最近导入批次仅已确认工作表参与匹配与计算
批次银行账户流水期间明细数覆盖状态解析状态上传时间
+
+
+

流水导入

+

上传银行导出的 Excel 流水文件,系统按表头识别银行模板、解析期间与明细并做覆盖校验。当前账期 2026-07,仅已审核通过的账户可导入。

+
+
+ +
+
+
+ 上传流水先选账户,再选择或拖入流水文件 +
+
+
+ + + 仅已审核通过的账户可上传;选择账户与文件后方可解析 +
+
+ + +
+ +
拖放银行流水文件到此处
+
点击选择文件,支持银行导出的 Excel
+
.XLS / .XLSX · MAX 20MB
+
+ +
+ + + + + + + 请先选择流水文件,选择后此按钮方可点击 + +
+
导入规则
+
+
· 同一账户同一月份重复导入时,新批次覆盖旧批次
+
· 导入后系统自动与对方公司流水轧算往来余额
+
· 无法解析的行转入审核中心,不影响其余明细入库
+
+
+
+
+ +
+
+ 历史导入批次2026 年 7 月账期 · 本公司全部账户 +
+
+ + + + + + + + + +
批次银行账户流水期间明细数覆盖状态解析状态上传时间操作
+
+
+ 暂无批次 + 同一账户同一月份重复导入时,新批次覆盖旧批次 +
+
+
-

手工记录

登记个人过账或银行流水之外的往来事实

1 笔待总账复核
-
-
-

新增记录

A公司 · 提交人:牛女士

-
-
-
-
-
- - -
-

手工记录与银行原始流水分开留痕;总账复核通过后才纳入往来计算。

+
+
+

手工记录

+

登记未通过银行流水体现的往来业务,如个人过账、现金收付与未入账回单。提交后由集团总账会计审核,通过后计入本公司往来台账。

+
+
+ +
+
+
+ 登记手工往来带 * 为必填项,提交后进入待审核状态
-
- -
-

本公司记录

按提交时间倒序

-
编号 / 日期方向 / 科目资金来源对方摘要金额(元)状态
MR-DEMO-0012026.07.29付款其他应收个人过账B公司集团内部公司个人代付后转回80,000.00待总账复核
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
手工记录与银行原始流水分开留痕
总账复核通过后才纳入往来计算,撤回后需重新登记提交。
+
+ +
+
+ +
+
+ 已提交记录账期 2026-07 内本公司提交的手工往来,0 笔待总账复核 +
+
+ + + + + + + + +
日期方向对方科目金额(元)摘要审核状态操作
+
+ +
+ 共 0 条 + 审核由集团总账会计在 1 个工作日内完成 +
+
-

流水管理

仅查询和导出 A公司的银行流水

-
-
当前结果 3金额单位:元
日期银行账户方向对方户名 / 账号摘要银行流水号归集状态金额
2026.07.18中信银行 · 5316转出B公司尾号 9481往来款CIT260718018双边匹配10,000,000.00
2026.07.12建设银行 · 0845转出A公司尾号 5316同名账户调拨CCB260712031同公司调拨2,000,000.00
2026.07.06中信银行 · 5316转入B公司尾号 9481归还往来款CIT260706041双边匹配3,200,000.00
+
+
+

流水管理

+

本公司名下银行账户的原始流水及归集状态。集团内往来流水将进入往来确认与归集链路,外部流水不参与归集。

+
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ + + + + + + + + + + + + + +
日期银行账户方向对方单位摘要银行流水号归集状态金额
+ +
+ 共 0 笔 + +
+
-

往来确认

系统计算为主,只处理无法确定的匹配与科目

-
自动确认18 笔无需人工处理
单边待匹配1 笔需选择对方证据
科目待确认1 笔需选择会计科目
-

待确认事项

每次确认都会记录操作人、时间和依据

匹配

2026.07.18 · 转出 280.00 万元

中信银行 5316 → B公司 · 摘要“往来款”

选择对方银行证据
科目

2026.06.27 · 转出 600.00 万元

建设银行 0845 → C公司 · 摘要“资金调拨”

+
+
+

往来确认

+

处理本公司流水与对方公司对账出现的单边记录,并确认往来科目归属。7 月结账日因故顺延至 08-29,逾期未确认将阻断本公司结账。

+
+
+ +
+ +
+
5 项待确认,是 7 月结账的阻断项
+
含单边流水匹配 3 项、科目确认 2 项。请于 2026-08-29(7 月顺延结账日)前处理完毕,否则集团无法对贵公司执行 7 月结账。
+
+
+ +
+ + +
+ +
+
+
+
+ 2026-07-14 付 ¥3,200,000.00 给金牛置业摘要:煤炭采购款 + 单边 +
+
+
+
本方流水号
MY-ICBC-3305-20260714-018
+
本方账户
工商银行基本户 · 尾号 3305
+
对方户名
河南金牛置业有限公司
+
+
+
+ + + + 提示:置业中行尾号 8821 账户 07-06 至 07-16 流水断档,其 07-15 记录为对方后补手工记录,匹配度较低,请优先核对建行 6642。 +
+
+ + 查看本方流水 +
+
+ +
+
+ 2026-07-22 付 ¥950,000.00 给金牛物流摘要:7 月矿区运输费 + 单边 +
+
+
+
本方流水号
MY-ICBC-3305-20260722-031
+
本方账户
工商银行基本户 · 尾号 3305
+
对方户名
河南金牛物流有限公司
+
+
+
+ + + +
+
+ + 查看本方流水 +
+
+ +
+
+ 2026-07-09 收 ¥1,860,000.00 自金牛贸易摘要:精煤销售款 + 单边 +
+
+
+
本方流水号
MY-BOC-9916-20260709-007
+
本方账户
中国银行一般户 · 尾号 9916
+
对方户名
河南金牛贸易有限公司
+
+
+
+ + + +
+
+ + 查看本方流水 +
+
+
+ + +
+ +
-

银行账户

A公司登记账户与流水覆盖区间

- +
+
+

银行账户

+

本公司名下已登记银行账户。仅审核通过的账户可上传流水,账号对外仅显示尾号。

+
+
+ +
+
+ +
+ +
+
新账户需总账管理员审核通过后方可上传流水
+
提交登记后由集团总账管理员核对开户信息,审核周期一般 1 个工作日;审核结果将通过站内通知告知。
+
+
+ +
+
+ 本公司账户0 个 · 统计截止 2026-08-20 +
+
+ + + + + + + + + + + + +
银行 / 账号账户类型启用状态流水覆盖区间审核状态操作
+
+ +
+ 共 0 个账户 + 仅审核通过的账户可上传流水并参与归集 +
+
-

通知

系统提醒与总账管理员通知

-
7 月工商银行流水尚未上传系统自动提醒 · 今天 08:00 · 截止 08.05

账户尾号 9481 缺少 07.01—07.21 流水。

未读
请完成 7 月银行流水确认系统管理员 · 今天 09:30 · 截止 08.08

请先处理断档和两项待确认往来,再提交公司确认。

未读
6 月账期已完成集团结账系统通知 · 07.05 18:10已读
+
+
+

通知

+

系统与总账管理员发送给本公司的全部消息。7 月账期结账因故顺延至 2026-08-29,请优先处理未读与处理中事项。

+
+
+ +
+
+ +
+
+ + + + +
+ +
+
+ 未读 + 系统 +
+
7 月流水存在断档风险:交行 尾号 7710 未上传
+
2026-08-20 14:32 · 交行 7710 账户(待审核)7 月流水尚未上传,距 7 月结账日(顺延至 08-29)仅剩 9 天,请尽快补传。
+
+
+
+
+ 未读 + 系统 +
+
您有 2 笔单边流水待选择对方证据
+
2026-08-19 09:15 · 与金牛置业的煤炭采购款 ¥3,200,000.00 等 2 笔流水仅有本方记录,需选择对方银行流水佐证。
+
+
+
+
+ 未读 + 总账管理员 +
+
总账管理员提醒:请于 08-29 前完成待确认事项
+
2026-08-18 16:40 · 7 月账期结账顺延至 08-29,贵公司当前仍有 5 项往来确认未完成,请合理安排时间。
+
+
+
+
+ 处理中 + 系统 +
+
手工记录科目待确认:其他应收 ¥86,500.00
+
2026-08-15 10:02 · 07-28 录入的矿区备用金垫付记录,往来科目「其他应收」待总账管理员复核。
+
+
+
+
+ 处理中 + 总账管理员 +
+
往来确认回复:置业已确认 320 万煤炭采购款
+
2026-08-12 11:26 · 金牛置业已确认 07-14 煤炭采购款 ¥3,200,000.00,请补充本方工行 3305 账户流水证据完成闭环。
+
+
+
+
+ 已完成 + 系统 +
+
流水导入成功:中行 尾号 9916 · 42 笔
+
2026-08-06 15:52 · 7 月中行一般户流水已导入,42 笔全部校验通过,无重复记录。
+
+
+
+ 已完成 + 系统 +
+
流水导入成功:工行 尾号 3305 · 86 笔
+
2026-08-06 15:48 · 7 月工行基本户流水已导入,86 笔全部校验通过,无重复记录。
+
+
+
+ 已完成 + 总账管理员 +
+
交行 尾号 7710 账户开户资料已受理
+
2026-07-22 13:10 · 新账户开户资料已提交总账管理员,审核通过后方可启用并上传流水。
+
+
+
+ 已完成 + 系统 +
+
2026 年 6 月账期已结账
+
2026-07-05 09:00 · 6 月账期已按每月 5 日结账规则完成结账,期末数据已锁定,不可再修改。
+
+
+
+ + +
- -
-

上传银行流水

A公司 · 系统将识别表头与银行模板

-
-
-
-
- -
-

登记银行账户

账户提交后由总账管理员复核

-
-
-
-
+ + + + + + + + + + + + + +