06/05 Optimize app: report upgrades

This commit is contained in:
2026-06-05 14:22:19 -04:00
parent 2f62417c9f
commit 04acefd9f8
6 changed files with 486 additions and 25 deletions
+88 -2
View File
@@ -238,6 +238,15 @@
</div>
{% endif %}
<!-- Spending Anomalies -->
<div id="anomaly-card" class="pcard mb-4" style="display:none;">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-exclamation-triangle" style="color:#f59e0b;font-size:15px;"></i>
<span class="pcard-title mb-0">Unusual Spending Detected</span>
</div>
<div id="anomaly-list"></div>
</div>
<!-- Accounts + Recent Transactions -->
<div class="row g-3">
<div class="col-12 col-lg-4">
@@ -287,9 +296,10 @@
<a href="{{ url_for('transactions.index') }}" style="font-size:12px;color:#3b82f6;">View all</a>
</div>
{% if recent_txns %}
<div class="table-wrap">
<table class="pfm-table">
<thead>
<tr><th>Date</th><th>Description</th><th>Category</th><th class="text-end">Amount</th></tr>
<tr><th>Date</th><th>Description</th><th class="d-mob-none">Category</th><th class="text-end">Amount</th></tr>
</thead>
<tbody>
{% for txn in recent_txns %}
@@ -299,7 +309,7 @@
<div style="font-size:13px;font-weight:500;">{{ txn.description }}</div>
<div style="font-size:11px;color:var(--muted);">{{ txn.account.name if txn.account else '—' }}</div>
</td>
<td>
<td class="d-mob-none">
{% if txn.category %}
<span style="font-size:12px;"><i class="bi {{ txn.category.icon }}" style="color:{{ txn.category.color }};"></i> {{ txn.category.name }}</span>
{% else %}<span class="text-muted" style="font-size:12px;"></span>{% endif %}
@@ -311,6 +321,7 @@
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted small">No transactions yet. <a href="{{ url_for('transactions.new', type='expense') }}">Add one</a>.</p>
{% endif %}
@@ -345,6 +356,50 @@
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script>
// ── Period memory (localStorage) ─────────────────────────────────────────────
(function () {
var KEY_P = 'pfm_dash_period', KEY_Q = 'pfm_dash_params';
var params = new URLSearchParams(window.location.search);
// If landing with no period param, restore last saved period
if (!params.has('period') && !params.has('date_from')) {
var saved = localStorage.getItem(KEY_Q);
if (saved) {
window.location.replace('/?' + saved);
return;
}
}
// Save current period whenever a period button is clicked
document.querySelectorAll('a.period-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var url = new URL(this.href, location.origin);
var p = url.searchParams.get('period');
if (p) {
localStorage.setItem(KEY_P, p);
localStorage.setItem(KEY_Q, url.search.slice(1));
}
});
});
// Save when custom range form is submitted
var customForm = document.querySelector('#customModal form');
if (customForm) {
customForm.addEventListener('submit', function () {
var fd = new FormData(this);
localStorage.setItem(KEY_P, 'custom');
localStorage.setItem(KEY_Q, new URLSearchParams(fd).toString());
});
}
// Save the current period (handles direct URL visits with ?period=xxx)
var cur = params.get('period');
if (cur) {
localStorage.setItem(KEY_P, cur);
localStorage.setItem(KEY_Q, params.toString());
}
})();
(function(){
const ctx = document.getElementById('cashflowChart').getContext('2d');
new Chart(ctx, {
@@ -500,6 +555,37 @@ function refreshFxRate() {
});
})();
// ── Spending anomalies ───────────────────────────────────────────────────────
(function(){
const card = document.getElementById('anomaly-card');
const list = document.getElementById('anomaly-list');
if (!card || !list) return;
const SYM = '{{ current_user.currency_symbol }}';
function fmt(n){ return SYM + Math.abs(n).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2}); }
fetch('/api/anomalies')
.then(r => r.json())
.then(data => {
const items = data.anomalies || [];
if (!items.length) return; // stay hidden
list.innerHTML = items.map(a => `
<div class="d-flex justify-content-between align-items-center py-2" style="border-bottom:1px solid var(--border);font-size:13px;">
<div class="d-flex align-items-center gap-2">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;flex-shrink:0;"></span>
<div>
<div class="fw-medium">${a.description}</div>
<div class="text-muted" style="font-size:11px;">${a.category} · ${a.date}</div>
</div>
</div>
<div class="text-end flex-shrink-0 ms-3">
<div class="mono text-expense">${fmt(a.amount)}</div>
<div style="font-size:11px;color:#f59e0b;">${a.multiple.toFixed(1)}× avg ${fmt(a.avg_amount)}</div>
</div>
</div>`).join('');
card.style.display = '';
})
.catch(() => {}); // silently ignore on error
})();
function toggleFxChart(){
const wrap = document.getElementById('fxChartWrap');
wrap.style.display = wrap.style.display === 'none' ? 'block' : 'none';