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 @@
@@ -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 %}