from flask import Blueprint, render_template, request, jsonify from flask_login import login_required, current_user from sqlalchemy import func, extract from app.extensions import db from app.models.account import Account from app.models.transaction import Transaction from app.models.category import Category from app.services.fx_service import get_today_rate, get_rate_history, force_refresh from app.services.ai_service import get_latest_daily_insight from app.services.account_service import get_total_assets, get_total_liabilities, recalc_all from datetime import date, datetime, timedelta import calendar dashboard_bp = Blueprint('dashboard', __name__) def _parse_date_range(period): """Return (date_from, date_to, label) for a given period string.""" today = date.today() if period == 'last_month': first = (today.replace(day=1) - timedelta(days=1)).replace(day=1) last = today.replace(day=1) - timedelta(days=1) label = first.strftime('%B %Y') elif period == 'custom': try: date_from = datetime.strptime(request.args.get('date_from', ''), '%Y-%m-%d').date() date_to = datetime.strptime(request.args.get('date_to', ''), '%Y-%m-%d').date() except ValueError: date_from = today.replace(day=1) date_to = today return date_from, date_to, 'Custom Range' else: # this_month (default) first = today.replace(day=1) last = today label = first.strftime('%B %Y') return first, last, label @dashboard_bp.route('/') @login_required def index(): period = request.args.get('period', 'this_month') date_from, date_to, period_label = _parse_date_range(period) # ── Summary cards ──────────────────────────────── total_income = db.session.query( func.coalesce(func.sum(Transaction.amount), 0) ).filter( Transaction.transaction_type == 'income', Transaction.date >= date_from, Transaction.date <= date_to, ).scalar() total_expense = db.session.query( func.coalesce(func.sum(Transaction.amount), 0) ).filter( Transaction.transaction_type == 'expense', Transaction.date >= date_from, Transaction.date <= date_to, ).scalar() net_cash_flow = float(total_income) - float(total_expense) # ── Accounts ───────────────────────────────────── recalc_all() accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all() total_assets = get_total_assets() total_liabilities = get_total_liabilities() net_worth = total_assets - total_liabilities # ── Top expense categories ──────────────────────── top_categories = db.session.query( Category.name, Category.color, Category.icon, func.sum(Transaction.amount).label('total') ).join(Transaction, Transaction.category_id == Category.id)\ .filter( Transaction.transaction_type == 'expense', Transaction.date >= date_from, Transaction.date <= date_to, ).group_by(Category.id)\ .order_by(func.sum(Transaction.amount).desc())\ .limit(5).all() # ── Cash flow chart (last 6 months) ────────────── chart_months = [] chart_income = [] chart_expense = [] today = date.today() for i in range(5, -1, -1): # go back i months from current month_date = (today.replace(day=1) - timedelta(days=i * 28)).replace(day=1) last_day = calendar.monthrange(month_date.year, month_date.month)[1] m_start = month_date m_end = month_date.replace(day=last_day) inc = db.session.query( func.coalesce(func.sum(Transaction.amount), 0) ).filter( Transaction.transaction_type == 'income', Transaction.date >= m_start, Transaction.date <= m_end, ).scalar() exp = db.session.query( func.coalesce(func.sum(Transaction.amount), 0) ).filter( Transaction.transaction_type == 'expense', Transaction.date >= m_start, Transaction.date <= m_end, ).scalar() chart_months.append(month_date.strftime('%b %Y')) chart_income.append(float(inc)) chart_expense.append(float(exp)) # ── Recent transactions ─────────────────────────── recent_txns = Transaction.query\ .filter(Transaction.transaction_type.in_(['income', 'expense']))\ .order_by(Transaction.date.desc(), Transaction.id.desc())\ .limit(8).all() # ── USD/VND rate ────────────────────────────────── fx = get_today_rate() fx_history = get_rate_history(30) fx_history_data = { 'dates': [r.date.strftime('%b %d') for r in fx_history], 'rates': [float(r.usd_to_vnd) for r in fx_history], } # ── AI insight ────────────────────────────────────── ai_insight = get_latest_daily_insight() return render_template('dashboard/index.html', period=period, period_label=period_label, date_from=date_from, date_to=date_to, total_income=float(total_income), total_expense=float(total_expense), net_cash_flow=net_cash_flow, accounts=accounts, total_assets=total_assets, total_liabilities=total_liabilities, net_worth=net_worth, top_categories=top_categories, chart_months=chart_months, chart_income=chart_income, chart_expense=chart_expense, recent_txns=recent_txns, fx=fx, fx_history_data=fx_history_data, ai_insight=ai_insight) @dashboard_bp.route('/api/fx-history') @login_required def fx_history_api(): history = get_rate_history(30) return jsonify({ 'dates': [r.date.strftime('%b %d') for r in history], 'rates': [float(r.usd_to_vnd) for r in history], }) @dashboard_bp.route('/api/fx-refresh', methods=['POST']) @login_required def fx_refresh(): """Force-refresh today's USD/VND rate.""" result = force_refresh() if result: return jsonify({ 'rate': result['rate'], 'date': result['date'].strftime('%b %d, %Y'), 'source': result['source'], 'is_stale': result['is_stale'], }) return jsonify({'error': 'Could not fetch rate'}), 503