diff --git a/.gitignore b/.gitignore
index e3171da..e06217b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,7 @@ nul
vendor_pkgs/
node_modules/
package-lock.json
+
+# local vendor for agent test env (not shipped)
+.vendor/
+vendor_wheels/
diff --git a/src/bank_importer/dashboard.py b/src/bank_importer/dashboard.py
index 3723649..7dd40bc 100644
--- a/src/bank_importer/dashboard.py
+++ b/src/bank_importer/dashboard.py
@@ -42,8 +42,11 @@ def audit_counts(connection: sqlite3.Connection) -> dict[str, int]:
Priority mapping (aligned with current admin audit UI semantics):
- high: unresolved / needs_review match exceptions
- - medium: pending bank-account registrations
+ - medium: pending bank-account registrations + pending manual records
- low: reserved for future low-risk queues (currently always 0)
+
+ Homepage card, sidebar badge and audit-center heading must all read this
+ same payload (via ``/api/admin/dashboard`` → ``audit``).
"""
high = connection.execute(
"""
@@ -55,9 +58,19 @@ def audit_counts(connection: sqlite3.Connection) -> dict[str, int]:
AND d.classification IN ('unresolved', 'needs_review')
"""
).fetchone()["n"]
- medium = connection.execute(
+ medium_accounts = connection.execute(
"SELECT COUNT(*) AS n FROM bank_accounts WHERE status = 'pending'"
).fetchone()["n"]
+ medium_manuals = connection.execute(
+ """
+ SELECT COUNT(*) AS n
+ FROM manual_records m
+ JOIN current_manual_record_decisions c ON c.record_id = m.id
+ JOIN manual_record_decisions d ON d.id = c.decision_id
+ WHERE d.state = 'pending'
+ """
+ ).fetchone()["n"]
+ medium = int(medium_accounts) + int(medium_manuals)
low = 0
return {
"total": int(high) + int(medium) + int(low),
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index b7d0582..026a952 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -57,6 +57,96 @@ class DashboardUnitTests(unittest.TestCase):
self.assertEqual(0, counts["high"])
self.assertEqual(1, counts["total"])
+ def test_pending_manual_counts_as_medium(self) -> None:
+ from bank_importer import auth, manual_records
+
+ now = master_data.utc_now()
+ a = self.connection.execute(
+ "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
+ "VALUES ('甲公司', NULL, NULL, 'active', ?, ?)",
+ (now, now),
+ ).lastrowid
+ b = self.connection.execute(
+ "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
+ "VALUES ('乙公司', NULL, NULL, 'active', ?, ?)",
+ (now, now),
+ ).lastrowid
+ self.connection.commit()
+ auth.create_user(
+ self.connection, "cashier-a", "CashierPass123", "company", company_id=int(a)
+ )
+ actor = self.connection.execute(
+ "SELECT * FROM users WHERE username = 'cashier-a'"
+ ).fetchone()
+ manual_records.submit(
+ self.connection,
+ company_id=int(a),
+ counterparty_company_id=int(b),
+ occurred_at="2026-02-01T09:00:00",
+ direction="incoming",
+ amount="100.00",
+ currency="CNY",
+ funding_source="other",
+ requested_subject="receivable",
+ request_key="hel157-manual-1",
+ actor=actor,
+ )
+ counts = dashboard.audit_counts(self.connection)
+ self.assertEqual(1, counts["medium"])
+ self.assertEqual(0, counts["high"])
+ self.assertEqual(1, counts["total"])
+
+ def test_audit_total_equals_queue_sum(self) -> None:
+ """Homepage / badge / audit heading must share one backend total."""
+ now = master_data.utc_now()
+ a = self.connection.execute(
+ "INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
+ "VALUES ('甲公司', NULL, NULL, 'active', ?, ?)",
+ (now, now),
+ ).lastrowid
+ self.connection.commit()
+ master_data.submit_bank_account(
+ self.connection,
+ company_id=int(a),
+ bank_name="工行",
+ account_type="一般户",
+ account_number="6222020000000002",
+ start_date="2026-01-01",
+ actor=None,
+ )
+ event_id = self.connection.execute(
+ "INSERT INTO canonical_transfer_events (lifecycle, created_at) VALUES ('active', ?)",
+ (now,),
+ ).lastrowid
+ decision_id = self.connection.execute(
+ """
+ INSERT INTO transfer_match_decisions (
+ event_id, revision, effective_at, amount, currency, classification,
+ pairing, locked, mode, rule_version, created_at
+ ) VALUES (?, 1, '2026-07-05T10:00:00', '100000.00', 'CNY', 'unresolved',
+ 'single', 0, 'manual', 'test', ?)
+ """,
+ (event_id, now),
+ ).lastrowid
+ self.connection.execute(
+ "INSERT INTO current_transfer_decisions (event_id, decision_id) VALUES (?, ?)",
+ (event_id, decision_id),
+ )
+ self.connection.commit()
+
+ counts = dashboard.audit_counts(self.connection)
+ payload = dashboard.build_dashboard(
+ self.connection, from_date="2026-01-01", cutoff="2026-08-20"
+ )
+ self.assertEqual(counts, payload["audit"])
+ self.assertEqual(
+ counts["total"],
+ counts["high"] + counts["medium"] + counts["low"],
+ )
+ self.assertEqual(1, counts["high"])
+ self.assertEqual(1, counts["medium"])
+ self.assertEqual(2, counts["total"])
+
def test_company_summaries_from_eligible_events(self) -> None:
now = master_data.utc_now()
a = self.connection.execute(
@@ -180,6 +270,49 @@ class DashboardApiTests(unittest.TestCase):
self.assertEqual(1, len(data["companies"]))
self.assertEqual("甲公司", data["companies"][0]["name"])
+ def test_audit_counts_sync_after_account_review(self) -> None:
+ """Pending account raises dashboard.audit; approve brings total back down."""
+ from bank_importer.db import connect as db_connect
+ from bank_importer import master_data as md
+
+ status, _, raw = self.admin.get("/api/admin/companies")
+ company_id = as_json(raw)["companies"][0]["id"]
+
+ connection = db_connect(self.db_path)
+ try:
+ account = md.submit_bank_account(
+ connection,
+ company_id=company_id,
+ bank_name="工行",
+ account_type="一般户",
+ account_number="6222020000000099",
+ start_date="2026-01-01",
+ actor=None,
+ )
+ account_id = account["id"]
+ finally:
+ connection.close()
+
+ status, _, raw = self.admin.get("/api/admin/dashboard")
+ before = as_json(raw)["audit"]
+ self.assertEqual(200, status)
+ self.assertGreaterEqual(before["medium"], 1)
+ self.assertGreaterEqual(before["total"], 1)
+
+ status, _, raw = self.admin.post_json(
+ f"/api/admin/accounts/{account_id}/review",
+ {"decision": "approve", "reason": "HEL-157 sync test"},
+ )
+ self.assertEqual(200, status, raw)
+
+ status, _, raw = self.admin.get("/api/admin/dashboard")
+ after = as_json(raw)["audit"]
+ self.assertEqual(after["medium"], before["medium"] - 1)
+ self.assertEqual(after["total"], before["total"] - 1)
+ self.assertEqual(
+ after["total"], after["high"] + after["medium"] + after["low"]
+ )
+
def test_company_detail_missing(self) -> None:
status, _, raw = self.admin.get(
"/api/admin/dashboard/companies/999999?from=2026-01-01&cutoff=2026-08-20"
diff --git a/web/admin.html b/web/admin.html
index 2289fde..395e633 100644
--- a/web/admin.html
+++ b/web/admin.html
@@ -5,7 +5,7 @@
管理端 · 金牛集团
-
+
跳到主要内容
@@ -20,7 +20,7 @@
日常
管理总览
往来查询
- 审核中心6
+ 审核中心0
流水管理
基础与结账
公司与账号
@@ -82,36 +82,21 @@
-
-
-
- 各公司往来分布期末净值 · 单位:万元
-
-
-
-
-
- 本周资金流入流出集团内往来 · 单位:万元
-
-
-
-
-
-
-
-
+
+
+
公司往来合计单位:万元
-
-
+
-
-
+
+
公司往来明细选择左侧公司查看会计式分级明细
-