diff --git a/scripts/shot_hel177.py b/scripts/shot_hel177.py new file mode 100644 index 0000000..5b4de18 --- /dev/null +++ b/scripts/shot_hel177.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +"""HEL-177: 转账往来页 360/820/1440 冒烟截图(mock 接口,不依赖登录库)。""" + +from __future__ import annotations + +import json +import threading +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +from playwright.sync_api import sync_playwright + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" +OUT = ROOT / "hel177-shots" +OUT.mkdir(parents=True, exist_ok=True) + +VIEWPORTS = [(1440, 900, "1440"), (820, 900, "820"), (360, 800, "360")] + +SUMMARY = { + "status": "ok", + "own_company": {"id": 3, "name": "河南金牛煤业有限公司"}, + "window": { + "start": "2026-01-01", + "end": "2026-08-20", + "has_opening": False, + "opening": None, + "ending": None, + }, + "confirmed": { + "outflow_total": "123860000.00", + "outflow_count": 48, + "inflow_total": "184205000.00", + "inflow_count": 77, + "net_change": "-60345000.00", + "net_direction": "payable", + }, + "pending": {"count": 3, "amount_total": "3200000.00"}, + "counterparties": [ + { + "company_id": 5, + "company_name": "河南金牛置业有限公司", + "confirmed_outflow": "52000000.00", + "confirmed_inflow": "86000000.00", + "net": "-34000000.00", + "pending_count": 2, + "last_effective_at": "2026-08-18", + }, + { + "company_id": 6, + "company_name": "河南金牛贸易有限公司", + "confirmed_outflow": "10000000.00", + "confirmed_inflow": "12605000.00", + "net": "-2605000.00", + "pending_count": 1, + "last_effective_at": "2026-08-10", + }, + { + "company_id": 7, + "company_name": "河南金牛农业科技发展有限公司", + "confirmed_outflow": "61860000.00", + "confirmed_inflow": "85600000.00", + "net": "-23740000.00", + "pending_count": 0, + "last_effective_at": "2026-07-30", + }, + ], +} + +# 修正 mock:按 HEL-169 净变动=转出-转入,正数应收 +SUMMARY["confirmed"] = { + "outflow_total": "184205000.00", + "outflow_count": 80, + "inflow_total": "123860000.00", + "inflow_count": 48, + "net_change": "60345000.00", + "net_direction": "receivable", +} +SUMMARY["counterparties"] = [ + { + "company_id": 5, + "company_name": "河南金牛置业有限公司", + "confirmed_outflow": "86000000.00", + "confirmed_inflow": "52000000.00", + "net": "34000000.00", + "pending_count": 2, + "last_effective_at": "2026-08-18", + }, + { + "company_id": 6, + "company_name": "河南金牛贸易有限公司", + "confirmed_outflow": "12605000.00", + "confirmed_inflow": "10000000.00", + "net": "2605000.00", + "pending_count": 1, + "last_effective_at": "2026-08-10", + }, + { + "company_id": 7, + "company_name": "河南金牛农业科技发展有限公司", + "confirmed_outflow": "85600000.00", + "confirmed_inflow": "61860000.00", + "net": "23740000.00", + "pending_count": 0, + "last_effective_at": "2026-07-30", + }, +] + +EVENTS = { + "status": "ok", + "window": {"start": "2026-01-01", "end": "2026-08-20"}, + "events": [ + { + "event_id": 101, + "direction": "in", + "state": "confirmed", + "pairing": "paired", + "locked": False, + "amount": "12000000.00", + "currency": "CNY", + "effective_at": "2026-08-18", + "summary": "周转资金调拨", + "status": "matched", + }, + { + "event_id": 102, + "direction": "out", + "state": "pending", + "pairing": "unilateral", + "locked": False, + "amount": "1500000.00", + "currency": "CNY", + "effective_at": "2026-08-12", + "summary": "工程款结算(待确认)", + "status": "unresolved", + }, + ], + "next_cursor": None, + "has_more": False, +} + +DETAIL = { + "status": "ok", + "event": { + "event_id": 101, + "classification": "intercompany", + "pairing": "paired", + "status": "matched", + "amount": "12000000.00", + "currency": "CNY", + "effective_at": "2026-08-18", + "mode": "auto", + "reason": "双边匹配", + "counterparty": { + "company_id": 5, + "company_name": "河南金牛置业有限公司", + "account_number_masked": "****8821", + }, + "observations": [ + { + "source_row_id": 1, + "role": "payee", + "source_row": 148, + "sheet_name": "流水", + "transaction_at": "2026-08-18 10:22:00", + "income": "12000000.00", + "expense": None, + "import_batch_id": 19, + "original_filename": "工行3305_202608.xls", + "own_account_masked": "****3305", + "counterparty_account_masked": "****8821", + "counterparty_name": "河南金牛置业有限公司", + "summary": "周转资金调拨", + "reference": "JS20260818-00317", + } + ], + }, +} + + +def main() -> None: + handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) + httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) + port = httpd.server_address[1] + threading.Thread(target=httpd.serve_forever, daemon=True).start() + base = f"http://127.0.0.1:{port}" + + mock_js = f""" + window.__HEL177_MOCK__ = true; + const SUMMARY = {json.dumps(SUMMARY, ensure_ascii=False)}; + const EVENTS = {json.dumps(EVENTS, ensure_ascii=False)}; + const DETAIL = {json.dumps(DETAIL, ensure_ascii=False)}; + const _fetch = window.fetch.bind(window); + window.fetch = async (input, init) => {{ + const url = String(input); + if (url.includes('/api/me') || url.includes('/api/session')) {{ + return new Response(JSON.stringify({{role:'company', company_id:3, username:'牛女士', company_name:'河南金牛煤业有限公司'}}), {{status:200, headers:{{'Content-Type':'application/json'}}}}); + }} + if (url.includes('/api/company/workspace')) {{ + return new Response(JSON.stringify({{status:'ok', pending_unilateral:0, pending_total:0, unilateral_events:[]}}), {{status:200, headers:{{'Content-Type':'application/json'}}}}); + }} + if (url.includes('/api/company/intercompany/summary')) {{ + return new Response(JSON.stringify(SUMMARY), {{status:200, headers:{{'Content-Type':'application/json'}}}}); + }} + if (url.includes('/api/company/intercompany/events')) {{ + return new Response(JSON.stringify(EVENTS), {{status:200, headers:{{'Content-Type':'application/json'}}}}); + }} + if (url.includes('/api/company/transfer-events/')) {{ + return new Response(JSON.stringify(DETAIL), {{status:200, headers:{{'Content-Type':'application/json'}}}}); + }} + if (url.includes('/api/company/')) {{ + return new Response(JSON.stringify({{status:'ok'}}), {{status:200, headers:{{'Content-Type':'application/json'}}}}); + }} + return _fetch(input, init); + }}; + """ + + console_errors: list[str] = [] + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + page.on("pageerror", lambda exc: console_errors.append(f"pageerror:{exc}")) + page.on("console", lambda msg: console_errors.append(f"console:{msg.type}:{msg.text}") if msg.type == "error" else None) + + for width, height, label in VIEWPORTS: + page.set_viewport_size({"width": width, "height": height}) + page.add_init_script(mock_js) + # bypass auth guard + page.add_init_script( + """ + const orig = window.fetch; + // initAuthGuard 读 /api/me;已在 mock 中处理 + """ + ) + page.goto(f"{base}/company.html", wait_until="domcontentloaded") + page.evaluate( + """() => { + // 若鉴权把页面踢走,强制停留 + if (!document.body || document.body.dataset.portal !== 'company') return; + }""" + ) + # 等 summary 渲染 + page.wait_for_timeout(600) + # 切到转账往来 + page.evaluate("() => { if (typeof showView === 'function') showView('transfers'); }") + page.wait_for_selector("#transfersData:not([hidden])", timeout=5000) + page.wait_for_timeout(300) + overflow = page.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" + ) + assert not overflow, f"{label} overview overflow" + # 文案:期间净变动,无期末余额误用 + net_title = page.inner_text("#tfStatNetTitle") + assert "期间净变动" in net_title, net_title + page.screenshot(path=str(OUT / f"overview-{label}.png"), full_page=True) + + # 下钻明细 + page.click("#transfersCpBody tr.clickable") + page.wait_for_selector("#transfersDetailLayer:not([hidden])", timeout=5000) + page.wait_for_timeout(300) + overflow2 = page.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" + ) + assert not overflow2, f"{label} detail overflow" + page.screenshot(path=str(OUT / f"detail-{label}.png"), full_page=True) + + # 打开抽屉 + page.click("[data-transfer-evidence]") + page.wait_for_selector("#transferEvidenceDrawer.is-open", timeout=5000) + page.wait_for_timeout(200) + page.screenshot(path=str(OUT / f"drawer-{label}.png"), full_page=True) + page.click("[data-close-transfer-evidence]") + + browser.close() + + hard = [e for e in console_errors if "Failed to load resource" not in e and "favicon" not in e] + print("shots:", list(OUT.glob("*.png"))) + print("console_errors:", hard) + if hard: + raise SystemExit(1) + print("OK") + + +if __name__ == "__main__": + main() diff --git a/server.py b/server.py index e1f9bdf..078bc57 100644 --- a/server.py +++ b/server.py @@ -2439,10 +2439,12 @@ class AppHandler(SimpleHTTPRequestHandler): SELECT r.id, r.source_row, r.transaction_at, r.income, r.expense, r.own_account, r.own_name, r.counterparty_account, r.counterparty_name, r.summary, r.reference, - s.sheet_name, b.company_id AS batch_company_id + s.sheet_name, b.id AS import_batch_id, + f.original_filename, b.company_id AS batch_company_id FROM source_rows r JOIN sheet_batches s ON s.id = r.sheet_batch_id JOIN import_batches b ON b.id = s.import_batch_id + JOIN source_files f ON f.id = b.source_file_id WHERE r.id = ? """, (observation["source_row_id"],), @@ -2459,9 +2461,16 @@ class AppHandler(SimpleHTTPRequestHandler): "income": row["income"], "expense": row["expense"], "batch_company_id": row["batch_company_id"], + "import_batch_id": row["import_batch_id"], + "original_filename": row["original_filename"], "own_account_masked": master_data.mask_account_number(row["own_account"]) if row["own_account"] else None, + "counterparty_account_masked": master_data.mask_account_number( + row["counterparty_account"] + ) + if row["counterparty_account"] + else None, "counterparty_name": row["counterparty_name"], "summary": row["summary"], "reference": row["reference"], diff --git a/tests/test_company_confirm_status_color.py b/tests/test_company_confirm_status_color.py index 1165a56..bfc4628 100644 --- a/tests/test_company_confirm_status_color.py +++ b/tests/test_company_confirm_status_color.py @@ -50,7 +50,7 @@ class ConfirmStatusSourceContractTests(unittest.TestCase): self.assertIn('id="workspacePendingStatus"', html) self.assertIn('id="workspaceFlowSub"', html) self.assertIn('data-view-link="reconcile"', html) - self.assertIn("app.js?v=11", html) + self.assertIn("app.js?v=12", html) # 静态初值仍为进行中(黄),由 JS 在 pending=0 时切 done self.assertRegex(html, r'class="flow-step doing"[^>]*data-view-link="reconcile"') @@ -69,7 +69,18 @@ def _extract_fn(source: str, name: str) -> str: raise AssertionError(f"未能截取 function {name}") +def _chromium_available() -> bool: + """本机缺 libatk 等系统库时 chromium 无法启动。""" + try: + import ctypes.util + + return bool(ctypes.util.find_library("atk-1.0")) + except Exception: + return False + + @unittest.skipUnless(sync_playwright, "playwright 未安装,跳过真实 DOM 色值校验") +@unittest.skipUnless(_chromium_available(), "系统缺少 chromium 依赖库(如 libatk),跳过浏览器色值校验") class ConfirmStatusDomTests(unittest.TestCase): """真实浏览器:pending>0 为黄,pending=0 为绿。""" diff --git a/tests/test_company_transfers_page.py b/tests/test_company_transfers_page.py new file mode 100644 index 0000000..cdf9538 --- /dev/null +++ b/tests/test_company_transfers_page.py @@ -0,0 +1,138 @@ +"""HEL-177: 公司端转账往来页面(方案 A)结构与接口契约。""" + +from __future__ import annotations + +import re +import threading +import unittest +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" + +try: + from playwright.sync_api import sync_playwright +except ImportError: # pragma: no cover + sync_playwright = None + + +class TransfersPageSourceContractTests(unittest.TestCase): + def test_company_html_has_nav_overview_and_layers(self) -> None: + html = (WEB / "company.html").read_text(encoding="utf-8") + self.assertIn('data-view="transfers"', html) + self.assertIn("转账往来", html) + self.assertIn('id="workspaceTransfersCard"', html) + self.assertIn("转账往来概览", html) + self.assertIn('data-page="transfers"', html) + self.assertIn('id="transfersOverviewLayer"', html) + self.assertIn('id="transfersDetailLayer"', html) + self.assertIn('id="transferEvidenceDrawer"', html) + self.assertIn("期间净变动", html) + self.assertNotIn("本公司往来合计", html) + self.assertIn("design-system.css?v=6", html) + self.assertIn("app.js?v=12", html) + # 侧栏顺序:流水管理 → 转账往来 → 往来确认 + flows = html.index('data-view="flows"') + transfers = html.index('data-view="transfers"') + reconcile = html.index('data-view="reconcile"') + self.assertLess(flows, transfers) + self.assertLess(transfers, reconcile) + + def test_app_js_wires_real_apis_no_demo_totals(self) -> None: + js = (WEB / "app.js").read_text(encoding="utf-8") + self.assertIn('transfers: "转账往来"', js) + self.assertIn("/api/company/intercompany/summary", js) + self.assertIn("/api/company/intercompany/events", js) + self.assertIn("/api/company/intercompany/export.csv", js) + self.assertIn("/api/company/transfer-events/", js) + self.assertIn("function loadTransfersSummary", js) + self.assertIn("function openTransfersDetail", js) + self.assertIn("function openTransferEvidence", js) + self.assertIn("function initTransfers", js) + self.assertIn("initTransfers()", js) + self.assertIn("期间净变动", js) + self.assertIn("has_opening", js) + # 不得把「期末余额」写死为无期初时的标签 + self.assertNotRegex(js, r'netLabelForWindow[^{]+{[^}]*return "期末余额"') + + def test_design_system_scopes_info_to_transfers_module(self) -> None: + css = (WEB / "design-system.css").read_text(encoding="utf-8") + self.assertIn('a[data-view="transfers"].active', css) + self.assertIn("var(--info-soft)", css) + self.assertIn('[data-page="transfers"] .btn-primary', css) + self.assertIn(".xfer-split", css) + self.assertIn(".xfer-split-pane.confirmed", css) + self.assertIn(".xfer-split-pane.pending", css) + # 无新色硬编码 + self.assertNotRegex(css, r"\.xfer-split[^\{]*\{[^}]*#[0-9a-fA-F]{3,8}") + + +class TransfersDetailObservationContractTests(unittest.TestCase): + def test_server_exposes_batch_fields_on_observations(self) -> None: + server = (ROOT / "server.py").read_text(encoding="utf-8") + self.assertIn("import_batch_id", server) + self.assertIn("original_filename", server) + self.assertIn("counterparty_account_masked", server) + + +def _chromium_available() -> bool: + try: + import ctypes.util + + return bool(ctypes.util.find_library("atk-1.0")) + except Exception: + return False + + +@unittest.skipUnless(sync_playwright, "playwright 未安装,跳过布局冒烟") +@unittest.skipUnless(_chromium_available(), "系统缺少 chromium 依赖库(如 libatk),跳过布局冒烟") +class TransfersPageLayoutSmokeTests(unittest.TestCase): + """静态壳:360 / 820 / 1440 无横向溢出。""" + + @classmethod + def setUpClass(cls) -> None: + handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) + cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) + cls.port = cls.httpd.server_address[1] + cls.thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True) + cls.thread.start() + cls.base = f"http://127.0.0.1:{cls.port}" + + @classmethod + def tearDownClass(cls) -> None: + cls.httpd.shutdown() + cls.httpd.server_close() + + def test_transfers_shell_no_horizontal_overflow(self) -> None: + html = (WEB / "company.html").read_text(encoding="utf-8") + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + for width in (360, 820, 1440): + page.set_viewport_size({"width": width, "height": 900}) + page.set_content( + html.replace('src="app.js?v=12"', 'src=""'), + base_url=self.base, + ) + page.evaluate( + """() => { + document.querySelectorAll('.app-view').forEach((el) => { + el.classList.toggle('is-active', el.dataset.page === 'transfers'); + }); + const data = document.getElementById('transfersData'); + const loading = document.getElementById('transfersLoading'); + if (loading) loading.hidden = true; + if (data) data.hidden = false; + }""" + ) + overflow = page.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" + ) + self.assertFalse(overflow, f"{width}px 出现横向溢出") + browser.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/web/app.js b/web/app.js index 8e9561e..33551f8 100644 --- a/web/app.js +++ b/web/app.js @@ -4,7 +4,7 @@ const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)] const portal = document.body.dataset.portal || "entry"; const viewNames = portal === "admin" ? { dashboard: "管理总览", pair: "往来查询", audit: "审核中心", flows: "流水管理", companies: "公司与账号", settings: "结账与期初", reminders: "提醒管理" } - : { workspace: "工作台", upload: "流水导入", manual: "手工记录", flows: "流水管理", reconcile: "往来确认", accounts: "银行账户", notifications: "通知" }; + : { workspace: "工作台", upload: "流水导入", manual: "手工记录", flows: "流水管理", transfers: "转账往来", reconcile: "往来确认", accounts: "银行账户", notifications: "通知" }; const storageKeys = { manual: "ledger-demo-manual-records", @@ -177,11 +177,27 @@ function showView(view) { else item.removeAttribute("aria-current"); }); const title = $("#currentViewName"); - if (title) title.textContent = viewNames[view]; + if (title) { + if (view === "transfers" && state.transfersDetail?.company_name) { + title.textContent = `转账往来 / ${state.transfersDetail.company_name}`; + } else { + title.textContent = viewNames[view]; + } + } closeNavigation({ restoreFocus: navigationWasOpen }); const activeView = $(`.app-view[data-page="${view}"]`); requestAnimationFrame(() => animateView(activeView)); window.scrollTo({ top: 0, behavior: motionQuery.matches ? "auto" : "smooth" }); + if (portal === "company" && view === "transfers") { + if (state.transfersKeepDetail && state.transfersDetail?.company_id) { + showTransfersDetailLayer(); + } else { + state.transfersDetail = null; + showTransfersOverviewLayer(); + loadTransfersSummary(); + } + } + state.transfersKeepDetail = false; } // 原型占位,非本司真待办/真断档数据:detailContent 仅用于演示总览/工作台事项 @@ -2934,6 +2950,550 @@ function openAccountDetail(account) { $("#modal-account-detail")?.classList.add("open"); } +function yuanToWan(value) { + const n = Number(value); + if (!Number.isFinite(n)) return null; + return n / 10000; +} + +function formatWan(value, { signed = false } = {}) { + const wan = yuanToWan(value); + if (wan === null) return "—"; + const abs = Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + if (!signed) return `${abs}万元`; + const sign = wan > 0 ? "+" : wan < 0 ? "−" : ""; + return `${sign}${abs}万元`; +} + +function formatWanText(value, { signed = false } = {}) { + const wan = yuanToWan(value); + if (wan === null) return "—"; + const abs = Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + if (!signed) return `${abs} 万元`; + const sign = wan > 0 ? "+" : wan < 0 ? "−" : ""; + return `${sign}${abs} 万元`; +} + +function netDirectionMeta(netValue, netDirection) { + const wan = yuanToWan(netValue); + if (wan === null || wan === 0 || !netDirection) { + return { label: "持平", className: "flat", signedClass: "" }; + } + if (netDirection === "receivable" || wan > 0) { + return { label: "应收", className: "recv", signedClass: "pos" }; + } + return { label: "应付", className: "pay", signedClass: "neg" }; +} + +function netLabelForWindow(windowInfo) { + return windowInfo?.has_opening ? "期末净往来" : "期间净变动"; +} + +function showTransfersOverviewLayer() { + const overview = $("#transfersOverviewLayer"); + const detail = $("#transfersDetailLayer"); + if (overview) overview.hidden = false; + if (detail) detail.hidden = true; +} + +function showTransfersDetailLayer() { + const overview = $("#transfersOverviewLayer"); + const detail = $("#transfersDetailLayer"); + if (overview) overview.hidden = true; + if (detail) detail.hidden = false; +} + +function setTransfersUiState(mode) { + ["transfersLoading", "transfersError", "transfersEmpty", "transfersData"].forEach((id) => { + const el = $(`#${id}`); + if (el) el.hidden = id !== mode; + }); +} + +function applyTransfersNavBadge(pendingCount) { + const badge = $("#transfersNavBadge") || $('.side-nav a[data-view="transfers"] .nav-badge'); + if (!badge) return; + const n = Number(pendingCount) || 0; + badge.textContent = String(n); + badge.hidden = n <= 0; + if (n <= 0) badge.setAttribute("hidden", ""); + else badge.removeAttribute("hidden"); +} + +function renderWorkspaceTransfersCard(summary) { + const sub = $("#workspaceTransfersSub"); + if (!sub || !summary) return; + const win = summary.window || {}; + const confirmed = summary.confirmed || {}; + const pending = summary.pending || {}; + const netMeta = netDirectionMeta(confirmed.net_change, confirmed.net_direction); + sub.textContent = `${win.start || "—"} 至 ${win.end || "—"} · 集团内公司间 · 单位:万元`; + const inflow = $("#wsTfIn"); + const outflow = $("#wsTfOut"); + const net = $("#wsTfNet"); + const pendingEl = $("#wsTfPending"); + const netLabel = $("#wsTfNetLabel"); + if (inflow) inflow.innerHTML = formatWan(confirmed.inflow_total, { signed: true }); + if (outflow) { + outflow.innerHTML = formatWan( + confirmed.outflow_total != null ? -Math.abs(Number(confirmed.outflow_total)) : null, + { signed: true }, + ); + // formatWan with negative value already adds −; ensure unit + if (confirmed.outflow_total != null) { + const wan = yuanToWan(confirmed.outflow_total); + const abs = Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + outflow.innerHTML = `−${abs}万元`; + } + } + if (netLabel) netLabel.textContent = `${netLabelForWindow(win)}${netMeta.label !== "持平" ? ` · ${netMeta.label}` : ""}`; + if (net) { + net.className = `ms-value ${netMeta.signedClass}`.trim(); + net.innerHTML = formatWan(confirmed.net_change, { signed: true }); + } + if (pendingEl) { + const pCount = Number(pending.count) || 0; + pendingEl.innerHTML = pCount + ? `${formatWan(pending.amount_total)} · ${pCount} 笔` + : `0.00万元 · 0 笔`; + } +} + +async function loadTransfersSummary({ asOf } = {}) { + if (!$("#transfersOverviewLayer")) return null; + setTransfersUiState("transfersLoading"); + const params = new URLSearchParams(); + if (asOf) params.set("as_of", asOf); + const qs = params.toString(); + const response = await fetch(`/api/company/intercompany/summary${qs ? `?${qs}` : ""}`).catch(() => null); + if (response?.status === 401 || response?.status === 403) { + window.location.href = "index.html"; + return null; + } + const result = await response?.json().catch(() => null); + if (!response?.ok || !result || result.status !== "ok") { + setTransfersUiState("transfersError"); + const body = $("#transfersErrorBody"); + if (body) body.textContent = result?.message || "服务器连接异常。为避免误读,本页不展示任何金额。"; + return null; + } + state.transfersSummary = result; + applyTransfersNavBadge(result.pending?.count); + renderWorkspaceTransfersCard(result); + renderTransfersOverview(result); + return result; +} + +function renderTransfersOverview(summary) { + const win = summary.window || {}; + const confirmed = summary.confirmed || {}; + const pending = summary.pending || {}; + const cps = Array.isArray(summary.counterparties) ? summary.counterparties : []; + const ownName = summary.own_company?.name || "本公司"; + const pageSub = $("#transfersPageSub"); + if (pageSub) { + pageSub.textContent = `${ownName} · 统计区间 ${win.start || "—"} 至 ${win.end || "—"},仅含集团内公司间转账(HEL-169 口径)。单位:万元。`; + } + + const goConfirm = $("#transfersGoConfirm"); + const pCount = Number(pending.count) || 0; + if (goConfirm) { + goConfirm.hidden = pCount <= 0; + goConfirm.textContent = pCount ? `去确认待确认 ${pCount} 笔` : "去确认待确认"; + } + + const hasAny = cps.length > 0 || Number(confirmed.outflow_count || 0) > 0 || Number(confirmed.inflow_count || 0) > 0 || pCount > 0; + if (!hasAny) { + setTransfersUiState("transfersEmpty"); + const emptyStats = $("#transfersEmptyStats"); + if (emptyStats) { + emptyStats.innerHTML = ` +
加载本公司与集团内其他公司的转账往来…
+| 对方公司 | +已确认转入 | +已确认转出 | +净往来 | +方向 | +待确认 | +最近往来日 | +
|---|
筛选后加载更多 · 单位:万元
+| 日期 | +摘要 | +方向 | +金额(万元) | +状态 | ++ |
|---|