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κ°€ μ—†λŠ” κ³„μ’Œ/μžμ‚°μ€ μžμ‚° μœ λ™μ„± κ³„μ‚°μ—μ„œ μ œμ™Έν–ˆμŠ΅λ‹ˆλ‹€. κ³„μ’Œ ν™”λ©΄μ—μ„œ κΈ°μ€€ μž”μ•‘κ³Ό κΈ°μ€€ μ‹œκ°μ„ μž…λ ₯ν•˜μ„Έμš”.")