Files
Personal-Finance-Management/app/templates/plaid/preview.html
T

202 lines
9.1 KiB
HTML

{% extends "base.html" %}
{% block title %}Plaid Sync Preview{% endblock %}
{% block page_title %}Sync Preview — {{ item.institution_name }}{% endblock %}
{% block content %}
<form method="POST" action="{{ url_for('plaid.sync_confirm') }}" id="importForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="pcard mb-3">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
<div>
<div style="font-size:14px;font-weight:600;">{{ item.institution_name }}</div>
<div style="font-size:12px;color:var(--muted);">
<span id="selectedCount">{{ count }}</span> of {{ count }} transactions selected
</div>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('plaid.index') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">Cancel</a>
<button type="submit" class="btn btn-sm btn-success" id="importBtn" style="font-size:12px;">
<i class="bi bi-check-lg me-1"></i>Import <span id="importBtnCount">{{ count }}</span> Transaction(s)
</button>
</div>
</div>
<!-- Bulk actions -->
<div class="mt-3 pt-3 d-flex align-items-center flex-wrap gap-2" style="border-top:1px solid var(--border);">
<span style="font-size:12px;color:var(--muted);font-weight:500;">Bulk set selected:</span>
<button type="button" class="btn btn-sm btn-outline-danger" id="bulkExpense" style="font-size:12px;">
<i class="bi bi-dash-circle me-1"></i>Expense
</button>
<button type="button" class="btn btn-sm btn-outline-success" id="bulkIncome" style="font-size:12px;">
<i class="bi bi-plus-circle me-1"></i>Income
</button>
<div class="d-flex align-items-center gap-1 ms-1">
<select id="bulkCategory" class="form-select form-select-sm" style="min-width:150px;font-size:12px;">
<option value="">— Category —</option>
{% for c in categories %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkApplyCat" style="font-size:12px;">Apply</button>
</div>
</div>
</div>
{% set ns = namespace(cur_acct_id=None) %}
{% for txn in preview %}
{% set pa = plaid_accounts.get(txn.plaid_account_id) %}
{% set acct_id = pa.pfm_account_id if pa else None %}
{% if acct_id != ns.cur_acct_id %}
{% if ns.cur_acct_id is not none %}</div></div>{% endif %}
{% set ns.cur_acct_id = acct_id %}
<div class="pcard p-0 mb-3">
<div class="px-4 py-2" style="background:#f8fafc;border-bottom:1px solid var(--border);border-radius:12px 12px 0 0;">
<div style="font-size:13px;font-weight:600;">
{% if pa and pa.pfm_account %}{{ pa.pfm_account.name }}{% else %}Unknown Account{% endif %}
</div>
{% if pa %}
<div style="font-size:11px;color:var(--muted);">{{ pa.display_name }} · {{ pa.account_type | title }}</div>
{% endif %}
</div>
<div>
<table class="pfm-table mb-0">
<thead>
<tr>
<th style="width:36px;padding-left:16px;">
<input type="checkbox" class="section-all" checked
style="width:15px;height:15px;cursor:pointer;">
</th>
<th style="white-space:nowrap;">Date</th>
<th>Description</th>
<th style="width:150px;">Type</th>
<th>Category</th>
<th class="text-end" style="padding-right:20px;">Amount</th>
</tr>
</thead>
<tbody>
{% endif %}
<tr class="preview-row" data-id="{{ txn.plaid_id }}">
<td style="padding-left:16px;">
<input type="checkbox" name="selected" value="{{ txn.plaid_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') }}
</td>
<td style="font-size:13px;">{{ txn.description }}</td>
<td>
<select name="type_{{ txn.plaid_id }}" class="type-select form-select form-select-sm"
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>
<select name="category_{{ txn.plaid_id }}" class="cat-select form-select form-select-sm"
style="font-size:12px;min-width:140px;">
<option value="">— Uncategorised —</option>
{% for c in categories %}
<option value="{{ c.id }}" {% if txn.category_id == c.id %}selected{% endif %}>{{ c.name }}</option>
{% endfor %}
</select>
</td>
<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;">
<span class="sign">{% if txn.transaction_type == 'income' %}+{% else %}-{% endif %}</span>{{ txn.amount | currency }}
</td>
</tr>
{% endfor %}
{% if ns.cur_acct_id is not none %}</tbody></table></div></div>{% endif %}
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-success">
<i class="bi bi-check-lg me-1"></i>Confirm Import (<span id="importBtnCount2">{{ count }}</span> transactions)
</button>
</div>
</form>
<script>
(function () {
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;
document.querySelectorAll('.preview-row').forEach(row => {
row.style.opacity = row.querySelector('.row-check').checked ? '1' : '0.4';
});
// Sync per-section checkboxes
document.querySelectorAll('.section-all').forEach(sa => {
const tbody = sa.closest('table').querySelector('tbody');
const rowChk = tbody.querySelectorAll('.row-check');
const chkd = tbody.querySelectorAll('.row-check:checked').length;
sa.indeterminate = chkd > 0 && chkd < rowChk.length;
sa.checked = chkd === rowChk.length;
});
}
// Section select-all
document.querySelectorAll('.section-all').forEach(sa => {
sa.addEventListener('change', () => {
const tbody = sa.closest('table').querySelector('tbody');
tbody.querySelectorAll('.row-check').forEach(cb => { cb.checked = sa.checked; });
updateCount();
});
});
document.addEventListener('change', e => {
if (e.target.classList.contains('row-check')) 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 () { applyTypeStyle(this); });
});
// Bulk: 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: 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 = '';
});
})();
</script>
{% endblock %}