06/03 Optimize codes, fix transactions import functions

This commit is contained in:
2026-06-03 10:44:36 -04:00
parent faacd4d91a
commit 3ce779ccfd
3 changed files with 79 additions and 10 deletions
+12
View File
@@ -221,6 +221,18 @@ def edit(id):
title='Edit Transaction') title='Edit Transaction')
@transactions_bp.route('/<int:id>/set-category', methods=['POST'])
@login_required
def set_category(id):
"""AJAX endpoint: update only the category of a transaction."""
txn = db.get_or_404(Transaction, id)
data = request.get_json(silent=True) or {}
raw = data.get('category_id')
txn.category_id = int(raw) if raw else None
db.session.commit()
return jsonify({'ok': True, 'category_id': txn.category_id})
@transactions_bp.route('/<int:id>/delete', methods=['POST']) @transactions_bp.route('/<int:id>/delete', methods=['POST'])
@login_required @login_required
def delete(id): def delete(id):
+7 -3
View File
@@ -200,10 +200,14 @@ def parse_transaction(teller_txn, pfm_account_id, category_id_map):
if counterparty and counterparty.upper() != description.upper(): if counterparty and counterparty.upper() != description.upper():
description = counterparty description = counterparty
# Map Teller category to PFM category # Auto-categorize: keyword match on description first (most accurate),
# then fall back to Teller's own category field.
from app.services.bank_import_service import auto_categorize
pfm_cat_name = auto_categorize(description)
if not pfm_cat_name:
teller_cat = (details.get('category') or '').lower() teller_cat = (details.get('category') or '').lower()
pfm_cat_name = CATEGORY_MAP.get(teller_cat, 'Other') pfm_cat_name = CATEGORY_MAP.get(teller_cat, '')
category_id = category_id_map.get(pfm_cat_name) category_id = category_id_map.get(pfm_cat_name) if pfm_cat_name else None
return { return {
'teller_id': teller_txn['id'], 'teller_id': teller_txn['id'],
+59 -6
View File
@@ -82,12 +82,24 @@
<div style="font-size:13px;font-weight:500;">{{ txn.description }}</div> <div style="font-size:13px;font-weight:500;">{{ txn.description }}</div>
{% if txn.notes %}<div style="font-size:11px;color:var(--muted);">{{ txn.notes | truncate(60) }}</div>{% endif %} {% if txn.notes %}<div style="font-size:11px;color:var(--muted);">{{ txn.notes | truncate(60) }}</div>{% endif %}
</td> </td>
<td class="d-mob-none"> <td class="d-mob-none" style="min-width:150px;">
{% if txn.category %} <div class="d-flex align-items-center gap-1">
<span style="font-size:12px;white-space:nowrap;"> <i id="cat-icon-{{ txn.id }}"
<i class="bi {{ txn.category.icon }}" style="color:{{ txn.category.color }};"></i> {{ txn.category.name }} class="bi {{ txn.category.icon if txn.category else 'bi-tag' }}"
</span> style="font-size:12px;color:{{ txn.category.color if txn.category else '#94a3b8' }};flex-shrink:0;"></i>
{% else %}<span class="text-muted" style="font-size:12px;"></span>{% endif %} <select class="cat-select"
data-txn-id="{{ txn.id }}"
title="Change category"
style="font-size:12px;border:1px solid transparent;border-radius:4px;background:transparent;padding:2px 4px;cursor:pointer;max-width:130px;color:var(--text);">
<option value="">— None —</option>
{% for cat in categories %}
<option value="{{ cat.id }}"
data-icon="{{ cat.icon }}"
data-color="{{ cat.color }}"
{% if txn.category_id == cat.id %}selected{% endif %}>{{ cat.name }}</option>
{% endfor %}
</select>
</div>
</td> </td>
<td class="d-mob-none" style="font-size:12px;color:var(--muted);">{{ txn.account.name if txn.account else '—' }}</td> <td class="d-mob-none" style="font-size:12px;color:var(--muted);">{{ txn.account.name if txn.account else '—' }}</td>
<td class="text-end mono {% if txn.transaction_type=='income' %}text-income{% else %}text-expense{% endif %}" style="font-size:13px;font-weight:600;white-space:nowrap;"> <td class="text-end mono {% if txn.transaction_type=='income' %}text-income{% else %}text-expense{% endif %}" style="font-size:13px;font-weight:600;white-space:nowrap;">
@@ -141,3 +153,44 @@
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<style>
.cat-select:hover { border-color: var(--border) !important; background: #f8fafc !important; }
.cat-select:focus { outline: none; border-color: #3b82f6 !important; background: #fff !important; }
</style>
<script>
const CSRF = document.querySelector('meta[name="csrf-token"]').content;
document.querySelectorAll('.cat-select').forEach(sel => {
sel.addEventListener('change', function () {
const txnId = this.dataset.txnId;
const catId = this.value || null;
const opt = this.options[this.selectedIndex];
const icon = document.getElementById('cat-icon-' + txnId);
if (icon) {
icon.className = 'bi ' + (catId ? opt.dataset.icon : 'bi-tag');
icon.style.color = catId ? opt.dataset.color : '#94a3b8';
}
fetch(`/transactions/${txnId}/set-category`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF },
body: JSON.stringify({ category_id: catId ? parseInt(catId) : null }),
})
.then(r => r.json())
.then(data => {
if (data.ok) {
sel.style.color = '#10b981';
setTimeout(() => sel.style.color = '', 1500);
}
})
.catch(() => {
sel.style.color = '#ef4444';
setTimeout(() => sel.style.color = '', 2000);
});
});
});
</script>
{% endblock %}