Files
Moon-Household-Budget/templates/index.html
2026-06-06 19:12:43 -05:00

386 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 &nbsp;·&nbsp;
${{ tax_set_aside | money }} set aside &nbsp;·&nbsp;
{% 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 %}