158 lines
7.9 KiB
HTML
158 lines
7.9 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Schwab Connection{% endblock %}
|
|
{% block page_title %}Schwab Bank Sync{% endblock %}
|
|
|
|
{% block topbar_actions %}
|
|
<a href="{{ url_for('settings.index') }}" class="btn btn-sm btn-outline-secondary me-1" style="font-size:12px;"><i class="bi bi-arrow-left me-1"></i>Settings</a>
|
|
{% if connection %}
|
|
<form method="POST" action="{{ url_for('schwab.disconnect') }}"
|
|
onsubmit="return confirm('Disconnect Schwab? Imported transactions are kept.')" class="d-inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger" style="font-size:12px;">Disconnect</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
{% if not schwab_configured %}
|
|
<div class="pcard mb-4" style="border-left:4px solid #f59e0b;">
|
|
<div class="d-flex align-items-start gap-3">
|
|
<i class="bi bi-exclamation-triangle-fill text-warning mt-1" style="font-size:1.4rem;flex-shrink:0;"></i>
|
|
<div>
|
|
<div style="font-weight:600;font-size:14px;margin-bottom:6px;">Schwab credentials not configured</div>
|
|
<p style="font-size:13px;color:var(--muted);margin-bottom:10px;">
|
|
Register your app at <strong>developer.schwab.com</strong>, then add these to your <code>.env</code>:
|
|
</p>
|
|
<pre style="font-size:12px;background:#f8fafc;padding:12px;border-radius:8px;margin:0;overflow-x:auto;">SCHWAB_CLIENT_ID=your-client-id
|
|
SCHWAB_CLIENT_SECRET=your-client-secret
|
|
SCHWAB_REDIRECT_URI=https://pfm.ngodanguyen.tech/schwab/callback</pre>
|
|
<p style="font-size:12px;color:var(--muted);margin-top:8px;">
|
|
Also register <code>https://pfm.ngodanguyen.tech/schwab/callback</code> as the redirect URI in the Schwab developer portal, then restart the app.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if connection %}
|
|
<!-- Connected state -->
|
|
<div class="pcard mb-4" style="border-left:4px solid #10b981;">
|
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div style="width:42px;height:42px;border-radius:10px;background:#d1fae5;color:#065f46;display:flex;align-items:center;justify-content:center;font-size:20px;">
|
|
<i class="bi bi-bank2"></i>
|
|
</div>
|
|
<div>
|
|
<div style="font-weight:600;font-size:15px;">Charles Schwab</div>
|
|
<div style="font-size:12px;color:var(--muted);">
|
|
Connected {{ connection.created_at.strftime('%b %d, %Y') }} ·
|
|
Last synced: {{ connection.last_synced_at.strftime('%b %d, %H:%M') if connection.last_synced_at else 'Never' }}
|
|
{% if connection.token_is_expired %}
|
|
· <span style="color:#f59e0b;"><i class="bi bi-exclamation-circle me-1"></i>Token expired — sync will auto-refresh</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<a href="{{ url_for('schwab.map_accounts') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">
|
|
<i class="bi bi-diagram-2 me-1"></i>Manage Mapping
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if accounts %}
|
|
<div class="pcard p-0">
|
|
<div class="d-flex justify-content-between align-items-center px-4 py-3" style="border-bottom:1px solid var(--border);">
|
|
<span class="pcard-title mb-0">Accounts</span>
|
|
</div>
|
|
{% for sa in accounts %}
|
|
<div class="d-flex justify-content-between align-items-center px-4 py-3" style="{% if not loop.last %}border-bottom:1px solid var(--border);{% endif %}">
|
|
<div class="d-flex align-items-center gap-2" style="min-width:0;">
|
|
<div style="width:32px;height:32px;border-radius:8px;background:#dbeafe;color:#1e40af;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;">
|
|
<i class="bi bi-bank"></i>
|
|
</div>
|
|
<div style="min-width:0;">
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
<span style="font-size:13px;font-weight:500;">{{ sa.account_name }}</span>
|
|
{% if sa.last_sync_date %}
|
|
<span style="font-size:10px;color:var(--muted);">synced {{ sa.last_sync_date.strftime('%b %d') }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div style="font-size:11px;color:var(--muted);">
|
|
{{ sa.account_number_display }} · {{ sa.account_type }}
|
|
{% if sa.pfm_account %}
|
|
· <span style="color:var(--muted);">→ {{ sa.pfm_account.name }}</span>
|
|
{% else %}
|
|
· <span style="color:#f59e0b;">Not mapped</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2 flex-shrink-0 ms-2">
|
|
{% if sa.pfm_account %}
|
|
<!-- Sync balance + investment positions -->
|
|
<form method="POST" action="{{ url_for('schwab.sync_snapshot', schwab_account_id=sa.id) }}"
|
|
style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm btn-outline-success" style="font-size:11px;"
|
|
title="Pull live balance and investment positions from Schwab">
|
|
<i class="bi bi-arrow-clockwise me-1"></i>Balance & Positions
|
|
</button>
|
|
</form>
|
|
<!-- Sync transactions -->
|
|
<a href="{{ url_for('schwab.sync_preview_view', schwab_account_id=sa.id) }}"
|
|
class="btn btn-sm btn-outline-primary" style="font-size:11px;"
|
|
title="Import new transactions">
|
|
<i class="bi bi-cloud-download me-1"></i>Transactions
|
|
</a>
|
|
<!-- Reset sync cursor -->
|
|
<form method="POST" action="{{ url_for('schwab.full_resync', schwab_account_id=sa.id) }}"
|
|
style="display:inline;"
|
|
onsubmit="return confirm('Reset sync cursor for {{ sa.account_name }}?\nNext sync re-fetches 90 days. Duplicates are skipped.')">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm btn-outline-warning" style="font-size:11px;"
|
|
title="Re-fetch full 90-day history on next sync">
|
|
<i class="bi bi-arrow-counterclockwise"></i>
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<a href="{{ url_for('schwab.map_accounts') }}"
|
|
class="btn btn-sm btn-outline-warning" style="font-size:11px;">Map Account</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% else %}
|
|
<div class="pcard text-center py-5">
|
|
<i class="bi bi-diagram-2 text-muted" style="font-size:3rem;"></i>
|
|
<h5 class="mt-3 mb-1">No accounts mapped</h5>
|
|
<p class="text-muted small mb-3">Link each Schwab account to a PFM account to start syncing.</p>
|
|
<a href="{{ url_for('schwab.map_accounts') }}" class="btn btn-primary btn-sm">Map Accounts</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<!-- Not connected -->
|
|
<div class="pcard text-center py-5 col-12 col-lg-6 mx-auto">
|
|
<i class="bi bi-bank2 text-muted" style="font-size:3rem;"></i>
|
|
<h5 class="mt-3 mb-1">Connect Charles Schwab</h5>
|
|
<p class="text-muted small mb-4">
|
|
Sync checking, savings, and brokerage transactions directly from Schwab.
|
|
</p>
|
|
{% if schwab_configured %}
|
|
<a href="{{ url_for('schwab.connect') }}" class="btn btn-primary">
|
|
<i class="bi bi-link-45deg me-1"></i>Connect Schwab Account
|
|
</a>
|
|
{% else %}
|
|
<button class="btn btn-primary" disabled>
|
|
<i class="bi bi-link-45deg me-1"></i>Connect Schwab Account
|
|
</button>
|
|
<p class="text-muted small mt-2">Configure credentials first (see above)</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endblock %}
|