06/05 Optimize app: report upgrades
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user