HEL-342: 管理总览 KPI 绑定真实 dashboard 接口
去掉写死的演示金额与账期进度,改由 /api/admin/dashboard 的 totals 与 period_progress 填充;无数据走空态。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
b905cba52c
commit
b2bcd47e62
@@ -8,6 +8,7 @@ as null so the UI shows an em dash rather than a guessed label.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from calendar import monthrange
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import sqlite3
|
||||
@@ -22,6 +23,12 @@ def today_shanghai() -> str:
|
||||
return datetime.now(timezone(timedelta(hours=8))).date().isoformat()
|
||||
|
||||
|
||||
def _month_bounds(year_month: str) -> tuple[str, str]:
|
||||
year, month = (int(part) for part in year_month.split("-"))
|
||||
last = monthrange(year, month)[1]
|
||||
return f"{year_month}-01", f"{year_month}-{last:02d}"
|
||||
|
||||
|
||||
def _q2(value: Decimal) -> str:
|
||||
return str(value.quantize(TWOPLACES, rounding=ROUND_HALF_UP))
|
||||
|
||||
@@ -209,6 +216,108 @@ def company_summaries(
|
||||
return items, totals
|
||||
|
||||
|
||||
def period_progress(
|
||||
connection: sqlite3.Connection, *, year_month: str
|
||||
) -> dict[str, object]:
|
||||
"""Per-company coverage of the cutoff month: done / in progress / unsubmitted.
|
||||
|
||||
Only companies with at least one active bank account are counted — the same
|
||||
rule as monthly-close submission checks. Covered = confirmed source rows in
|
||||
the month, or an approved no-business attestation that spans the month.
|
||||
"""
|
||||
start, end = _month_bounds(year_month)
|
||||
enabled_rows = connection.execute(
|
||||
"SELECT DISTINCT company_id FROM bank_accounts WHERE status = 'active'"
|
||||
).fetchall()
|
||||
enabled_ids = {int(row["company_id"]) for row in enabled_rows}
|
||||
empty = {
|
||||
"year_month": year_month,
|
||||
"enabled_count": 0,
|
||||
"done": 0,
|
||||
"in_progress": 0,
|
||||
"unsubmitted": 0,
|
||||
"percent": 0,
|
||||
}
|
||||
if not enabled_ids:
|
||||
return empty
|
||||
|
||||
submitted = {
|
||||
int(row["company_id"])
|
||||
for row in connection.execute(
|
||||
"""
|
||||
SELECT DISTINCT b.company_id AS 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 sheet_reviews rv ON rv.sheet_batch_id = s.id AND rv.review_status = 'confirmed'
|
||||
WHERE date(r.transaction_at) >= date(?) AND date(r.transaction_at) <= date(?)
|
||||
""",
|
||||
(start, end),
|
||||
).fetchall()
|
||||
}
|
||||
attested = {
|
||||
int(row["company_id"])
|
||||
for row in connection.execute(
|
||||
"""
|
||||
SELECT DISTINCT a.company_id AS company_id
|
||||
FROM no_business_attestations a
|
||||
WHERE a.status = 'approved'
|
||||
AND date(a.gap_start) <= date(?)
|
||||
AND date(a.gap_end) >= date(?)
|
||||
""",
|
||||
(end, start),
|
||||
).fetchall()
|
||||
}
|
||||
covered = (submitted | attested) & enabled_ids
|
||||
|
||||
pending: set[int] = set()
|
||||
for row in connection.execute(
|
||||
"""
|
||||
SELECT DISTINCT p.company_id AS company_id
|
||||
FROM current_transfer_decisions c
|
||||
JOIN transfer_match_decisions d ON d.id = c.decision_id
|
||||
JOIN canonical_transfer_events e ON e.id = c.event_id
|
||||
JOIN transfer_decision_participants p ON p.decision_id = d.id
|
||||
WHERE e.lifecycle = 'active'
|
||||
AND d.classification IN ('unresolved', 'needs_review')
|
||||
AND date(d.effective_at) >= date(?) AND date(d.effective_at) <= date(?)
|
||||
""",
|
||||
(start, end),
|
||||
):
|
||||
pending.add(int(row["company_id"]))
|
||||
for row in connection.execute(
|
||||
"""
|
||||
SELECT DISTINCT m.company_id AS company_id
|
||||
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'
|
||||
AND date(m.occurred_at) >= date(?) AND date(m.occurred_at) <= date(?)
|
||||
""",
|
||||
(start, end),
|
||||
):
|
||||
pending.add(int(row["company_id"]))
|
||||
for row in connection.execute(
|
||||
"SELECT DISTINCT company_id FROM bank_accounts WHERE status = 'pending'"
|
||||
):
|
||||
pending.add(int(row["company_id"]))
|
||||
|
||||
pending_enabled = pending & enabled_ids
|
||||
done = covered - pending_enabled
|
||||
in_progress = pending_enabled
|
||||
unsubmitted = enabled_ids - covered - pending_enabled
|
||||
enabled_count = len(enabled_ids)
|
||||
percent = round(100 * len(done) / enabled_count) if enabled_count else 0
|
||||
return {
|
||||
"year_month": year_month,
|
||||
"enabled_count": enabled_count,
|
||||
"done": len(done),
|
||||
"in_progress": len(in_progress),
|
||||
"unsubmitted": len(unsubmitted),
|
||||
"percent": percent,
|
||||
}
|
||||
|
||||
|
||||
def company_peer_groups(
|
||||
connection: sqlite3.Connection,
|
||||
company_id: int,
|
||||
@@ -378,13 +487,15 @@ def build_dashboard(
|
||||
companies, totals = company_summaries(
|
||||
connection, from_date=from_date, cutoff=cutoff_date
|
||||
)
|
||||
period_label = f"{period.year}-{period.month:02d}"
|
||||
return {
|
||||
"from_date": from_date,
|
||||
"cutoff": cutoff_date,
|
||||
"period_month": period_month,
|
||||
"period_label": f"{period.year}-{period.month:02d}",
|
||||
"period_label": period_label,
|
||||
"audit": audit_counts(connection),
|
||||
"totals": totals,
|
||||
"period_progress": period_progress(connection, year_month=period_label),
|
||||
"companies": companies,
|
||||
"weekly_flow": weekly_flow(connection, cutoff=cutoff_date),
|
||||
"opening_status": "unavailable",
|
||||
|
||||
@@ -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=16", html)
|
||||
self.assertIn("app.js?v=17", html)
|
||||
# 静态初值仍为进行中(黄),由 JS 在 pending=0 时切 done
|
||||
self.assertRegex(html, r'class="flow-step doing"[^>]*data-view-link="reconcile"')
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ class TransfersPageSourceContractTests(unittest.TestCase):
|
||||
self.assertIn('id="transferEvidenceDrawer"', html)
|
||||
self.assertIn("期间净变动", html)
|
||||
self.assertNotIn("本公司往来合计", html)
|
||||
self.assertIn("design-system.css?v=11", html)
|
||||
self.assertIn("app.js?v=16", html)
|
||||
self.assertIn("design-system.css?v=12", html)
|
||||
self.assertIn("app.js?v=17", html)
|
||||
# 侧栏顺序:流水管理 → 转账往来 → 往来确认
|
||||
flows = html.index('data-view="flows"')
|
||||
transfers = html.index('data-view="transfers"')
|
||||
|
||||
@@ -33,6 +33,42 @@ class DashboardUnitTests(unittest.TestCase):
|
||||
self.assertEqual(0, payload["audit"]["total"])
|
||||
self.assertEqual([], payload["companies"])
|
||||
self.assertEqual(7, len(payload["weekly_flow"]["labels"]))
|
||||
self.assertEqual(0, payload["totals"]["company_count"])
|
||||
self.assertEqual(0, payload["totals"]["detail_count"])
|
||||
self.assertEqual("0.00", payload["totals"]["debit_wan"])
|
||||
self.assertEqual("0.00", payload["totals"]["credit_wan"])
|
||||
progress = payload["period_progress"]
|
||||
self.assertEqual("2026-08", progress["year_month"])
|
||||
self.assertEqual(0, progress["enabled_count"])
|
||||
self.assertEqual(0, progress["done"])
|
||||
self.assertEqual(0, progress["in_progress"])
|
||||
self.assertEqual(0, progress["unsubmitted"])
|
||||
self.assertEqual(0, progress["percent"])
|
||||
|
||||
def test_period_progress_unsubmitted_when_account_active_no_flows(self) -> None:
|
||||
now = master_data.utc_now()
|
||||
cursor = self.connection.execute(
|
||||
"INSERT INTO companies (name, credit_code, cashier_name, status, created_at, updated_at) "
|
||||
"VALUES ('甲公司', NULL, NULL, 'active', ?, ?)",
|
||||
(now, now),
|
||||
)
|
||||
company_id = int(cursor.lastrowid)
|
||||
self.connection.execute(
|
||||
"""
|
||||
INSERT INTO bank_accounts (
|
||||
company_id, account_number, bank_name, account_type, status,
|
||||
created_at, updated_at
|
||||
) VALUES (?, '6222020000000099', '工行', '一般户', 'active', ?, ?)
|
||||
""",
|
||||
(company_id, now, now),
|
||||
)
|
||||
self.connection.commit()
|
||||
progress = dashboard.period_progress(self.connection, year_month="2026-08")
|
||||
self.assertEqual(1, progress["enabled_count"])
|
||||
self.assertEqual(0, progress["done"])
|
||||
self.assertEqual(0, progress["in_progress"])
|
||||
self.assertEqual(1, progress["unsubmitted"])
|
||||
self.assertEqual(0, progress["percent"])
|
||||
|
||||
def test_pending_account_counts_as_medium(self) -> None:
|
||||
now = master_data.utc_now()
|
||||
@@ -267,6 +303,9 @@ class DashboardApiTests(unittest.TestCase):
|
||||
self.assertEqual(200, status)
|
||||
self.assertEqual("ok", data["status"])
|
||||
self.assertIn("audit", data)
|
||||
self.assertIn("totals", data)
|
||||
self.assertIn("period_progress", data)
|
||||
self.assertIn("debit_wan", data["totals"])
|
||||
self.assertEqual(1, len(data["companies"]))
|
||||
self.assertEqual("甲公司", data["companies"][0]["name"])
|
||||
|
||||
@@ -494,5 +533,39 @@ class DashboardApiTests(unittest.TestCase):
|
||||
self.assertEqual([], detail["groups"])
|
||||
|
||||
|
||||
class DashboardPageContractTests(unittest.TestCase):
|
||||
"""HEL-343 阻断项:管理总览 KPI 不得写死演示金额。"""
|
||||
|
||||
def test_admin_html_kpi_placeholders_not_demo_amounts(self) -> None:
|
||||
html = (Path(__file__).resolve().parents[1] / "web" / "admin.html").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn('id="dashPageSub"', html)
|
||||
self.assertIn('id="dashDebitValue"', html)
|
||||
self.assertIn('id="dashCreditValue"', html)
|
||||
self.assertIn('id="dashPeriodPercent"', html)
|
||||
self.assertIn('id="dashPeriodBar"', html)
|
||||
self.assertIn('id="dashPeriodFoot"', html)
|
||||
self.assertNotIn("42,040.30", html)
|
||||
self.assertNotIn("39,040.30", html)
|
||||
self.assertNotIn("8.11 亿", html)
|
||||
self.assertNotIn("392 笔", html)
|
||||
self.assertNotIn("3 家已完成 · 2 家在途 · 1 家未提交", html)
|
||||
self.assertNotRegex(html, r'id="flowStart"[^>]*value="2026-07-01"')
|
||||
self.assertIn('id="pending-check-all"', html)
|
||||
self.assertIn("pending-check-all", html)
|
||||
|
||||
def test_app_js_binds_dashboard_totals(self) -> None:
|
||||
js = (Path(__file__).resolve().parents[1] / "web" / "app.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("function applyDashKpis(", js)
|
||||
self.assertIn("applyDashKpis(data)", js)
|
||||
self.assertIn("dashDebitValue", js)
|
||||
self.assertIn("dashCreditValue", js)
|
||||
self.assertIn("period_progress", js)
|
||||
self.assertIn("master.disabled = boxes.length === 0", js)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -37,8 +37,8 @@ class RemindersPageSourceContractTests(unittest.TestCase):
|
||||
self.assertIn('id="reminder-tbody"', html)
|
||||
self.assertIn('id="reminder-tabs"', html)
|
||||
self.assertIn('id="reminder-detail-drawer"', html)
|
||||
self.assertIn("design-system.css?v=11", html)
|
||||
self.assertIn("app.js?v=16", html)
|
||||
self.assertIn("design-system.css?v=12", html)
|
||||
self.assertIn("app.js?v=17", html)
|
||||
pending = html.index('id="pending-reminders-card"')
|
||||
history = html.index('id="reminder-history-card"')
|
||||
send = html.index('id="send-reminder-card"')
|
||||
@@ -135,7 +135,7 @@ class RemindersPageLayoutSmokeTests(unittest.TestCase):
|
||||
|
||||
def test_send_flow_columns_and_no_page_overflow(self) -> None:
|
||||
html = (WEB / "admin.html").read_text(encoding="utf-8")
|
||||
self.assertIn("app.js?v=16", html)
|
||||
self.assertIn("app.js?v=17", html)
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page()
|
||||
|
||||
+16
-16
@@ -11,7 +11,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=11" />
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
</head>
|
||||
<body class="is-app" data-portal="admin">
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
@@ -62,8 +62,8 @@
|
||||
<div class="topbar">
|
||||
<span class="crumb">管理端 / <b id="currentViewName">管理总览</b></span>
|
||||
<div class="topbar-right">
|
||||
<span class="tag">账期 2026-07</span>
|
||||
<span class="tag">统计截止 2026-08-20</span>
|
||||
<span class="tag" id="topbarPeriod">账期 —</span>
|
||||
<span class="tag" id="topbarCutoff">统计截止 —</span>
|
||||
<button class="btn btn-sm" data-view-link="settings">结账检查</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,20 +73,20 @@
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>管理总览</h1>
|
||||
<p class="page-sub">2026-01-01 至 2026-08-20,集团 6 家成员公司相互往来累计发生 8.11 亿元、392 笔明细。页面上任意数字与公司均可逐级穿透,直至银行原始流水。</p>
|
||||
<p class="page-sub" id="dashPageSub">正在读取集团往来汇总…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-4">
|
||||
<div class="card stat-card gold">
|
||||
<div class="stat-label">往来借方总额 · 年初至今</div>
|
||||
<div class="stat-value">42,040.30<span class="unit">万元</span></div>
|
||||
<div class="stat-foot">6 家公司合计 · 明细 392 笔</div>
|
||||
<div class="stat-value" id="dashDebitValue">—<span class="unit">万元</span></div>
|
||||
<div class="stat-foot" id="dashDebitFoot">加载中…</div>
|
||||
</div>
|
||||
<div class="card stat-card gold">
|
||||
<div class="stat-label">往来贷方总额 · 年初至今</div>
|
||||
<div class="stat-value">39,040.30<span class="unit">万元</span></div>
|
||||
<div class="stat-foot">6 家公司合计 · 与借方同源互证</div>
|
||||
<div class="stat-value" id="dashCreditValue">—<span class="unit">万元</span></div>
|
||||
<div class="stat-foot" id="dashCreditFoot">加载中…</div>
|
||||
</div>
|
||||
<div class="card stat-card warn" data-view-link="audit" style="cursor: pointer;">
|
||||
<div class="stat-label">待审核事项 · 审核中心</div>
|
||||
@@ -94,10 +94,10 @@
|
||||
<div class="stat-foot" id="dashAuditFoot">高 0 项 · 中 0 项 · 其余 0 项低风险</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">7 月账期确认进度</div>
|
||||
<div class="stat-value">50<span class="unit">%</span></div>
|
||||
<div class="progress" style="margin-top: 10px;"><span style="width: 50%;"></span></div>
|
||||
<div class="stat-foot" style="margin-top: 8px;">3 家已完成 · 2 家在途 · 1 家未提交</div>
|
||||
<div class="stat-label" id="dashPeriodLabel">账期确认进度</div>
|
||||
<div class="stat-value" id="dashPeriodPercent">—<span class="unit">%</span></div>
|
||||
<div class="progress" style="margin-top: 10px;"><span id="dashPeriodBar" style="width: 0%;"></span></div>
|
||||
<div class="stat-foot" id="dashPeriodFoot" style="margin-top: 8px;">加载中…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -428,11 +428,11 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="flowStart">日期起</label>
|
||||
<input class="input" id="flowStart" type="date" value="2026-07-01" />
|
||||
<input class="input" id="flowStart" type="date" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="flowEnd">日期止</label>
|
||||
<input class="input" id="flowEnd" type="date" value="2026-07-31" />
|
||||
<input class="input" id="flowEnd" type="date" />
|
||||
</div>
|
||||
<div class="field" style="min-width: 200px;">
|
||||
<label for="flowKeyword">关键词</label>
|
||||
@@ -766,7 +766,7 @@
|
||||
<div class="card-head">
|
||||
<span class="card-title">待提醒清单<span class="sub">系统按流水提交、断档、待确认自动发现,点发送即送达对应公司</span></span>
|
||||
<div class="row" style="gap: 10px; align-items: center;">
|
||||
<label class="row" style="gap:6px;flex:none;"><input type="checkbox" class="ds-check" id="pending-check-all" data-check-all aria-label="全选待提醒" /></label>
|
||||
<label class="row" style="gap:6px;flex:none;"><input type="checkbox" class="ds-check" id="pending-check-all" data-check-all aria-label="全选待提醒" disabled /></label>
|
||||
<span class="meta" id="pending-summary">—</span>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="pending-send-all" disabled>全部一键发送</button>
|
||||
</div>
|
||||
@@ -1228,6 +1228,6 @@
|
||||
</div>
|
||||
<div class="toast-region" id="toastRegion" aria-live="polite"></div>
|
||||
<script src="theme.js?v=1"></script>
|
||||
<script src="app.js?v=16"></script>
|
||||
<script src="app.js?v=17"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+67
@@ -1467,6 +1467,63 @@ function updateDashAuditCard(audit) {
|
||||
applyAuditCounts(audit, { fromApi: true });
|
||||
}
|
||||
|
||||
function formatDashWan(value) {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return "0.00";
|
||||
return Math.abs(num).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function applyDashKpis(data) {
|
||||
const totals = data?.totals || {};
|
||||
const from = data?.from_date || "—";
|
||||
const cutoff = data?.cutoff || "—";
|
||||
const periodLabel = data?.period_label || "";
|
||||
const companyCount = Number(totals.company_count) || 0;
|
||||
const detailCount = Number(totals.detail_count) || 0;
|
||||
const debit = formatDashWan(totals.debit_wan);
|
||||
const credit = formatDashWan(totals.credit_wan);
|
||||
|
||||
const pageSub = $("#dashPageSub");
|
||||
if (pageSub) {
|
||||
if (!companyCount && !detailCount) {
|
||||
pageSub.textContent = `${from} 至 ${cutoff},暂无成员公司往来数据。导入并归集银行流水后,金额将显示在此。页面上任意数字与公司均可逐级穿透,直至银行原始流水。`;
|
||||
} else {
|
||||
pageSub.textContent = `${from} 至 ${cutoff},集团 ${companyCount} 家成员公司相互往来累计发生 ${debit} 万元、${detailCount} 笔明细。页面上任意数字与公司均可逐级穿透,直至银行原始流水。`;
|
||||
}
|
||||
}
|
||||
const debitVal = $("#dashDebitValue");
|
||||
if (debitVal) debitVal.innerHTML = `${debit}<span class="unit">万元</span>`;
|
||||
const debitFoot = $("#dashDebitFoot");
|
||||
if (debitFoot) debitFoot.textContent = `${companyCount} 家公司合计 · 明细 ${detailCount} 笔`;
|
||||
const creditVal = $("#dashCreditValue");
|
||||
if (creditVal) creditVal.innerHTML = `${credit}<span class="unit">万元</span>`;
|
||||
const creditFoot = $("#dashCreditFoot");
|
||||
if (creditFoot) creditFoot.textContent = `${companyCount} 家公司合计 · 与借方同源互证`;
|
||||
|
||||
const progress = data?.period_progress || {};
|
||||
const ym = progress.year_month || periodLabel;
|
||||
const monthNum = ym ? Number(String(ym).slice(5, 7)) : 0;
|
||||
const labelEl = $("#dashPeriodLabel");
|
||||
if (labelEl) labelEl.textContent = monthNum ? `${monthNum} 月账期确认进度` : "账期确认进度";
|
||||
const percent = Number(progress.percent) || 0;
|
||||
const pctEl = $("#dashPeriodPercent");
|
||||
if (pctEl) pctEl.innerHTML = `${percent}<span class="unit">%</span>`;
|
||||
const bar = $("#dashPeriodBar");
|
||||
if (bar) bar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
|
||||
const periodFoot = $("#dashPeriodFoot");
|
||||
if (periodFoot) {
|
||||
const enabled = Number(progress.enabled_count) || 0;
|
||||
periodFoot.textContent = enabled
|
||||
? `${progress.done || 0} 家已完成 · ${progress.in_progress || 0} 家在途 · ${progress.unsubmitted || 0} 家未提交`
|
||||
: "尚无已启用银行账户";
|
||||
}
|
||||
|
||||
const tbPeriod = $("#topbarPeriod");
|
||||
if (tbPeriod) tbPeriod.textContent = periodLabel ? `账期 ${periodLabel}` : "账期 —";
|
||||
const tbCutoff = $("#topbarCutoff");
|
||||
if (tbCutoff) tbCutoff.textContent = cutoff && cutoff !== "—" ? `统计截止 ${cutoff}` : "统计截止 —";
|
||||
}
|
||||
|
||||
function renderDashCompanyRows(companies, selectedId) {
|
||||
const list = $("#dashCompanyRows");
|
||||
if (!list) return;
|
||||
@@ -1681,11 +1738,15 @@ async function loadAdminDashboard() {
|
||||
const response = await fetch(`/api/admin/dashboard?from=${encodeURIComponent(from)}`).catch(() => null);
|
||||
if (!response?.ok) {
|
||||
$("#dashCompanyRows").innerHTML = '<div class="dash-company-empty muted">总览加载失败</div>';
|
||||
const pageSub = $("#dashPageSub");
|
||||
if (pageSub) pageSub.textContent = "总览加载失败,请刷新后重试。";
|
||||
return;
|
||||
}
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!data || data.status !== "ok") {
|
||||
$("#dashCompanyRows").innerHTML = '<div class="dash-company-empty muted">总览加载失败</div>';
|
||||
const pageSub = $("#dashPageSub");
|
||||
if (pageSub) pageSub.textContent = "总览加载失败,请刷新后重试。";
|
||||
return;
|
||||
}
|
||||
state.dashCompanies = data.companies || [];
|
||||
@@ -1693,6 +1754,7 @@ async function loadAdminDashboard() {
|
||||
state.dashCutoff = data.cutoff;
|
||||
state.dashPeriodMonth = data.period_month;
|
||||
updateDashAuditCard(data.audit);
|
||||
applyDashKpis(data);
|
||||
const listSub = $("#dashListSub");
|
||||
if (listSub) {
|
||||
listSub.textContent = `${data.from_date} 至 ${data.cutoff} · 单位:万元 · 左侧选择公司,右侧查看其往来明细`;
|
||||
@@ -1837,8 +1899,13 @@ function syncPendingCheckAll() {
|
||||
if (!master) return;
|
||||
const boxes = $$(".pending-check");
|
||||
const checked = boxes.filter((el) => el.checked).length;
|
||||
master.disabled = boxes.length === 0;
|
||||
master.checked = boxes.length > 0 && checked === boxes.length;
|
||||
master.indeterminate = checked > 0 && checked < boxes.length;
|
||||
if (boxes.length === 0) {
|
||||
master.checked = false;
|
||||
master.indeterminate = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdminRemindersHistory(sourceFilter) {
|
||||
|
||||
+6
-6
@@ -11,7 +11,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=11" />
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
</head>
|
||||
<body class="is-app" data-portal="company">
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
@@ -62,8 +62,8 @@
|
||||
<div class="topbar">
|
||||
<span class="crumb">公司业务端 / <b id="currentViewName">工作台</b></span>
|
||||
<div class="topbar-right">
|
||||
<span class="tag">账期 2026-07</span>
|
||||
<span class="tag">统计截止 2026-08-20</span>
|
||||
<span class="tag" id="topbarPeriod">账期 —</span>
|
||||
<span class="tag" id="topbarCutoff">统计截止 —</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -415,11 +415,11 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="flowStart">日期起</label>
|
||||
<input class="input num-input" id="flowStart" type="date" value="2026-07-01" />
|
||||
<input class="input num-input" id="flowStart" type="date" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="flowEnd">日期止</label>
|
||||
<input class="input num-input" id="flowEnd" type="date" value="2026-08-20" />
|
||||
<input class="input num-input" id="flowEnd" type="date" />
|
||||
</div>
|
||||
<div class="field" style="min-width: 200px;">
|
||||
<label for="flowKeyword">关键词</label>
|
||||
@@ -1044,6 +1044,6 @@
|
||||
|
||||
<div class="toast-region" id="toastRegion" aria-live="polite"></div>
|
||||
<script src="theme.js?v=1"></script>
|
||||
<script src="app.js?v=16"></script>
|
||||
<script src="app.js?v=17"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1111,7 +1111,7 @@ a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, trans
|
||||
|
||||
/* ─── 入口页(index) ────────────────────────────────────────────── */
|
||||
.portal-wrap { min-height: 100vh; display: grid; place-items: center; padding: 40px 24px; }
|
||||
.portal-inner { width: 100%; max-width: 880px; }
|
||||
.portal-inner { width: 100%; max-width: 880px; position: relative; }
|
||||
.portal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 28px; }
|
||||
.portal-card {
|
||||
display: block;
|
||||
|
||||
+2
-2
@@ -10,12 +10,12 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=11" />
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="portal-wrap">
|
||||
<div class="portal-inner">
|
||||
<div class="login-theme" style="position:absolute;top:18px;right:18px;width:148px;">
|
||||
<div class="login-theme">
|
||||
<div class="theme-seg" role="group" aria-label="主题">
|
||||
<button type="button" data-theme-set="day" class="is-active" aria-pressed="true">日间</button>
|
||||
<button type="button" data-theme-set="night" aria-pressed="false">夜间</button>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=11" />
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
</head>
|
||||
<body class="login-page" data-role="admin">
|
||||
<div class="login-wrap">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=11" />
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
</head>
|
||||
<body class="login-page" data-role="company">
|
||||
<div class="login-wrap">
|
||||
|
||||
Reference in New Issue
Block a user