first commit
This commit is contained in:
14
app/templates/admin/audit.html
Normal file
14
app/templates/admin/audit.html
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="hero-panel mb-4">
|
||||
<div class="app-section-title mb-0">
|
||||
<span class="feature-icon"><i class="fa-solid fa-clipboard-list"></i></span>
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ t('admin.audit') }}</h1>
|
||||
<div class="text-body-secondary">{{ t('admin.audit_subtitle') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card"><div class="card-body"><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>{{ t('common.date') }}</th><th>User</th><th>Action</th><th>Target</th><th>Details</th></tr></thead><tbody>{% for log in logs %}<tr><td>{{ log.created_at.strftime('%Y-%m-%d %H:%M') }}</td><td>{{ log.user.email if log.user else '-' }}</td><td>{{ log.action }}</td><td>{{ log.target_type }} #{{ log.target_id }}</td><td class="small text-body-secondary">{{ log.details }}</td></tr>{% else %}<tr><td colspan="5" class="text-body-secondary">{{ t('stats.no_data') }}</td></tr>{% endfor %}</tbody></table></div></div></div>
|
||||
{% endblock %}
|
||||
23
app/templates/admin/categories.html
Normal file
23
app/templates/admin/categories.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm"><div class="card-body">
|
||||
<h1 class="h4 mb-3">{{ t('admin.categories') }}</h1>
|
||||
<form method="post">{{ form.hidden_tag() }}
|
||||
<div class="row g-3">
|
||||
<div class="col-12">{{ form.key.label(class='form-label') }}{{ form.key(class='form-control') }}</div>
|
||||
<div class="col-md-6">{{ form.name_pl.label(class='form-label') }}{{ form.name_pl(class='form-control') }}</div>
|
||||
<div class="col-md-6">{{ form.name_en.label(class='form-label') }}{{ form.name_en(class='form-control') }}</div>
|
||||
<div class="col-md-6">{{ form.color.label(class='form-label') }}{{ form.color(class='form-select') }}</div>
|
||||
<div class="col-12">{{ form.submit(class='btn btn-primary w-100') }}</div>
|
||||
</div>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-lg-8"><div class="card shadow-sm"><div class="card-body"><div class="table-responsive"><table class="table">
|
||||
<thead><tr><th>{{ t('common.name') }}</th><th>PL</th><th>EN</th><th>{{ t('common.status') }}</th></tr></thead>
|
||||
<tbody>{% for category in categories %}<tr><td>{{ category.key }}</td><td>{{ category.name_pl }}</td><td>{{ category.name_en }}</td><td>{{ t('common.active') if category.is_active else t('common.inactive') }}</td></tr>{% endfor %}</tbody>
|
||||
</table></div></div></div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
app/templates/admin/dashboard.html
Normal file
32
app/templates/admin/dashboard.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="hero-panel mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div class="app-section-title mb-0">
|
||||
<span class="feature-icon"><i class="fa-solid fa-shield-halved"></i></span>
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ t('admin.title') }}</h1>
|
||||
<div class="text-body-secondary">{{ t('admin.subtitle') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<form method="post" action="{{ url_for('admin.run_reports') }}"><button class="btn btn-outline-primary"><i class="fa-solid fa-paper-plane me-2"></i>Run reports</button></form>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.users') }}"><i class="fa-solid fa-users me-2"></i>{{ t('admin.users') }}</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.categories') }}"><i class="fa-solid fa-tags me-2"></i>{{ t('admin.categories') }}</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.settings') }}"><i class="fa-solid fa-gears me-2"></i>{{ t('admin.settings') }}</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('admin.audit') }}"><i class="fa-solid fa-clipboard-list me-2"></i>{{ t('admin.audit') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-stats mb-4">
|
||||
<div class="card metric-card"><div class="d-flex justify-content-between align-items-center"><div><div class="text-body-secondary">{{ t('admin.users') }}</div><div class="fs-3 fw-bold">{{ stats.users }}</div></div><span class="metric-icon"><i class="fa-solid fa-users"></i></span></div></div>
|
||||
<div class="card metric-card"><div class="d-flex justify-content-between align-items-center"><div><div class="text-body-secondary">{{ t('admin.categories') }}</div><div class="fs-3 fw-bold">{{ stats.categories }}</div></div><span class="metric-icon"><i class="fa-solid fa-tags"></i></span></div></div>
|
||||
<div class="card metric-card"><div class="d-flex justify-content-between align-items-center"><div><div class="text-body-secondary">{{ t('admin.audit') }}</div><div class="fs-3 fw-bold">{{ stats.audit_logs }}</div></div><span class="metric-icon"><i class="fa-solid fa-clipboard-list"></i></span></div></div>
|
||||
<div class="card metric-card"><div class="d-flex justify-content-between align-items-center"><div><div class="text-body-secondary">Admins</div><div class="fs-3 fw-bold">{{ stats.admins }}</div></div><span class="metric-icon"><i class="fa-solid fa-user-shield"></i></span></div></div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6"><div class="card"><div class="card-body"><h2 class="h5 mb-3"><i class="fa-solid fa-server me-2"></i>{{ t('admin.system') }}</h2><div class="small text-body-secondary d-grid gap-2"><div><strong>{{ t('admin.python') }}:</strong> {{ system.python }}</div><div><strong>{{ t('admin.platform') }}:</strong> {{ system.platform }}</div><div><strong>{{ t('admin.environment') }}:</strong> {{ system.flask_env }}</div><div><strong>{{ t('admin.instance_path') }}:</strong> {{ system.instance_path }}</div><div><strong>{{ t('admin.uploads') }}:</strong> {{ system.upload_count }}</div><div><strong>{{ t('admin.previews') }}:</strong> {{ system.preview_count }}</div><div><strong>{{ t('admin.webhook') }}:</strong> {{ t('common.enabled') if system.webhook_enabled else t('common.disabled') }}</div><div><strong>{{ t('admin.scheduler') }}:</strong> {{ t('common.enabled') if system.scheduler_enabled else t('common.disabled') }}</div></div></div></div></div>
|
||||
<div class="col-lg-6"><div class="card"><div class="card-body"><h2 class="h5 mb-3"><i class="fa-solid fa-database me-2"></i>{{ t('admin.database') }}</h2><div class="small text-body-secondary d-grid gap-2"><div><strong>Engine:</strong> {{ db_info.engine }}</div><div><strong>URL:</strong> {{ db_info.url }}</div><div><strong>Version:</strong> {{ db_version }}</div><div><strong>Max upload MB:</strong> {{ system.max_upload_mb }}</div></div></div></div></div>
|
||||
<div class="col-12"><div class="card"><div class="card-body"><div class="d-flex justify-content-between align-items-center mb-3"><h2 class="h5 mb-0"><i class="fa-solid fa-clock-rotate-left me-2"></i>{{ t('admin.audit') }}</h2><a href="{{ url_for('admin.audit') }}" class="btn btn-sm btn-outline-secondary">{{ t('common.view_all') }}</a></div><div class="table-responsive"><table class="table align-middle mb-0"><thead><tr><th>{{ t('common.date') }}</th><th>Action</th><th>Target</th><th>Details</th></tr></thead><tbody>{% for log in recent_logs %}<tr><td>{{ log.created_at.strftime('%Y-%m-%d %H:%M') }}</td><td>{{ log.action }}</td><td>{{ log.target_type }} #{{ log.target_id }}</td><td class="small text-body-secondary">{{ log.details }}</td></tr>{% else %}<tr><td colspan="4" class="text-body-secondary">{{ t('stats.no_data') }}</td></tr>{% endfor %}</tbody></table></div></div></div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
app/templates/admin/settings.html
Normal file
49
app/templates/admin/settings.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="mx-auto" style="max-width:1080px">
|
||||
<div class="app-section-title mb-4">
|
||||
<span class="feature-icon"><i class="fa-solid fa-gears"></i></span>
|
||||
<div><h1 class="h3 mb-0">{{ t('admin.settings') }}</h1><div class="text-body-secondary">{{ t('admin.settings_subtitle') }}</div></div>
|
||||
</div>
|
||||
<form method="post" class="row g-3 settings-section">
|
||||
<div class="col-12 col-xl-6"><div class="card h-100"><div class="card-body">
|
||||
<h2 class="h5 mb-3">{{ t('admin.section_general') }}</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8"><label class="form-label">{{ t('admin.company_name') }}</label><input class="form-control" name="company_name" value="{{ values.get('company_name','') }}"></div>
|
||||
<div class="col-md-4"><label class="form-label">{{ t('admin.max_upload_mb') }}</label><input class="form-control" name="max_upload_mb" value="{{ values.get('max_upload_mb','10') }}"></div>
|
||||
<div class="col-12 form-check ms-2"><input class="form-check-input" type="checkbox" name="registration_enabled" {% if values.get('registration_enabled') == 'true' %}checked{% endif %}><label class="form-check-label">{{ t('admin.registration_enabled') }}</label></div>
|
||||
</div>
|
||||
</div></div></div>
|
||||
|
||||
<div class="col-12 col-xl-6"><div class="card h-100"><div class="card-body">
|
||||
<h2 class="h5 mb-3">{{ t('admin.smtp_section') }}</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8"><label class="form-label">{{ t('admin.smtp_host') }}</label><input class="form-control" name="smtp_host" value="{{ values.get('smtp_host','') }}"></div>
|
||||
<div class="col-md-4"><label class="form-label">{{ t('admin.smtp_port') }}</label><input class="form-control" name="smtp_port" value="{{ values.get('smtp_port','465') }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('admin.smtp_security') }}</label><select class="form-select" name="smtp_security"><option value="plain" {% if values.get('smtp_security')=='plain' %}selected{% endif %}>{{ t('admin.smtp_plain') }}</option><option value="starttls" {% if values.get('smtp_security')=='starttls' %}selected{% endif %}>STARTTLS</option><option value="ssl" {% if values.get('smtp_security')=='ssl' %}selected{% endif %}>SSL/TLS</option></select></div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('admin.smtp_sender') }}</label><input class="form-control" name="smtp_sender" value="{{ values.get('smtp_sender','') }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('admin.smtp_username') }}</label><input class="form-control" name="smtp_username" value="{{ values.get('smtp_username','') }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('admin.smtp_password') }}</label><input class="form-control" type="password" name="smtp_password" value="{{ values.get('smtp_password','') }}"></div>
|
||||
</div>
|
||||
</div></div></div>
|
||||
|
||||
<div class="col-12 col-xl-6"><div class="card h-100"><div class="card-body">
|
||||
<h2 class="h5 mb-3">{{ t('admin.section_reports') }}</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 form-check ms-2"><input class="form-check-input" type="checkbox" name="reports_enabled" {% if values.get('reports_enabled','true') == 'true' %}checked{% endif %}><label class="form-check-label">{{ t('admin.reports_enabled') }}</label></div>
|
||||
<div class="col-12 form-check ms-2"><input class="form-check-input" type="checkbox" name="report_scheduler_enabled" {% if values.get('report_scheduler_enabled') == 'true' %}checked{% endif %}><label class="form-check-label">{{ t('admin.enable_scheduler') }}</label></div>
|
||||
<div class="col-12"><div class="form-text">{{ t('admin.reports_hint') }}</div></div>
|
||||
</div>
|
||||
</div></div></div>
|
||||
|
||||
<div class="col-12 col-xl-6"><div class="card h-100"><div class="card-body">
|
||||
<h2 class="h5 mb-3">{{ t('admin.section_integrations') }}</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-label">{{ t('admin.webhook_token') }}</label><input class="form-control" name="webhook_api_token" value="{{ values.get('webhook_api_token','') }}"></div>
|
||||
</div>
|
||||
</div></div></div>
|
||||
|
||||
<div class="col-12"><button class="btn btn-primary"><i class="fa-solid fa-floppy-disk me-2"></i>{{ t('common.save') }}</button></div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
app/templates/admin/users.html
Normal file
26
app/templates/admin/users.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-4"><div class="card shadow-sm"><div class="card-body">
|
||||
<h1 class="h4 mb-3">{{ t('admin.users') }}</h1>
|
||||
<form method="post">{{ form.hidden_tag() }}
|
||||
<div class="mb-2"><label class="form-label">{{ t('user.full_name') }}</label>{{ form.full_name(class='form-control') }}</div>
|
||||
<div class="mb-2"><label class="form-label">{{ t('user.email') }}</label>{{ form.email(class='form-control') }}</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6"><label class="form-label">{{ t('common.role') }}</label>{{ form.role(class='form-select') }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.language') }}</label>{{ form.language(class='form-select') }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.reports') }}</label>{{ form.report_frequency(class='form-select') }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.theme') }}</label>{{ form.theme(class='form-select') }}</div>
|
||||
</div>
|
||||
<div class="form-check mt-2">{{ form.is_active_user(class='form-check-input') }} <label class="form-check-label">{{ t('user.active') }}</label></div>
|
||||
<div class="form-check">{{ form.must_change_password(class='form-check-input') }} <label class="form-check-label">{{ t('user.must_change_password') }}</label></div>
|
||||
<button class="btn btn-primary mt-3">{{ t('common.save') }}</button>
|
||||
{% if editing_user %}<a class="btn btn-outline-secondary mt-3" href="{{ url_for('admin.users') }}">{{ t('common.cancel') }}</a>{% endif %}
|
||||
</form>
|
||||
</div></div></div>
|
||||
<div class="col-xl-8"><div class="card shadow-sm"><div class="card-body"><div class="table-responsive"><table class="table align-middle">
|
||||
<thead><tr><th>{{ t('common.name') }}</th><th>{{ t('user.email') }}</th><th>{{ t('common.role') }}</th><th>{{ t('preferences.language') }}</th><th>{{ t('preferences.reports') }}</th><th>{{ t('common.status') }}</th><th></th></tr></thead>
|
||||
<tbody>{% for user in users %}<tr><td>{{ user.full_name }}</td><td>{{ user.email }}</td><td>{{ user.role }}</td><td>{{ user.language }}</td><td>{{ user.report_frequency }}</td><td>{% if user.is_active_user %}<span class="badge text-bg-success">{{ t('common.active') }}</span>{% else %}<span class="badge text-bg-secondary">{{ t('common.inactive') }}</span>{% endif %}{% if user.must_change_password %}<span class="badge text-bg-warning">{{ t('user.must_change_password_short') }}</span>{% endif %}</td><td class="text-end"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('admin.users', edit=user.id) }}"><i class="fa-solid fa-pen-to-square"></i></a><form method="post" action="{{ url_for('admin.toggle_password_change', user_id=user.id) }}" class="d-inline">{{ csrf_token() if csrf_token else '' }}<button class="btn btn-sm btn-outline-secondary">{{ t('common.toggle') }}</button></form></td></tr>{% endfor %}</tbody>
|
||||
</table></div></div></div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
7
app/templates/auth/forgot_password.html
Normal file
7
app/templates/auth/forgot_password.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center"><div class="col-12 col-md-6 col-xl-4"><div class="card border-0 shadow-sm"><div class="card-body p-4">
|
||||
<h1 class="h4 mb-3">{{ t('auth.reset_request') }}</h1>
|
||||
<form method="post">{{ form.hidden_tag() }}<div class="d-none">{{ form.website() }}</div><div class="mb-3">{{ form.email.label(class_='form-label') }}{{ form.email(class_='form-control') }}</div>{{ form.submit(class_='btn btn-primary w-100') }}</form>
|
||||
</div></div></div></div>
|
||||
{% endblock %}
|
||||
28
app/templates/auth/login.html
Normal file
28
app/templates/auth/login.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<div class="card shadow-sm border-0 login-card">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<div class="brand-icon mb-3"><i class="fa-solid fa-wallet"></i></div>
|
||||
<h1 class="h3 mb-1">{{ t('auth.login_title') }}</h1>
|
||||
<p class="text-secondary mb-0">{{ t('auth.login_subtitle') }}</p>
|
||||
</div>
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="d-none">{{ form.website() }}</div>
|
||||
<div class="mb-3">{{ form.email.label(class_='form-label') }}{{ form.email(class_='form-control') }}</div>
|
||||
<div class="mb-3">{{ form.password.label(class_='form-label') }}{{ form.password(class_='form-control') }}</div>
|
||||
<div class="form-check mb-3">{{ form.remember_me(class_='form-check-input') }} {{ form.remember_me.label(class_='form-check-label') }}</div>
|
||||
{{ form.submit(class_='btn btn-primary w-100') }}
|
||||
</form>
|
||||
<div class="d-flex justify-content-between mt-3 small">
|
||||
<a href="{{ url_for('auth.forgot_password') }}">{{ t('auth.forgot_password') }}</a>
|
||||
<a href="{{ url_for('auth.register') }}">{{ t('auth.register') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
app/templates/auth/register.html
Normal file
11
app/templates/auth/register.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center"><div class="col-12 col-md-7 col-xl-5"><div class="card border-0 shadow-sm"><div class="card-body p-4">
|
||||
<h1 class="h3 mb-3">{{ t('auth.register') }}</h1>
|
||||
<form method="post">{{ form.hidden_tag() }}<div class="d-none">{{ form.website() }}</div>
|
||||
<div class="mb-3">{{ form.full_name.label(class_='form-label') }}{{ form.full_name(class_='form-control') }}</div>
|
||||
<div class="mb-3">{{ form.email.label(class_='form-label') }}{{ form.email(class_='form-control') }}</div>
|
||||
<div class="mb-3">{{ form.password.label(class_='form-label') }}{{ form.password(class_='form-control') }}</div>
|
||||
<div class="mb-3">{{ form.confirm_password.label(class_='form-label') }}{{ form.confirm_password(class_='form-control') }}</div>
|
||||
{{ form.submit(class_='btn btn-primary w-100') }}</form></div></div></div></div>
|
||||
{% endblock %}
|
||||
7
app/templates/auth/reset_password.html
Normal file
7
app/templates/auth/reset_password.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center"><div class="col-12 col-md-6 col-xl-4"><div class="card border-0 shadow-sm"><div class="card-body p-4">
|
||||
<h1 class="h4 mb-3">{{ t('auth.new_password') }}</h1>
|
||||
<form method="post">{{ form.hidden_tag() }}<div class="mb-3">{{ form.password.label(class_='form-label') }}{{ form.password(class_='form-control') }}</div><div class="mb-3">{{ form.confirm_password.label(class_='form-label') }}{{ form.confirm_password(class_='form-control') }}</div>{{ form.submit(class_='btn btn-primary w-100') }}</form>
|
||||
</div></div></div></div>
|
||||
{% endblock %}
|
||||
95
app/templates/base.html
Normal file
95
app/templates/base.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ current_language }}" data-bs-theme="{{ current_user.theme if current_user.is_authenticated else 'light' }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Expense Monitor</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||
<link rel="stylesheet" href="{{ asset_url('css/app.css') }}">
|
||||
<link rel="manifest" href="{{ url_for('manifest') }}">
|
||||
</head>
|
||||
<body class="theme-{{ current_user.theme if current_user.is_authenticated else 'light' }}">
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="app-shell">
|
||||
<aside class="app-sidebar d-none d-lg-flex flex-column">
|
||||
<a class="sidebar-brand mb-4" href="{{ url_for('main.dashboard') }}">
|
||||
<span class="brand-mark"><i class="fa-solid fa-wallet"></i></span>
|
||||
<span>
|
||||
Expense Monitor
|
||||
<small>{{ t('brand.subtitle') }}</small>
|
||||
</span>
|
||||
</a>
|
||||
<nav class="sidebar-nav nav flex-column gap-1">
|
||||
<a class="nav-link" href="{{ url_for('main.dashboard') }}"><i class="fa-solid fa-house"></i><span>{{ t('nav.dashboard') }}</span></a>
|
||||
<a class="nav-link" href="{{ url_for('expenses.list_expenses') }}"><i class="fa-solid fa-receipt"></i><span>{{ t('nav.expenses') }}</span></a>
|
||||
<a class="nav-link" href="{{ url_for('main.statistics') }}"><i class="fa-solid fa-chart-line"></i><span>{{ t('nav.statistics') }}</span></a>
|
||||
<a class="nav-link" href="{{ url_for('expenses.budgets') }}"><i class="fa-solid fa-bullseye"></i><span>{{ t('nav.budgets') }}</span></a>
|
||||
<a class="nav-link" href="{{ url_for('expenses.create_expense') }}"><i class="fa-solid fa-plus"></i><span>{{ t('nav.add_expense') }}</span></a>
|
||||
<a class="nav-link" href="{{ url_for('main.preferences') }}"><i class="fa-solid fa-sliders"></i><span>{{ t('nav.preferences') }}</span></a>
|
||||
{% if current_user.is_admin() %}
|
||||
<a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="fa-solid fa-shield-halved"></i><span>{{ t('nav.admin') }}</span></a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="sidebar-user mt-auto">
|
||||
<div class="small text-body-secondary">{{ current_user.full_name }}</div>
|
||||
<div class="fw-semibold text-truncate">{{ current_user.email }}</div>
|
||||
<a class="btn btn-outline-secondary w-100 mt-3" href="{{ url_for('auth.logout') }}"><i class="fa-solid fa-right-from-bracket me-2"></i>{{ t('nav.logout') }}</a>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="app-main">
|
||||
{% endif %}
|
||||
<nav class="navbar app-navbar navbar-expand-lg sticky-top">
|
||||
<div class="container-fluid px-3 px-lg-4">
|
||||
<a class="navbar-brand fw-semibold d-flex align-items-center gap-2" href="{{ url_for('main.dashboard' if current_user.is_authenticated else 'auth.login') }}">
|
||||
<span class="brand-mark"><i class="fa-solid fa-wallet"></i></span>
|
||||
<span class="navbar-brand-text">Expense Monitor<small>{{ t('brand.subtitle') }}</small></span>
|
||||
</a>
|
||||
<div class="d-flex align-items-center ms-auto gap-2 order-lg-2">
|
||||
<form method="post" action="{{ url_for('main.set_language') }}" class="language-picker">
|
||||
{% if csrf_token %}{{ csrf_token() }}{% endif %}
|
||||
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}">
|
||||
<button class="btn btn-sm btn-light flag-btn {% if current_language=='pl' %}active{% endif %}" name="language" value="pl" type="submit" title="Polski">🇵🇱</button>
|
||||
<button class="btn btn-sm btn-light flag-btn {% if current_language=='en' %}active{% endif %}" name="language" value="en" type="submit" title="English">🇬🇧</button>
|
||||
</form>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain" aria-controls="navMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse order-lg-1" id="navMain">
|
||||
{% if current_user.is_authenticated %}
|
||||
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2 py-2 py-lg-0 d-lg-none">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.dashboard') }}"><i class="fa-solid fa-house me-2"></i>{{ t('nav.dashboard') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('expenses.list_expenses') }}"><i class="fa-solid fa-receipt me-2"></i>{{ t('nav.expenses') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.statistics') }}"><i class="fa-solid fa-chart-line me-2"></i>{{ t('nav.statistics') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('expenses.budgets') }}"><i class="fa-solid fa-bullseye me-2"></i>{{ t('nav.budgets') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('expenses.create_expense') }}"><i class="fa-solid fa-plus me-2"></i>{{ t('nav.add_expense') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.preferences') }}"><i class="fa-solid fa-sliders me-2"></i>{{ t('nav.preferences') }}</a></li>
|
||||
{% if current_user.is_admin() %}<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="fa-solid fa-shield-halved me-2"></i>{{ t('nav.admin') }}</a></li>{% endif %}
|
||||
<li class="nav-item ms-lg-2"><a class="btn btn-outline-secondary btn-sm" href="{{ url_for('auth.logout') }}"><i class="fa-solid fa-right-from-bracket me-2"></i>{{ t('nav.logout') }}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container{% if current_user.is_authenticated %}-fluid{% endif %} py-4 app-content">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show border-0 shadow-sm" role="alert">{{ message }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% if current_user.is_authenticated %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||
<script src="{{ asset_url('js/app.js') }}"></script>
|
||||
<script>if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('{{ url_for('service_worker') }}').catch(()=>{}));}</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
15
app/templates/errors/error.html
Normal file
15
app/templates/errors/error.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-8 col-xl-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="display-4 fw-bold text-primary mb-2">{{ status_code }}</div>
|
||||
<h1 class="h3">{{ title }}</h1>
|
||||
<p class="text-secondary mb-4">{{ message }}</p>
|
||||
<a class="btn btn-primary" href="{{ url_for('main.dashboard') if current_user.is_authenticated else url_for('auth.login') }}">Go back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
app/templates/expenses/budgets.html
Normal file
25
app/templates/expenses/budgets.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm"><div class="card-body">
|
||||
<h1 class="h4">{{ t('budgets.add') }}</h1>
|
||||
<form method="post">{{ form.hidden_tag() }}
|
||||
<div class="mb-2">{{ form.category_id.label(class='form-label') }}{{ form.category_id(class='form-select') }}</div>
|
||||
<div class="row g-2"><div class="col">{{ form.year.label(class='form-label') }}{{ form.year(class='form-control') }}</div><div class="col">{{ form.month.label(class='form-label') }}{{ form.month(class='form-control') }}</div></div>
|
||||
<div class="mt-2">{{ form.amount.label(class='form-label') }}{{ form.amount(class='form-control') }}</div>
|
||||
<div class="mt-2">{{ form.alert_percent.label(class='form-label') }}{{ form.alert_percent(class='form-control') }}</div>
|
||||
<button class="btn btn-primary mt-3">{{ t('common.save') }}</button>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm"><div class="card-body">
|
||||
<h2 class="h5">{{ t('budgets.title') }}</h2>
|
||||
<div class="table-responsive"><table class="table"><thead><tr><th>{{ t('expenses.category') }}</th><th>{{ t('common.year') }}</th><th>{{ t('common.month') }}</th><th>{{ t('expenses.amount') }}</th></tr></thead><tbody>
|
||||
{% for budget in budgets %}<tr><td>{{ budget.category.localized_name(current_language) }}</td><td>{{ budget.year }}</td><td>{{ budget.month }}</td><td>{{ budget.amount }}</td></tr>{% else %}<tr><td colspan="4" class="text-body-secondary">{{ t('expenses.empty') }}</td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
73
app/templates/expenses/create.html
Normal file
73
app/templates/expenses/create.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="card mx-auto" style="max-width: 1080px;">
|
||||
<div class="card-body p-4 p-lg-5">
|
||||
<div class="app-section-title">
|
||||
<span class="feature-icon"><i class="fa-solid fa-file-circle-plus"></i></span>
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ t('expenses.edit') if expense else t('expenses.new') }}</h1>
|
||||
<div class="text-body-secondary">{{ t('expenses.form_subtitle') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">{{ form.amount.label(class='form-label') }}{{ form.amount(class='form-control') }}</div>
|
||||
<div class="col-md-6">{{ form.purchase_date.label(class='form-label') }}{{ form.purchase_date(class='form-control') }}</div>
|
||||
<div class="col-md-7">{{ form.category_id.label(class='form-label') }}{{ form.category_id(class='form-select') }}</div>
|
||||
<div class="col-md-5">{{ form.payment_method.label(class='form-label') }}{{ form.payment_method(class='form-select') }}</div>
|
||||
<div class="col-md-8">{{ form.title.label(class='form-label') }}{{ form.title(class='form-control', placeholder=t('expenses.placeholder_title')) }}</div>
|
||||
<div class="col-md-4">{{ form.currency.label(class='form-label') }}{{ form.currency(class='form-select') }}</div>
|
||||
<div class="col-md-6">{{ form.vendor.label(class='form-label') }}{{ form.vendor(class='form-control', placeholder=t('expenses.placeholder_vendor')) }}</div>
|
||||
<div class="col-md-6">
|
||||
{{ form.document.label(class='form-label') }}
|
||||
<div class="upload-actions d-grid gap-2" id="documentUploadActions">
|
||||
<button class="btn btn-outline-primary d-none" type="button" id="cameraCaptureButton"><i class="fa-solid fa-camera me-2"></i>{{ t('expenses.take_photo') }}</button>
|
||||
<button class="btn btn-outline-secondary" type="button" id="filePickerButton"><i class="fa-solid fa-upload me-2"></i>{{ t('expenses.select_files') }}</button>
|
||||
</div>
|
||||
<div class="drop-upload-zone mt-2 d-none d-lg-flex" id="dropUploadZone"><i class="fa-solid fa-cloud-arrow-up me-2"></i><span>{{ t('expenses.drop_files_here') }}</span></div><div class="form-text mt-2" id="documentInputHint" data-desktop-hint="{{ t('expenses.upload_hint_desktop') }}" data-mobile-hint="{{ t('expenses.upload_hint_mobile') }}">{{ t('expenses.upload_hint_desktop') }}</div>
|
||||
{{ form.document(class='d-none', id='documentInput', accept='.jpg,.jpeg,.png,.heic,.pdf,image/*,application/pdf', multiple=true) }}
|
||||
</div>
|
||||
<div class="col-12">{{ form.description.label(class='form-label') }}{{ form.description(class='form-control', rows='3', placeholder=t('expenses.placeholder_description')) }}</div>
|
||||
<div class="col-md-5">{{ form.tags.label(class='form-label') }}{{ form.tags(class='form-control', placeholder=t('expenses.placeholder_tags')) }}</div>
|
||||
<div class="col-md-3">{{ form.status.label(class='form-label') }}{{ form.status(class='form-select') }}</div>
|
||||
<div class="col-md-4">{{ form.recurring_period.label(class='form-label') }}{{ form.recurring_period(class='form-select') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card h-100 document-editor-card"><div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h5 mb-0"><i class="fa-solid fa-wand-magic-sparkles me-2"></i>{{ t('expenses.document_tools') }}</h2>
|
||||
<span class="badge soft-badge">{{ t('expenses.webp_preview') }}</span>
|
||||
</div>
|
||||
<div class="document-editor-toolbar mb-3 d-flex flex-wrap gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary js-rotate" type="button" data-step="-90"><i class="fa-solid fa-rotate-left me-1"></i>-90°</button>
|
||||
<button class="btn btn-sm btn-outline-secondary js-rotate" type="button" data-step="90"><i class="fa-solid fa-rotate-right me-1"></i>+90°</button>
|
||||
<button class="btn btn-sm btn-outline-secondary js-scale" type="button" data-step="-10"><i class="fa-solid fa-magnifying-glass-minus me-1"></i>-10%</button>
|
||||
<button class="btn btn-sm btn-outline-secondary js-scale" type="button" data-step="10"><i class="fa-solid fa-magnifying-glass-plus me-1"></i>+10%</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" id="editorReset"><i class="fa-solid fa-arrows-rotate me-1"></i>{{ t('common.reset') }}</button>
|
||||
</div>
|
||||
<div class="editor-meta row g-2 mb-3">
|
||||
<div class="col-6">{{ form.rotate.label(class='form-label small') }}{{ form.rotate(class='form-control', readonly=true, id='rotateField') }}</div>
|
||||
<div class="col-6">{{ form.scale_percent.label(class='form-label small') }}{{ form.scale_percent(class='form-control', readonly=true, id='scaleField') }}</div>
|
||||
</div>
|
||||
<div class="document-preview-shell">
|
||||
<div id="documentPreviewStage" class="document-preview-stage">
|
||||
<img id="documentPreviewImage" alt="preview" class="d-none">
|
||||
<div id="documentPreviewEmpty" class="document-preview-empty"><i class="fa-regular fa-image"></i><div>{{ t('expenses.upload_to_edit') }}</div></div>
|
||||
<div id="cropSelection" class="crop-selection d-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note mt-3">{{ t('expenses.crop_note') }}</div>
|
||||
{% if expense and expense.attachments %}<div class="mt-3"><div class="small text-body-secondary mb-2">Existing files</div><div class="d-flex flex-wrap gap-2">{% for item in expense.attachments %}{% if item.preview_filename %}<a class="d-inline-block" target="_blank" href="{{ url_for('static', filename='previews/' ~ item.preview_filename) }}"><img class="expense-row-thumb" src="{{ url_for('static', filename='previews/' ~ item.preview_filename) }}" alt="preview"></a>{% else %}<span class="badge text-bg-secondary">{{ item.original_filename }}</span>{% endif %}{% endfor %}</div></div>{% endif %}
|
||||
<div class="d-flex gap-3 mt-3 flex-wrap"><div class="form-check">{{ form.is_refund(class='form-check-input') }} {{ form.is_refund.label(class='form-check-label') }}</div><div class="form-check">{{ form.is_business(class='form-check-input') }} {{ form.is_business.label(class='form-check-label') }}</div></div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary mt-4 px-4"><i class="fa-solid fa-floppy-disk me-2"></i>{{ t('expenses.save') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
210
app/templates/expenses/list.html
Normal file
210
app/templates/expenses/list.html
Normal file
@@ -0,0 +1,210 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="hero-panel mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
|
||||
<div>
|
||||
<div class="app-section-title mb-2">
|
||||
<span class="feature-icon"><i class="fa-solid fa-receipt"></i></span>
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ t('expenses.list') }}</h1>
|
||||
<div class="text-body-secondary">{{ selected_year }}-{{ '%02d'|format(selected_month) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('expenses.export_csv', **request.args) }}"><i class="fa-solid fa-file-csv me-2"></i>{{ t('expenses.export_csv') }}</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('expenses.export_pdf', **request.args) }}"><i class="fa-solid fa-file-pdf me-2"></i>{{ t('expenses.export_pdf') }}</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('expenses.create_expense') }}"><i class="fa-solid fa-plus me-2"></i>{{ t('nav.add_expense') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="month-switcher mb-3">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('expenses.list_expenses', year=selected_year if selected_month>1 else selected_year-1, month=selected_month-1 if selected_month>1 else 12, category_id=filters.category_id or None, payment_method=filters.payment_method or None, status=filters.status or None, q=filters.q or None, sort_by=filters.sort_by, sort_dir=filters.sort_dir, group_by=filters.group_by) }}"><i class="fa-solid fa-chevron-left me-2"></i>{{ t('common.previous') }}</a>
|
||||
<form class="center-panel" method="get">
|
||||
<i class="fa-regular fa-calendar"></i>
|
||||
<input class="form-control" style="max-width:130px" type="number" name="year" value="{{ selected_year }}">
|
||||
<input class="form-control" style="max-width:110px" type="number" name="month" value="{{ selected_month }}" min="1" max="12">
|
||||
<input type="hidden" name="category_id" value="{{ filters.category_id or 0 }}">
|
||||
<input type="hidden" name="payment_method" value="{{ filters.payment_method }}">
|
||||
<input type="hidden" name="status" value="{{ filters.status }}">
|
||||
<input type="hidden" name="q" value="{{ filters.q }}">
|
||||
<input type="hidden" name="sort_by" value="{{ filters.sort_by }}">
|
||||
<input type="hidden" name="sort_dir" value="{{ filters.sort_dir }}">
|
||||
<input type="hidden" name="group_by" value="{{ filters.group_by }}">
|
||||
<button class="btn btn-outline-primary"><i class="fa-solid fa-arrow-right me-2"></i>OK</button>
|
||||
</form>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('expenses.list_expenses', year=selected_year if selected_month<12 else selected_year+1, month=selected_month+1 if selected_month<12 else 1, category_id=filters.category_id or None, payment_method=filters.payment_method or None, status=filters.status or None, q=filters.q or None, sort_by=filters.sort_by, sort_dir=filters.sort_dir, group_by=filters.group_by) }}">{{ t('common.next') }}<i class="fa-solid fa-chevron-right ms-2"></i></a>
|
||||
</div>
|
||||
|
||||
<div class="quick-stats expense-list-stats mb-3">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{{ t('expenses.filtered_total') }}</div>
|
||||
<div class="metric-value">{{ '%.2f'|format(month_total) }}</div>
|
||||
<div class="small text-body-secondary">{{ expenses|length }} {{ t('expenses.results') }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{{ t('expenses.active_sort') }}</div>
|
||||
<div class="metric-value fs-5">{{ dict(sort_options).get(filters.sort_by, t('expenses.date')) }}</div>
|
||||
<div class="small text-body-secondary">{{ t('expenses.' ~ filters.sort_dir) if filters.sort_dir in ['asc', 'desc'] else filters.sort_dir }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{{ t('expenses.grouping') }}</div>
|
||||
<div class="metric-value fs-5">{{ t('expenses.group_' ~ filters.group_by) if filters.group_by in ['category','payment_method','status','none'] else filters.group_by }}</div>
|
||||
<div class="small text-body-secondary">{{ grouped_expenses|length }} {{ t('expenses.sections') }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{{ t('expenses.categories_count') }}</div>
|
||||
<div class="metric-value">{{ categories|length }}</div>
|
||||
<div class="small text-body-secondary">{{ t('expenses.month_view') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="expense-filters card card-body border-0 shadow-sm">
|
||||
<div class="row g-3 align-items-end">
|
||||
<input type="hidden" name="year" value="{{ selected_year }}">
|
||||
<input type="hidden" name="month" value="{{ selected_month }}">
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label small">{{ t('common.search') }}</label>
|
||||
<div class="search-input-wrap">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<input class="form-control ps-5" name="q" value="{{ filters.q }}" placeholder="{{ t('expenses.search_placeholder') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-2">
|
||||
<label class="form-label small">{{ t('expenses.category') }}</label>
|
||||
<select class="form-select" name="category_id">
|
||||
<option value="0">{{ t('common.all') }}</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}" {% if filters.category_id==category.id %}selected{% endif %}>{{ category.localized_name(current_language) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-2">
|
||||
<label class="form-label small">{{ t('expenses.payment_method') }}</label>
|
||||
<select class="form-select" name="payment_method">
|
||||
<option value="">{{ t('common.all') }}</option>
|
||||
<option value="card" {% if filters.payment_method=='card' %}selected{% endif %}>{{ t('expenses.payment_card') }}</option>
|
||||
<option value="cash" {% if filters.payment_method=='cash' %}selected{% endif %}>{{ t('expenses.payment_cash') }}</option>
|
||||
<option value="transfer" {% if filters.payment_method=='transfer' %}selected{% endif %}>{{ t('expenses.payment_transfer') }}</option>
|
||||
<option value="blik" {% if filters.payment_method=='blik' %}selected{% endif %}>{{ t('expenses.payment_blik') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-2">
|
||||
<label class="form-label small">{{ t('expenses.status') }}</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">{{ t('common.all') }}</option>
|
||||
<option value="new" {% if filters.status=='new' %}selected{% endif %}>{{ t('expenses.status_new') }}</option>
|
||||
<option value="needs_review" {% if filters.status=='needs_review' %}selected{% endif %}>{{ t('expenses.status_needs_review') }}</option>
|
||||
<option value="confirmed" {% if filters.status=='confirmed' %}selected{% endif %}>{{ t('expenses.status_confirmed') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-2">
|
||||
<label class="form-label small">{{ t('expenses.sort_by') }}</label>
|
||||
<select class="form-select" name="sort_by">
|
||||
{% for value, label in sort_options %}
|
||||
<option value="{{ value }}" {% if filters.sort_by==value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-2">
|
||||
<label class="form-label small">{{ t('expenses.sort_direction') }}</label>
|
||||
<select class="form-select" name="sort_dir">
|
||||
<option value="desc" {% if filters.sort_dir=='desc' %}selected{% endif %}>{{ t('expenses.desc') }}</option>
|
||||
<option value="asc" {% if filters.sort_dir=='asc' %}selected{% endif %}>{{ t('expenses.asc') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-2">
|
||||
<label class="form-label small">{{ t('expenses.group_by') }}</label>
|
||||
<select class="form-select" name="group_by">
|
||||
<option value="category" {% if filters.group_by=='category' %}selected{% endif %}>{{ t('expenses.group_category') }}</option>
|
||||
<option value="payment_method" {% if filters.group_by=='payment_method' %}selected{% endif %}>{{ t('expenses.group_payment_method') }}</option>
|
||||
<option value="status" {% if filters.group_by=='status' %}selected{% endif %}>{{ t('expenses.group_status') }}</option>
|
||||
<option value="none" {% if filters.group_by=='none' %}selected{% endif %}>{{ t('expenses.group_none') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-4 d-flex gap-2">
|
||||
<button class="btn btn-primary flex-grow-1"><i class="fa-solid fa-filter me-2"></i>{{ t('common.filter') }}</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('expenses.list_expenses', year=selected_year, month=selected_month) }}"><i class="fa-solid fa-rotate-left me-2"></i>{{ t('common.reset') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if budgets %}<div class="alert alert-info border-0 shadow-sm"><i class="fa-solid fa-bullseye me-2"></i>{{ t('budgets.title') }}: {% for budget in budgets %}{{ budget.category.localized_name(current_language) }} {{ budget.amount }}{% if not loop.last %}, {% endif %}{% endfor %}</div>{% endif %}
|
||||
|
||||
{% if expenses %}
|
||||
<div class="expense-groups d-grid gap-3">
|
||||
{% for group in grouped_expenses %}
|
||||
<section class="card expense-group-card">
|
||||
<div class="card-header expense-group-header">
|
||||
<div>
|
||||
<div class="h5 mb-1">{{ group['label'] }}</div>
|
||||
<div class="small text-body-secondary">{{ group['items']|length }} {{ t('expenses.results') }}</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="small text-body-secondary">{{ t('expenses.filtered_total') }}</div>
|
||||
<div class="h5 mb-0">{{ '%.2f'|format(group['total']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% for expense in group['items'] %}
|
||||
<article class="expense-list-item">
|
||||
<div class="expense-list-main">
|
||||
<div class="expense-list-thumb-wrap">
|
||||
{% if expense.preview_filename %}
|
||||
<img class="expense-row-thumb" src="{{ url_for('static', filename='previews/' ~ expense.preview_filename) }}" alt="preview">
|
||||
{% else %}
|
||||
<span class="soft-icon"><i class="fa-solid fa-receipt"></i></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="expense-list-copy">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-1">
|
||||
<span class="expense-title">{{ expense.title }}</span>
|
||||
<span class="badge rounded-pill soft-badge">{{ expense.category.localized_name(current_language) if expense.category else t('common.uncategorized') }}</span>
|
||||
<span class="badge text-bg-light border">{{ expense.purchase_date }}</span>
|
||||
</div>
|
||||
<div class="expense-meta-row">
|
||||
{% if expense.vendor %}<span><i class="fa-solid fa-store me-1"></i>{{ expense.vendor }}</span>{% endif %}
|
||||
{% if expense.payment_method %}<span><i class="fa-regular fa-credit-card me-1"></i>{{ t('expenses.payment_' ~ expense.payment_method) if expense.payment_method in ['card','cash','transfer','blik'] else expense.payment_method }}</span>{% endif %}
|
||||
{% if expense.tags %}<span><i class="fa-solid fa-tags me-1"></i>{{ expense.tags }}</span>{% endif %}
|
||||
{% if expense.status %}<span><i class="fa-solid fa-shield-halved me-1"></i>{{ t('expenses.status_' ~ expense.status) if expense.status in ['new','needs_review','confirmed'] else expense.status }}</span>{% endif %}
|
||||
</div>
|
||||
{% if expense.description %}<div class="small text-body-secondary mt-2">{{ expense.description }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="expense-list-side">
|
||||
<div class="expense-amount">{{ expense.amount }} {{ expense.currency }}</div>
|
||||
<div class="expense-actions">
|
||||
{% if expense.all_previews %}
|
||||
{% for preview_name in expense.all_previews[:3] %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary preview-trigger" data-bs-toggle="modal" data-bs-target="#previewModal" data-preview="{{ url_for('static', filename='previews/' ~ preview_name) }}"><i class="fa-solid fa-image"></i></button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-outline-primary" href="{{ url_for('expenses.edit_expense', expense_id=expense.id) }}"><i class="fa-solid fa-pen-to-square me-1"></i>{{ t('expenses.edit') }}</a>
|
||||
<form method="post" action="{{ url_for('expenses.delete_expense', expense_id=expense.id) }}" class="d-inline">{{ csrf_token() if csrf_token else '' }}<button class="btn btn-sm btn-outline-danger"><i class="fa-solid fa-trash"></i></button></form>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card"><div class="empty-state"><i class="fa-solid fa-wallet"></i><div>{{ t('expenses.empty') }}</div></div></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="modal fade" id="previewModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content glass-card">
|
||||
<div class="modal-header border-0">
|
||||
<h2 class="h5 mb-0"><i class="fa-solid fa-image me-2"></i>{{ t('expenses.preview') }}</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img id="previewModalImage" src="" alt="preview" class="img-fluid rounded-4 border">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
1
app/templates/mail/expense_report.html
Normal file
1
app/templates/mail/expense_report.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends "mail/layout.html" %}{% block body %}<h2 style="margin-top:0">Raport wydatków</h2><p>Okres: <strong>{{ period_label }}</strong></p><p>Suma: <strong>{{ total }} {{ currency }}</strong></p><table style="width:100%;border-collapse:collapse;margin-top:16px">{% for expense in expenses %}<tr><td style="padding:8px 0;border-bottom:1px solid #eef2f7">{{ expense.purchase_date }}</td><td style="padding:8px 0;border-bottom:1px solid #eef2f7">{{ expense.title }}</td><td style="padding:8px 0;border-bottom:1px solid #eef2f7;text-align:right">{{ expense.amount }} {{ expense.currency }}</td></tr>{% endfor %}</table>{% endblock %}
|
||||
6
app/templates/mail/expense_report.txt
Normal file
6
app/templates/mail/expense_report.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Raport wydatków
|
||||
Okres: {{ period_label }}
|
||||
Suma: {{ total }} {{ currency }}
|
||||
|
||||
{% for expense in expenses %}- {{ expense.purchase_date }} | {{ expense.title }} | {{ expense.amount }} {{ expense.currency }}
|
||||
{% endfor %}
|
||||
1
app/templates/mail/layout.html
Normal file
1
app/templates/mail/layout.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html><body style="margin:0;padding:0;background:#f4f7fb;font-family:Arial,sans-serif;color:#162033"><div style="max-width:640px;margin:24px auto;background:#fff;border-radius:20px;overflow:hidden;border:1px solid #e6ebf2"><div style="padding:24px 28px;background:linear-gradient(135deg,#172554,#2563eb);color:#fff"><h1 style="margin:0;font-size:22px">Expense Monitor</h1></div><div style="padding:28px">{% block body %}{% endblock %}</div></div></body></html>
|
||||
1
app/templates/mail/new_account.html
Normal file
1
app/templates/mail/new_account.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends "mail/layout.html" %}{% block body %}<h2 style="margin-top:0">Nowe konto</h2><p>Twoje konto zostało utworzone.</p><p>Login: <strong>{{ user.email }}</strong></p><p>Hasło tymczasowe: <strong>{{ temp_password }}</strong></p><p>Po pierwszym logowaniu zmień hasło.</p>{% endblock %}
|
||||
4
app/templates/mail/new_account.txt
Normal file
4
app/templates/mail/new_account.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Nowe konto
|
||||
Login: {{ user.email }}
|
||||
Hasło tymczasowe: {{ temp_password }}
|
||||
Po pierwszym logowaniu zmień hasło.
|
||||
1
app/templates/mail/password_reset.html
Normal file
1
app/templates/mail/password_reset.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends "mail/layout.html" %}{% block body %}<h2 style="margin-top:0">Reset hasła</h2><p>Otrzymaliśmy prośbę o zmianę hasła dla konta {{ user.email }}.</p><p><a href="{{ reset_link }}" style="display:inline-block;background:#2563eb;color:#fff;text-decoration:none;padding:12px 18px;border-radius:12px">Ustaw nowe hasło</a></p><p>Jeśli to nie Ty, zignoruj tę wiadomość.</p>{% endblock %}
|
||||
4
app/templates/mail/password_reset.txt
Normal file
4
app/templates/mail/password_reset.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Reset hasła
|
||||
|
||||
Użyj linku, aby ustawić nowe hasło:
|
||||
{{ reset_link }}
|
||||
1
app/templates/main/dashboard.html
Normal file
1
app/templates/main/dashboard.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'base.html' %}{% block content %}<div class="hero-panel mb-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3"><div><div class="app-section-title mb-2"><span class="feature-icon"><i class="fa-solid fa-chart-pie"></i></span><div><h1 class="h3 mb-0">{{ t('dashboard.title') }}</h1><div class="text-body-secondary">{{ selected_year }}-{{ '%02d'|format(selected_month) }}</div></div></div><div class="footer-note">{{ t('dashboard.latest') }} • {{ t('dashboard.categories') }}</div></div><div class="text-end"><div class="small text-body-secondary">{{ t('dashboard.total') }}</div><div class="display-6 fw-bold">{{ total }}</div></div></div></div>{% if alerts %}<div class="alert alert-warning border-0 shadow-sm"><i class="fa-solid fa-triangle-exclamation me-2"></i><strong>{{ t('dashboard.alerts') }}:</strong> {% for alert in alerts %}{{ alert.category }} {{ '%.0f'|format(alert.ratio) }}%{% if not loop.last %}, {% endif %}{% endfor %}</div>{% endif %}<div class="quick-stats mb-4"><div class="metric-card"><div class="metric-label">{{ t('dashboard.total') }}</div><div class="metric-value">{{ total }}</div></div><div class="metric-card"><div class="metric-label">{{ t('dashboard.latest') }}</div><div class="metric-value">{{ expenses|length }}</div></div><div class="metric-card"><div class="metric-label">{{ t('dashboard.categories') }}</div><div class="metric-value">{{ category_totals|length }}</div></div></div><div class="row g-3"><div class="col-lg-4"><div class="card h-100"><div class="card-body"><div class="app-section-title"><span class="soft-icon"><i class="fa-solid fa-clock-rotate-left"></i></span><h2 class="h5 mb-0">{{ t('dashboard.latest') }}</h2></div>{% if expenses %}<div class="list-group list-group-flush">{% for expense in expenses[:10] %}<div class="list-group-item px-0 d-flex justify-content-between align-items-center gap-3"><div class="d-flex align-items-center gap-3"><span class="soft-icon"><i class="fa-solid fa-receipt"></i></span><div><div class="fw-semibold">{{ expense.title }}</div><div class="small text-body-secondary">{{ expense.purchase_date }} · {{ expense.vendor }}</div></div></div><div class="text-end fw-semibold">{{ expense.amount }} {{ expense.currency }}</div></div>{% endfor %}</div>{% else %}<div class="empty-state"><i class="fa-solid fa-box-open"></i><div>{{ t('dashboard.empty') }}</div></div>{% endif %}</div></div></div><div class="col-lg-4"><div class="card h-100"><div class="card-body"><div class="app-section-title"><span class="soft-icon"><i class="fa-solid fa-chart-pie"></i></span><h2 class="h5 mb-0">{{ t('dashboard.categories') }}</h2></div><div class="chart-card"><canvas id="dashboard-category-chart"></canvas></div></div></div></div><div class="col-lg-4"><div class="card h-100"><div class="card-body"><div class="app-section-title"><span class="soft-icon"><i class="fa-solid fa-credit-card"></i></span><h2 class="h5 mb-0">{{ t('stats.payment_methods') }}</h2></div><div class="chart-card"><canvas id="dashboard-payment-chart"></canvas></div></div></div></div></div>{% endblock %}{% block scripts %}<script>window.dashboardCategoryData={{ chart_categories|tojson }};window.dashboardPaymentData={{ chart_payments|tojson }};</script>{% endblock %}
|
||||
35
app/templates/main/preferences.html
Normal file
35
app/templates/main/preferences.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-6">
|
||||
<div class="card"><div class="card-body">
|
||||
<h1 class="h4 mb-3">{{ t('preferences.title') }}</h1>
|
||||
<form method="post">{{ form.hidden_tag() }}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.language') }}</label>{{ form.language(class='form-select') }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.theme') }}</label>{{ form.theme(class='form-select') }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.reports') }}</label>{{ form.report_frequency(class='form-select', disabled=(not report_options_enabled)) }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.currency') }}</label>{{ form.default_currency(class='form-select') }}</div>
|
||||
</div>
|
||||
{% if not report_options_enabled %}<div class="alert alert-secondary mt-3 mb-0">{{ t('preferences.reports_disabled') }}</div>{% endif %}
|
||||
<button class="btn btn-primary mt-3">{{ t('preferences.save') }}</button>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<div class="card"><div class="card-body">
|
||||
<h2 class="h4 mb-3">{{ t('preferences.my_categories') }}</h2>
|
||||
<form method="post">{{ category_form.hidden_tag() }}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><label class="form-label">{{ t('preferences.category_key') }}</label>{{ category_form.key(class='form-control') }}</div>
|
||||
<div class="col-md-4"><label class="form-label">{{ t('preferences.category_name_pl') }}</label>{{ category_form.name_pl(class='form-control') }}</div>
|
||||
<div class="col-md-4"><label class="form-label">{{ t('preferences.category_name_en') }}</label>{{ category_form.name_en(class='form-control') }}</div>
|
||||
<div class="col-md-6"><label class="form-label">{{ t('preferences.category_color') }}</label>{{ category_form.color(class='form-select') }}</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary mt-3">{{ t('common.save') }}</button>
|
||||
</form>
|
||||
<div class="mt-4 d-flex flex-wrap gap-2">{% for category in my_categories %}<span class="badge text-bg-{{ category.color }}">{{ category.localized_name(current_language) }}</span>{% else %}<span class="text-body-secondary">{{ t('common.no_data') }}</span>{% endfor %}</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
app/templates/main/statistics.html
Normal file
37
app/templates/main/statistics.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
{% set month_names = [t('common.month_1'), t('common.month_2'), t('common.month_3'), t('common.month_4'), t('common.month_5'), t('common.month_6'), t('common.month_7'), t('common.month_8'), t('common.month_9'), t('common.month_10'), t('common.month_11'), t('common.month_12')] %}
|
||||
<div class="hero-panel mb-4">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div class="app-section-title mb-0"><span class="feature-icon"><i class="fa-solid fa-chart-simple"></i></span><div><h1 class="h3 mb-0">{{ t('stats.title') }}</h1><div class="text-body-secondary">{{ t('stats.subtitle') }}</div></div></div>
|
||||
<form method="get" class="row g-2 align-items-end">
|
||||
<div class="col-auto"><label class="form-label small">{{ t('common.year') }}</label><input class="form-control" type="number" name="year" value="{{ selected_year }}"></div>
|
||||
<div class="col-auto"><label class="form-label small">{{ t('common.month') }}</label><select class="form-select" name="month"><option value="0">{{ t('common.all') }}</option>{% for m in range(1,13) %}<option value="{{ m }}" {% if selected_month==m %}selected{% endif %}>{{ month_names[m-1] }}</option>{% endfor %}</select></div>
|
||||
<div class="col-auto"><label class="form-label small">{{ t('stats.range_from') }}</label><input class="form-control" type="number" name="start_year" value="{{ start_year }}"></div>
|
||||
<div class="col-auto"><label class="form-label small">{{ t('stats.range_to') }}</label><input class="form-control" type="number" name="end_year" value="{{ end_year }}"></div>
|
||||
<div class="col-auto"><button class="btn btn-outline-primary"><i class="fa-solid fa-filter me-2"></i>{{ t('common.apply') }}</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-stats mb-4" id="stats-overview"></div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-xl-8"><div class="card"><div class="card-body"><h2 class="h5">{{ t('stats.monthly_trend') }}</h2><div class="chart-card chart-canvas"><canvas id="chart-monthly"></canvas></div></div></div></div>
|
||||
<div class="col-xl-4"><div class="card h-100"><div class="card-body"><h2 class="h5 mb-3">{{ t('stats.top_expenses') }}</h2><div id="top-expenses"></div></div></div></div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-6"><div class="card"><div class="card-body"><h2 class="h5">{{ t('dashboard.categories') }}</h2><div class="chart-card chart-canvas"><canvas id="chart-categories"></canvas></div></div></div></div>
|
||||
<div class="col-xl-6"><div class="card"><div class="card-body"><h2 class="h5">{{ t('stats.payment_methods') }}</h2><div class="chart-card chart-canvas"><canvas id="chart-payments"></canvas></div></div></div></div>
|
||||
<div class="col-xl-6"><div class="card"><div class="card-body"><h2 class="h5">{{ t('stats.long_term') }}</h2><div class="chart-card chart-canvas"><canvas id="chart-range"></canvas></div></div></div></div>
|
||||
<div class="col-xl-6"><div class="card"><div class="card-body"><h2 class="h5">{{ t('stats.quarterly') }}</h2><div class="chart-card chart-canvas"><canvas id="chart-quarterly"></canvas></div></div></div></div>
|
||||
<div class="col-12"><div class="card"><div class="card-body"><h2 class="h5">{{ t('stats.weekdays') }}</h2><div class="chart-card chart-canvas"><canvas id="chart-weekdays"></canvas></div></div></div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
window.expenseStatsYear={{ selected_year|tojson }};
|
||||
window.expenseStatsMonth={{ selected_month|tojson }};
|
||||
window.expenseStatsStartYear={{ start_year|tojson }};
|
||||
window.expenseStatsEndYear={{ end_year|tojson }};
|
||||
window.expenseStatsText={total:{{ t('dashboard.total')|tojson }},count:{{ t('stats.count')|tojson }},average:{{ t('stats.average')|tojson }},refunds:{{ t('stats.refunds')|tojson }},vs_prev:{{ t('stats.vs_prev')|tojson }},no_data:{{ t('common.no_data')|tojson }}};
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user