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
+17 -1
View File
@@ -71,6 +71,8 @@ def index():
account_id = request.args.get('account_id', '', type=str) account_id = request.args.get('account_id', '', type=str)
date_from = request.args.get('date_from', '') date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '') date_to = request.args.get('date_to', '')
amount_min = request.args.get('amount_min', '')
amount_max = request.args.get('amount_max', '')
from datetime import timedelta from datetime import timedelta
today = date.today() today = date.today()
@@ -93,7 +95,12 @@ def index():
).order_by(Transaction.date.desc(), Transaction.id.desc()) ).order_by(Transaction.date.desc(), Transaction.id.desc())
if search: if search:
query = query.filter(Transaction.description.ilike(f'%{search}%')) query = query.filter(
or_(
Transaction.description.ilike(f'%{search}%'),
Transaction.notes.ilike(f'%{search}%'),
)
)
try: try:
if category_id: if category_id:
query = query.filter(Transaction.category_id == int(category_id)) query = query.filter(Transaction.category_id == int(category_id))
@@ -111,6 +118,13 @@ def index():
query = query.filter(Transaction.date <= datetime.strptime(date_to, '%Y-%m-%d').date()) query = query.filter(Transaction.date <= datetime.strptime(date_to, '%Y-%m-%d').date())
except ValueError: except ValueError:
pass pass
try:
if amount_min:
query = query.filter(Transaction.amount >= float(amount_min))
if amount_max:
query = query.filter(Transaction.amount <= float(amount_max))
except (ValueError, TypeError):
pass
pagination = query.paginate(page=page, per_page=30, error_out=False) pagination = query.paginate(page=page, per_page=30, error_out=False)
@@ -137,6 +151,8 @@ def index():
account_id=account_id, account_id=account_id,
date_from=date_from, date_from=date_from,
date_to=date_to, date_to=date_to,
amount_min=amount_min,
amount_max=amount_max,
active_quick=active_quick, active_quick=active_quick,
this_month_from=this_month_from, this_month_from=this_month_from,
this_month_to=this_month_to, this_month_to=this_month_to,
+113 -7
View File
@@ -43,13 +43,14 @@
<!-- Filters --> <!-- Filters -->
<div class="pcard pcard-sm mb-3"> <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 }}"> <input type="hidden" name="tab" value="{{ tab }}">
<!-- Row 1: main filters -->
<div class="col-12 col-md-3"> <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>
<div class="col-6 col-md-2"> <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> <option value="">All categories</option>
{% for cat in categories %} {% for cat in categories %}
<option value="{{ cat.id }}" {% if category_id == cat.id|string %}selected{% endif %}>{{ cat.name }}</option> <option value="{{ cat.id }}" {% if category_id == cat.id|string %}selected{% endif %}>{{ cat.name }}</option>
@@ -57,7 +58,7 @@
</select> </select>
</div> </div>
<div class="col-6 col-md-2"> <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> <option value="">All accounts</option>
{% for acct in accounts %} {% for acct in accounts %}
<option value="{{ acct.id }}" {% if account_id == acct.id|string %}selected{% endif %}>{{ acct.name }}</option> <option value="{{ acct.id }}" {% if account_id == acct.id|string %}selected{% endif %}>{{ acct.name }}</option>
@@ -65,14 +66,35 @@
</select> </select>
</div> </div>
<div class="col-6 col-md-2"> <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>
<div class="col-6 col-md-2"> <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>
<div class="col-12 col-md-1 d-flex gap-1"> <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> <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> </div>
</form> </form>
</div> </div>
@@ -356,5 +378,89 @@ document.getElementById('bulk-cat-btn')?.addEventListener('click', () => {
}) })
.catch(() => alert('Network error — please try again.')); .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> </script>
{% endblock %} {% endblock %}