164 lines
6.5 KiB
Python
164 lines
6.5 KiB
Python
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
|
|
from app.services.ai_service import get_latest_daily_insight
|
|
from app.services.account_service import get_total_assets, get_total_liabilities
|
|
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 ─────────────────────────────────────
|
|
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],
|
|
})
|