05/31 Phase 3

This commit is contained in:
2026-05-31 16:18:37 -04:00
parent 9f2a0acc38
commit 1560041cdb
13 changed files with 1152 additions and 10 deletions
+2 -2
View File
@@ -189,10 +189,10 @@
</a>
<div class="sb-section">Planning</div>
<a href="#" class="sb-link">
<a href="{{ url_for('budgets.index') }}" class="sb-link {% if request.blueprint == 'budgets' %}active{% endif %}">
<i class="bi bi-pie-chart"></i><span class="lt">Budgets</span>
</a>
<a href="#" class="sb-link">
<a href="{{ url_for('goals.index') }}" class="sb-link {% if request.blueprint == 'goals' %}active{% endif %}">
<i class="bi bi-bullseye"></i><span class="lt">Goals</span>
</a>
+46
View File
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block page_title %}{{ title }} — {{ month_label }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12 col-md-7 col-lg-5">
<div class="pcard">
<form method="POST" novalidate>
{{ form.hidden_tag() }}
{{ form.month() }}
<div class="mb-3">
{{ form.category_id.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.category_id(class="form-select" + (" is-invalid" if form.category_id.errors else ""), **({'disabled': 'disabled'} if budget else {})) }}
{% if budget %}
<input type="hidden" name="category_id" value="{{ budget.category_id }}">
{% endif %}
</div>
<div class="mb-3">
{{ form.limit_amount.label(class="form-label fw-medium", style="font-size:13px;") }}
<div class="input-group">
<span class="input-group-text" style="font-size:13px;">{{ current_user.currency_symbol }}</span>
{{ form.limit_amount(class="form-control" + (" is-invalid" if form.limit_amount.errors else ""), placeholder="0.00") }}
</div>
{% for e in form.limit_amount.errors %}<div class="text-danger mt-1" style="font-size:12px;">{{ e }}</div>{% endfor %}
</div>
<div class="mb-4">
<div class="form-check">
{{ form.rollover_enabled(class="form-check-input") }}
{{ form.rollover_enabled.label(class="form-check-label", style="font-size:13px;") }}
</div>
<small class="text-muted" style="font-size:12px;">Unused budget rolls over to next month.</small>
</div>
<div class="d-flex gap-2">
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('budgets.index', month=month) }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+150
View File
@@ -0,0 +1,150 @@
{% extends "base.html" %}
{% block title %}Budgets{% endblock %}
{% block page_title %}Budgets{% endblock %}
{% block extra_css %}
.progress-bar-budget { height: 8px; border-radius: 4px; background: #f1f5f9; overflow: hidden; }
.progress-bar-fill { height: 100%; border-radius: 4px; transition: width .6s ease; }
.budget-over { background: #fee2e2; border-color: #fca5a5 !important; }
{% endblock %}
{% block topbar_actions %}
<a href="{{ url_for('budgets.new', month=month) }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Add Budget</a>
{% endblock %}
{% block content %}
<!-- Month nav -->
<div class="d-flex align-items-center justify-content-between mb-4">
<a href="{{ url_for('budgets.index', month=prev_month) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-chevron-left"></i></a>
<h5 class="mb-0 fw-semibold">{{ month_label }}</h5>
<a href="{{ url_for('budgets.index', month=next_month) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-chevron-right"></i></a>
</div>
<!-- Overall summary -->
{% if total_budget > 0 %}
<div class="pcard mb-4">
<div class="row g-3 align-items-center">
<div class="col-12 col-md-8">
<div class="d-flex justify-content-between mb-2">
<span style="font-size:13px;font-weight:500;">Total Budget</span>
<span class="mono" style="font-size:13px;">
<span class="{% if total_spent > total_budget %}text-expense{% else %}text-income{% endif %}">{{ total_spent | currency }}</span>
<span class="text-muted"> / {{ total_budget | currency }}</span>
</span>
</div>
<div class="progress-bar-budget">
<div class="progress-bar-fill" style="width:{{ overall_pct }}%;background:{% if overall_pct >= 100 %}#ef4444{% elif overall_pct >= 80 %}#f59e0b{% else %}#10b981{% endif %};"></div>
</div>
<div class="d-flex justify-content-between mt-1">
<small class="text-muted">{{ overall_pct }}% used</small>
<small class="{% if total_budget - total_spent >= 0 %}text-income{% else %}text-expense{% endif %}">
{{ (total_budget - total_spent) | currency }} {% if total_budget - total_spent >= 0 %}remaining{% else %}over{% endif %}
</small>
</div>
</div>
<div class="col-12 col-md-4 text-md-end">
<!-- Copy from prev month -->
<form method="POST" action="{{ url_for('budgets.copy_month') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="from_month" value="{{ prev_month }}">
<input type="hidden" name="to_month" value="{{ month }}">
<button type="submit" class="btn btn-sm btn-outline-secondary" style="font-size:12px;"
onclick="return confirm('Copy budgets from previous month?')">
<i class="bi bi-copy me-1"></i>Copy from {{ prev_month }}
</button>
</form>
</div>
</div>
</div>
{% endif %}
<!-- Budget Items -->
{% if summary %}
<div class="pcard p-0">
<table class="pfm-table">
<thead>
<tr>
<th style="padding-left:20px;">Category</th>
<th>Spent</th>
<th class="d-none d-md-table-cell">Limit</th>
<th>Progress</th>
<th class="d-none d-md-table-cell text-end">Remaining</th>
<th class="text-end" style="padding-right:20px;">Actions</th>
</tr>
</thead>
<tbody>
{% for item in summary %}
<tr class="{% if item.is_over %}budget-over{% endif %}">
<td style="padding-left:20px;">
<div class="d-flex align-items-center gap-2">
{% if item.category %}
<div style="width:28px;height:28px;border-radius:7px;background:{{ item.category.color }}22;color:{{ item.category.color }};display:flex;align-items:center;justify-content:center;font-size:14px;">
<i class="bi {{ item.category.icon }}"></i>
</div>
<span style="font-size:13px;font-weight:500;">{{ item.category.name }}</span>
{% endif %}
{% if not item.has_budget %}
<span style="font-size:10px;background:#fef9c3;color:#854d0e;border-radius:4px;padding:1px 5px;">no budget</span>
{% endif %}
{% if item.budget and item.budget.rollover_amount and item.budget.rollover_amount > 0 %}
<span style="font-size:10px;background:#dbeafe;color:#1e40af;border-radius:4px;padding:1px 5px;" title="Includes {{ item.budget.rollover_amount | currency }} rollover">+rollover</span>
{% endif %}
</div>
</td>
<td>
<span class="mono {% if item.is_over %}text-expense{% endif %}" style="font-size:13px;font-weight:500;">{{ item.spent | currency }}</span>
</td>
<td class="d-none d-md-table-cell">
{% if item.limit is not none %}
<span class="mono text-muted" style="font-size:12px;">{{ item.limit | currency }}</span>
{% else %}
<span class="text-muted" style="font-size:12px;"></span>
{% endif %}
</td>
<td style="min-width:120px;">
{% if item.pct is not none %}
<div class="progress-bar-budget">
<div class="progress-bar-fill" style="width:{{ item.pct }}%;background:{% if item.pct >= 100 %}#ef4444{% elif item.pct >= 80 %}#f59e0b{% else %}#10b981{% endif %};"></div>
</div>
<small class="text-muted" style="font-size:10px;">{{ item.pct }}%</small>
{% else %}
<span class="text-muted" style="font-size:12px;"></span>
{% endif %}
</td>
<td class="d-none d-md-table-cell text-end">
{% if item.remaining is not none %}
<span class="mono {% if item.remaining >= 0 %}text-income{% else %}text-expense{% endif %}" style="font-size:12px;">{{ item.remaining | currency }}</span>
{% endif %}
</td>
<td class="text-end" style="padding-right:20px;white-space:nowrap;">
{% if item.budget %}
<a href="{{ url_for('budgets.edit', id=item.budget.id) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;padding:2px 8px;">Edit</a>
<form method="POST" action="{{ url_for('budgets.delete', id=item.budget.id) }}" style="display:inline;" onsubmit="return confirm('Remove this budget?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-danger ms-1" style="font-size:11px;padding:2px 8px;">Del</button>
</form>
{% else %}
<a href="{{ url_for('budgets.new', month=month) }}" class="btn btn-sm btn-outline-primary" style="font-size:11px;padding:2px 8px;">Set Budget</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="pcard text-center py-5">
<i class="bi bi-pie-chart text-muted" style="font-size:3rem;"></i>
<h5 class="mt-3 mb-1">No budgets for {{ month_label }}</h5>
<p class="text-muted small mb-3">Set spending limits per category to track your budget.</p>
<a href="{{ url_for('budgets.new', month=month) }}" class="btn btn-primary btn-sm">Set First Budget</a>
{% set prev_year, prev_mo = prev_month.split('-') %}
<form method="POST" action="{{ url_for('budgets.copy_month') }}" class="mt-2 d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="from_month" value="{{ prev_month }}">
<input type="hidden" name="to_month" value="{{ month }}">
<button type="submit" class="btn btn-outline-secondary btn-sm">Copy from {{ prev_month }}</button>
</form>
</div>
{% endif %}
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block title %}Add Contribution{% endblock %}
{% block page_title %}Add Contribution{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12 col-md-7 col-lg-5">
<!-- Goal summary card -->
<div class="pcard mb-3" style="border-left: 4px solid {{ goal.color }};">
<div class="d-flex align-items-center gap-2 mb-3">
<div style="width:36px;height:36px;border-radius:9px;background:{{ goal.color }}22;color:{{ goal.color }};display:flex;align-items:center;justify-content:center;font-size:18px;">
<i class="bi {{ goal.icon }}"></i>
</div>
<div>
<div style="font-size:14px;font-weight:600;">{{ goal.name }}</div>
<div style="font-size:12px;color:var(--muted);">{{ goal.progress_percent }}% complete</div>
</div>
</div>
<div style="height:8px;background:#f1f5f9;border-radius:4px;overflow:hidden;">
<div style="height:8px;border-radius:4px;background:{{ goal.color }};width:{{ goal.progress_percent }}%;"></div>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="mono" style="font-size:13px;color:{{ goal.color }};font-weight:600;">{{ goal.current_amount | currency }}</span>
<span class="mono text-muted" style="font-size:12px;">/ {{ goal.target_amount | currency }}</span>
</div>
<div style="font-size:12px;color:var(--muted);margin-top:4px;">
Remaining: <span class="mono text-income">{{ (goal.target_amount - goal.current_amount) | currency }}</span>
</div>
</div>
<div class="pcard">
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.amount.label(class="form-label fw-medium", style="font-size:13px;") }}
<div class="input-group">
<span class="input-group-text" style="font-size:13px;">{{ current_user.currency_symbol }}</span>
{{ form.amount(class="form-control", placeholder="0.00") }}
</div>
{% for e in form.amount.errors %}<div class="text-danger mt-1" style="font-size:12px;">{{ e }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.date.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.date(class="form-control") }}
</div>
<div class="mb-4">
{{ form.notes.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.notes(class="form-control", placeholder="Optional note") }}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" style="background:{{ goal.color }};border-color:{{ goal.color }};">Add Contribution</button>
<a href="{{ url_for('goals.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+66
View File
@@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block title %}{{ goal.name }} — History{% endblock %}
{% block page_title %}{{ goal.name }}{% endblock %}
{% block topbar_actions %}
<a href="{{ url_for('goals.contribute', id=goal.id) }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Add Contribution</a>
{% endblock %}
{% block content %}
<!-- Goal summary -->
<div class="pcard mb-4" style="border-left:4px solid {{ goal.color }};">
<div class="row align-items-center g-3">
<div class="col-12 col-md-8">
<div style="height:10px;background:#f1f5f9;border-radius:5px;overflow:hidden;">
<div style="height:10px;border-radius:5px;background:{{ goal.color }};width:{{ goal.progress_percent }}%;transition:width .6s;"></div>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="mono" style="font-size:14px;font-weight:600;color:{{ goal.color }};">{{ goal.current_amount | currency }}</span>
<span class="mono text-muted" style="font-size:13px;">{{ goal.target_amount | currency }}</span>
</div>
<small class="text-muted">{{ goal.progress_percent }}% complete</small>
</div>
<div class="col-12 col-md-4 text-md-end">
{% if projection %}
<div style="font-size:12px;color:var(--muted);">Projected completion</div>
<div style="font-size:15px;font-weight:600;">{{ projection.strftime('%B %Y') }}</div>
{% endif %}
{% if goal.target_date %}
<div style="font-size:12px;color:var(--muted);">Target: {{ goal.target_date.strftime('%b %d, %Y') }}</div>
{% endif %}
</div>
</div>
</div>
<!-- Contribution history -->
<div class="pcard p-0">
<div class="d-flex justify-content-between align-items-center px-4 py-3" style="border-bottom:1px solid var(--border);">
<span class="pcard-title mb-0">Contribution History</span>
<span class="text-muted" style="font-size:12px;">{{ contribs|length }} contributions</span>
</div>
{% if contribs %}
<table class="pfm-table">
<thead><tr><th style="padding-left:20px;">Date</th><th>Amount</th><th>Notes</th><th class="text-end" style="padding-right:20px;">Actions</th></tr></thead>
<tbody>
{% for c in contribs %}
<tr>
<td style="padding-left:20px;font-size:12px;color:var(--muted);">{{ c.date.strftime('%b %d, %Y') }}</td>
<td><span class="mono text-income" style="font-size:13px;font-weight:500;">+{{ c.amount | currency }}</span></td>
<td style="font-size:12px;color:var(--muted);">{{ c.notes or '—' }}</td>
<td class="text-end" style="padding-right:20px;">
<form method="POST" action="{{ url_for('goals.delete_contribution', id=c.id) }}" onsubmit="return confirm('Remove this contribution?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-danger" style="font-size:11px;padding:2px 8px;">Del</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="text-center py-4">
<p class="text-muted small mb-0">No contributions yet.</p>
</div>
{% endif %}
</div>
{% endblock %}
+103
View File
@@ -0,0 +1,103 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block page_title %}{{ title }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<div class="pcard">
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.name(class="form-control" + (" is-invalid" if form.name.errors else ""), placeholder="e.g. Vacation Fund") }}
{% for e in form.name.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
</div>
<div class="row g-3 mb-3">
<div class="col-6">
{{ form.target_amount.label(class="form-label fw-medium", style="font-size:13px;") }}
<div class="input-group">
<span class="input-group-text" style="font-size:13px;">{{ current_user.currency_symbol }}</span>
{{ form.target_amount(class="form-control", placeholder="0.00") }}
</div>
</div>
<div class="col-6">
{{ form.target_date.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.target_date(class="form-control") }}
</div>
</div>
<div class="mb-3">
{{ form.linked_account_id.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.linked_account_id(class="form-select") }}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.description(class="form-control", rows=2, placeholder="What are you saving for?") }}
</div>
<!-- Color -->
<div class="mb-3">
<label class="form-label fw-medium" style="font-size:13px;">Color</label>
<div class="d-flex gap-2 flex-wrap">
{% for c in colors %}
<label style="cursor:pointer;">
<input type="radio" name="color" value="{{ c }}" style="display:none;" {% if (goal and goal.color==c) or (not goal and loop.first) %}checked{% endif %}>
<div style="width:26px;height:26px;border-radius:50%;background:{{ c }};border:3px solid transparent;" class="color-swatch" data-color="{{ c }}"></div>
</label>
{% endfor %}
</div>
{{ form.color(type="hidden", id="colorInput") }}
</div>
<!-- Icon -->
<div class="mb-4">
<label class="form-label fw-medium" style="font-size:13px;">Icon</label>
<div class="d-flex gap-2 flex-wrap">
{% for icon_val in icons %}
<label style="cursor:pointer;">
<input type="radio" name="icon" value="{{ icon_val }}" style="display:none;" {% if goal and goal.icon==icon_val %}checked{% elif not goal and loop.first %}checked{% endif %}>
<div style="width:34px;height:34px;border-radius:8px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:16px;border:2px solid transparent;" class="icon-swatch">
<i class="bi {{ icon_val }}"></i>
</div>
</label>
{% endfor %}
</div>
{{ form.icon(type="hidden", id="iconInput") }}
</div>
<div class="d-flex gap-2">
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('goals.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.querySelectorAll('.color-swatch').forEach(function(sw) {
const inp = sw.closest('label').querySelector('input');
inp.addEventListener('change', function() {
document.getElementById('colorInput').value = this.value;
document.querySelectorAll('.color-swatch').forEach(s => { s.style.borderColor='transparent'; s.style.outline='none'; });
sw.style.borderColor='#fff'; sw.style.outline='2px solid '+this.value;
});
if (inp.checked) { sw.style.borderColor='#fff'; sw.style.outline='2px solid '+sw.dataset.color; document.getElementById('colorInput').value=sw.dataset.color; }
});
document.querySelectorAll('.icon-swatch').forEach(function(sw) {
const inp = sw.closest('label').querySelector('input');
inp.addEventListener('change', function() {
document.getElementById('iconInput').value = this.value;
document.querySelectorAll('.icon-swatch').forEach(s => { s.style.background='#f1f5f9'; s.style.borderColor='transparent'; });
sw.style.background='#dbeafe'; sw.style.borderColor='#3b82f6';
});
if (inp.checked) { sw.style.background='#dbeafe'; sw.style.borderColor='#3b82f6'; document.getElementById('iconInput').value=inp.value; }
});
</script>
{% endblock %}
+142
View File
@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}Goals{% endblock %}
{% block page_title %}Goals & Savings{% endblock %}
{% block extra_css %}
.goal-card { border-radius: 12px; border: 1px solid var(--border); background: var(--card-bg); padding: 18px 20px; }
.goal-progress { height: 10px; background: #f1f5f9; border-radius: 5px; overflow: hidden; margin: 10px 0 6px; }
.goal-progress-fill { height: 100%; border-radius: 5px; transition: width .6s ease; }
{% endblock %}
{% block topbar_actions %}
<a href="{{ url_for('goals.new') }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>New Goal</a>
{% endblock %}
{% block content %}
<!-- Emergency Fund -->
{% if emergency and emergency.avg_monthly_expense > 0 %}
<div class="pcard mb-4" style="border-left:4px solid #f59e0b;">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<span style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.07em;color:#92400e;">Emergency Fund</span>
<div style="font-size:13px;color:var(--muted);margin-top:2px;">Based on avg monthly expense of <span class="mono">{{ emergency.avg_monthly_expense | currency }}</span></div>
</div>
<span class="mono fw-bold" style="font-size:18px;color:{% if emergency.months_covered >= 6 %}#10b981{% elif emergency.months_covered >= 3 %}#f59e0b{% else %}#ef4444{% endif %};">
{{ emergency.months_covered }}mo
</span>
</div>
<div class="row g-3">
<div class="col-6">
<div style="font-size:11px;color:var(--muted);margin-bottom:4px;">3-Month Target: <span class="mono">{{ emergency.target_3mo | currency }}</span></div>
<div style="height:6px;background:#f1f5f9;border-radius:3px;"><div style="height:6px;border-radius:3px;background:#f59e0b;width:{{ emergency.pct_3mo }}%;"></div></div>
<small class="text-muted" style="font-size:10px;">{{ emergency.pct_3mo }}%</small>
</div>
<div class="col-6">
<div style="font-size:11px;color:var(--muted);margin-bottom:4px;">6-Month Target: <span class="mono">{{ emergency.target_6mo | currency }}</span></div>
<div style="height:6px;background:#f1f5f9;border-radius:3px;"><div style="height:6px;border-radius:3px;background:#10b981;width:{{ emergency.pct_6mo }}%;"></div></div>
<small class="text-muted" style="font-size:10px;">{{ emergency.pct_6mo }}%</small>
</div>
</div>
<div style="font-size:12px;margin-top:8px;color:var(--muted);">
Liquid assets: <span class="mono text-income">{{ emergency.liquid_assets | currency }}</span>
</div>
</div>
{% endif %}
<!-- Active Goals -->
{% if active_goals %}
<div class="row g-3 mb-4">
{% for goal in active_goals %}
<div class="col-12 col-md-6 col-xl-4">
<div class="goal-card" style="border-left: 4px solid {{ goal.color }};">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-center gap-2">
<div style="width:36px;height:36px;border-radius:9px;background:{{ goal.color }}22;color:{{ goal.color }};display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;">
<i class="bi {{ goal.icon }}"></i>
</div>
<div>
<div style="font-size:14px;font-weight:600;">{{ goal.name }}</div>
{% if goal.target_date %}
<div style="font-size:11px;color:var(--muted);">By {{ goal.target_date.strftime('%b %d, %Y') }}</div>
{% endif %}
</div>
</div>
<div class="dropdown">
<button class="btn btn-sm" style="background:none;border:none;color:var(--muted);padding:2px 6px;" data-bs-toggle="dropdown"><i class="bi bi-three-dots"></i></button>
<ul class="dropdown-menu dropdown-menu-end" style="font-size:13px;">
<li><a class="dropdown-item" href="{{ url_for('goals.contribute', id=goal.id) }}"><i class="bi bi-plus-circle me-2"></i>Add Contribution</a></li>
<li><a class="dropdown-item" href="{{ url_for('goals.contributions', id=goal.id) }}"><i class="bi bi-clock-history me-2"></i>History</a></li>
<li><a class="dropdown-item" href="{{ url_for('goals.edit', id=goal.id) }}"><i class="bi bi-pencil me-2"></i>Edit</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="POST" action="{{ url_for('goals.delete', id=goal.id) }}" onsubmit="return confirm('Delete this goal?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="dropdown-item text-danger"><i class="bi bi-trash me-2"></i>Delete</button>
</form>
</li>
</ul>
</div>
</div>
<div class="goal-progress">
<div class="goal-progress-fill" style="width:{{ goal.progress_percent }}%;background:{{ goal.color }};"></div>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="mono" style="font-size:13px;font-weight:600;color:{{ goal.color }};">{{ goal.current_amount | currency }}</span>
<span class="text-muted mono" style="font-size:12px;">/ {{ goal.target_amount | currency }}</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-2">
<small class="text-muted">{{ goal.progress_percent }}% complete</small>
{% if projections[goal.id] %}
<small class="text-muted"><i class="bi bi-calendar-check me-1"></i>~{{ projections[goal.id].strftime('%b %Y') }}</small>
{% endif %}
</div>
{% if goal.description %}
<div style="font-size:12px;color:var(--muted);margin-top:8px;">{{ goal.description | truncate(80) }}</div>
{% endif %}
<a href="{{ url_for('goals.contribute', id=goal.id) }}" class="btn btn-sm w-100 mt-3" style="background:{{ goal.color }}22;color:{{ goal.color }};border:1px solid {{ goal.color }}44;font-size:12px;">
<i class="bi bi-plus-lg me-1"></i>Add Contribution
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="pcard text-center py-5 mb-4">
<i class="bi bi-bullseye text-muted" style="font-size:3rem;"></i>
<h5 class="mt-3 mb-1">No active goals</h5>
<p class="text-muted small mb-3">Set savings goals and track your progress.</p>
<a href="{{ url_for('goals.new') }}" class="btn btn-primary btn-sm">Create First Goal</a>
</div>
{% endif %}
<!-- Completed Goals -->
{% if completed_goals %}
<div class="pcard">
<div class="pcard-title mb-3">Completed Goals 🎉</div>
<table class="pfm-table">
<thead><tr><th>Goal</th><th>Target</th><th>Completed</th></tr></thead>
<tbody>
{% for goal in completed_goals %}
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<div style="width:26px;height:26px;border-radius:6px;background:{{ goal.color }}22;color:{{ goal.color }};display:flex;align-items:center;justify-content:center;font-size:13px;">
<i class="bi {{ goal.icon }}"></i>
</div>
<span style="font-size:13px;">{{ goal.name }}</span>
</div>
</td>
<td><span class="mono" style="font-size:13px;">{{ goal.target_amount | currency }}</span></td>
<td style="font-size:12px;color:var(--muted);">{{ goal.completed_at.strftime('%b %d, %Y') if goal.completed_at else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}