06/05 Optimize app

This commit is contained in:
2026-06-05 15:23:23 -04:00
parent db44d6057b
commit 9c9aa694c4
9 changed files with 781 additions and 19 deletions
+141
View File
@@ -94,4 +94,145 @@
</div>
</div>
</div>
<!-- Cash Flow Projection -->
<div class="pcard mt-3">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<span class="pcard-title mb-0">Projected Cash Flow</span>
<div class="d-flex gap-1">
<button class="btn btn-sm proj-btn btn-primary" data-days="30" style="font-size:12px;">30d</button>
<button class="btn btn-sm proj-btn btn-outline-secondary" data-days="60" style="font-size:12px;">60d</button>
<button class="btn btn-sm proj-btn btn-outline-secondary" data-days="90" style="font-size:12px;">90d</button>
</div>
</div>
<!-- Summary stats -->
<div class="row g-2 mb-3" id="proj-stats">
<div class="col-6 col-md-3">
<div class="pcard pcard-sm text-center p-2" style="background:#f0fdf4;">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;">Income</div>
<div id="proj-income" class="mono text-income fw-bold" style="font-size:15px;"></div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="pcard pcard-sm text-center p-2" style="background:#fef2f2;">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;">Expenses</div>
<div id="proj-expense" class="mono text-expense fw-bold" style="font-size:15px;"></div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="pcard pcard-sm text-center p-2">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;">Net</div>
<div id="proj-net" class="mono fw-bold" style="font-size:15px;"></div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="pcard pcard-sm text-center p-2" style="background:#eff6ff;">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;">End Balance</div>
<div id="proj-end" class="mono text-invest fw-bold" style="font-size:15px;"></div>
</div>
</div>
</div>
<!-- Chart -->
<div id="proj-chart-wrap" style="position:relative;height:200px;">
<canvas id="projChart"></canvas>
</div>
<div id="proj-loading" class="text-center py-4 text-muted" style="display:none;">
<span class="spinner-border spinner-border-sm me-2"></span>Loading…
</div>
<!-- Event list -->
<div id="proj-events" class="mt-3" style="max-height:260px;overflow-y:auto;"></div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script>
const SYM = '{{ current_user.currency_symbol }}';
const fmtC = v => SYM + Math.abs(v).toLocaleString(undefined,{minimumFractionDigits:0,maximumFractionDigits:0});
let projChart = null;
function loadProjection(days) {
// update active button
document.querySelectorAll('.proj-btn').forEach(b => {
const active = b.dataset.days == days;
b.className = 'btn btn-sm proj-btn ' + (active ? 'btn-primary' : 'btn-outline-secondary');
b.style.fontSize = '12px';
});
document.getElementById('proj-loading').style.display = '';
document.getElementById('proj-chart-wrap').style.display = 'none';
fetch('/settings/recurring/projection?days=' + days)
.then(r => r.json())
.then(data => {
document.getElementById('proj-loading').style.display = 'none';
document.getElementById('proj-chart-wrap').style.display = '';
// Summary stats
document.getElementById('proj-income').textContent = fmtC(data.total_income);
document.getElementById('proj-expense').textContent = fmtC(data.total_expense);
const net = data.net;
const netEl = document.getElementById('proj-net');
netEl.textContent = (net >= 0 ? '+' : '-') + fmtC(Math.abs(net));
netEl.style.color = net >= 0 ? '#10b981' : '#ef4444';
document.getElementById('proj-end').textContent = fmtC(data.ending_balance);
// Chart
if (projChart) projChart.destroy();
const ctx = document.getElementById('projChart').getContext('2d');
projChart = new Chart(ctx, {
data: {
labels: data.labels,
datasets: [
{ type:'bar', label:'Income', data:data.income, backgroundColor:'#10b98133', borderColor:'#10b981', borderWidth:1.5, borderRadius:3, yAxisID:'y' },
{ type:'bar', label:'Expense', data:data.expense, backgroundColor:'#ef444433', borderColor:'#ef4444', borderWidth:1.5, borderRadius:3, yAxisID:'y' },
{ type:'line', label:'Balance', data:data.balance, borderColor:'#3b82f6', backgroundColor:'transparent', borderWidth:2, pointRadius:3, tension:.3, yAxisID:'y2' },
]
},
options: {
responsive:true, maintainAspectRatio:false,
interaction:{ mode:'index', intersect:false },
plugins:{ legend:{ position:'bottom', labels:{ font:{ size:11 }, boxWidth:10 } } },
scales:{
y: { position:'left', grid:{ color:'#f1f5f9' }, ticks:{ font:{size:10}, callback: v=>fmtC(v) } },
y2: { position:'right', grid:{ display:false }, ticks:{ font:{size:10}, callback: v=>fmtC(v) } },
x: { grid:{ display:false }, ticks:{ font:{size:10} } },
}
}
});
// Event list
const evList = document.getElementById('proj-events');
if (!data.events.length) {
evList.innerHTML = '<p class="text-muted small">No recurring transactions in this period.</p>';
return;
}
evList.innerHTML = data.events.map(ev => `
<div class="d-flex justify-content-between align-items-center py-2" style="border-top:1px solid var(--border);font-size:13px;">
<div>
<span class="fw-medium">${ev.description}</span>
<span class="text-muted ms-2" style="font-size:11px;">${ev.date}</span>
</div>
<span class="mono ${ev.type === 'income' ? 'text-income' : 'text-expense'}" style="font-size:13px;white-space:nowrap;">
${ev.type === 'income' ? '+' : '-'}${SYM}${Math.abs(ev.amount).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}
</span>
</div>`).join('');
})
.catch(() => {
document.getElementById('proj-loading').style.display = 'none';
document.getElementById('proj-chart-wrap').style.display = '';
});
}
document.querySelectorAll('.proj-btn').forEach(btn => {
btn.addEventListener('click', () => loadProjection(parseInt(btn.dataset.days)));
});
// Load 30-day projection on page load
loadProjection(30);
</script>
{% endblock %}