diff --git a/app/routes/transactions.py b/app/routes/transactions.py index 3b5cd69..0fb66bd 100644 --- a/app/routes/transactions.py +++ b/app/routes/transactions.py @@ -71,6 +71,8 @@ def index(): account_id = request.args.get('account_id', '', type=str) date_from = request.args.get('date_from', '') 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 today = date.today() @@ -93,7 +95,12 @@ def index(): ).order_by(Transaction.date.desc(), Transaction.id.desc()) 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: if 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()) except ValueError: 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) @@ -137,6 +151,8 @@ def index(): account_id=account_id, date_from=date_from, date_to=date_to, + amount_min=amount_min, + amount_max=amount_max, active_quick=active_quick, this_month_from=this_month_from, this_month_to=this_month_to, diff --git a/app/templates/transactions/index.html b/app/templates/transactions/index.html index 1d62731..ee15294 100644 --- a/app/templates/transactions/index.html +++ b/app/templates/transactions/index.html @@ -43,13 +43,14 @@
-
+ +
- +
- {% for cat in categories %} @@ -57,7 +58,7 @@
- {% for acct in accounts %} @@ -65,14 +66,35 @@
- +
- +
- + +
+ +
+ +
+
+ +
+
+ Saved filters: + +
@@ -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 = ` + + `; + 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 = 'Saved!'; + btn.classList.replace('btn-outline-secondary', 'btn-success'); + setTimeout(() => { + btn.innerHTML = 'Save'; + btn.classList.replace('btn-success', 'btn-outline-secondary'); + }, 1500); + }); + + renderMenu(); +})(); {% endblock %}