Files
Personal-Finance-Management/app/templates/utilities/index.html
T

298 lines
16 KiB
HTML

{% extends "base.html" %}
{% block title %}Utilities{% endblock %}
{% block page_title %}Utilities{% endblock %}
{% block extra_css %}
.util-card { border-radius: 12px; border: 1px solid var(--border); background: var(--card-bg); padding: 16px 18px; height: 100%; }
.util-chip { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 10px; text-transform: uppercase; letter-spacing: .05em; }
.trend-up { color: #ef4444; }
.trend-down { color: #10b981; }
{% endblock %}
{% block topbar_actions %}
<a href="{{ url_for('utilities.bills') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;"><i class="bi bi-list-ul me-1"></i><span class="btn-label">All Bills</span></a>
<a href="{{ url_for('utilities.providers') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;"><i class="bi bi-building me-1"></i><span class="btn-label">Providers</span></a>
<a href="{{ url_for('utilities.bill_new') }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i><span class="btn-label">New Bill</span></a>
{% endblock %}
{% block content %}
<!-- Stat cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-xl-3">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">This Month</div>
<div class="stat-value text-expense">{{ summary.this_month | currency }}</div>
{% if summary.month_change_pct is not none %}
<small class="{% if summary.month_change_pct > 0 %}trend-up{% else %}trend-down{% endif %}" style="font-size:11px;">
<i class="bi bi-arrow-{% if summary.month_change_pct > 0 %}up{% else %}down{% endif %}-short"></i>{{ summary.month_change_pct | abs }}% vs last month
</small>
{% endif %}
</div>
<div class="stat-icon" style="background:#ede9fe;color:#5b21b6;"><i class="bi bi-lightning-charge"></i></div>
</div>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Monthly Average</div>
<div class="stat-value">{{ summary.avg_monthly | currency }}</div>
<small class="text-muted" style="font-size:11px;">Last 12 months</small>
</div>
<div class="stat-icon" style="background:#e0f2fe;color:#075985;"><i class="bi bi-bar-chart"></i></div>
</div>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Year to Date</div>
<div class="stat-value">{{ summary.ytd | currency }}</div>
</div>
<div class="stat-icon" style="background:#fef3c7;color:#92400e;"><i class="bi bi-calendar3"></i></div>
</div>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="stat-card" style="{% if summary.overdue_count %}border-color:#fecaca;{% endif %}">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Unpaid</div>
<div class="stat-value {% if summary.overdue_count %}text-expense{% endif %}">{{ summary.unpaid_total | currency }}</div>
<small class="text-muted" style="font-size:11px;">
{{ summary.unpaid_count }} bill{{ '' if summary.unpaid_count == 1 else 's' }}{% if summary.overdue_count %} · <span class="text-expense fw-bold">{{ summary.overdue_count }} overdue</span>{% endif %}
</small>
</div>
<div class="stat-icon" style="background:{% if summary.overdue_count %}#fee2e2;color:#991b1b{% else %}#f1f5f9;color:#475569{% endif %};"><i class="bi bi-receipt"></i></div>
</div>
</div>
</div>
</div>
<!-- Upcoming due -->
{% if summary.upcoming %}
<div class="pcard mb-4" style="border-left:4px solid {% if summary.overdue_count %}#ef4444{% else %}#f59e0b{% endif %};">
<div class="pcard-title mb-3">Bills Due</div>
<div class="table-wrap">
<table class="pfm-table">
<thead><tr><th>Provider</th><th>Period</th><th class="d-mob-none">Due</th><th class="text-end">Amount</th><th class="text-end">Action</th></tr></thead>
<tbody>
{% for b in summary.upcoming %}
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<div style="width:28px;height:28px;border-radius:7px;background:{{ b.provider.color }}22;color:{{ b.provider.color }};display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;">
<i class="bi {{ b.provider.icon }}"></i>
</div>
<a href="{{ url_for('utilities.provider_detail', id=b.provider_id) }}" style="font-size:13px;font-weight:600;text-decoration:none;color:inherit;">{{ b.provider.name }}</a>
</div>
</td>
<td style="font-size:12px;color:var(--muted);">{{ b.period_label }}</td>
<td class="d-mob-none" style="font-size:12px;">
{{ b.due_date.strftime('%b %d') }}
{% if b.status == 'overdue' %}
<span class="util-chip" style="background:#fee2e2;color:#991b1b;">{{ b.days_until_due | abs }}d late</span>
{% elif b.status == 'due_soon' %}
<span class="util-chip" style="background:#fef3c7;color:#92400e;">in {{ b.days_until_due }}d</span>
{% endif %}
</td>
<td class="text-end mono fw-bold" style="font-size:13px;">{{ b.amount | currency }}</td>
<td class="text-end">
<a href="{{ url_for('utilities.bill_pay', id=b.id) }}" class="btn btn-sm btn-primary" style="font-size:11px;padding:3px 10px;">Pay</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="row g-3 mb-4">
<!-- Monthly spend chart -->
<div class="col-12 col-xl-8">
<div class="pcard h-100">
<div class="pcard-title mb-3">Utility Spend — Last 12 Months</div>
{% if chart.datasets %}
<div style="height:280px;"><canvas id="monthlyChart"></canvas></div>
{% else %}
<div class="text-center text-muted py-5" style="font-size:13px;">No bills recorded yet.</div>
{% endif %}
</div>
</div>
<!-- By type -->
<div class="col-12 col-xl-4">
<div class="pcard h-100">
<div class="pcard-title mb-3">By Type — 12 Months</div>
{% if type_totals %}
{% set grand = type_totals | sum(attribute='total') %}
{% for t in type_totals %}
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span style="font-size:12px;font-weight:600;"><i class="bi {{ t.icon }} me-1" style="color:{{ t.color }};"></i>{{ t.label }}</span>
<span class="mono" style="font-size:12px;">{{ t.total | currency }}</span>
</div>
<div style="height:6px;background:#f1f5f9;border-radius:3px;">
<div style="height:6px;border-radius:3px;background:{{ t.color }};width:{{ (t.total / grand * 100) if grand else 0 }}%;"></div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center text-muted py-4" style="font-size:13px;">No data yet.</div>
{% endif %}
</div>
</div>
</div>
<!-- Providers -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="pcard-title mb-0">Providers</div>
<a href="{{ url_for('utilities.provider_new') }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;"><i class="bi bi-plus-lg me-1"></i>Add Provider</a>
</div>
{% if providers %}
<div class="row g-3">
{% for p in providers %}
{% set s = summaries[p.id] %}
<div class="col-12 col-md-6 col-xl-4">
<div class="util-card" style="border-left:4px solid {{ p.color }};">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="d-flex align-items-center gap-2">
<div style="width:36px;height:36px;border-radius:9px;background:{{ p.color }}22;color:{{ p.color }};display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;">
<i class="bi {{ p.icon }}"></i>
</div>
<div>
<div style="font-size:14px;font-weight:600;">{{ p.name }}</div>
<div style="font-size:11px;color:var(--muted);">{{ p.type_label }}{% if s.unpaid_count %} · <span class="text-expense fw-bold">{{ s.unpaid_count }} unpaid</span>{% endif %}</div>
</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('utilities.bill_new', provider_id=p.id) }}"><i class="bi bi-plus-circle me-2"></i>Add Bill</a></li>
<li><a class="dropdown-item" href="{{ url_for('utilities.provider_detail', id=p.id) }}"><i class="bi bi-graph-up me-2"></i>History &amp; Usage</a></li>
<li><a class="dropdown-item" href="{{ url_for('utilities.provider_edit', id=p.id) }}"><i class="bi bi-pencil me-2"></i>Edit</a></li>
</ul>
</div>
</div>
{% if s.latest %}
<div class="d-flex justify-content-between align-items-end">
<div>
<div style="font-size:11px;color:var(--muted);">Latest · {{ s.latest.period_start.strftime('%b %Y') }}</div>
<div class="mono fw-bold" style="font-size:18px;">{{ s.latest.amount | currency }}</div>
</div>
<div class="text-end">
{% if s.amount_change_pct is not none %}
<div class="{% if s.amount_change_pct > 0 %}trend-up{% else %}trend-down{% endif %}" style="font-size:12px;font-weight:600;">
<i class="bi bi-arrow-{% if s.amount_change_pct > 0 %}up{% else %}down{% endif %}-short"></i>{{ s.amount_change_pct | abs }}%
</div>
<div style="font-size:10px;color:var(--muted);">vs prev period</div>
{% endif %}
</div>
</div>
{% if s.latest.usage %}
<div class="d-flex justify-content-between mt-2 pt-2" style="border-top:1px solid var(--border);font-size:12px;">
<span class="text-muted">Usage</span>
<span class="mono">{{ '%.1f' | format(s.latest.usage | float) }} {{ s.latest.usage_unit or p.usage_unit }}
{% if s.usage_change_pct is not none %}
<span class="{% if s.usage_change_pct > 0 %}trend-up{% else %}trend-down{% endif %}" style="font-size:11px;">({{ '+' if s.usage_change_pct > 0 else '' }}{{ s.usage_change_pct }}%)</span>
{% endif %}
</span>
</div>
{% endif %}
{% if s.latest.rate_per_unit %}
<div class="d-flex justify-content-between mt-1" style="font-size:12px;">
<span class="text-muted">Unit rate</span>
<span class="mono">{{ current_user.currency_symbol }}{{ '%.4f' | format(s.latest.rate_per_unit) }} / {{ s.latest.usage_unit or p.usage_unit }}</span>
</div>
{% endif %}
<div class="d-flex justify-content-between mt-1" style="font-size:12px;">
<span class="text-muted">12-month total</span>
<span class="mono">{{ s.total_12mo | currency }}</span>
</div>
{% if s.latest.status != 'paid' %}
<a href="{{ url_for('utilities.bill_pay', id=s.latest.id) }}" class="btn btn-sm w-100 mt-3" style="background:{{ p.color }}22;color:{{ p.color }};border:1px solid {{ p.color }}44;font-size:12px;">
<i class="bi bi-check2-circle me-1"></i>Mark Latest Paid
</a>
{% else %}
<a href="{{ url_for('utilities.bill_new', provider_id=p.id) }}" class="btn btn-sm w-100 mt-3" style="background:{{ p.color }}22;color:{{ p.color }};border:1px solid {{ p.color }}44;font-size:12px;">
<i class="bi bi-plus-lg me-1"></i>Add Bill
</a>
{% endif %}
{% else %}
<div class="text-center text-muted py-3" style="font-size:12px;">No bills yet</div>
<a href="{{ url_for('utilities.bill_new', provider_id=p.id) }}" class="btn btn-sm w-100" style="background:{{ p.color }}22;color:{{ p.color }};border:1px solid {{ p.color }}44;font-size:12px;">
<i class="bi bi-plus-lg me-1"></i>Add First Bill
</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="pcard text-center py-5">
<i class="bi bi-lightning-charge text-muted" style="font-size:3rem;"></i>
<h5 class="mt-3 mb-1">No utility providers yet</h5>
<p class="text-muted small mb-3">Add your electricity, water, gas, and internet providers to track bills and usage.</p>
<a href="{{ url_for('utilities.provider_new') }}" class="btn btn-primary btn-sm">Add First Provider</a>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
{% if chart.datasets %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script>
(function () {
const dark = document.documentElement.getAttribute('data-pfm-dark') === '1';
const grid = dark ? 'rgba(148,163,184,.15)' : 'rgba(148,163,184,.25)';
const tick = dark ? '#94a3b8' : '#64748b';
const sym = {{ current_user.currency_symbol | tojson }};
new Chart(document.getElementById('monthlyChart'), {
type: 'bar',
data: {
labels: {{ chart.labels | tojson }},
datasets: {{ chart.datasets | tojson }}.map(function (d) {
return { label: d.label, data: d.data, backgroundColor: d.color, borderRadius: 4, borderSkipped: false };
})
},
options: {
responsive: true, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: 'bottom', labels: { boxWidth: 10, boxHeight: 10, font: { size: 11 }, color: tick } },
tooltip: {
callbacks: {
label: function (c) { return c.dataset.label + ': ' + sym + c.parsed.y.toFixed(2); },
footer: function (items) {
const total = items.reduce(function (s, i) { return s + i.parsed.y; }, 0);
return 'Total: ' + sym + total.toFixed(2);
}
}
}
},
scales: {
x: { stacked: true, grid: { display: false }, ticks: { font: { size: 11 }, color: tick } },
y: { stacked: true, grid: { color: grid }, ticks: { font: { size: 11 }, color: tick, callback: function (v) { return sym + v; } } }
}
}
});
})();
</script>
{% endif %}
{% endblock %}