06/02 Modified bank sync, records selectable
This commit is contained in:
+17
-3
@@ -199,16 +199,20 @@ def sync_preview_view(teller_account_id):
|
|||||||
]
|
]
|
||||||
session['teller_account_id'] = 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()}
|
||||||
|
|
||||||
return render_template('teller/preview.html',
|
return render_template('teller/preview.html',
|
||||||
ta=ta,
|
ta=ta,
|
||||||
preview=preview,
|
preview=preview,
|
||||||
count=len(preview))
|
count=len(preview),
|
||||||
|
cat_map=cat_map)
|
||||||
|
|
||||||
|
|
||||||
@teller_bp.route('/sync/confirm', methods=['POST'])
|
@teller_bp.route('/sync/confirm', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def sync_confirm():
|
def sync_confirm():
|
||||||
"""Import the previewed transactions."""
|
"""Import selected previewed transactions with optional type overrides."""
|
||||||
raw = session.pop('teller_preview', [])
|
raw = session.pop('teller_preview', [])
|
||||||
ta_id = session.pop('teller_account_id', None)
|
ta_id = session.pop('teller_account_id', None)
|
||||||
|
|
||||||
@@ -218,13 +222,23 @@ def sync_confirm():
|
|||||||
|
|
||||||
ta = db.get_or_404(TellerAccount, ta_id)
|
ta = db.get_or_404(TellerAccount, ta_id)
|
||||||
|
|
||||||
# Reconstruct parsed list with date objects
|
selected_ids = set(request.form.getlist('selected'))
|
||||||
|
|
||||||
from datetime import date as date_cls
|
from datetime import date as date_cls
|
||||||
parsed = []
|
parsed = []
|
||||||
for r in raw:
|
for r in raw:
|
||||||
|
if selected_ids and r['teller_id'] not in selected_ids:
|
||||||
|
continue
|
||||||
r['date'] = date_cls.fromisoformat(r['date'])
|
r['date'] = date_cls.fromisoformat(r['date'])
|
||||||
|
override = request.form.get(f'type_{r["teller_id"]}')
|
||||||
|
if override in ('income', 'expense'):
|
||||||
|
r['transaction_type'] = override
|
||||||
parsed.append(r)
|
parsed.append(r)
|
||||||
|
|
||||||
|
if not parsed:
|
||||||
|
flash('No transactions selected. Nothing was imported.', 'warning')
|
||||||
|
return redirect(url_for('teller.index'))
|
||||||
|
|
||||||
imported, skipped = import_transactions(parsed, ta)
|
imported, skipped = import_transactions(parsed, ta)
|
||||||
flash(f'Imported {imported} transaction(s) from {ta.account_name}. '
|
flash(f'Imported {imported} transaction(s) from {ta.account_name}. '
|
||||||
f'Skipped {skipped} duplicate(s).', 'success')
|
f'Skipped {skipped} duplicate(s).', 'success')
|
||||||
|
|||||||
@@ -3,65 +3,75 @@
|
|||||||
{% block page_title %}Sync Preview — {{ ta.account_name }}{% endblock %}
|
{% block page_title %}Sync Preview — {{ ta.account_name }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if preview %}
|
||||||
|
<form method="POST" action="{{ url_for('teller.sync_confirm') }}" id="importForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<div class="pcard mb-3 d-flex justify-content-between align-items-center">
|
<div class="pcard mb-3 d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:14px;font-weight:600;">{{ ta.institution_name }} — {{ ta.account_name }}</div>
|
<div style="font-size:14px;font-weight:600;">{{ ta.institution_name }} — {{ ta.account_name }}</div>
|
||||||
<div style="font-size:12px;color:var(--muted);">
|
<div style="font-size:12px;color:var(--muted);">
|
||||||
Mapped to: <strong>{{ ta.pfm_account.name }}</strong> ·
|
Mapped to: <strong>{{ ta.pfm_account.name }}</strong> ·
|
||||||
{{ count }} transaction(s) found
|
<span id="selectedCount">{{ count }}</span> of {{ count }} selected
|
||||||
{% if ta.last_sync_date %}· Last sync: {{ ta.last_sync_date.strftime('%b %d, %Y') }}{% endif %}
|
{% if ta.last_sync_date %}· Last sync: {{ ta.last_sync_date.strftime('%b %d, %Y') }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="{{ url_for('teller.index') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">Cancel</a>
|
<a href="{{ url_for('teller.index') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">Cancel</a>
|
||||||
{% if count > 0 %}
|
<button type="submit" class="btn btn-sm btn-success" style="font-size:12px;" id="importBtn">
|
||||||
<form method="POST" action="{{ url_for('teller.sync_confirm') }}">
|
<i class="bi bi-check-lg me-1"></i>Import <span id="importBtnCount">{{ count }}</span> Transaction(s)
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
</button>
|
||||||
<button type="submit" class="btn btn-sm btn-success" style="font-size:12px;">
|
|
||||||
<i class="bi bi-check-lg me-1"></i>Import {{ count }} Transaction(s)
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if preview %}
|
|
||||||
<div class="pcard p-0">
|
<div class="pcard p-0">
|
||||||
<table class="pfm-table">
|
<table class="pfm-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="padding-left:20px;">Date</th>
|
<th style="width:36px;padding-left:16px;">
|
||||||
|
<input type="checkbox" id="selectAll" checked
|
||||||
|
style="width:15px;height:15px;cursor:pointer;"
|
||||||
|
title="Select / deselect all">
|
||||||
|
</th>
|
||||||
|
<th style="white-space:nowrap;">Date</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Type</th>
|
<th style="width:160px;">Type</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th class="text-end" style="padding-right:20px;">Amount</th>
|
<th class="text-end" style="padding-right:20px;">Amount</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for txn in preview %}
|
{% for txn in preview %}
|
||||||
<tr>
|
<tr class="preview-row" data-id="{{ txn.teller_id }}">
|
||||||
<td style="padding-left:20px;font-size:12px;color:var(--muted);white-space:nowrap;">
|
<td style="padding-left:16px;">
|
||||||
|
<input type="checkbox" name="selected" value="{{ txn.teller_id }}"
|
||||||
|
class="row-check" checked
|
||||||
|
style="width:15px;height:15px;cursor:pointer;">
|
||||||
|
</td>
|
||||||
|
<td style="font-size:12px;color:var(--muted);white-space:nowrap;">
|
||||||
{{ txn.date.strftime('%b %d, %Y') }}
|
{{ txn.date.strftime('%b %d, %Y') }}
|
||||||
</td>
|
</td>
|
||||||
<td style="font-size:13px;">{{ txn.description }}</td>
|
<td style="font-size:13px;">{{ txn.description }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge {% if txn.transaction_type == 'income' %}badge-income{% else %}badge-expense{% endif %}" style="font-size:11px;">
|
<select name="type_{{ txn.teller_id }}"
|
||||||
{{ txn.transaction_type | title }}
|
class="type-select form-select form-select-sm"
|
||||||
</span>
|
style="width:130px;font-size:12px;border-radius:6px;
|
||||||
|
{% if txn.transaction_type == 'income' %}border-color:#10b981;color:#10b981;{% else %}border-color:#ef4444;color:#ef4444;{% endif %}">
|
||||||
|
<option value="expense" {% if txn.transaction_type == 'expense' %}selected{% endif %}>Expense</option>
|
||||||
|
<option value="income" {% if txn.transaction_type == 'income' %}selected{% endif %}>Income</option>
|
||||||
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td style="font-size:12px;color:var(--muted);">
|
<td style="font-size:12px;color:var(--muted);">
|
||||||
{% if txn.category_id %}
|
{% if txn.category_id and txn.category_id in cat_map %}
|
||||||
{% for cat in [txn.category_id] %}
|
{{ cat_map[txn.category_id] }}
|
||||||
{# Look up category name via the category_id #}
|
|
||||||
{{ txn.notes | replace('Teller:', '') }}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span style="color:#f59e0b;">Uncategorised</span>
|
<span style="color:#f59e0b;">Uncategorised</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end mono {% if txn.transaction_type == 'income' %}text-income{% else %}text-expense{% endif %}"
|
<td class="text-end mono amount-cell
|
||||||
|
{% if txn.transaction_type == 'income' %}text-income{% else %}text-expense{% endif %}"
|
||||||
style="font-size:13px;font-weight:600;padding-right:20px;">
|
style="font-size:13px;font-weight:600;padding-right:20px;">
|
||||||
{% if txn.transaction_type == 'income' %}+{% else %}-{% endif %}{{ txn.amount | currency }}
|
<span class="sign">{% if txn.transaction_type == 'income' %}+{% else %}-{% endif %}</span>{{ txn.amount | currency }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -70,14 +80,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mt-3">
|
<div class="d-flex justify-content-end mt-3">
|
||||||
<form method="POST" action="{{ url_for('teller.sync_confirm') }}">
|
<button type="submit" class="btn btn-success" id="importBtn2">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<i class="bi bi-check-lg me-1"></i>Confirm Import (<span id="importBtnCount2">{{ count }}</span> transactions)
|
||||||
<button type="submit" class="btn btn-success">
|
</button>
|
||||||
<i class="bi bi-check-lg me-1"></i>Confirm Import ({{ count }} transactions)
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="pcard text-center py-5">
|
<div class="pcard text-center py-5">
|
||||||
<i class="bi bi-check-circle text-success" style="font-size:3rem;"></i>
|
<i class="bi bi-check-circle text-success" style="font-size:3rem;"></i>
|
||||||
@@ -86,4 +95,52 @@
|
|||||||
<a href="{{ url_for('teller.index') }}" class="btn btn-sm btn-outline-secondary">Back</a>
|
<a href="{{ url_for('teller.index') }}" class="btn btn-sm btn-outline-secondary">Back</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const selectAll = document.getElementById('selectAll');
|
||||||
|
const checks = document.querySelectorAll('.row-check');
|
||||||
|
const countEl = document.getElementById('selectedCount');
|
||||||
|
const btnCount = document.getElementById('importBtnCount');
|
||||||
|
const btnCount2 = document.getElementById('importBtnCount2');
|
||||||
|
|
||||||
|
function updateCount() {
|
||||||
|
const n = document.querySelectorAll('.row-check:checked').length;
|
||||||
|
countEl.textContent = n;
|
||||||
|
btnCount.textContent = n;
|
||||||
|
btnCount2.textContent = n;
|
||||||
|
// Dim unchecked rows
|
||||||
|
document.querySelectorAll('.preview-row').forEach(row => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll.addEventListener('change', () => {
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 ? '+' : '-';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user