Files
jarca-ledger-mcp/dashboard/page_views/overview.py
T

104 lines
7.2 KiB
Python

from __future__ import annotations
# ruff: noqa: F405
import pandas as pd
import plotly.express as px
import streamlit as st
from jarca_ledger_mcp.adapters.streamlit import overview_gateway
from jarca_ledger_mcp.adapters.streamlit import reconciliation_gateway
from dashboard.common import * # noqa: F403, F405
def page_overview(db_path: str) -> None:
health = overview_gateway.health_check(db_path)
st.caption(f"DB: `{health.get('db_path')}`")
month = current_kst_month()
cashflow = overview_gateway.get_monthly_cashflow_report(month, db_path=db_path)
asset_report = overview_gateway.get_asset_liquidity_report(db_path=db_path)
budget_report = overview_gateway.get_budget_vs_actual_report(month, db_path=db_path)
uncategorized_res = overview_gateway.list_uncategorized_transactions(limit=1000, db_path=db_path)
transfer_queue_res = overview_gateway.list_cross_transfer_review_queue(limit=300, db_path=db_path)
reconciliation_res = reconciliation_gateway.get_balance_reconciliation_report(db_path=db_path)
review_indicators_res = reconciliation_gateway.get_review_operating_indicators(db_path=db_path)
review_indicators = review_indicators_res.get("indicators", {}) if review_indicators_res.get("success") else {}
backups_res = overview_gateway.list_ledger_backups(limit=1)
imports_res = overview_gateway.list_ledger_import_batches(limit=1, db_path=db_path)
render_section_label("💎", "이번 달 수입/지출 현황")
st.caption(f"기준: KST 현재 월 {month}, transactions.transaction_date, ledger_categories.cashflow_kind 실제 현금흐름")
c1, c2, c3, c4 = st.columns(4)
c1.metric("실제 수입", format_krw(cashflow.get("income", 0)))
c2.metric("실제 지출", format_krw(cashflow.get("expenses", 0)))
c3.metric("순현금흐름", format_krw(cashflow.get("net_cashflow", 0)))
c4.metric("저축률", format_percent(cashflow.get("savings_rate")))
render_section_label("🏦", "자산 유동성 현황")
st.caption("기준: service 공통 평가 헬퍼(accounts.last_balance + manual_assets 보강). 거래 합계로 비거래 자산을 추정하지 않습니다.")
a1, a2, a3 = st.columns(3)
a1.metric("자산 합계(부채 제외)", format_krw(asset_report.get("total_assets", 0)))
a2.metric("총부채", format_krw(asset_report.get("total_liabilities", 0)))
a3.metric("순자산", format_krw(asset_report.get("net_worth", 0)))
buckets_df = pd.DataFrame(asset_report.get("buckets", []))
if not buckets_df.empty:
buckets_df["label"] = buckets_df["liquidity_bucket"].map(LIQUIDITY_BUCKET_LABELS).fillna(buckets_df["liquidity_bucket"])
buckets_df["금액"] = buckets_df["amount"].map(format_krw)
buckets_df["구성비"] = buckets_df["asset_share"].map(format_percent)
st.dataframe(buckets_df[["label", "liquidity_bucket", "금액", "account_count", "구성비"]], width="stretch", height=250)
chart_df = buckets_df[(buckets_df["liquidity_bucket"] != "loan") & (buckets_df["amount"] != 0)]
if not chart_df.empty:
st.plotly_chart(px.pie(chart_df, names="label", values="amount", title="자산 구성(loan 제외)"), width="stretch")
render_section_label("🧭", "자산 구성")
source_accounts = pd.DataFrame(asset_report.get("accounts", []))
manual_assets = pd.DataFrame(asset_report.get("manual_assets", []))
if not source_accounts.empty:
source_accounts["bucket_label"] = source_accounts["liquidity_bucket"].map(LIQUIDITY_BUCKET_LABELS).fillna(source_accounts["liquidity_bucket"])
source_accounts["last_balance_display"] = source_accounts["last_balance"].map(format_krw)
show_cols = ["name", "type", "bucket_label", "last_balance_display", "last_synced_at", "memo"]
st.dataframe(source_accounts[show_cols], width="stretch", height=220)
else:
st.info("last_balance가 입력된 활성 계좌/자산 항목이 없습니다.")
if not manual_assets.empty:
manual_assets["bucket_label"] = manual_assets["liquidity_bucket"].map(LIQUIDITY_BUCKET_LABELS).fillna(manual_assets["liquidity_bucket"])
manual_assets["valuation_display"] = manual_assets["amount"].map(format_krw)
st.caption("수기 자산/부채(manual_assets) — 비거래 자산 권위 소스")
st.dataframe(manual_assets[["name", "bucket_label", "valuation_display", "valuation_date", "source_note", "memo"]], width="stretch", height=180)
render_section_label("📉", "지출 구조/예산")
budgets = pd.DataFrame(budget_report.get("items", []))
if not budgets.empty:
over_count = int((budgets["status"] == "over").sum()) if "status" in budgets else 0
b1, b2, b3 = st.columns(3)
b1.metric("월 예산", format_krw(budget_report.get("total_budget", 0)))
b2.metric("예산 대상 지출", format_krw(budget_report.get("total_actual", 0)))
b3.metric("초과 항목", f"{over_count}건")
budgets["budget_display"] = budgets["budget"].map(format_krw)
budgets["actual_display"] = budgets["actual"].map(format_krw)
budgets["remaining_display"] = budgets["remaining"].map(format_krw)
budgets["usage_rate_display"] = budgets["usage_rate"].map(format_percent)
st.dataframe(budgets[["category", "subcategory", "person", "budget_display", "actual_display", "remaining_display", "usage_rate_display", "status"]], width="stretch", height=240)
else:
st.info(f"{month} 예산이 없습니다. 예산 메뉴에서 월별 예산을 등록하면 이 영역에 표시됩니다.")
render_section_label("⚠️", "운영 경고")
review_items = get_transfer_review_items(transfer_queue_res)
reconciliation_items = reconciliation_res.get("items", []) if reconciliation_res.get("success") else []
reconciliation_review = [item for item in reconciliation_items if item.get("status") == "REVIEW_NEEDED"]
missing_balance = asset_report.get("missing_balance_accounts", [])
w1, w2, w3, w4, w5 = st.columns(5)
w1.metric("미처리 review", f"{review_indicators.get('pending_review_count', len(review_items)):,}건")
w2.metric("보류/재검토", f"{review_indicators.get('held_recheck_count', 0):,}건")
w3.metric("미대조 계좌", f"{review_indicators.get('unreconciled_account_count', len(reconciliation_review)):,}개")
w4.metric("최근 대조 마감", review_indicators.get("last_reconciliation_closed_at") or "-")
w5.metric("미분류 추천 대기", f"{review_indicators.get('uncategorized_recommendation_backlog', uncategorized_res.get('count', 0)):,}건")
latest_backup = (backups_res.get("backups") or [{}])[0] if backups_res.get("success") else {}
latest_import = (imports_res.get("import_batches") or [{}])[0] if imports_res.get("success") else {}
st.caption(
"상세 이동: 미분류 → 미분류, 확인필요 큐 → 확인필요 큐, 잔액대조 필요/잔액 미입력 → 계좌, "
f"최근 백업: {latest_backup.get('name', '없음')}, 최근 import: {latest_import.get('source', '없음')} {latest_import.get('imported_at', '')}"
)
if missing_balance:
st.warning("last_balance가 없는 계좌/자산은 자산 유동성 계산에서 제외했습니다. 계좌 화면에서 기준 잔액과 기준 시각을 입력하세요.")