05/31 Phase 2

This commit is contained in:
2026-05-31 11:17:53 -04:00
parent 1c34dcbd28
commit 9f2a0acc38
18 changed files with 1817 additions and 391 deletions
+60
View File
@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block page_title %}{{ title }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-6">
<div class="pcard" style="border-top: 4px solid {% if txn_type=='income' %}var(--income){% else %}var(--expense){% endif %};">
<form method="POST" novalidate>
{{ form.hidden_tag() }}
{{ form.transaction_type() }}
<div class="mb-3">
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""), placeholder="What was this for?") }}
{% for e in form.description.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
</div>
<div class="row g-3 mb-3">
<div class="col-6">
{{ form.amount.label(class="form-label fw-medium", style="font-size:13px;") }}
<div class="input-group">
<span class="input-group-text" style="font-size:13px;">{{ current_user.currency_symbol }}</span>
{{ form.amount(class="form-control" + (" is-invalid" if form.amount.errors else ""), placeholder="0.00") }}
</div>
{% for e in form.amount.errors %}<div class="text-danger mt-1" style="font-size:12px;">{{ e }}</div>{% endfor %}
</div>
<div class="col-6">
{{ form.date.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.date(class="form-control" + (" is-invalid" if form.date.errors else "")) }}
</div>
</div>
<div class="mb-3">
{{ form.account_id.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.account_id(class="form-select" + (" is-invalid" if form.account_id.errors else "")) }}
{% for e in form.account_id.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.category_id.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.category_id(class="form-select") }}
</div>
<div class="mb-4">
{{ form.notes.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.notes(class="form-control", rows=2, placeholder="Optional notes") }}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn {% if txn_type=='income' %}btn-success{% else %}btn-danger{% endif %}">
Save {{ txn_type | title }}
</button>
<a href="{{ url_for('transactions.index', tab=txn_type) }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+130
View File
@@ -0,0 +1,130 @@
{% 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 me-1"></i>Income</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.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>
{% 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 -->
<div class="pcard p-0">
{% if transactions %}
<table class="pfm-table">
<thead>
<tr>
<th style="padding-left:20px;">Date</th>
<th>Description</th>
<th>Category</th>
<th>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>
{% if txn.category %}
<span style="font-size:12px;white-space:nowrap;">
<i class="bi {{ txn.category.icon }}" style="color:{{ txn.category.color }};"></i> {{ txn.category.name }}
</span>
{% else %}<span class="text-muted" style="font-size:12px;"></span>{% endif %}
</td>
<td 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) }}" 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>Showing {{ ((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">
<i class="bi bi-inbox text-muted" style="font-size:2.5rem;"></i>
<p class="text-muted mt-2 mb-3">No {{ tab }} transactions found.</p>
<a href="{{ url_for('transactions.new', type=tab) }}" class="btn btn-sm btn-primary">Add {{ tab | title }}</a>
</div>
{% endif %}
</div>
{% endblock %}
+58
View File
@@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}Transfer{% endblock %}
{% block page_title %}Transfer Between Accounts{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-12 col-md-8 col-lg-5">
<div class="pcard" style="border-top: 4px solid #3b82f6;">
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.from_account_id.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.from_account_id(class="form-select") }}
</div>
<div class="text-center my-2" style="font-size:20px;color:var(--muted);">
<i class="bi bi-arrow-down"></i>
</div>
<div class="mb-3">
{{ form.to_account_id.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.to_account_id(class="form-select") }}
</div>
<div class="row g-3 mb-3">
<div class="col-6">
{{ form.amount.label(class="form-label fw-medium", style="font-size:13px;") }}
<div class="input-group">
<span class="input-group-text" style="font-size:13px;">{{ current_user.currency_symbol }}</span>
{{ form.amount(class="form-control", placeholder="0.00") }}
</div>
</div>
<div class="col-6">
{{ form.date.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.date(class="form-control") }}
</div>
</div>
<div class="mb-3">
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.description(class="form-control", placeholder="Transfer description") }}
</div>
<div class="mb-4">
{{ form.notes.label(class="form-label fw-medium", style="font-size:13px;") }}
{{ form.notes(class="form-control", rows=2) }}
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Record Transfer</button>
<a href="{{ url_for('transactions.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}