Files
Personal-Finance-Management/app/templates/transactions/index.html
T
2026-06-03 16:43:39 -04:00

197 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Transactions{% endblock %}
{% block page_title %}Transactions{% endblock %}
{% 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"></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-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"></i><span class="btn-label ms-1">Transfer</span></a>
{% endblock %}
{% block content %}
<!-- Tabs -->
<div class="d-flex gap-1 mb-3">
<a href="{{ url_for('transactions.index', tab='expense', q=search, account_id=account_id, category_id=category_id, date_from=date_from, date_to=date_to) }}"
class="btn btn-sm {% if tab=='expense' %}btn-danger{% else %}btn-outline-secondary{% endif %}" style="font-size:13px;">
<i class="bi bi-arrow-up-circle me-1"></i>Expenses
<span class="badge ms-1" style="font-size:10px;background:{% if tab=='expense' %}rgba(255,255,255,.25){% else %}#fee2e2;color:#991b1b{% endif %};">{{ expense_count }}</span>
</a>
<a href="{{ url_for('transactions.index', tab='income', q=search, account_id=account_id, category_id=category_id, date_from=date_from, date_to=date_to) }}"
class="btn btn-sm {% if tab=='income' %}btn-success{% else %}btn-outline-secondary{% endif %}" style="font-size:13px;">
<i class="bi bi-arrow-down-circle me-1"></i>Income
<span class="badge ms-1" style="font-size:10px;background:{% if tab=='income' %}rgba(255,255,255,.25){% else %}#d1fae5;color:#065f46{% endif %};">{{ income_count }}</span>
</a>
</div>
<!-- Filters -->
<div class="pcard pcard-sm mb-3">
<form method="GET" action="{{ url_for('transactions.index') }}" class="row g-2 align-items-end">
<input type="hidden" name="tab" value="{{ tab }}">
<div class="col-12 col-md-3">
<input type="text" name="q" class="form-control form-control-sm" placeholder="Search description…" value="{{ search }}">
</div>
<div class="col-6 col-md-2">
<select name="category_id" class="form-select form-select-sm">
<option value="">All categories</option>
{% for cat in categories %}
<option value="{{ cat.id }}" {% if category_id == cat.id|string %}selected{% endif %}>{{ cat.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-6 col-md-2">
<select name="account_id" class="form-select form-select-sm">
<option value="">All accounts</option>
{% for acct in accounts %}
<option value="{{ acct.id }}" {% if account_id == acct.id|string %}selected{% endif %}>{{ acct.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-6 col-md-2">
<input type="date" name="date_from" class="form-control form-control-sm" value="{{ date_from }}" placeholder="From">
</div>
<div class="col-6 col-md-2">
<input type="date" name="date_to" class="form-control form-control-sm" value="{{ date_to }}" placeholder="To">
</div>
<div class="col-12 col-md-1 d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary flex-grow-1"><i class="bi bi-search"></i></button>
<a href="{{ url_for('transactions.index', tab=tab) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-x-lg"></i></a>
</div>
</form>
</div>
<!-- Table -->
{% set has_filter = search or category_id or account_id or date_from or date_to %}
<div class="pcard p-0">
{% if transactions %}
<table class="pfm-table">
<thead>
<tr>
<th style="padding-left:20px;">Date</th>
<th>Description</th>
<th class="d-mob-none">Category</th>
<th class="d-mob-none">Account</th>
<th class="text-end">Amount</th>
<th class="text-end" style="padding-right:20px;">Actions</th>
</tr>
</thead>
<tbody>
{% for txn in transactions %}
<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="min-width:150px;">
<div class="d-flex align-items-center gap-1">
<i id="cat-icon-{{ txn.id }}"
class="bi {{ txn.category.icon if txn.category else 'bi-tag' }}"
style="font-size:12px;color:{{ txn.category.color if txn.category else '#94a3b8' }};flex-shrink:0;"></i>
<select class="cat-select"
data-txn-id="{{ txn.id }}"
title="Change category"
style="font-size:12px;border:1px solid transparent;border-radius:4px;background:transparent;padding:2px 4px;cursor:pointer;max-width:130px;color:var(--text);">
<option value="">— None —</option>
{% for cat in categories %}
<option value="{{ cat.id }}"
data-icon="{{ cat.icon }}"
data-color="{{ cat.color }}"
{% if txn.category_id == cat.id %}selected{% endif %}>{{ cat.name }}</option>
{% endfor %}
</select>
</div>
</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;">
{% if txn.transaction_type=='income' %}+{% else %}-{% endif %}{{ txn.amount | currency }}
</td>
<td class="text-end" style="padding-right:20px;white-space:nowrap;">
<a href="{{ url_for('transactions.edit', id=txn.id, next=request.full_path) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;padding:2px 8px;">Edit</a>
<form method="POST" action="{{ url_for('transactions.delete', id=txn.id) }}" style="display:inline;" onsubmit="return confirm('Delete this transaction?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-danger ms-1" style="font-size:11px;padding:2px 8px;">Del</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Pagination -->
{% 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>Page {{ pagination.page }} of {{ pagination.pages }} &nbsp;·&nbsp; {{ ((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('transactions.index', tab=tab, page=pagination.prev_num, q=search, category_id=category_id, account_id=account_id, date_from=date_from, date_to=date_to) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;">← Prev</a>
{% endif %}
{% if pagination.has_next %}
<a href="{{ url_for('transactions.index', tab=tab, page=pagination.next_num, q=search, category_id=category_id, account_id=account_id, date_from=date_from, date_to=date_to) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;">Next →</a>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-5 px-3">
{% if has_filter %}
<i class="bi bi-funnel text-muted" style="font-size:2.5rem;opacity:.4;"></i>
<p class="fw-semibold mt-3 mb-1">No matching transactions</p>
<p class="text-muted small mb-3">Try adjusting your filters or search term.</p>
<a href="{{ url_for('transactions.index', tab=tab) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x-lg me-1"></i>Clear filters
</a>
{% else %}
<i class="bi bi-arrow-left-right text-muted" style="font-size:2.5rem;opacity:.4;"></i>
<p class="fw-semibold mt-3 mb-1">No {{ tab }} transactions yet</p>
<p class="text-muted small mb-3">Record your first {{ tab }} to start tracking.</p>
<a href="{{ url_for('transactions.new', type=tab) }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-lg me-1"></i>Add {{ tab | title }}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<style>
.cat-select:hover { border-color: var(--border) !important; background: #f8fafc !important; }
.cat-select:focus { outline: none; border-color: #3b82f6 !important; background: #fff !important; }
</style>
<script>
const CSRF = document.querySelector('meta[name="csrf-token"]').content;
document.querySelectorAll('.cat-select').forEach(sel => {
sel.addEventListener('change', function () {
const txnId = this.dataset.txnId;
const catId = this.value || null;
const opt = this.options[this.selectedIndex];
const icon = document.getElementById('cat-icon-' + txnId);
if (icon) {
icon.className = 'bi ' + (catId ? opt.dataset.icon : 'bi-tag');
icon.style.color = catId ? opt.dataset.color : '#94a3b8';
}
fetch(`/transactions/${txnId}/set-category`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF },
body: JSON.stringify({ category_id: catId ? parseInt(catId) : null }),
})
.then(r => r.json())
.then(data => {
if (data.ok) {
sel.style.color = '#10b981';
setTimeout(() => sel.style.color = '', 1500);
}
})
.catch(() => {
sel.style.color = '#ef4444';
setTimeout(() => sel.style.color = '', 2000);
});
});
});
</script>
{% endblock %}