06/05 Optimize app: implement Plaid
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
{% for acct in accounts %}
|
||||
{% set ta = teller_map.get(acct.id) %}
|
||||
{% set sa = schwab_map.get(acct.id) %}
|
||||
{% set pa = plaid_map.get(acct.id) %}
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<div class="pcard" style="border-left: 4px solid {{ acct.color }};">
|
||||
|
||||
@@ -45,6 +46,9 @@
|
||||
{% if sa %}
|
||||
<span style="font-size:10px;background:#d1fae5;color:#065f46;border-radius:4px;padding:1px 5px;margin-left:4px;vertical-align:middle;">Schwab</span>
|
||||
{% endif %}
|
||||
{% if pa %}
|
||||
<span style="font-size:10px;background:#ede9fe;color:#5b21b6;border-radius:4px;padding:1px 5px;margin-left:4px;vertical-align:middle;">Plaid</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);">{{ acct.account_type | replace('_',' ') | title }}</div>
|
||||
</div>
|
||||
@@ -90,6 +94,35 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Plaid credit card billing details -->
|
||||
{% if pa and tab == 'credit' and (pa.cc_due_date or pa.cc_minimum_payment) %}
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
{% if pa.cc_due_date %}
|
||||
{% set days_left = (pa.cc_due_date - today).days %}
|
||||
<div style="flex:1;background:#f5f3ff;border-radius:8px;padding:10px 12px;">
|
||||
<div style="font-size:10px;color:#6d28d9;font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px;">Due Date</div>
|
||||
<div style="font-size:16px;font-weight:700;color:#0f172a;">{{ pa.cc_due_date.strftime('%b %d') }}</div>
|
||||
{% if days_left <= 0 %}
|
||||
<div style="font-size:10px;color:#ef4444;font-weight:600;">Overdue!</div>
|
||||
{% elif days_left <= 3 %}
|
||||
<div style="font-size:10px;color:#f59e0b;">{{ days_left }} day{{ 's' if days_left != 1 }} left</div>
|
||||
{% else %}
|
||||
<div style="font-size:10px;color:var(--muted);">{{ days_left }} days left</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if pa.cc_minimum_payment %}
|
||||
<div style="flex:1;background:#f5f3ff;border-radius:8px;padding:10px 12px;">
|
||||
<div style="font-size:10px;color:#6d28d9;font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px;">Min Payment</div>
|
||||
<div style="font-size:16px;font-weight:700;color:#0f172a;">{{ pa.cc_minimum_payment | currency }}</div>
|
||||
{% if pa.cc_last_statement_balance %}
|
||||
<div style="font-size:10px;color:var(--muted);">Stmt: {{ pa.cc_last_statement_balance | currency }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if acct.notes %}
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:10px;">{{ acct.notes }}</div>
|
||||
{% endif %}
|
||||
@@ -154,6 +187,37 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Plaid action buttons -->
|
||||
{% if pa %}
|
||||
<div class="d-flex gap-2 mt-2 pt-2" style="border-top:1px solid var(--border);">
|
||||
<button onclick="refreshPlaidBalance({{ pa.id }}, {{ acct.id }}, '{{ pa.account_type }}', this)"
|
||||
class="btn btn-sm btn-outline-primary flex-fill" style="font-size:11px;"
|
||||
title="Pull live balance from Plaid">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Refresh
|
||||
</button>
|
||||
<a href="{{ url_for('plaid.sync_preview_view', item_db_id=pa.item_id) }}"
|
||||
class="btn btn-sm btn-outline-primary flex-fill" style="font-size:11px;"
|
||||
title="Sync new transactions from Plaid">
|
||||
<i class="bi bi-cloud-download me-1"></i>Sync
|
||||
</a>
|
||||
{% if pa.account_type == 'credit' %}
|
||||
<form method="POST" action="{{ url_for('plaid.refresh_liabilities_view', item_db_id=pa.item_id) }}" style="flex:1;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="next" value="{{ url_for('accounts.index', tab='credit') }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary w-100" style="font-size:11px;"
|
||||
title="Refresh due date and minimum payment from Plaid">
|
||||
<i class="bi bi-credit-card me-1"></i>Billing
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if pa.last_sync_date %}
|
||||
<div style="font-size:10px;color:var(--muted);margin-top:4px;text-align:right;">
|
||||
Last synced {{ pa.last_sync_date.strftime('%b %d') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Schwab action buttons -->
|
||||
{% if sa %}
|
||||
<div class="d-flex gap-2 mt-2 pt-2" style="border-top:1px solid var(--border);">
|
||||
@@ -206,6 +270,54 @@
|
||||
<script>
|
||||
const CSRF = document.querySelector('meta[name="csrf-token"]').content;
|
||||
|
||||
function refreshPlaidBalance(paId, acctId, acctType, btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>…';
|
||||
fetch('/plaid/balance/' + paId, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': CSRF },
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-arrow-clockwise me-1"></i>Refresh';
|
||||
if (data.status === 'ok') {
|
||||
btn.style.color = '#10b981';
|
||||
btn.title = 'Balance updated just now';
|
||||
setTimeout(() => { btn.style.color = ''; btn.title = 'Pull live balance from Plaid'; }, 3000);
|
||||
// Update displayed balance element
|
||||
const sym = '{{ current_user.currency_symbol }}';
|
||||
const fmt = v => {
|
||||
const n = parseFloat(v);
|
||||
const abs = Math.abs(n).toLocaleString(undefined, {minimumFractionDigits:2,maximumFractionDigits:2});
|
||||
return (n < 0 ? '-' : '') + sym + abs;
|
||||
};
|
||||
const balEl = document.getElementById('acct-bal-' + acctId);
|
||||
if (balEl && data.balance != null) {
|
||||
const isCc = acctType === 'credit';
|
||||
if (isCc) {
|
||||
const owed = Math.max(0, -parseFloat(data.balance));
|
||||
balEl.textContent = (owed > 0 ? '-' : '') + fmt(owed);
|
||||
} else {
|
||||
balEl.textContent = fmt(data.balance);
|
||||
}
|
||||
balEl.style.color = '#10b981';
|
||||
setTimeout(() => balEl.style.color = '', 2500);
|
||||
}
|
||||
} else {
|
||||
btn.style.color = '#ef4444';
|
||||
btn.title = data.error || 'Refresh failed';
|
||||
setTimeout(() => { btn.style.color = ''; btn.title = 'Pull live balance from Plaid'; }, 4000);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-arrow-clockwise me-1"></i>Refresh';
|
||||
btn.style.color = '#ef4444';
|
||||
setTimeout(() => { btn.style.color = ''; }, 4000);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshTellerBalance(taId, acctId, isCreditCard, btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>…';
|
||||
|
||||
Reference in New Issue
Block a user