budgeting app
This commit is contained in:
parent
72fed6e258
commit
233083fa4f
304
budgeting_website/app.py
Normal file
304
budgeting_website/app.py
Normal file
@ -0,0 +1,304 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
||||
import pandas as pd
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from plotly.utils import PlotlyJSONEncoder
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "your-secret-key"
|
||||
app.config['UPLOAD_FOLDER'] = 'uploads'
|
||||
app.config['DATA_FOLDER'] = 'data'
|
||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
||||
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
os.makedirs(app.config['DATA_FOLDER'], exist_ok=True)
|
||||
|
||||
REQUIRED_COLUMNS = [
|
||||
"Date", "Transaction ID", "Transaction Type", "Currency",
|
||||
"Amount", "Fee", "Net Amount", "Asset Type", "Asset Price",
|
||||
"Asset Amount", "Status", "Notes", "Name of sender/receiver", "Account"
|
||||
]
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() == 'csv'
|
||||
|
||||
|
||||
MERCHANT_MAP = {
|
||||
"fast_food": ["mcdonald", "wendy", "kfc", "taco", "popeyes", "burger", "subway", "chipotle"],
|
||||
"restaurant": ["olive garden", "applebee", "chili", "roadhouse", "ihop", "panda express", "buffalo wild wings"],
|
||||
"shopping": ["walmart", "target", "amazon", "ebay", "costco", "best buy"],
|
||||
"subscription": ["netflix", "spotify", "hulu", "apple", "youtube", "icloud", "google", "xbox"],
|
||||
"gas": ["kwik trip", "shell", "bp", "exxon", "chevron"],
|
||||
"banking": ["transfer", "add cash", "savings internal transfer", "interest"],
|
||||
}
|
||||
|
||||
def classify_merchant(name):
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
return "unknown"
|
||||
|
||||
name = name.strip().lower()
|
||||
|
||||
for category, keywords in MERCHANT_MAP.items():
|
||||
if any(k in name for k in keywords):
|
||||
return category
|
||||
|
||||
words = name.title().split()
|
||||
|
||||
if len(words) in [2, 3] and all(w.isalpha() and w[0].isupper() for w in words):
|
||||
return "person"
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
|
||||
def generate_recommendations(df):
|
||||
tips = []
|
||||
now = datetime.now()
|
||||
current_month = now.strftime('%Y-%m')
|
||||
last_month = (now - relativedelta(months=1)).strftime('%Y-%m')
|
||||
|
||||
df['Month'] = df['Date'].dt.strftime('%Y-%m')
|
||||
|
||||
recurring = (
|
||||
df[df['Amount'] < 0]
|
||||
.groupby('Name of sender/receiver')['Month']
|
||||
.nunique()
|
||||
.reset_index()
|
||||
)
|
||||
recurring = recurring[recurring['Month'] >= 2]
|
||||
|
||||
for _, row in recurring.iterrows():
|
||||
name = row['Name of sender/receiver']
|
||||
kind = classify_merchant(name)
|
||||
|
||||
if kind == "subscription":
|
||||
tips.append(f"You're regularly paying {name}, which looks like a subscription.")
|
||||
elif kind == "person":
|
||||
tips.append(f"You've been sending money to {name} frequently — consider if it's necessary.")
|
||||
elif kind == "unknown":
|
||||
tips.append(f"You’ve made payments to {name} in {row['Month']} different months. Review if it's essential.")
|
||||
|
||||
potential_subs = df[
|
||||
(df['Amount'] < 0) &
|
||||
(df['Amount'].abs() <= 20) &
|
||||
(df['Name of sender/receiver'].str.lower().str.contains("netflix|spotify|hulu|apple|google|amazon|prime", na=False))
|
||||
]
|
||||
|
||||
for name in potential_subs['Name of sender/receiver'].unique():
|
||||
total = abs(potential_subs[potential_subs['Name of sender/receiver'] == name]['Amount'].sum())
|
||||
tips.append(f"You may be subscribed to {name} — you've spent ${total:.2f} there this month.")
|
||||
|
||||
# --- Fast food / Restaurant alerts ---
|
||||
for kind in ['fast_food', 'restaurant']:
|
||||
matches = df[
|
||||
df['Name of sender/receiver']
|
||||
.str.lower()
|
||||
.fillna('')
|
||||
.apply(lambda x: any(k in x for k in MERCHANT_MAP[kind])) & (df['Amount'] < 0)
|
||||
]
|
||||
|
||||
if not matches.empty:
|
||||
totals = matches.groupby('Name of sender/receiver')['Amount'].sum().abs()
|
||||
for vendor, amt in totals.items():
|
||||
if kind == "fast_food":
|
||||
tips.append(f"You spent ${amt:.2f} at {vendor} this month. Maybe try cooking more?")
|
||||
else:
|
||||
tips.append(f"You spent ${amt:.2f} dining at {vendor} this month.")
|
||||
|
||||
# --- Month-to-month trend per vendor ---
|
||||
top_this_month = (
|
||||
df[(df['Month'] == current_month) & (df['Amount'] < 0)]
|
||||
.groupby('Name of sender/receiver')['Amount']
|
||||
.sum()
|
||||
.abs()
|
||||
.sort_values(ascending=False)
|
||||
)
|
||||
|
||||
top_last_month = (
|
||||
df[(df['Month'] == last_month) & (df['Amount'] < 0)]
|
||||
.groupby('Name of sender/receiver')['Amount']
|
||||
.sum()
|
||||
.abs()
|
||||
)
|
||||
|
||||
for vendor, this_amt in top_this_month.head(5).items():
|
||||
last_amt = top_last_month.get(vendor, 0)
|
||||
if this_amt > last_amt and last_amt > 0:
|
||||
increase = this_amt - last_amt
|
||||
tips.append(f"You're spending ${increase:.2f} more at {vendor} this month than last month.")
|
||||
|
||||
if not tips:
|
||||
tips.append("You're doing great this month! 🎉 Keep up the mindful spending.")
|
||||
|
||||
return tips
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def clean_dataframe(df):
|
||||
# Strip trailing timezone for parsing
|
||||
df['Date'] = df['Date'].astype(str).str.replace(r'\s+[A-Z]{3,4}$', '', regex=True)
|
||||
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
|
||||
|
||||
# Convert amounts to numeric
|
||||
df['Amount'] = df['Amount'].replace('[\$,]', '', regex=True).astype(float)
|
||||
df['Fee'] = df['Fee'].replace('[\$,]', '', regex=True).astype(float)
|
||||
df['Net Amount'] = df['Net Amount'].replace('[\$,]', '', regex=True).astype(float)
|
||||
|
||||
df = df.dropna(subset=['Date', 'Amount'])
|
||||
|
||||
return df
|
||||
|
||||
def get_dataframe():
|
||||
if 'data_id' in session:
|
||||
path = os.path.join(app.config['DATA_FOLDER'], f"{session['data_id']}.csv")
|
||||
if os.path.exists(path):
|
||||
df = pd.read_csv(path)
|
||||
if all(col in df.columns for col in REQUIRED_COLUMNS):
|
||||
return clean_dataframe(df)
|
||||
return None
|
||||
|
||||
def save_dataframe(df):
|
||||
if 'data_id' not in session:
|
||||
session['data_id'] = str(uuid.uuid4())
|
||||
path = os.path.join(app.config['DATA_FOLDER'], f"{session['data_id']}.csv")
|
||||
df.to_csv(path, index=False)
|
||||
|
||||
def get_monthly_summary(df):
|
||||
df['Month'] = df['Date'].dt.strftime('%Y-%m')
|
||||
expenses = df[df['Amount'] < 0]
|
||||
income = df[df['Amount'] > 0]
|
||||
|
||||
monthly = pd.DataFrame()
|
||||
monthly['Total_Expenses'] = expenses.groupby('Month')['Amount'].sum().abs()
|
||||
monthly['Total_Income'] = income.groupby('Month')['Amount'].sum()
|
||||
monthly = monthly.fillna(0).reset_index()
|
||||
|
||||
monthly['Savings'] = monthly['Total_Income'] - monthly['Total_Expenses']
|
||||
monthly['Savings_Rate'] = (monthly['Savings'] / monthly['Total_Income'].replace(0, 1) * 100).round(1)
|
||||
|
||||
return monthly
|
||||
|
||||
def get_category_spending(df):
|
||||
expenses = df[df['Amount'] < 0]
|
||||
grouped = expenses.groupby('Name of sender/receiver')['Amount'].sum().abs().reset_index()
|
||||
grouped.columns = ['Category', 'Total_Spent']
|
||||
return grouped.sort_values(by='Total_Spent', ascending=False)
|
||||
|
||||
def generate_monthly_trend_chart(summary):
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Bar(x=summary['Month'], y=summary['Total_Income'], name='Income', marker_color='green'))
|
||||
fig.add_trace(go.Bar(x=summary['Month'], y=summary['Total_Expenses'], name='Expenses', marker_color='red'))
|
||||
fig.add_trace(go.Scatter(x=summary['Month'], y=summary['Savings'], mode='lines+markers', name='Net Savings', line=dict(color='blue')))
|
||||
fig.update_layout(title='Monthly Trends', barmode='group', height=400)
|
||||
return json.dumps(fig, cls=PlotlyJSONEncoder)
|
||||
|
||||
def generate_category_pie_chart(category_spending):
|
||||
top = category_spending.head(10)
|
||||
if len(category_spending) > 10:
|
||||
other_sum = category_spending.iloc[10:]['Total_Spent'].sum()
|
||||
top = pd.concat([top, pd.DataFrame([{'Category': 'Other', 'Total_Spent': other_sum}])])
|
||||
fig = px.pie(top, names='Category', values='Total_Spent', hole=0.4)
|
||||
fig.update_layout(title='Spending by Category', height=400)
|
||||
return json.dumps(fig, cls=PlotlyJSONEncoder)
|
||||
|
||||
def calculate_metrics(df):
|
||||
metrics = {}
|
||||
metrics['total_income'] = df[df['Amount'] > 0]['Amount'].sum()
|
||||
metrics['total_expenses'] = abs(df[df['Amount'] < 0]['Amount'].sum())
|
||||
metrics['net_worth'] = df['Net Amount'].iloc[-1] if not df.empty else 0
|
||||
|
||||
today = datetime.now()
|
||||
start = datetime(today.year, today.month, 1)
|
||||
last_month = start - relativedelta(months=1)
|
||||
|
||||
current = df[df['Date'] >= start]
|
||||
prev = df[(df['Date'] >= last_month) & (df['Date'] < start)]
|
||||
|
||||
current_expenses = abs(current[current['Amount'] < 0]['Amount'].sum())
|
||||
prev_expenses = abs(prev[prev['Amount'] < 0]['Amount'].sum())
|
||||
|
||||
metrics['current_month_expenses'] = current_expenses
|
||||
metrics['last_month_expenses'] = prev_expenses
|
||||
metrics['expense_change'] = round(((current_expenses - prev_expenses) / prev_expenses * 100) if prev_expenses else 0, 1)
|
||||
|
||||
return metrics
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
df = get_dataframe()
|
||||
if df is not None:
|
||||
summary = get_monthly_summary(df)
|
||||
categories = get_category_spending(df)
|
||||
trend = generate_monthly_trend_chart(summary)
|
||||
pie = generate_category_pie_chart(categories)
|
||||
metrics = calculate_metrics(df)
|
||||
recent = df.sort_values(by='Date', ascending=False).head(10).to_dict('records')
|
||||
tips = generate_recommendations(df)
|
||||
|
||||
return render_template("dashboard.html",
|
||||
monthly_trend_chart=trend,
|
||||
category_pie_chart=pie,
|
||||
metrics=metrics,
|
||||
monthly_summary=summary.to_dict('records'),
|
||||
recent_transactions=recent,
|
||||
recommendations=tips
|
||||
)
|
||||
return render_template("upload.html")
|
||||
|
||||
|
||||
@app.route('/transactions')
|
||||
def transactions():
|
||||
df = get_dataframe()
|
||||
if df is not None:
|
||||
df = df.sort_values(by='Date', ascending=False)
|
||||
return render_template("transactions.html", transactions=df.to_dict('records'))
|
||||
flash("No data available.")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload_file():
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
return redirect(request.url)
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
return redirect(request.url)
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
try:
|
||||
df = pd.read_csv(file)
|
||||
if not all(col in df.columns for col in REQUIRED_COLUMNS):
|
||||
flash("CSV is missing required columns.")
|
||||
return redirect(url_for('index'))
|
||||
save_dataframe(df)
|
||||
flash("Upload successful.")
|
||||
return redirect(url_for('index'))
|
||||
except Exception as e:
|
||||
flash(f"Error: {e}")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
flash("Only CSVs allowed.")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/clear')
|
||||
def clear_data():
|
||||
if 'data_id' in session:
|
||||
path = os.path.join(app.config['DATA_FOLDER'], f"{session['data_id']}.csv")
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
session.pop('data_id')
|
||||
flash("Data cleared.")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
1093
budgeting_website/data/3c9e4fdf-6d89-4630-998a-c10fe20f4c50.csv
Normal file
1093
budgeting_website/data/3c9e4fdf-6d89-4630-998a-c10fe20f4c50.csv
Normal file
File diff suppressed because it is too large
Load Diff
1093
budgeting_website/data/a7e6e27b-13c7-4216-abd6-a22aba216ffd.csv
Normal file
1093
budgeting_website/data/a7e6e27b-13c7-4216-abd6-a22aba216ffd.csv
Normal file
File diff suppressed because it is too large
Load Diff
183
budgeting_website/templates/dashboard.html
Normal file
183
budgeting_website/templates/dashboard.html
Normal file
@ -0,0 +1,183 @@
|
||||
|
||||
|
||||
<!-- templates/dashboard.html -->
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Financial Overview</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card metric-card h-100">
|
||||
<div class="metric-label">Total Income</div>
|
||||
<div class="metric-value positive">${{ "%0.2f"|format(metrics['total_income']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card metric-card h-100">
|
||||
<div class="metric-label">Total Expenses</div>
|
||||
<div class="metric-value negative">${{ "%0.2f"|format(metrics['total_expenses']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card metric-card h-100">
|
||||
<div class="metric-label">Net Worth</div>
|
||||
<div class="metric-value neutral">${{ "%0.2f"|format(metrics['net_worth']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card metric-card h-100">
|
||||
<div class="metric-label">Current Month Expenses</div>
|
||||
<div class="metric-value negative">${{ "%0.2f"|format(metrics['current_month_expenses']) }}</div>
|
||||
<div class="small {% if metrics['expense_change'] < 0 %}positive{% elif metrics['expense_change'] > 0 %}negative{% else %}neutral{% endif %}">
|
||||
{% if metrics['expense_change'] < 0 %}
|
||||
▼ {{ metrics['expense_change']|abs }}% from last month
|
||||
{% elif metrics['expense_change'] > 0 %}
|
||||
▲ {{ metrics['expense_change'] }}% from last month
|
||||
{% else %}
|
||||
No change from last month
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Smart Recommendations</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for tip in recommendations %}
|
||||
<li class="list-group-item">💡 {{ tip | safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Monthly Income and Expenses</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container" id="monthly-trend-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Spending by Category</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container" id="category-pie-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">Monthly Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Month</th>
|
||||
<th>Income</th>
|
||||
<th>Expenses</th>
|
||||
<th>Savings</th>
|
||||
<th>Savings Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for month in monthly_summary %}
|
||||
<tr>
|
||||
<td>{{ month['Month'] }}</td>
|
||||
<td class="positive">${{ "%0.2f"|format(month['Total_Income']) }}</td>
|
||||
<td class="negative">${{ "%0.2f"|format(month['Total_Expenses']) }}</td>
|
||||
<td class="{% if month['Savings'] >= 0 %}positive{% else %}negative{% endif %}">
|
||||
${{ "%0.2f"|format(month['Savings']) }}
|
||||
</td>
|
||||
<td>
|
||||
{% if month['Total_Income'] > 0 %}
|
||||
{{ month['Savings_Rate'] }}%
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">Recent Transactions</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Account</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in recent_transactions %}
|
||||
<tr>
|
||||
<td>{{ transaction['Date'].strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ transaction['Name of sender/receiver'] }}</td>
|
||||
<td class="{% if transaction['Amount'] >= 0 %}positive{% else %}negative{% endif %}">
|
||||
${{ "%0.2f"|format(transaction['Amount']) }}
|
||||
</td>
|
||||
<td>{{ transaction['Account'] }}</td>
|
||||
<td>{{ transaction['Status'] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<a href="/transactions" class="btn btn-outline-primary">View All Transactions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Render monthly trend chart
|
||||
var monthlyTrendData = {{ monthly_trend_chart|safe }};
|
||||
Plotly.newPlot('monthly-trend-chart', monthlyTrendData.data, monthlyTrendData.layout);
|
||||
|
||||
// Render category pie chart
|
||||
var categoryPieData = {{ category_pie_chart|safe }};
|
||||
Plotly.newPlot('category-pie-chart', categoryPieData.data, categoryPieData.layout);
|
||||
|
||||
// Resize charts when window is resized
|
||||
window.addEventListener('resize', function() {
|
||||
Plotly.relayout('monthly-trend-chart', {
|
||||
'width': document.getElementById('monthly-trend-chart').offsetWidth
|
||||
});
|
||||
Plotly.relayout('category-pie-chart', {
|
||||
'width': document.getElementById('category-pie-chart').offsetWidth
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
115
budgeting_website/templates/layout.html
Normal file
115
budgeting_website/templates/layout.html
Normal file
@ -0,0 +1,115 @@
|
||||
<!-- templates/layout.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Financial Dashboard</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/plotly.js-dist@2.12.1/plotly.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f5f5f5;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
.navbar {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card-header {
|
||||
background-color: rgba(63, 81, 181, 0.1);
|
||||
font-weight: bold;
|
||||
}
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.metric-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.positive {
|
||||
color: #4CAF50;
|
||||
}
|
||||
.negative {
|
||||
color: #FF5252;
|
||||
}
|
||||
.neutral {
|
||||
color: #2196F3;
|
||||
}
|
||||
.chart-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.footer {
|
||||
background-color: #f1f1f1;
|
||||
padding: 20px 0;
|
||||
margin-top: 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">Financial Dashboard</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/transactions">Transactions</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex">
|
||||
<a href="/clear" class="btn btn-outline-light">Clear Data</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mb-5">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container text-center">
|
||||
<p>Financial Dashboard - Powered by Flask and Plotly</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
90
budgeting_website/templates/transactions.html
Normal file
90
budgeting_website/templates/transactions.html
Normal file
@ -0,0 +1,90 @@
|
||||
|
||||
|
||||
<!-- templates/transactions.html -->
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>All Transactions</h2>
|
||||
<a href="/" class="btn btn-outline-primary">Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
Transaction History
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="Search transactions...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" id="transactionsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Currency</th>
|
||||
<th>Fee</th>
|
||||
<th>Net Amount</th>
|
||||
<th>Type</th>
|
||||
<th>Account</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in transactions %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if transaction['Date'] is string %}
|
||||
{{ transaction['Date'] }}
|
||||
{% else %}
|
||||
{{ transaction['Date'].strftime('%Y-%m-%d') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>{{ transaction['Name of sender/receiver'] }}</td>
|
||||
<td class="{% if transaction['Amount'] >= 0 %}positive{% else %}negative{% endif %}">
|
||||
{{ "%0.2f"|format(transaction['Amount']) }}
|
||||
</td>
|
||||
<td>{{ transaction['Currency'] }}</td>
|
||||
<td>{{ "%0.2f"|format(transaction['Fee']) }}</td>
|
||||
<td>{{ "%0.2f"|format(transaction['Net Amount']) }}</td>
|
||||
<td>{{ transaction['Transaction Type'] }}</td>
|
||||
<td>{{ transaction['Account'] }}</td>
|
||||
<td>{{ transaction['Status'] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple search functionality
|
||||
document.getElementById('searchInput').addEventListener('keyup', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const tableRows = document.querySelectorAll('#transactionsTable tbody tr');
|
||||
|
||||
tableRows.forEach(row => {
|
||||
const rowText = row.textContent.toLowerCase();
|
||||
if (rowText.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
31
budgeting_website/templates/upload.html
Normal file
31
budgeting_website/templates/upload.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!-- templates/upload.html -->
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Upload Your Financial Data
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
Upload a CSV file with your financial transactions to get started. The file should have the following columns:
|
||||
</p>
|
||||
<pre class="bg-light p-3 rounded">
|
||||
"Date","Transaction ID","Transaction Type","Currency","Amount","Fee","Net Amount",
|
||||
"Asset Type","Asset Price","Asset Amount","Status","Notes","Name of sender/receiver","Account"
|
||||
</pre>
|
||||
|
||||
<form action="/upload" method="post" enctype="multipart/form-data" class="mt-4">
|
||||
<div class="mb-3">
|
||||
<label for="fileInput" class="form-label">Select CSV File</label>
|
||||
<input class="form-control" type="file" id="fileInput" name="file" accept=".csv">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Upload and Analyze</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user