06/05 Optimize app: filters

This commit is contained in:
2026-06-05 15:06:14 -04:00
parent 1a4e68b422
commit db44d6057b
2 changed files with 130 additions and 8 deletions
+113 -7
View File
@@ -43,13 +43,14 @@
<!-- Filters -->
<div class="pcard pcard-sm mb-3">
<form method="GET" action="{{ url_for('transactions.index') }}" class="row g-2 align-items-end">
<form id="filter-form" method="GET" action="{{ url_for('transactions.index') }}" class="row g-2 align-items-end">
<input type="hidden" name="tab" value="{{ tab }}">
<!-- Row 1: main filters -->
<div class="col-12 col-md-3">
<input type="text" name="q" class="form-control form-control-sm" placeholder="Search description…" value="{{ search }}">
<input type="text" name="q" id="f-q" class="form-control form-control-sm" placeholder="Search description &amp; notes…" value="{{ search }}">
</div>
<div class="col-6 col-md-2">
<select name="category_id" class="form-select form-select-sm">
<select name="category_id" id="f-cat" class="form-select form-select-sm">
<option value="">All categories</option>
{% for cat in categories %}
<option value="{{ cat.id }}" {% if category_id == cat.id|string %}selected{% endif %}>{{ cat.name }}</option>
@@ -57,7 +58,7 @@
</select>
</div>
<div class="col-6 col-md-2">
<select name="account_id" class="form-select form-select-sm">
<select name="account_id" id="f-acct" class="form-select form-select-sm">
<option value="">All accounts</option>
{% for acct in accounts %}
<option value="{{ acct.id }}" {% if account_id == acct.id|string %}selected{% endif %}>{{ acct.name }}</option>
@@ -65,14 +66,35 @@
</select>
</div>
<div class="col-6 col-md-2">
<input type="date" name="date_from" class="form-control form-control-sm" value="{{ date_from }}" placeholder="From">
<input type="date" name="date_from" id="f-df" class="form-control form-control-sm" value="{{ date_from }}" placeholder="From">
</div>
<div class="col-6 col-md-2">
<input type="date" name="date_to" class="form-control form-control-sm" value="{{ date_to }}" placeholder="To">
<input type="date" name="date_to" id="f-dt" class="form-control form-control-sm" value="{{ date_to }}" placeholder="To">
</div>
<div class="col-12 col-md-1 d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary flex-grow-1"><i class="bi bi-search"></i></button>
<a href="{{ url_for('transactions.index', tab=tab) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-x-lg"></i></a>
<a href="{{ url_for('transactions.index', tab=tab) }}" class="btn btn-sm btn-outline-secondary" title="Clear filters"><i class="bi bi-x-lg"></i></a>
</div>
<!-- Row 2: amount range + preset controls -->
<div class="col-6 col-md-2">
<input type="number" name="amount_min" id="f-amin" class="form-control form-control-sm" placeholder="Min amount" min="0" step="0.01" value="{{ amount_min }}">
</div>
<div class="col-6 col-md-2">
<input type="number" name="amount_max" id="f-amax" class="form-control form-control-sm" placeholder="Max amount" min="0" step="0.01" value="{{ amount_max }}">
</div>
<div class="col-12 col-md-8 d-flex align-items-center gap-2 flex-wrap">
<span style="font-size:11px;color:var(--muted);">Saved filters:</span>
<div class="dropdown">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" style="font-size:12px;" id="preset-dropdown" data-bs-toggle="dropdown">
<i class="bi bi-bookmark me-1"></i><span id="preset-label">Load preset</span>
</button>
<ul class="dropdown-menu" id="preset-menu" style="font-size:13px;min-width:200px;">
<li><span class="dropdown-item text-muted" id="no-presets-msg">No saved presets</span></li>
</ul>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" style="font-size:12px;" id="save-preset-btn" title="Save current filter as preset">
<i class="bi bi-bookmark-plus me-1"></i>Save
</button>
</div>
</form>
</div>
@@ -356,5 +378,89 @@ document.getElementById('bulk-cat-btn')?.addEventListener('click', () => {
})
.catch(() => alert('Network error — please try again.'));
});
/* ── Saved filter presets (localStorage) ── */
(function () {
const PKEY = 'pfm_txn_presets';
function loadPresets() {
try { return JSON.parse(localStorage.getItem(PKEY) || '[]'); }
catch { return []; }
}
function savePresets(list) {
localStorage.setItem(PKEY, JSON.stringify(list));
}
function readForm() {
return {
q: document.getElementById('f-q')?.value || '',
category_id:document.getElementById('f-cat')?.value || '',
account_id: document.getElementById('f-acct')?.value || '',
date_from: document.getElementById('f-df')?.value || '',
date_to: document.getElementById('f-dt')?.value || '',
amount_min: document.getElementById('f-amin')?.value || '',
amount_max: document.getElementById('f-amax')?.value || '',
tab: '{{ tab }}',
};
}
function applyPreset(p) {
const base = '/transactions/?' + new URLSearchParams(p).toString();
window.location.href = base;
}
function renderMenu() {
const presets = loadPresets();
const menu = document.getElementById('preset-menu');
const noMsg = document.getElementById('no-presets-msg');
// remove old preset items (keep no-presets-msg li)
menu.querySelectorAll('.preset-item').forEach(el => el.remove());
if (!presets.length) {
noMsg.style.display = '';
return;
}
noMsg.style.display = 'none';
presets.forEach((p, idx) => {
const li = document.createElement('li');
li.className = 'preset-item d-flex align-items-center px-2 gap-1';
li.innerHTML = `
<button type="button" class="dropdown-item py-1 flex-grow-1 text-start" style="font-size:13px;">${p.name}</button>
<button type="button" class="btn btn-sm p-0 text-danger preset-del" title="Delete" data-idx="${idx}" style="font-size:13px;line-height:1;background:none;border:none;">
<i class="bi bi-x-circle"></i>
</button>`;
li.querySelector('.dropdown-item').addEventListener('click', () => applyPreset(p.filters));
li.querySelector('.preset-del').addEventListener('click', (e) => {
e.stopPropagation();
const list = loadPresets();
list.splice(idx, 1);
savePresets(list);
renderMenu();
});
menu.appendChild(li);
});
}
document.getElementById('save-preset-btn')?.addEventListener('click', () => {
const name = prompt('Name this filter preset:');
if (!name?.trim()) return;
const list = loadPresets();
list.push({ name: name.trim(), filters: readForm() });
savePresets(list);
renderMenu();
// Flash the save button
const btn = document.getElementById('save-preset-btn');
btn.innerHTML = '<i class="bi bi-bookmark-check-fill me-1"></i>Saved!';
btn.classList.replace('btn-outline-secondary', 'btn-success');
setTimeout(() => {
btn.innerHTML = '<i class="bi bi-bookmark-plus me-1"></i>Save';
btn.classList.replace('btn-success', 'btn-outline-secondary');
}, 1500);
});
renderMenu();
})();
</script>
{% endblock %}