05/31 Phase 2

This commit is contained in:
2026-05-31 11:17:53 -04:00
parent 1c34dcbd28
commit 9f2a0acc38
18 changed files with 1817 additions and 391 deletions
+151 -3
View File
@@ -1,10 +1,158 @@
from flask import Blueprint, render_template
from flask_login import login_required
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.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():
return render_template('dashboard/index.html')
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],
}
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)
@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],
})