06/03 Optimize codes, fix transactions import functions
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user