386 lines
15 KiB
HTML
386 lines
15 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||
<div class="page-header">
|
||
<h1>{{ month_name }} {{ year }} — Overview</h1>
|
||
</div>
|
||
|
||
<!-- Top summary cards -->
|
||
<div class="card-grid" style="margin-bottom:1.5rem;">
|
||
<div class="card">
|
||
<div class="card-label">Income</div>
|
||
<div class="card-value" style="color:#5A9E68;">${{ total_income | money }}</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Spent</div>
|
||
<div class="card-value">${{ (total_spending + total_bills) | money }}</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-label">Saved to Funds</div>
|
||
<div class="card-value">${{ total_sinking | money }}</div>
|
||
</div>
|
||
<div class="card" style="border: 2px solid {% if net >= 0 %}#A8C5A0{% else %}#C57A7A{% endif %};">
|
||
<div class="card-label">{{ 'Left Over' if net >= 0 else 'Over Budget' }}</div>
|
||
<div class="card-value" style="color:{% if net >= 0 %}#5A9E68{% else %}#c07070{% endif %};">
|
||
${{ net | abs | money }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Plain-English summary -->
|
||
{% if month_summary %}
|
||
<div style="background:var(--warm-white);border:1px solid var(--border);border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1.5rem;">
|
||
<div style="font-size:0.78rem;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-light);margin-bottom:0.5rem;">How's it going?</div>
|
||
{% for line in month_summary %}
|
||
<div style="font-size:0.9rem;color:var(--text-dark);margin-bottom:0.25rem;">{{ line | bold_md }}</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Cash flow bar -->
|
||
{% if total_income > 0 %}
|
||
{% set spent_pct = [(total_spending + total_bills) / total_income * 100, 100] | min | int %}
|
||
{% set sinking_pct = [total_sinking / total_income * 100, 100 - spent_pct] | min | int %}
|
||
<div style="margin-bottom:2rem;">
|
||
<div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--text-mid);margin-bottom:0.35rem;">
|
||
<span>Where did the money go?</span>
|
||
<span>${{ total_income | money }} income this month</span>
|
||
</div>
|
||
<div style="display:flex;height:18px;border-radius:999px;overflow:hidden;background:#EDE8E2;">
|
||
<div style="width:{{ spent_pct }}%;background:#C57A7A;" title="Spent"></div>
|
||
<div style="width:{{ sinking_pct }}%;background:#A8C5A0;" title="Savings"></div>
|
||
</div>
|
||
<div style="display:flex;gap:1.25rem;font-size:0.78rem;color:var(--text-mid);margin-top:0.4rem;">
|
||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#C57A7A;margin-right:3px;"></span>Spent {{ spent_pct }}%</span>
|
||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#A8C5A0;margin-right:3px;"></span>Saved {{ sinking_pct }}%</span>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if spending_by_cat %}
|
||
<div style="display:flex;gap:1.5rem;align-items:center;margin-bottom:2rem;flex-wrap:wrap;">
|
||
<div style="flex:0 0 220px;"><canvas id="overview-donut" width="220" height="220"></canvas></div>
|
||
<div style="flex:1;min-width:180px;">
|
||
<div style="font-size:0.82rem;font-weight:600;margin-bottom:0.5rem;color:var(--text-light);text-transform:uppercase;letter-spacing:0.05em;">{{ month_name }} Spending</div>
|
||
{% for cat, amt in spending_by_cat | dictsort(by='value', reverse=true) %}
|
||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.83rem;">
|
||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:{{ colors.get(cat,'#ddd') }};flex-shrink:0;"></span>
|
||
<span style="flex:1;">{{ cat }}</span>
|
||
<span style="font-weight:600;">${{ "%.0f" | format(amt) }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;">
|
||
|
||
<!-- Left column -->
|
||
<div>
|
||
|
||
<!-- Income by stream -->
|
||
<div class="budget-section">
|
||
<div class="budget-section-header" style="border-left-color:#7AB87A;">
|
||
<h3>Income</h3>
|
||
<a href="{{ url_for('income_view', year=year, month=month) }}" class="edit-link">manage →</a>
|
||
</div>
|
||
{% if income_by_stream %}
|
||
<table class="budget-table">
|
||
<tbody>
|
||
{% for s in income_streams %}
|
||
{% set amt = income_by_stream.get(s.name, 0) %}
|
||
<tr>
|
||
<td>{{ s.name }}</td>
|
||
<td><span class="owner-tag" style="background:{{ owner_colors.get(s.owner,'#ddd') }};font-size:0.68rem;">{{ s.owner }}</span></td>
|
||
<td class="amount {% if amt == 0 %}muted{% endif %}">${{ amt | money }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="total-row">
|
||
<td colspan="2">Total · <span class="muted" style="font-weight:400;font-size:0.82rem;">{{ year }} YTD ${{ "%.0f"|format(income_ytd_total) }}</span></td>
|
||
<td class="amount">${{ total_income | money }}</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
{% else %}
|
||
<p class="muted" style="padding:0.75rem 0;">No income logged yet. <a href="{{ url_for('income_view', year=year, month=month) }}">Log income →</a></p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Bills -->
|
||
<div class="budget-section" style="margin-top:1.25rem;">
|
||
<div class="budget-section-header" style="border-left-color:#C57A7A;">
|
||
<h3>Bills This Month</h3>
|
||
<a href="{{ url_for('bill_calendar', year=year, month=month) }}" class="edit-link">calendar →</a>
|
||
</div>
|
||
{% if monthly_bills %}
|
||
<table class="budget-table">
|
||
<tbody>
|
||
{% for b in monthly_bills %}
|
||
<tr>
|
||
<td>{{ b.name }}</td>
|
||
<td class="muted">day {{ b.due_day }}</td>
|
||
<td class="amount">{% if b.amount %}${{ b.amount | money }}{% else %}<span class="muted">—</span>{% endif %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="total-row">
|
||
<td colspan="2">Total</td>
|
||
<td class="amount">${{ total_bills | money }}</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
{% else %}
|
||
<p class="muted" style="padding:0.75rem 0;">No bills this month. <a href="{{ url_for('bills_list') }}">Manage bills →</a></p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Sinking contributions -->
|
||
{% if sinking_contributions %}
|
||
<div class="budget-section" style="margin-top:1.25rem;">
|
||
<div class="budget-section-header" style="border-left-color:#C5BBA0;">
|
||
<h3>Saved to Funds</h3>
|
||
<a href="{{ url_for('sinking_view') }}" class="edit-link">manage →</a>
|
||
</div>
|
||
<table class="budget-table">
|
||
<tbody>
|
||
{% for name, amt in sinking_contributions.items() %}
|
||
<tr>
|
||
<td>{{ name }}</td>
|
||
<td class="amount">${{ amt | money }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="total-row">
|
||
<td>Total</td>
|
||
<td class="amount">${{ total_sinking | money }}</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
|
||
<!-- Right column -->
|
||
<div>
|
||
|
||
<!-- Cash accounts -->
|
||
{% if cash_accounts %}
|
||
<div class="budget-section" style="margin-bottom:1.25rem;">
|
||
<div class="budget-section-header" style="border-left-color:#7AB87A;">
|
||
<h3>Account Balances</h3>
|
||
<a href="{{ url_for('net_worth_view') }}" class="edit-link">manage →</a>
|
||
</div>
|
||
<table class="budget-table">
|
||
<tbody>
|
||
{% for a in cash_accounts %}
|
||
<tr>
|
||
<td>{{ a.name }}</td>
|
||
<td class="muted" style="font-size:0.78rem;">{{ a.type or '' }}</td>
|
||
<td class="amount">${{ (a.balance or 0) | money }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="total-row">
|
||
<td colspan="2">Total Cash</td>
|
||
<td class="amount">${{ total_cash | money }}</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="budget-section" style="margin-bottom:1.25rem;">
|
||
<div class="budget-section-header" style="border-left-color:#7AB87A;">
|
||
<h3>Account Balances</h3>
|
||
</div>
|
||
<p class="muted" style="padding:0.5rem 0;font-size:0.85rem;">No accounts added yet. <a href="{{ url_for('net_worth_view') }}">Add accounts →</a></p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Spending by category -->
|
||
<div class="budget-section">
|
||
<div class="budget-section-header" style="border-left-color:#D4A96A;">
|
||
<h3>Spending by Category</h3>
|
||
<a href="{{ url_for('transactions_view') }}" class="edit-link">all transactions →</a>
|
||
</div>
|
||
{% if spending_by_cat %}
|
||
{% set max_spend = spending_by_cat.values() | max %}
|
||
<table class="budget-table">
|
||
<thead><tr><th>Category</th><th>Budgeted</th><th>Actual</th><th></th></tr></thead>
|
||
<tbody>
|
||
{% for cat, amt in spending_by_cat | dictsort(by='value', reverse=true) %}
|
||
{% set budgeted = budgeted_by_cat.get(cat, 0) %}
|
||
{% set over = budgeted > 0 and amt > budgeted %}
|
||
<tr{% if over %} style="background:#fdf0f0;"{% endif %}>
|
||
<td><span class="tag" style="background:{{ colors.get(cat,'#ddd') }};">{{ cat }}</span></td>
|
||
<td class="amount muted">{% if budgeted %}${{ budgeted | money }}{% else %}<span style="color:#ccc;">—</span>{% endif %}</td>
|
||
<td class="amount">
|
||
${{ amt | money }}
|
||
{% if over %}<span style="font-size:0.7rem;color:#c07070;"> over</span>{% endif %}
|
||
</td>
|
||
<td style="width:28%;padding-right:0.5rem;">
|
||
<div style="background:#EDE8E2;border-radius:4px;height:8px;">
|
||
<div style="background:{{ colors.get(cat,'#bbb') }};border-radius:4px;height:8px;width:{{ [(amt/max_spend*100)|int,100]|min }}%;"></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="total-row">
|
||
<td>Total</td>
|
||
<td class="amount muted">{% if budgeted_by_cat %}${{ "%.2f"|format(budgeted_by_cat.values()|sum) }}{% endif %}</td>
|
||
<td class="amount">${{ total_spending | money }}</td>
|
||
<td></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
{% else %}
|
||
<p class="muted" style="padding:0.75rem 0;">No categorized transactions yet. <a href="{{ url_for('review') }}">Review transactions →</a></p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Recent transactions -->
|
||
{% if recent %}
|
||
<div class="budget-section" style="margin-top:1.25rem;">
|
||
<div class="budget-section-header" style="border-left-color:#8AABCC;">
|
||
<h3>Recent Transactions</h3>
|
||
<a href="{{ url_for('transactions_view') }}" class="edit-link">view all →</a>
|
||
</div>
|
||
<table class="budget-table">
|
||
<tbody>
|
||
{% for t in recent %}
|
||
<tr>
|
||
<td class="muted" style="white-space:nowrap;">{{ t.date }}</td>
|
||
<td style="max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ t.description }}</td>
|
||
<td class="amount">${{ t.amount | money }}</td>
|
||
<td><span class="tag" style="background:{{ colors.get(t.category,'#ddd') }};font-size:0.72rem;">{{ t.category }}</span></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Budget vs Actual chart -->
|
||
{% if budgeted_by_cat %}
|
||
<div class="budget-section" style="margin-top:1.5rem;">
|
||
<div class="budget-section-header" style="border-left-color:#D4A96A;">
|
||
<h3>Budget vs Actual — {{ month_name }}</h3>
|
||
</div>
|
||
<div style="position:relative;height:{{ [budgeted_by_cat | length * 42, 200] | max }}px;">
|
||
<canvas id="budget-actual-chart"></canvas>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Tax estimate widget -->
|
||
<div style="background:#fff8e6;border:1px solid #e8d49a;border-radius:var(--radius);padding:1rem 1.25rem;margin-top:1.5rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;">
|
||
<div>
|
||
<div style="font-weight:600;margin-bottom:0.2rem;">🧾 {{ year }} Tax Estimate</div>
|
||
<div style="font-size:0.82rem;color:var(--text-light);">
|
||
${{ tax_estimate.total | money }} estimated owed ·
|
||
${{ tax_set_aside | money }} set aside ·
|
||
{% if tax_still_needed > 0 %}
|
||
<span style="color:#c07070;font-weight:600;">${{ tax_still_needed | money }} still needed</span>
|
||
{% else %}
|
||
<span style="color:#5A9E68;font-weight:600;">fully covered ✓</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<a href="{{ url_for('taxes_view') }}" class="btn btn-ghost" style="font-size:0.82rem;">View Details →</a>
|
||
</div>
|
||
|
||
<!-- Net summary bar -->
|
||
<div class="leftover-bar {% if net < 0 %}leftover-negative{% endif %}" style="margin-top:2rem;">
|
||
<div>
|
||
<div style="font-weight:600;">{{ month_name }} {{ year }} — Net</div>
|
||
<div style="font-size:0.82rem;color:var(--text-light);margin-top:0.2rem;">
|
||
${{ "%.2f"|format(total_income) }} income − ${{ "%.2f"|format(total_spending + total_bills) }} spent − ${{ "%.2f"|format(total_sinking) }} saved
|
||
</div>
|
||
</div>
|
||
<div class="leftover-amount">{% if net >= 0 %}+{% endif %}${{ net | money }}</div>
|
||
</div>
|
||
|
||
<script>
|
||
{% if spending_by_cat %}
|
||
const donutLabels = {{ spending_by_cat.keys() | list | tojson }};
|
||
const donutValues = {{ spending_by_cat.values() | list | tojson }};
|
||
const donutColors = [{% for cat in spending_by_cat %}'{{ colors.get(cat, "#ddd") }}',{% endfor %}];
|
||
|
||
new Chart(document.getElementById('overview-donut'), {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: donutLabels,
|
||
datasets: [{
|
||
data: donutValues,
|
||
backgroundColor: donutColors,
|
||
borderWidth: 2,
|
||
borderColor: '#faf7f2',
|
||
hoverOffset: 6,
|
||
}]
|
||
},
|
||
options: {
|
||
cutout: '65%',
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.toFixed(2) } }
|
||
},
|
||
animation: { duration: 400 },
|
||
}
|
||
});
|
||
{% endif %}
|
||
|
||
{% if budgeted_by_cat %}
|
||
const baLabels = {{ budgeted_by_cat.keys() | list | tojson }};
|
||
const baBudgeted = {{ budgeted_by_cat.values() | list | tojson }};
|
||
const baActual = [{% for cat in budgeted_by_cat %}{{ spending_by_cat.get(cat, 0) }},{% endfor %}];
|
||
const baColors = [{% for cat in budgeted_by_cat %}'{{ colors.get(cat, "#ddd") }}',{% endfor %}];
|
||
|
||
new Chart(document.getElementById('budget-actual-chart'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: baLabels,
|
||
datasets: [
|
||
{
|
||
label: 'Budgeted',
|
||
data: baBudgeted,
|
||
backgroundColor: baColors.map(c => c + '55'),
|
||
borderColor: baColors,
|
||
borderWidth: 1.5,
|
||
borderRadius: 4,
|
||
},
|
||
{
|
||
label: 'Actual',
|
||
data: baActual,
|
||
backgroundColor: baColors,
|
||
borderRadius: 4,
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
indexAxis: 'y',
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { position: 'top', labels: { font: { size: 12 }, padding: 16 } },
|
||
tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.x.toFixed(2) } }
|
||
},
|
||
scales: {
|
||
x: { ticks: { callback: v => '$' + v, font: { size: 11 } }, grid: { color: '#EDE8E2' } },
|
||
y: { ticks: { font: { size: 12 } } }
|
||
}
|
||
}
|
||
});
|
||
{% endif %}
|
||
</script>
|
||
{% endblock %}
|