From 8481e9c2148535b450e3e1f998fddf19e12f010f Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 5 Jun 2026 09:29:41 -0400 Subject: [PATCH] 06/05 Optimize app --- app/routes/schwab.py | 9 ++- app/routes/teller.py | 9 ++- app/templates/bank_import/index.html | 24 +++++- app/templates/schwab/preview.html | 109 ++++++++++++++++++------- app/templates/teller/preview.html | 117 +++++++++++++++++++-------- 5 files changed, 201 insertions(+), 67 deletions(-) diff --git a/app/routes/schwab.py b/app/routes/schwab.py index 9f23ad3..5384c2d 100644 --- a/app/routes/schwab.py +++ b/app/routes/schwab.py @@ -200,7 +200,8 @@ def sync_preview_view(schwab_account_id): return redirect(url_for('schwab.index')) from app.models.category import Category - cat_map = {c.id: c.name for c in Category.query.filter_by(is_active=True).all()} + cats = Category.query.filter_by(is_active=True).order_by(Category.name).all() + cat_map = {c.id: c.name for c in cats} session['schwab_preview'] = [ { @@ -222,7 +223,8 @@ def sync_preview_view(schwab_account_id): sa=sa, preview=preview, count=len(preview), - cat_map=cat_map) + cat_map=cat_map, + categories=cats) # ── Sync: confirm ───────────────────────────────────────────────────────────── @@ -248,6 +250,9 @@ def sync_confirm(): override = request.form.get(f'type_{r["schwab_id"]}') if override in ('income', 'expense'): r['transaction_type'] = override + cat_override = request.form.get(f'category_{r["schwab_id"]}', '') + if cat_override.isdigit(): + r['category_id'] = int(cat_override) parsed.append(r) if not parsed: diff --git a/app/routes/teller.py b/app/routes/teller.py index 9004ace..276e410 100644 --- a/app/routes/teller.py +++ b/app/routes/teller.py @@ -214,13 +214,15 @@ def sync_preview_view(teller_account_id): session['teller_account_id'] = teller_account_id from app.models.category import Category - cat_map = {c.id: c.name for c in Category.query.filter_by(is_active=True).all()} + cats = Category.query.filter_by(is_active=True).order_by(Category.name).all() + cat_map = {c.id: c.name for c in cats} return render_template('teller/preview.html', ta=ta, preview=preview, count=len(preview), - cat_map=cat_map) + cat_map=cat_map, + categories=cats) @teller_bp.route('/sync/confirm', methods=['POST']) @@ -256,6 +258,9 @@ def sync_confirm(): override = request.form.get(f'type_{r["teller_id"]}') if override in ('income', 'expense'): r['transaction_type'] = override + cat_override = request.form.get(f'category_{r["teller_id"]}', '') + if cat_override.isdigit(): + r['category_id'] = int(cat_override) parsed.append(r) if not parsed: diff --git a/app/templates/bank_import/index.html b/app/templates/bank_import/index.html index 1b4d687..93168a5 100644 --- a/app/templates/bank_import/index.html +++ b/app/templates/bank_import/index.html @@ -177,9 +177,18 @@
-
+
+
+ + +
-
- Cancel - + +
+ + +
@@ -35,7 +57,7 @@ Description Schwab Type PFM Type - Category + Category Amount @@ -61,12 +83,14 @@ - - {% if txn.category_id and txn.category_id in cat_map %} - {{ cat_map[txn.category_id] }} - {% else %} - Uncategorised - {% endif %} + + cb.addEventListener('change', updateCount)); + function applyTypeStyle(sel) { + const isIncome = sel.value === 'income'; + sel.style.borderColor = isIncome ? '#10b981' : '#ef4444'; + sel.style.color = isIncome ? '#10b981' : '#ef4444'; + const row = sel.closest('.preview-row'); + const cell = row.querySelector('.amount-cell'); + const sign = row.querySelector('.sign'); + cell.classList.toggle('text-income', isIncome); + cell.classList.toggle('text-expense', !isIncome); + sign.textContent = isIncome ? '+' : '-'; + } + document.querySelectorAll('.type-select').forEach(sel => { - sel.addEventListener('change', function () { - const isIncome = this.value === 'income'; - this.style.borderColor = isIncome ? '#10b981' : '#ef4444'; - this.style.color = isIncome ? '#10b981' : '#ef4444'; - const row = this.closest('.preview-row'); - const cell = row.querySelector('.amount-cell'); - const sign = row.querySelector('.sign'); - cell.classList.toggle('text-income', isIncome); - cell.classList.toggle('text-expense', !isIncome); - sign.textContent = isIncome ? '+' : '-'; + sel.addEventListener('change', function () { applyTypeStyle(this); }); + }); + + // Bulk: set type + document.getElementById('bulkExpense').addEventListener('click', () => { + document.querySelectorAll('.row-check:checked').forEach(cb => { + const sel = cb.closest('.preview-row').querySelector('.type-select'); + sel.value = 'expense'; + applyTypeStyle(sel); }); }); + document.getElementById('bulkIncome').addEventListener('click', () => { + document.querySelectorAll('.row-check:checked').forEach(cb => { + const sel = cb.closest('.preview-row').querySelector('.type-select'); + sel.value = 'income'; + applyTypeStyle(sel); + }); + }); + + // Bulk: set category + document.getElementById('bulkApplyCat').addEventListener('click', () => { + const catId = document.getElementById('bulkCategory').value; + if (!catId) return; + document.querySelectorAll('.row-check:checked').forEach(cb => { + const sel = cb.closest('.preview-row').querySelector('.cat-select'); + if (sel) sel.value = catId; + }); + document.getElementById('bulkCategory').value = ''; + }); })(); {% endblock %} diff --git a/app/templates/teller/preview.html b/app/templates/teller/preview.html index 3d80ab0..e91fb32 100644 --- a/app/templates/teller/preview.html +++ b/app/templates/teller/preview.html @@ -7,20 +7,42 @@ -
-
-
{{ ta.institution_name }} — {{ ta.account_name }}
-
- Mapped to: {{ ta.pfm_account.name }} · - {{ count }} of {{ count }} selected - {% if ta.last_sync_date %}· Last sync: {{ ta.last_sync_date.strftime('%b %d, %Y') }}{% endif %} +
+
+
+
{{ ta.institution_name }} — {{ ta.account_name }}
+
+ Mapped to: {{ ta.pfm_account.name }} · + {{ count }} of {{ count }} selected + {% if ta.last_sync_date %}· Last sync: {{ ta.last_sync_date.strftime('%b %d, %Y') }}{% endif %} +
+
+
+ Cancel +
-
- Cancel - + +
+ + +
@@ -61,12 +83,14 @@ - - {% if txn.category_id and txn.category_id in cat_map %} - {{ cat_map[txn.category_id] }} - {% else %} - Uncategorised - {% endif %} + + { const cb = row.querySelector('.row-check'); row.style.opacity = cb.checked ? '1' : '0.4'; }); - // Sync select-all state selectAll.indeterminate = n > 0 && n < checks.length; selectAll.checked = n === checks.length; } @@ -123,24 +145,51 @@ checks.forEach(cb => { cb.checked = selectAll.checked; }); updateCount(); }); - checks.forEach(cb => cb.addEventListener('change', updateCount)); - // Update type-select color + amount sign on change - document.querySelectorAll('.type-select').forEach(sel => { - sel.addEventListener('change', function () { - const isIncome = this.value === 'income'; - this.style.borderColor = isIncome ? '#10b981' : '#ef4444'; - this.style.color = isIncome ? '#10b981' : '#ef4444'; + // Type-select color + amount sign + function applyTypeStyle(sel) { + const isIncome = sel.value === 'income'; + sel.style.borderColor = isIncome ? '#10b981' : '#ef4444'; + sel.style.color = isIncome ? '#10b981' : '#ef4444'; + const row = sel.closest('.preview-row'); + const cell = row.querySelector('.amount-cell'); + const sign = row.querySelector('.sign'); + cell.classList.toggle('text-income', isIncome); + cell.classList.toggle('text-expense', !isIncome); + sign.textContent = isIncome ? '+' : '-'; + } - const row = this.closest('.preview-row'); - const cell = row.querySelector('.amount-cell'); - const sign = row.querySelector('.sign'); - cell.classList.toggle('text-income', isIncome); - cell.classList.toggle('text-expense', !isIncome); - sign.textContent = isIncome ? '+' : '-'; + document.querySelectorAll('.type-select').forEach(sel => { + sel.addEventListener('change', function () { applyTypeStyle(this); }); + }); + + // Bulk: set type + document.getElementById('bulkExpense').addEventListener('click', () => { + document.querySelectorAll('.row-check:checked').forEach(cb => { + const sel = cb.closest('.preview-row').querySelector('.type-select'); + sel.value = 'expense'; + applyTypeStyle(sel); }); }); + document.getElementById('bulkIncome').addEventListener('click', () => { + document.querySelectorAll('.row-check:checked').forEach(cb => { + const sel = cb.closest('.preview-row').querySelector('.type-select'); + sel.value = 'income'; + applyTypeStyle(sel); + }); + }); + + // Bulk: set category + document.getElementById('bulkApplyCat').addEventListener('click', () => { + const catId = document.getElementById('bulkCategory').value; + if (!catId) return; + document.querySelectorAll('.row-check:checked').forEach(cb => { + const sel = cb.closest('.preview-row').querySelector('.cat-select'); + if (sel) sel.value = catId; + }); + document.getElementById('bulkCategory').value = ''; + }); })(); {% endblock %}