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

281 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ provider.name }}{% endblock %}
{% block page_title %}{{ provider.name }}{% endblock %}
{% block extra_css %}
.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.index') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">← Utilities</a>
<a href="{{ url_for('utilities.provider_edit', id=provider.id) }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;"><i class="bi bi-pencil me-1"></i><span class="btn-label">Edit</span></a>
<a href="{{ url_for('utilities.bill_new', provider_id=provider.id) }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i><span class="btn-label">Add Bill</span></a>
{% endblock %}
{% block content %}
<!-- Header -->
<div class="pcard mb-4" style="border-left:4px solid {{ provider.color }};">
<div class="d-flex align-items-center gap-3 flex-wrap">
<div style="width:46px;height:46px;border-radius:11px;background:{{ provider.color }}22;color:{{ provider.color }};display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">
<i class="bi {{ provider.icon }}"></i>
</div>
<div class="flex-grow-1">
<div style="font-size:16px;font-weight:600;">{{ provider.name }}</div>
<div style="font-size:12px;color:var(--muted);">
{{ provider.type_label }}
{% if provider.account_number %} · Acct <span class="mono">{{ provider.account_number }}</span>{% endif %}
{% if provider.default_account %} · Paid from {{ provider.default_account.name }}{% endif %}
{% if provider.billing_day %} · Bills on day {{ provider.billing_day }}{% endif %}
</div>
</div>
</div>
{% if provider.notes %}
<div style="font-size:12px;color:var(--muted);margin-top:10px;padding-top:10px;border-top:1px solid var(--border);">{{ provider.notes }}</div>
{% endif %}
</div>
<!-- Stats -->
<div class="row g-3 mb-4">
<div class="col-6 col-xl-3">
<div class="stat-card">
<div class="stat-label">Latest Bill</div>
<div class="stat-value">{{ stats.latest.amount | currency if stats.latest else '—' }}</div>
{% if stats.amount_change_pct is not none %}
<small class="{% if stats.amount_change_pct > 0 %}trend-up{% else %}trend-down{% endif %}" style="font-size:11px;">
<i class="bi bi-arrow-{% if stats.amount_change_pct > 0 %}up{% else %}down{% endif %}-short"></i>{{ stats.amount_change_pct | abs }}% vs prev
</small>
{% endif %}
</div>
</div>
<div class="col-6 col-xl-3">
<div class="stat-card">
<div class="stat-label">Average Bill</div>
<div class="stat-value">{{ stats.avg_amount | currency }}</div>
<small class="text-muted" style="font-size:11px;">Last 12 months</small>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="stat-card">
<div class="stat-label">12-Month Total</div>
<div class="stat-value">{{ stats.total_12mo | currency }}</div>
<small class="text-muted" style="font-size:11px;">{{ stats.bill_count }} bill{{ '' if stats.bill_count == 1 else 's' }} on record</small>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="stat-card">
<div class="stat-label">{% if provider.tracks_usage %}Avg Usage{% else %}Year over Year{% endif %}</div>
{% if provider.tracks_usage %}
<div class="stat-value">{{ '%.1f' | format(stats.avg_usage) if stats.avg_usage else '—' }}<span style="font-size:13px;color:var(--muted);"> {{ provider.usage_unit }}</span></div>
{% if stats.usage_change_pct is not none %}
<small class="{% if stats.usage_change_pct > 0 %}trend-up{% else %}trend-down{% endif %}" style="font-size:11px;">
<i class="bi bi-arrow-{% if stats.usage_change_pct > 0 %}up{% else %}down{% endif %}-short"></i>{{ stats.usage_change_pct | abs }}% latest vs prev
</small>
{% endif %}
{% else %}
<div class="stat-value">{% if stats.yoy_change_pct is not none %}{{ '+' if stats.yoy_change_pct > 0 else '' }}{{ stats.yoy_change_pct }}%{% else %}—{% endif %}</div>
<small class="text-muted" style="font-size:11px;">Latest vs same period last year</small>
{% endif %}
</div>
</div>
</div>
<!-- Chart -->
{% if series.labels %}
<div class="pcard mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="pcard-title mb-0">Billing History</div>
{% if series.has_usage %}
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary active" style="font-size:11px;" data-mode="amount">Amount</button>
<button class="btn btn-outline-secondary" style="font-size:11px;" data-mode="usage">Usage</button>
<button class="btn btn-outline-secondary" style="font-size:11px;" data-mode="rate">Unit Rate</button>
</div>
{% endif %}
</div>
<div style="height:300px;"><canvas id="histChart"></canvas></div>
</div>
{% endif %}
<!-- Bills -->
<div class="pcard p-0">
<div class="d-flex justify-content-between align-items-center p-3" style="border-bottom:1px solid var(--border);">
<div class="pcard-title mb-0">Bills</div>
</div>
{% if bills %}
<div class="table-wrap">
<table class="pfm-table wide">
<thead>
<tr>
<th>Period</th>
<th class="d-mob-none">Due</th>
<th class="text-end">Amount</th>
{% if provider.tracks_usage %}
<th class="text-end d-mob-none">Usage</th>
<th class="text-end d-mob-none">Rate</th>
{% endif %}
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for b in bills %}
<tr>
<td style="font-size:12px;">
{{ b.period_label }}
{% if b.notes %}<div style="font-size:11px;color:var(--muted);">{{ b.notes | truncate(60) }}</div>{% endif %}
</td>
<td class="d-mob-none" style="font-size:12px;">{{ b.due_date.strftime('%b %d, %Y') if b.due_date else '—' }}</td>
<td class="text-end mono fw-bold" style="font-size:13px;">{{ b.amount | currency }}</td>
{% if provider.tracks_usage %}
<td class="text-end mono d-mob-none" style="font-size:12px;">
{% if b.usage %}{{ '%.1f' | format(b.usage | float) }} <span style="color:var(--muted);">{{ b.usage_unit or provider.usage_unit }}</span>{% else %}—{% endif %}
</td>
<td class="text-end mono d-mob-none" style="font-size:12px;">
{% if b.rate_per_unit %}{{ current_user.currency_symbol }}{{ '%.4f' | format(b.rate_per_unit) }}{% else %}—{% endif %}
</td>
{% endif %}
<td>
{% if b.status == 'paid' %}
<span class="util-chip" style="background:#d1fae5;color:#065f46;">Paid</span>
{% if b.paid_date %}<div style="font-size:10px;color:var(--muted);margin-top:2px;">{{ b.paid_date.strftime('%b %d') }}</div>{% endif %}
{% elif 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;">Due in {{ b.days_until_due }}d</span>
{% else %}
<span class="util-chip" style="background:#f1f5f9;color:#475569;">Unpaid</span>
{% endif %}
</td>
<td class="text-end">
<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;">
{% if not b.is_paid %}
<li><a class="dropdown-item" href="{{ url_for('utilities.bill_pay', id=b.id) }}"><i class="bi bi-check2-circle me-2"></i>Mark Paid</a></li>
<li><a class="dropdown-item" href="{{ url_for('utilities.bill_link', id=b.id) }}"><i class="bi bi-link-45deg me-2"></i>Link Existing Payment</a></li>
{% else %}
{% if b.transaction_id %}
<li><a class="dropdown-item" href="{{ url_for('transactions.edit', id=b.transaction_id) }}"><i class="bi bi-receipt me-2"></i>View Payment</a></li>
{% endif %}
<li>
<form method="POST" action="{{ url_for('utilities.bill_unpay', id=b.id) }}" onsubmit="return confirm('Reopen this bill? A payment transaction created by PFM will be deleted.')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="dropdown-item"><i class="bi bi-arrow-counterclockwise me-2"></i>Reopen</button>
</form>
</li>
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('utilities.bill_edit', id=b.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('utilities.bill_delete', id=b.id) }}" onsubmit="return confirm('Delete this bill?')">
<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>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if pagination.pages > 1 %}
<div class="d-flex justify-content-between align-items-center p-3" style="border-top:1px solid var(--border);font-size:12px;color:var(--muted);">
<span>Page {{ pagination.page }} of {{ pagination.pages }} &nbsp;·&nbsp; {{ pagination.total }} bills</span>
<div class="d-flex gap-2">
{% if pagination.has_prev %}
<a href="{{ url_for('utilities.provider_detail', id=provider.id, page=pagination.prev_num) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;">← Prev</a>
{% endif %}
{% if pagination.has_next %}
<a href="{{ url_for('utilities.provider_detail', id=provider.id, page=pagination.next_num) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;">Next →</a>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-receipt text-muted" style="font-size:2.5rem;"></i>
<h6 class="mt-3 mb-1">No bills recorded</h6>
<p class="text-muted small mb-3">Add a bill to start tracking cost{% if provider.tracks_usage %} and usage{% endif %}.</p>
<a href="{{ url_for('utilities.bill_new', provider_id=provider.id) }}" class="btn btn-primary btn-sm">Add First Bill</a>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
{% if series.labels %}
<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 }};
const color = {{ provider.color | tojson }};
const S = {{ series | tojson }};
const MODES = {
amount: { label: 'Amount', data: S.amounts, type: 'bar', fmt: function (v) { return sym + v.toFixed(2); } },
usage: { label: 'Usage (' + S.unit + ')', data: S.usage, type: 'line', fmt: function (v) { return v.toFixed(1) + ' ' + S.unit; } },
rate: { label: 'Rate per ' + S.unit, data: S.rates, type: 'line', fmt: function (v) { return sym + v.toFixed(4); } }
};
let chart = null;
function render(mode) {
const m = MODES[mode];
if (chart) chart.destroy();
chart = new Chart(document.getElementById('histChart'), {
type: m.type,
data: {
labels: S.labels,
datasets: [{
label: m.label,
data: m.data,
backgroundColor: m.type === 'bar' ? color : color + '22',
borderColor: color,
borderWidth: 2,
borderRadius: 4,
tension: .3,
fill: m.type === 'line',
pointRadius: 3,
spanGaps: true
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { callbacks: { label: function (c) { return m.label + ': ' + m.fmt(c.parsed.y); } } }
},
scales: {
x: { grid: { display: false }, ticks: { font: { size: 11 }, color: tick } },
y: { grid: { color: grid }, ticks: { font: { size: 11 }, color: tick,
callback: function (v) { return mode === 'amount' ? sym + v : v; } } }
}
}
});
}
render('amount');
document.querySelectorAll('[data-mode]').forEach(function (btn) {
btn.addEventListener('click', function () {
document.querySelectorAll('[data-mode]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
render(btn.dataset.mode);
});
});
})();
</script>
{% endif %}
{% endblock %}