06/02 Optimize app
This commit is contained in:
@@ -157,6 +157,37 @@ def delete(id):
|
|||||||
return redirect(url_for('accounts.index'))
|
return redirect(url_for('accounts.index'))
|
||||||
|
|
||||||
|
|
||||||
|
@accounts_bp.route('/<int:id>/payments')
|
||||||
|
@login_required
|
||||||
|
def payments(id):
|
||||||
|
account = db.get_or_404(Account, id)
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
|
||||||
|
pagination = Transaction.query.filter(
|
||||||
|
Transaction.transaction_type == 'transfer',
|
||||||
|
Transaction.to_account_id == account.id,
|
||||||
|
).order_by(Transaction.date.desc(), Transaction.id.desc()).paginate(
|
||||||
|
page=page, per_page=30, error_out=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Running total paid this month
|
||||||
|
today = date.today()
|
||||||
|
paid_this_month = db.session.query(
|
||||||
|
func.coalesce(func.sum(Transaction.amount), 0)
|
||||||
|
).filter(
|
||||||
|
Transaction.transaction_type == 'transfer',
|
||||||
|
Transaction.to_account_id == account.id,
|
||||||
|
extract('year', Transaction.date) == today.year,
|
||||||
|
extract('month', Transaction.date) == today.month,
|
||||||
|
).scalar()
|
||||||
|
|
||||||
|
return render_template('accounts/payments.html',
|
||||||
|
account=account,
|
||||||
|
pagination=pagination,
|
||||||
|
payments=pagination.items,
|
||||||
|
paid_this_month=float(paid_this_month))
|
||||||
|
|
||||||
|
|
||||||
@accounts_bp.route('/<int:id>/adjust', methods=['GET', 'POST'])
|
@accounts_bp.route('/<int:id>/adjust', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def adjust_balance(id):
|
def adjust_balance(id):
|
||||||
|
|||||||
@@ -90,16 +90,16 @@
|
|||||||
<div class="d-flex gap-2 mt-3">
|
<div class="d-flex gap-2 mt-3">
|
||||||
<a href="{{ url_for('transactions.transfer', to_account_id=acct.id, description='Credit Card Payment — ' ~ acct.name) }}"
|
<a href="{{ url_for('transactions.transfer', to_account_id=acct.id, description='Credit Card Payment — ' ~ acct.name) }}"
|
||||||
class="btn btn-sm flex-fill btn-danger" style="font-size:12px;">
|
class="btn btn-sm flex-fill btn-danger" style="font-size:12px;">
|
||||||
<i class="bi bi-send me-1"></i>Pay Card
|
<i class="bi bi-send me-1"></i>Pay
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('accounts.payments', id=acct.id) }}"
|
||||||
|
class="btn btn-sm flex-fill btn-outline-secondary" style="font-size:12px;">
|
||||||
|
<i class="bi bi-clock-history me-1"></i>Payments
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('transactions.index', tab='expense', account_id=acct.id) }}"
|
<a href="{{ url_for('transactions.index', tab='expense', account_id=acct.id) }}"
|
||||||
class="btn btn-sm flex-fill btn-outline-secondary" style="font-size:12px;">
|
class="btn btn-sm flex-fill btn-outline-secondary" style="font-size:12px;">
|
||||||
<i class="bi bi-list-ul me-1"></i>Charges
|
<i class="bi bi-list-ul me-1"></i>Charges
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('transactions.new', type='expense') }}"
|
|
||||||
class="btn btn-sm" style="font-size:12px;background:#f1f5f9;color:#475569;border:none;" title="Add Charge">
|
|
||||||
<i class="bi bi-plus-lg"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="d-flex gap-2 mt-3">
|
<div class="d-flex gap-2 mt-3">
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Payments — {{ account.name }}{% endblock %}
|
||||||
|
{% block page_title %}Card Payments{% endblock %}
|
||||||
|
|
||||||
|
{% block topbar_actions %}
|
||||||
|
<a href="{{ url_for('transactions.transfer', to_account_id=account.id, description='Credit Card Payment — ' ~ account.name) }}"
|
||||||
|
class="btn btn-sm btn-danger" style="font-size:12px;">
|
||||||
|
<i class="bi bi-send"></i><span class="btn-label ms-1">Pay Card</span>
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<!-- Account header -->
|
||||||
|
<div class="pcard mb-3" style="border-left: 4px solid {{ account.color }};">
|
||||||
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div style="width:36px;height:36px;border-radius:9px;background:{{ account.color }}22;color:{{ account.color }};display:flex;align-items:center;justify-content:center;font-size:18px;">
|
||||||
|
<i class="bi {{ account.icon }}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600;font-size:14px;">{{ account.name }}</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);">Payment History</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<div style="text-align:right;">
|
||||||
|
<div style="font-size:10px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em;">Paid This Month</div>
|
||||||
|
<div class="mono text-income" style="font-size:18px;font-weight:700;">+{{ paid_this_month | currency }}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right;">
|
||||||
|
{% set owed = [0, -account.balance] | max %}
|
||||||
|
<div style="font-size:10px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.04em;">Still Owed</div>
|
||||||
|
<div class="mono {% if owed > 0 %}text-expense{% endif %}" style="font-size:18px;font-weight:700;{% if owed == 0 %}color:#10b981;{% endif %}">
|
||||||
|
{% if owed > 0 %}-{% endif %}{{ owed | currency }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Back link -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<a href="{{ url_for('accounts.index', tab='credit') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Back to Accounts
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('transactions.index', tab='expense', account_id=account.id) }}" class="btn btn-sm btn-outline-secondary ms-1" style="font-size:12px;">
|
||||||
|
<i class="bi bi-list-ul me-1"></i>View Charges
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if payments %}
|
||||||
|
<div class="pcard p-0">
|
||||||
|
<table class="pfm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="padding-left:20px;">Date</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="d-mob-none">From Account</th>
|
||||||
|
<th class="text-end" style="padding-right:20px;">Amount Paid</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for txn in payments %}
|
||||||
|
<tr>
|
||||||
|
<td style="padding-left:20px;font-size:12px;color:var(--muted);white-space:nowrap;">
|
||||||
|
{{ txn.date.strftime('%b %d, %Y') }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<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 %}
|
||||||
|
</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 text-income" style="font-size:13px;font-weight:600;padding-right:20px;">
|
||||||
|
+{{ txn.amount | currency }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if pagination.pages > 1 %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center px-4 py-3" style="border-top:1px solid var(--border);font-size:12px;color:var(--muted);">
|
||||||
|
<span>{{ ((pagination.page-1)*30)+1 }}–{{ [pagination.page*30, pagination.total]|min }} of {{ pagination.total }}</span>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
{% if pagination.has_prev %}
|
||||||
|
<a href="{{ url_for('accounts.payments', id=account.id, page=pagination.prev_num) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;">← Prev</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if pagination.has_next %}
|
||||||
|
<a href="{{ url_for('accounts.payments', id=account.id, page=pagination.next_num) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;">Next →</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="pcard text-center py-5">
|
||||||
|
<i class="bi bi-send text-muted" style="font-size:3rem;opacity:.4;"></i>
|
||||||
|
<p class="fw-semibold mt-3 mb-1">No payments recorded yet</p>
|
||||||
|
<p class="text-muted small mb-3">Use "Pay Card" to record a payment from your checking or savings account.</p>
|
||||||
|
<a href="{{ url_for('transactions.transfer', to_account_id=account.id, description='Credit Card Payment — ' ~ account.name) }}"
|
||||||
|
class="btn btn-sm btn-danger">
|
||||||
|
<i class="bi bi-send me-1"></i>Record First Payment
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
+13
-3
@@ -165,12 +165,22 @@
|
|||||||
.pcard { padding: 14px; }
|
.pcard { padding: 14px; }
|
||||||
.stat-card { padding: 14px 16px; }
|
.stat-card { padding: 14px 16px; }
|
||||||
.stat-card .stat-value { font-size: 20px; }
|
.stat-card .stat-value { font-size: 20px; }
|
||||||
/* Tables: scroll horizontally on small screens.
|
/* Tables: scroll horizontally on small screens */
|
||||||
Targets both manual .table-wrap and the common .pcard.p-0 pattern. */
|
|
||||||
.table-wrap, .pcard.p-0 { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
.table-wrap, .pcard.p-0 { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||||
.pfm-table { min-width: 560px; }
|
.pfm-table { min-width: 580px; }
|
||||||
/* Hide low-priority columns */
|
/* Hide low-priority columns */
|
||||||
.d-mob-none { display: none !important; }
|
.d-mob-none { display: none !important; }
|
||||||
|
/* Topbar title: truncate so action buttons always fit */
|
||||||
|
.tb-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||||
|
/* Flash: stretch full width */
|
||||||
|
.flash-wrap { left: 8px; right: 8px; max-width: none; }
|
||||||
|
}
|
||||||
|
/* Extra-small screens: hide button label text, keep icons */
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
#main { padding: 10px; }
|
||||||
|
.pcard { padding: 12px; }
|
||||||
|
.btn-label { display: none; }
|
||||||
|
.tb-right .btn { padding-left: 8px; padding-right: 8px; }
|
||||||
}
|
}
|
||||||
.sb-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1039; }
|
.sb-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1039; }
|
||||||
.sb-overlay.on { display: block; }
|
.sb-overlay.on { display: block; }
|
||||||
|
|||||||
@@ -11,21 +11,21 @@
|
|||||||
|
|
||||||
{% block topbar_actions %}
|
{% block topbar_actions %}
|
||||||
<div class="d-flex align-items-center gap-1">
|
<div class="d-flex align-items-center gap-1">
|
||||||
<a href="?period=this_month" class="btn btn-sm btn-outline-secondary period-btn {% if period=='this_month' %}active{% endif %}" style="font-size:12px;">This Month</a>
|
<a href="?period=this_month" class="btn btn-sm btn-outline-secondary period-btn {% if period=='this_month' %}active{% endif %}" style="font-size:12px;"><i class="bi bi-calendar-check"></i><span class="btn-label ms-1">This Month</span></a>
|
||||||
<a href="?period=last_month" class="btn btn-sm btn-outline-secondary period-btn {% if period=='last_month' %}active{% endif %}" style="font-size:12px;">Last Month</a>
|
<a href="?period=last_month" class="btn btn-sm btn-outline-secondary period-btn {% if period=='last_month' %}active{% endif %}" style="font-size:12px;"><i class="bi bi-calendar"></i><span class="btn-label ms-1">Last Month</span></a>
|
||||||
<button class="btn btn-sm btn-outline-secondary period-btn {% if period=='custom' %}active{% endif %}" style="font-size:12px;" data-bs-toggle="modal" data-bs-target="#customModal">Custom</button>
|
<button class="btn btn-sm btn-outline-secondary period-btn {% if period=='custom' %}active{% endif %}" style="font-size:12px;" data-bs-toggle="modal" data-bs-target="#customModal"><i class="bi bi-calendar-range"></i><span class="btn-label ms-1">Custom</span></button>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
<div class="d-flex align-items-start align-items-sm-center justify-content-between flex-wrap gap-2 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="mb-0 fw-semibold">{{ period_label }}</h5>
|
<h5 class="mb-0 fw-semibold">{{ period_label }}</h5>
|
||||||
<small class="text-muted">{{ date_from.strftime('%b %d') }} – {{ date_to.strftime('%b %d, %Y') }}</small>
|
<small class="text-muted">{{ date_from.strftime('%b %d') }} – {{ date_to.strftime('%b %d, %Y') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<a href="{{ url_for('transactions.new', type='income') }}" class="btn btn-sm" style="background:#d1fae5;color:#065f46;font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Income</a>
|
<a href="{{ url_for('transactions.new', type='income') }}" class="btn btn-sm" style="background:#d1fae5;color:#065f46;font-size:12px;"><i class="bi bi-plus-lg"></i><span class="btn-label ms-1">Income</span></a>
|
||||||
<a href="{{ url_for('transactions.new', type='expense') }}" class="btn btn-sm" style="background:#fee2e2;color:#991b1b;font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Expense</a>
|
<a href="{{ url_for('transactions.new', type='expense') }}" class="btn btn-sm" style="background:#fee2e2;color:#991b1b;font-size:12px;"><i class="bi bi-dash-lg"></i><span class="btn-label ms-1">Expense</span></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="padding-left:20px;">Asset</th>
|
<th style="padding-left:20px;">Asset</th>
|
||||||
<th class="text-end">Price</th>
|
<th class="text-end">Price</th>
|
||||||
<th class="text-end">1D Chg</th>
|
<th class="text-end d-mob-none">1D Chg</th>
|
||||||
<th class="text-end">Value</th>
|
<th class="text-end">Value</th>
|
||||||
<th class="text-end" style="padding-right:8px;">P&L</th>
|
<th class="text-end" style="padding-right:8px;">P&L</th>
|
||||||
<th style="width:36px;"></th>
|
<th style="width:36px;"></th>
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
<span class="text-muted" style="font-size:12px;">—</span>
|
<span class="text-muted" style="font-size:12px;">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end" id="chg-{{ inv.id }}">
|
<td class="text-end d-mob-none" id="chg-{{ inv.id }}">
|
||||||
{% if inv.ticker %}
|
{% if inv.ticker %}
|
||||||
<span class="chg-badge chg-loading">…</span>
|
<span class="chg-badge chg-loading">…</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
{% block page_title %}Transactions{% endblock %}
|
{% block page_title %}Transactions{% endblock %}
|
||||||
|
|
||||||
{% block topbar_actions %}
|
{% block topbar_actions %}
|
||||||
<a href="{{ url_for('transactions.new', type='income') }}" class="btn btn-sm" style="background:#d1fae5;color:#065f46;font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Income</a>
|
<a href="{{ url_for('transactions.new', type='income') }}" class="btn btn-sm" style="background:#d1fae5;color:#065f46;font-size:12px;"><i class="bi bi-plus-lg"></i><span class="btn-label ms-1">Income</span></a>
|
||||||
<a href="{{ url_for('transactions.new', type='expense') }}" class="btn btn-sm ms-1" style="background:#fee2e2;color:#991b1b;font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Expense</a>
|
<a href="{{ url_for('transactions.new', type='expense') }}" class="btn btn-sm ms-1" style="background:#fee2e2;color:#991b1b;font-size:12px;"><i class="bi bi-dash-lg"></i><span class="btn-label ms-1">Expense</span></a>
|
||||||
<a href="{{ url_for('transactions.transfer') }}" class="btn btn-sm btn-outline-secondary ms-1" style="font-size:12px;"><i class="bi bi-arrow-left-right me-1"></i>Transfer</a>
|
<a href="{{ url_for('transactions.transfer') }}" class="btn btn-sm btn-outline-secondary ms-1" style="font-size:12px;"><i class="bi bi-arrow-left-right"></i><span class="btn-label ms-1">Transfer</span></a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -68,8 +68,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="padding-left:20px;">Date</th>
|
<th style="padding-left:20px;">Date</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Category</th>
|
<th class="d-mob-none">Category</th>
|
||||||
<th>Account</th>
|
<th class="d-mob-none">Account</th>
|
||||||
<th class="text-end">Amount</th>
|
<th class="text-end">Amount</th>
|
||||||
<th class="text-end" style="padding-right:20px;">Actions</th>
|
<th class="text-end" style="padding-right:20px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -82,14 +82,14 @@
|
|||||||
<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>
|
<td class="d-mob-none">
|
||||||
{% if txn.category %}
|
{% if txn.category %}
|
||||||
<span style="font-size:12px;white-space:nowrap;">
|
<span style="font-size:12px;white-space:nowrap;">
|
||||||
<i class="bi {{ txn.category.icon }}" style="color:{{ txn.category.color }};"></i> {{ txn.category.name }}
|
<i class="bi {{ txn.category.icon }}" style="color:{{ txn.category.color }};"></i> {{ txn.category.name }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}<span class="text-muted" style="font-size:12px;">—</span>{% endif %}
|
{% else %}<span class="text-muted" style="font-size:12px;">—</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td 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;">
|
||||||
{% if txn.transaction_type=='income' %}+{% else %}-{% endif %}{{ txn.amount | currency }}
|
{% if txn.transaction_type=='income' %}+{% else %}-{% endif %}{{ txn.amount | currency }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user