06/03 Optimize codes, fix transactions import functions
This commit is contained in:
@@ -221,6 +221,18 @@ def edit(id):
|
||||
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'])
|
||||
@login_required
|
||||
def delete(id):
|
||||
|
||||
@@ -200,10 +200,14 @@ def parse_transaction(teller_txn, pfm_account_id, category_id_map):
|
||||
if counterparty and counterparty.upper() != description.upper():
|
||||
description = counterparty
|
||||
|
||||
# Map Teller category to PFM category
|
||||
teller_cat = (details.get('category') or '').lower()
|
||||
pfm_cat_name = CATEGORY_MAP.get(teller_cat, 'Other')
|
||||
category_id = category_id_map.get(pfm_cat_name)
|
||||
# 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()
|
||||
pfm_cat_name = CATEGORY_MAP.get(teller_cat, '')
|
||||
category_id = category_id_map.get(pfm_cat_name) if pfm_cat_name else None
|
||||
|
||||
return {
|
||||
'teller_id': teller_txn['id'],
|
||||
|
||||
@@ -82,12 +82,24 @@
|
||||
<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 %}
|
||||
</td>
|
||||
<td class="d-mob-none">
|
||||
{% if txn.category %}
|
||||
<span style="font-size:12px;white-space:nowrap;">
|
||||
<i class="bi {{ txn.category.icon }}" style="color:{{ txn.category.color }};"></i> {{ txn.category.name }}
|
||||
</span>
|
||||
{% else %}<span class="text-muted" style="font-size:12px;">—</span>{% endif %}
|
||||
<td class="d-mob-none" style="min-width:150px;">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<i id="cat-icon-{{ txn.id }}"
|
||||
class="bi {{ txn.category.icon if txn.category else 'bi-tag' }}"
|
||||
style="font-size:12px;color:{{ txn.category.color if txn.category else '#94a3b8' }};flex-shrink:0;"></i>
|
||||
<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 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;">
|
||||
@@ -141,3 +153,44 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
Reference in New Issue
Block a user