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",
|
||||
|
||||
Reference in New Issue
Block a user