05/31 Phase 2
This commit is contained in:
+151
-3
@@ -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],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user