104 lines
7.2 KiB
Python
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가 없는 계좌/자산은 자산 유동성 계산에서 제외했습니다. 계좌 화면에서 기준 잔액과 기준 시각을 입력하세요.")
|