05/31 Phase 7
This commit is contained in:
@@ -213,7 +213,7 @@
|
||||
<a href="{{ url_for('categories.index') }}" class="sb-link {% if request.blueprint == 'categories' %}active{% endif %}">
|
||||
<i class="bi bi-tags"></i><span class="lt">Categories</span>
|
||||
</a>
|
||||
<a href="#" class="sb-link">
|
||||
<a href="{{ url_for('settings.profile') }}" class="sb-link {% if request.blueprint == 'settings' %}active{% endif %}">
|
||||
<i class="bi bi-gear"></i><span class="lt">Settings</span>
|
||||
</a>
|
||||
<a href="{{ url_for('auth.logout') }}" class="sb-link">
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Import Transactions{% endblock %}
|
||||
{% block page_title %}Import CSV{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcard">
|
||||
<div class="pcard-title mb-3">Upload CSV File</div>
|
||||
|
||||
<!-- Format guide -->
|
||||
<div class="mb-4 p-3" style="background:#f8fafc;border-radius:8px;font-size:12px;">
|
||||
<div class="fw-semibold mb-2" style="font-size:13px;">Required CSV format:</div>
|
||||
<code style="font-size:11px;background:#0f172a;color:#10b981;padding:8px 12px;border-radius:6px;display:block;line-height:1.8;">
|
||||
date,type,description,category,account,amount,notes<br>
|
||||
2025-01-15,expense,Groceries,Food & Dining,Checking,85.50,Weekly shop<br>
|
||||
2025-01-16,income,Salary,Salary,Checking,3000.00,
|
||||
</code>
|
||||
<div class="mt-2 text-muted">
|
||||
<strong>Required:</strong> date, type (income/expense), description, amount<br>
|
||||
<strong>Optional:</strong> category, account, notes<br>
|
||||
<strong>Date formats:</strong> YYYY-MM-DD, MM/DD/YYYY, DD/MM/YYYY
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.csv_file.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.csv_file(class="form-control" + (" is-invalid" if form.csv_file.errors else "")) }}
|
||||
{% for e in form.csv_file.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.default_account_id.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.default_account_id(class="form-select") }}
|
||||
<small class="text-muted" style="font-size:11px;">Used when account column is missing or unrecognised.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
{{ form.skip_duplicates(class="form-check-input") }}
|
||||
{{ form.skip_duplicates.label(class="form-check-label", style="font-size:13px;") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if parse_errors %}
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcard" style="border-color:#fca5a5;">
|
||||
<div class="pcard-title mb-2" style="color:#ef4444;">Parse Errors</div>
|
||||
{% for err in parse_errors %}
|
||||
<div style="font-size:12px;color:#ef4444;padding:4px 0;border-bottom:1px solid #fee2e2;">{{ err }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if preview %}
|
||||
<div class="col-12">
|
||||
<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">Preview — {{ preview | length }} rows ready to import</span>
|
||||
<form method="POST" action="{{ url_for('settings.import_confirm') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-success" style="font-size:12px;">
|
||||
<i class="bi bi-check-lg me-1"></i>Confirm Import
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<table class="pfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px;">Date</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Category</th>
|
||||
<th>Account</th>
|
||||
<th class="text-end" style="padding-right:20px;">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in preview[:20] %}
|
||||
<tr>
|
||||
<td style="padding-left:20px;font-size:12px;color:var(--muted);">{{ row.date.strftime('%b %d, %Y') }}</td>
|
||||
<td><span class="badge {% if row.transaction_type == 'income' %}badge-income{% else %}badge-expense{% endif %}" style="font-size:11px;">{{ row.transaction_type | title }}</span></td>
|
||||
<td style="font-size:13px;">{{ row.description }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">
|
||||
{{ row.category_name or '—' }}
|
||||
{% if row.category_name and not row.category_id %}
|
||||
<span title="Category not found — will be uncategorized" style="color:#f59e0b;">⚠</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">
|
||||
{{ row.account_name or '—' }}
|
||||
{% if not row.account_id %}
|
||||
<span title="Account not found — will be unlinked" style="color:#f59e0b;">⚠</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end mono {% if row.transaction_type == 'income' %}text-income{% else %}text-expense{% endif %}" style="font-size:13px;padding-right:20px;">{{ row.amount | currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if preview | length > 20 %}
|
||||
<tr><td colspan="6" class="text-center py-2" style="font-size:12px;color:var(--muted);">... and {{ (preview | length) - 20 }} more rows</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% block page_title %}Settings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<!-- Nav cards -->
|
||||
<div class="col-12 col-sm-6 col-lg-3">
|
||||
<a href="{{ url_for('settings.profile') }}" class="text-decoration-none">
|
||||
<div class="pcard text-center py-4" style="transition:all .15s;" onmouseover="this.style.borderColor='#3b82f6'" onmouseout="this.style.borderColor='var(--border)'">
|
||||
<i class="bi bi-person-circle" style="font-size:2rem;color:#3b82f6;"></i>
|
||||
<div style="font-size:14px;font-weight:600;margin-top:10px;">Profile</div>
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:4px;">Name, currency, timezone, AI model</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-3">
|
||||
<a href="{{ url_for('settings.recurring') }}" class="text-decoration-none">
|
||||
<div class="pcard text-center py-4" style="transition:all .15s;" onmouseover="this.style.borderColor='#10b981'" onmouseout="this.style.borderColor='var(--border)'">
|
||||
<i class="bi bi-repeat" style="font-size:2rem;color:#10b981;"></i>
|
||||
<div style="font-size:14px;font-weight:600;margin-top:10px;">Recurring</div>
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:4px;">Automate recurring income & expenses</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-3">
|
||||
<a href="{{ url_for('settings.import_csv') }}" class="text-decoration-none">
|
||||
<div class="pcard text-center py-4" style="transition:all .15s;" onmouseover="this.style.borderColor='#f59e0b'" onmouseout="this.style.borderColor='var(--border)'">
|
||||
<i class="bi bi-upload" style="font-size:2rem;color:#f59e0b;"></i>
|
||||
<div style="font-size:14px;font-weight:600;margin-top:10px;">Import</div>
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:4px;">Bulk import from CSV file</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-3">
|
||||
<a href="{{ url_for('settings.password') }}" class="text-decoration-none">
|
||||
<div class="pcard text-center py-4" style="transition:all .15s;" onmouseover="this.style.borderColor='#8b5cf6'" onmouseout="this.style.borderColor='var(--border)'">
|
||||
<i class="bi bi-shield-lock" style="font-size:2rem;color:#8b5cf6;"></i>
|
||||
<div style="font-size:14px;font-weight:600;margin-top:10px;">Security</div>
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:4px;">Change password</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming recurring -->
|
||||
{% if upcoming %}
|
||||
<div class="pcard mt-4">
|
||||
<div class="pcard-title mb-3">Upcoming Recurring (next 30 days)</div>
|
||||
<table class="pfm-table">
|
||||
<thead><tr><th>Date</th><th>Description</th><th>Type</th><th class="text-end">Amount</th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in upcoming[:10] %}
|
||||
<tr>
|
||||
<td style="font-size:12px;color:var(--muted);white-space:nowrap;">{{ item.date.strftime('%b %d') }}</td>
|
||||
<td style="font-size:13px;">{{ item.description }}</td>
|
||||
<td><span class="badge {% if item.type == 'income' %}badge-income{% else %}badge-expense{% endif %}" style="font-size:11px;">{{ item.type | title }}</span></td>
|
||||
<td class="text-end mono {% if item.type == 'income' %}text-income{% else %}text-expense{% endif %}" style="font-size:13px;">{{ item.amount | currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Change Password{% endblock %}
|
||||
{% block page_title %}Change Password{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-7 col-lg-5">
|
||||
<div class="pcard">
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.current_password.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.current_password(class="form-control" + (" is-invalid" if form.current_password.errors else "")) }}
|
||||
{% for e in form.current_password.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.new_password.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.new_password(class="form-control" + (" is-invalid" if form.new_password.errors else "")) }}
|
||||
{% for e in form.new_password.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
|
||||
<small class="text-muted" style="font-size:11px;">Minimum 6 characters.</small>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
{{ form.confirm_password.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.confirm_password(class="form-control" + (" is-invalid" if form.confirm_password.errors else "")) }}
|
||||
{% for e in form.confirm_password.errors %}<div class="invalid-feedback">{{ e }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('settings.profile') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Profile{% endblock %}
|
||||
{% block page_title %}Profile & Preferences{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcard">
|
||||
<div class="pcard-title mb-3">Profile</div>
|
||||
<form method="POST" action="{{ url_for('settings.profile') }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.display_name.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.display_name(class="form-control", placeholder="Your name") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.email.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.email(class="form-control", placeholder="email@example.com") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.timezone.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.timezone(class="form-select") }}
|
||||
</div>
|
||||
|
||||
<hr style="border-color:var(--border);">
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.currency.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.currency(class="form-select") }}
|
||||
<small class="text-muted" style="font-size:11px;">All transactions use this single currency.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.groq_model.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.groq_model(class="form-select") }}
|
||||
<small class="text-muted" style="font-size:11px;">AI model used for chat and daily insights.</small>
|
||||
</div>
|
||||
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('settings.password') }}" class="btn btn-outline-secondary ms-2">Change Password</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcard">
|
||||
<div class="pcard-title mb-3">Account Info</div>
|
||||
<table style="width:100%;font-size:13px;">
|
||||
<tr><td class="text-muted py-2" style="width:40%;border-bottom:1px solid var(--border);">Username</td><td class="py-2 fw-medium" style="border-bottom:1px solid var(--border);">{{ current_user.username }}</td></tr>
|
||||
<tr><td class="text-muted py-2" style="border-bottom:1px solid var(--border);">Last login</td><td class="py-2" style="border-bottom:1px solid var(--border);">{{ current_user.last_login.strftime('%b %d, %Y %H:%M') if current_user.last_login else '—' }}</td></tr>
|
||||
<tr><td class="text-muted py-2" style="border-bottom:1px solid var(--border);">Member since</td><td class="py-2" style="border-bottom:1px solid var(--border);">{{ current_user.created_at.strftime('%b %d, %Y') }}</td></tr>
|
||||
<tr><td class="text-muted py-2">Currency</td><td class="py-2 mono fw-bold">{{ current_user.currency_symbol }} {{ current_user.currency }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pcard mt-3">
|
||||
<div class="pcard-title mb-3">Quick Links</div>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<a href="{{ url_for('settings.recurring') }}" style="font-size:13px;color:var(--accent);"><i class="bi bi-repeat me-2"></i>Recurring Rules</a>
|
||||
<a href="{{ url_for('settings.import_csv') }}" style="font-size:13px;color:var(--accent);"><i class="bi bi-upload me-2"></i>Import CSV</a>
|
||||
<a href="{{ url_for('categories.index') }}" style="font-size:13px;color:var(--accent);"><i class="bi bi-tags me-2"></i>Manage Categories</a>
|
||||
<a href="{{ url_for('reports.index') }}" style="font-size:13px;color:var(--accent);"><i class="bi bi-file-earmark-bar-graph me-2"></i>Reports & Export</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,96 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Recurring Rules{% endblock %}
|
||||
{% block page_title %}Recurring Transactions{% endblock %}
|
||||
|
||||
{% block topbar_actions %}
|
||||
<form method="POST" action="{{ url_for('settings.recurring_run') }}" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary me-1" style="font-size:12px;" title="Process all due rules now">
|
||||
<i class="bi bi-play-fill me-1"></i>Run Now
|
||||
</button>
|
||||
</form>
|
||||
<a href="{{ url_for('settings.recurring_new') }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>New Rule</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="pcard p-0">
|
||||
<div class="px-4 py-3" style="border-bottom:1px solid var(--border);">
|
||||
<span class="pcard-title mb-0">Rules ({{ rules | length }})</span>
|
||||
</div>
|
||||
{% if rules %}
|
||||
<table class="pfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px;">Name</th>
|
||||
<th>Frequency</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end">Next Run</th>
|
||||
<th class="text-end" style="padding-right:20px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rule in rules %}
|
||||
<tr style="{% if not rule.is_active %}opacity:.5;{% endif %}">
|
||||
<td style="padding-left:20px;">
|
||||
<div style="font-size:13px;font-weight:500;">{{ rule.name }}</div>
|
||||
<div style="font-size:11px;color:var(--muted);">{{ rule.description }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" style="font-size:11px;background:#f1f5f9;color:var(--muted);">{{ rule.frequency | title }}</span>
|
||||
<span class="badge ms-1 {% if rule.transaction_type == 'income' %}badge-income{% else %}badge-expense{% endif %}" style="font-size:11px;">{{ rule.transaction_type | title }}</span>
|
||||
</td>
|
||||
<td class="text-end mono {% if rule.transaction_type == 'income' %}text-income{% else %}text-expense{% endif %}" style="font-size:13px;font-weight:500;">{{ rule.amount | currency }}</td>
|
||||
<td class="text-end" style="font-size:12px;color:var(--muted);white-space:nowrap;">
|
||||
{{ rule.next_run.strftime('%b %d') if rule.next_run else '—' }}
|
||||
</td>
|
||||
<td class="text-end" style="padding-right:20px;white-space:nowrap;">
|
||||
<a href="{{ url_for('settings.recurring_edit', id=rule.id) }}" class="btn btn-sm btn-outline-secondary" style="font-size:11px;padding:2px 7px;">Edit</a>
|
||||
<form method="POST" action="{{ url_for('settings.recurring_toggle', id=rule.id) }}" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary ms-1" style="font-size:11px;padding:2px 7px;">
|
||||
{% if rule.is_active %}Pause{% else %}Enable{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('settings.recurring_delete', id=rule.id) }}" style="display:inline;" onsubmit="return confirm('Delete this rule?')">
|
||||
<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 7px;">×</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-repeat text-muted" style="font-size:2.5rem;"></i>
|
||||
<p class="text-muted small mt-2 mb-2">No recurring rules yet.</p>
|
||||
<a href="{{ url_for('settings.recurring_new') }}" class="btn btn-sm btn-primary">Create First Rule</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="pcard">
|
||||
<div class="pcard-title mb-3">Upcoming (30 days)</div>
|
||||
{% if upcoming %}
|
||||
{% for item in upcoming[:15] %}
|
||||
<div class="d-flex justify-content-between align-items-center py-2" style="border-bottom:1px solid var(--border);">
|
||||
<div>
|
||||
<div style="font-size:13px;">{{ item.description }}</div>
|
||||
<div style="font-size:11px;color:var(--muted);">{{ item.date.strftime('%b %d, %Y') }}</div>
|
||||
</div>
|
||||
<span class="mono {% if item.type == 'income' %}text-income{% else %}text-expense{% endif %}" style="font-size:13px;font-weight:500;">
|
||||
{% if item.type == 'income' %}+{% else %}-{% endif %}{{ item.amount | currency }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted small">No upcoming recurring transactions.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,72 @@
|
||||
{% 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">
|
||||
<form method="POST" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-8">
|
||||
{{ form.name.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.name(class="form-control", placeholder="e.g. Monthly Rent") }}
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{{ form.transaction_type.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.transaction_type(class="form-select") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.description.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.description(class="form-control", placeholder="Transaction description") }}
|
||||
</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:12px;">{{ current_user.currency_symbol }}</span>
|
||||
{{ form.amount(class="form-control", placeholder="0.00") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{ form.frequency.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.frequency(class="form-select") }}
|
||||
</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") }}
|
||||
</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="row g-3 mb-4">
|
||||
<div class="col-6">
|
||||
{{ form.start_date.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.start_date(class="form-control") }}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{{ form.end_date.label(class="form-label fw-medium", style="font-size:13px;") }}
|
||||
{{ form.end_date(class="form-control") }}
|
||||
<small class="text-muted" style="font-size:11px;">Leave blank for no end date</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('settings.recurring') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -47,6 +47,36 @@
|
||||
{{ form.notes(class="form-control", rows=2, placeholder="Optional notes") }}
|
||||
</div>
|
||||
|
||||
|
||||
{% if txn and txn.receipt %}
|
||||
<div class="mb-3 p-3" style="background:#f8fafc;border-radius:8px;">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div style="font-size:13px;">
|
||||
<i class="bi bi-paperclip me-1 text-muted"></i>
|
||||
<a href="{{ url_for('settings.view_receipt', filename=txn.receipt.filename) }}" target="_blank">
|
||||
{{ txn.receipt.original_filename }}
|
||||
</a>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('settings.delete_receipt', txn_id=txn.id) }}" onsubmit="return confirm('Delete receipt?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" style="font-size:11px;padding:2px 8px;">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% elif txn %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium" style="font-size:13px;">Receipt</label>
|
||||
<form method="POST" action="{{ url_for('settings.upload_receipt', txn_id=txn.id) }}" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="file" name="receipt" class="form-control" accept=".png,.jpg,.jpeg,.gif,.pdf" style="font-size:12px;">
|
||||
<button type="submit" class="btn btn-outline-secondary" style="font-size:12px;">Upload</button>
|
||||
</div>
|
||||
<small class="text-muted" style="font-size:11px;">PNG, JPG, GIF, PDF — max 10MB</small>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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 }}
|
||||
|
||||
Reference in New Issue
Block a user