Initial commit
This commit is contained in:
commit
983cee0320
322 changed files with 57174 additions and 0 deletions
35
backup/first -fina app/app/templates/auth/login.html
Executable file
35
backup/first -fina app/app/templates/auth/login.html
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - FINA{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="glass-card auth-card">
|
||||
<div style="text-align: center; margin-bottom: 1rem;">
|
||||
<img src="{{ url_for('static', filename='images/fina-logo.png') }}" alt="FINA" style="height: 80px; border-radius: 50%;">
|
||||
</div>
|
||||
<h1>FINA</h1>
|
||||
<p class="subtitle">Login to your account</p>
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-link">
|
||||
Don't have an account? <a href="{{ url_for('auth.register') }}">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
45
backup/first -fina app/app/templates/auth/register.html
Executable file
45
backup/first -fina app/app/templates/auth/register.html
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Register - FINA{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="glass-card auth-card">
|
||||
<div style="text-align: center; margin-bottom: 1rem;">
|
||||
<img src="{{ url_for('static', filename='images/fina-logo.png') }}" alt="FINA" style="height: 80px; border-radius: 50%;">
|
||||
</div>
|
||||
<h1>FINA</h1>
|
||||
<p class="subtitle">Create your account</p>
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required minlength="6">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required minlength="6">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-link">
|
||||
Already have an account? <a href="{{ url_for('auth.login') }}">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
29
backup/first -fina app/app/templates/auth/verify_2fa.html
Executable file
29
backup/first -fina app/app/templates/auth/verify_2fa.html
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Verify 2FA - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="glass-card auth-card">
|
||||
<h1>🔐 Two-Factor Authentication</h1>
|
||||
<p class="subtitle">Enter the code from your authenticator app</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.verify_2fa') }}" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="token">Authentication Code</label>
|
||||
<input type="text" id="token" name="token" required autofocus
|
||||
pattern="[0-9]{6}" maxlength="6" placeholder="000000"
|
||||
style="font-size: 1.5rem; text-align: center; letter-spacing: 0.5rem;">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Verify & Login</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-link">
|
||||
<a href="{{ url_for('auth.login') }}">Back to Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
204
backup/first -fina app/app/templates/bank_import.html
Normal file
204
backup/first -fina app/app/templates/bank_import.html
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('bank.import_title') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1><i class="fas fa-file-import"></i> {{ _('bank.import_title') }}</h1>
|
||||
<p class="text-muted">{{ _('bank.import_subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>{{ _('bank.upload_file') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Upload Form -->
|
||||
<form method="POST" enctype="multipart/form-data" id="uploadForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-group">
|
||||
<label for="file">{{ _('bank.select_file') }}</label>
|
||||
<div class="custom-file-upload" id="dropZone">
|
||||
<input type="file"
|
||||
id="file"
|
||||
name="file"
|
||||
accept=".pdf,.csv"
|
||||
required>
|
||||
<div class="upload-placeholder">
|
||||
<i class="fas fa-cloud-upload-alt fa-3x"></i>
|
||||
<p>{{ _('bank.drag_drop') }}</p>
|
||||
<p class="text-muted">{{ _('bank.or_click') }}</p>
|
||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('file').click()">
|
||||
{{ _('bank.browse_files') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="file-selected" style="display: none;">
|
||||
<i class="fas fa-file fa-2x"></i>
|
||||
<p id="fileName"></p>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="clearFile()">
|
||||
<i class="fas fa-times"></i> {{ _('bank.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="alert alert-info">
|
||||
<strong><i class="fas fa-info-circle"></i> {{ _('bank.supported_formats') }}:</strong>
|
||||
<ul>
|
||||
<li>{{ _('bank.format_pdf') }}</li>
|
||||
<li>{{ _('bank.format_csv') }}</li>
|
||||
</ul>
|
||||
<p class="mb-0"><small>{{ _('bank.format_hint') }}</small></p>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ _('bank.not_all_banks_supported') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fas fa-upload"></i> {{ _('bank.upload_parse') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loadingState" style="display: none;">
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">{{ _('bank.processing') }}</span>
|
||||
</div>
|
||||
<p class="mt-3">{{ _('bank.processing') }}...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Section -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h4><i class="fas fa-question-circle"></i> {{ _('bank.how_it_works') }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ol>
|
||||
<li>{{ _('bank.step_1') }}</li>
|
||||
<li>{{ _('bank.step_2') }}</li>
|
||||
<li>{{ _('bank.step_3') }}</li>
|
||||
<li>{{ _('bank.step_4') }}</li>
|
||||
<li>{{ _('bank.step_5') }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.custom-file-upload {
|
||||
position: relative;
|
||||
border: 2px dashed #007bff;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-file-upload:hover {
|
||||
border-color: #0056b3;
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.custom-file-upload.drag-over {
|
||||
border-color: #28a745;
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.custom-file-upload input[type="file"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-placeholder i {
|
||||
color: #007bff;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.file-selected i {
|
||||
color: #28a745;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.custom-file-upload {
|
||||
padding: 30px 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// File selection
|
||||
document.getElementById('file').addEventListener('change', function(e) {
|
||||
if (this.files.length > 0) {
|
||||
document.querySelector('.upload-placeholder').style.display = 'none';
|
||||
document.querySelector('.file-selected').style.display = 'block';
|
||||
document.getElementById('fileName').textContent = this.files[0].name;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear file
|
||||
function clearFile() {
|
||||
document.getElementById('file').value = '';
|
||||
document.querySelector('.upload-placeholder').style.display = 'block';
|
||||
document.querySelector('.file-selected').style.display = 'none';
|
||||
}
|
||||
|
||||
// Drag and drop
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, () => {
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, () => {
|
||||
dropZone.classList.remove('drag-over');
|
||||
});
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function(e) {
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
document.getElementById('file').files = files;
|
||||
document.querySelector('.upload-placeholder').style.display = 'none';
|
||||
document.querySelector('.file-selected').style.display = 'block';
|
||||
document.getElementById('fileName').textContent = files[0].name;
|
||||
}
|
||||
});
|
||||
|
||||
// Show loading on submit
|
||||
document.getElementById('uploadForm').addEventListener('submit', function() {
|
||||
document.querySelector('.card-body').style.display = 'none';
|
||||
document.getElementById('loadingState').style.display = 'block';
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
304
backup/first -fina app/app/templates/bank_import_review.html
Normal file
304
backup/first -fina app/app/templates/bank_import_review.html
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('bank.review_title') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1><i class="fas fa-check-circle"></i> {{ _('bank.review_title') }}</h1>
|
||||
<p class="text-muted">{{ _('bank.review_subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-3">
|
||||
<h3 class="text-primary">{{ total_found }}</h3>
|
||||
<p class="text-muted">{{ _('bank.transactions_found') }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h3 class="text-info">{{ bank_format }}</h3>
|
||||
<p class="text-muted">{{ _('bank.detected_format') }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h3 class="text-success" id="selectedCount">0</h3>
|
||||
<p class="text-muted">{{ _('bank.selected') }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h3 class="text-warning" id="unmappedCount">{{ total_found }}</h3>
|
||||
<p class="text-muted">{{ _('bank.unmapped') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parse Errors (if any) -->
|
||||
{% if parse_errors %}
|
||||
<div class="alert alert-warning">
|
||||
<strong><i class="fas fa-exclamation-triangle"></i> {{ _('bank.parse_warnings') }}:</strong>
|
||||
<ul class="mb-0">
|
||||
{% for error in parse_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bank Format Warning -->
|
||||
{% if bank_format in ['generic', 'unknown', 'Generic'] %}
|
||||
<div class="alert alert-warning">
|
||||
<strong><i class="fas fa-exclamation-triangle"></i> {{ _('bank.format_not_recognized') }}</strong><br>
|
||||
{{ _('bank.format_not_recognized_hint') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Import Form -->
|
||||
<form method="POST" action="{{ url_for('main.bank_import_confirm') }}" id="importForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<!-- Bulk Actions -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input type="checkbox"
|
||||
class="form-check-input"
|
||||
id="selectAll"
|
||||
onchange="toggleAll(this)">
|
||||
<label class="form-check-label" for="selectAll">
|
||||
{{ _('bank.select_all') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-secondary"
|
||||
onclick="selectExpenses()">
|
||||
<i class="fas fa-arrow-down"></i> {{ _('bank.select_expenses') }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-secondary"
|
||||
onclick="selectIncome()">
|
||||
<i class="fas fa-arrow-up"></i> {{ _('bank.select_income') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transactions Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>{{ _('bank.transactions_to_import') }}</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th width="5%">
|
||||
<input type="checkbox" id="selectAllTable" onchange="toggleAll(this)">
|
||||
</th>
|
||||
<th width="10%">{{ _('bank.date') }}</th>
|
||||
<th width="35%">{{ _('bank.description') }}</th>
|
||||
<th width="15%">{{ _('bank.amount') }}</th>
|
||||
<th width="10%">{{ _('bank.type') }}</th>
|
||||
<th width="25%">{{ _('bank.category') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for trans in transactions %}
|
||||
<tr class="transaction-row {% if trans.amount < 0 %}expense-row{% else %}income-row{% endif %}">
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
name="selected_transactions"
|
||||
value="{{ loop.index0 }}"
|
||||
class="transaction-checkbox"
|
||||
onchange="updateCounts()">
|
||||
</td>
|
||||
<td>{{ trans.date.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
<small>{{ trans.description }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="{% if trans.amount < 0 %}text-danger{% else %}text-success{% endif %}">
|
||||
{% if trans.amount < 0 %}-{% else %}+{% endif %}
|
||||
{{ "%.2f"|format(trans.amount|abs) }} {{ _('expense.currency') }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if trans.amount < 0 %}
|
||||
<span class="badge badge-danger">{{ _('bank.expense') }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-success">{{ _('bank.income') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<select name="category_{{ loop.index0 }}"
|
||||
class="form-control form-control-sm category-select"
|
||||
onchange="updateCounts()">
|
||||
<option value="">{{ _('bank.select_category') }}</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}">{{ category.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<a href="{{ url_for('main.bank_import') }}" class="btn btn-secondary btn-block">
|
||||
<i class="fas fa-arrow-left"></i> {{ _('bank.back') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button type="submit" class="btn btn-primary btn-block" id="submitBtn" disabled>
|
||||
<i class="fas fa-check"></i> {{ _('bank.import_selected') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.transaction-row {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.transaction-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.category-select {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.expense-row {
|
||||
border-left: 3px solid #dc3545;
|
||||
}
|
||||
|
||||
.income-row {
|
||||
border-left: 3px solid #28a745;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Toggle all checkboxes
|
||||
function toggleAll(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('.transaction-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = checkbox.checked;
|
||||
});
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
// Select expenses only
|
||||
function selectExpenses() {
|
||||
const checkboxes = document.querySelectorAll('.expense-row .transaction-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = true;
|
||||
});
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
// Select income only
|
||||
function selectIncome() {
|
||||
const checkboxes = document.querySelectorAll('.income-row .transaction-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = true;
|
||||
});
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
// Update counts and enable/disable submit button
|
||||
function updateCounts() {
|
||||
const checkboxes = document.querySelectorAll('.transaction-checkbox:checked');
|
||||
const selectedCount = checkboxes.length;
|
||||
|
||||
// Count how many selected transactions have a category
|
||||
let mappedCount = 0;
|
||||
checkboxes.forEach(cb => {
|
||||
const row = cb.closest('tr');
|
||||
const categorySelect = row.querySelector('.category-select');
|
||||
if (categorySelect && categorySelect.value) {
|
||||
mappedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const unmappedCount = selectedCount - mappedCount;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('selectedCount').textContent = selectedCount;
|
||||
document.getElementById('unmappedCount').textContent = unmappedCount;
|
||||
|
||||
// Enable submit button only if at least one transaction is selected with a category
|
||||
document.getElementById('submitBtn').disabled = (mappedCount === 0);
|
||||
}
|
||||
|
||||
// Auto-select checkbox when category is selected
|
||||
document.querySelectorAll('.category-select').forEach(select => {
|
||||
select.addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
const row = this.closest('tr');
|
||||
const checkbox = row.querySelector('.transaction-checkbox');
|
||||
checkbox.checked = true;
|
||||
}
|
||||
updateCounts();
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm before submit
|
||||
document.getElementById('importForm').addEventListener('submit', function(e) {
|
||||
const selectedCount = document.querySelectorAll('.transaction-checkbox:checked').length;
|
||||
if (selectedCount === 0) {
|
||||
e.preventDefault();
|
||||
alert('{{ _('bank.no_transactions_selected') }}');
|
||||
return false;
|
||||
}
|
||||
|
||||
const unmappedCount = parseInt(document.getElementById('unmappedCount').textContent);
|
||||
if (unmappedCount > 0) {
|
||||
const msg = '{{ _('bank.confirm_unmapped') }}'.replace('{count}', unmappedCount);
|
||||
if (!confirm(msg)) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
document.getElementById('submitBtn').disabled = true;
|
||||
document.getElementById('submitBtn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> {{ _('bank.importing') }}...';
|
||||
});
|
||||
|
||||
// Initial update
|
||||
updateCounts();
|
||||
</script>
|
||||
{% endblock %}
|
||||
108
backup/first -fina app/app/templates/base.html
Executable file
108
backup/first -fina app/app/templates/base.html
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}FINA{% endblock %}</title>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="description" content="FINA - Track your expenses, manage categories, and visualize spending patterns">
|
||||
<meta name="theme-color" content="#5b5fc7">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="FINA">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- Manifest -->
|
||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/fina-logo.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='images/fina-logo.png') }}">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='images/fina-logo.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/fina-logo.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/fina-logo.png') }}">
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="glass-nav">
|
||||
<div class="nav-container">
|
||||
<a href="{{ url_for('main.dashboard') }}" class="nav-brand">
|
||||
<img src="{{ url_for('static', filename='images/fina-logo.png') }}" alt="FINA" class="nav-logo">
|
||||
<span>FINA</span>
|
||||
</a>
|
||||
|
||||
<!-- Global Search Bar -->
|
||||
<div class="nav-search">
|
||||
<form method="GET" action="{{ url_for('main.search_page') }}" class="nav-search-form">
|
||||
<input type="text"
|
||||
name="q"
|
||||
class="nav-search-input"
|
||||
placeholder="{{ _('search.quick_search') }}"
|
||||
autocomplete="off">
|
||||
<button type="submit" class="nav-search-btn">🔍</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="{{ url_for('main.create_category') }}">{{ _('nav.new_category') }}</a>
|
||||
<a href="{{ url_for('subscriptions.index') }}">{{ _('nav.subscriptions') }}</a>
|
||||
<a href="{{ url_for('main.bank_import') }}">
|
||||
<i class="fas fa-file-import"></i> {{ _('bank.import_title') }}
|
||||
</a>
|
||||
<a href="{{ url_for('main.predictions') }}">
|
||||
<i class="fas fa-chart-line"></i> {{ _('predictions.title') }}
|
||||
</a>
|
||||
<a href="{{ url_for('settings.index') }}">{{ _('nav.settings') }}</a>
|
||||
<div class="language-switcher">
|
||||
<button class="language-btn" onclick="toggleLanguageMenu()">
|
||||
{% if get_lang() == 'ro' %}🇷🇴{% elif get_lang() == 'es' %}🇪🇸{% else %}🇬🇧{% endif %}
|
||||
</button>
|
||||
<div class="language-menu" id="language-menu">
|
||||
<a href="{{ url_for('language.switch_language', lang='en') }}" class="language-option">🇬🇧 English</a>
|
||||
<a href="{{ url_for('language.switch_language', lang='ro') }}" class="language-option">🇷🇴 Română</a>
|
||||
<a href="{{ url_for('language.switch_language', lang='es') }}" class="language-option">🇪🇸 Español</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('auth.logout') }}">{{ _('nav.logout') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} glass-card">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- PWA Install Prompt -->
|
||||
<div id="pwa-install-prompt" class="pwa-prompt glass-card" style="display: none;">
|
||||
<div class="pwa-prompt-content">
|
||||
<img src="{{ url_for('static', filename='images/fina-logo.png') }}" alt="FINA" class="pwa-icon">
|
||||
<div class="pwa-prompt-text">
|
||||
<h3>{{ _('pwa.install_title') }}</h3>
|
||||
<p>{{ _('pwa.install_description') }}</p>
|
||||
</div>
|
||||
<div class="pwa-prompt-actions">
|
||||
<button id="pwa-install-btn" class="btn btn-primary">{{ _('pwa.install') }}</button>
|
||||
<button id="pwa-dismiss-btn" class="btn btn-secondary">{{ _('pwa.not_now') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
35
backup/first -fina app/app/templates/create_category.html
Executable file
35
backup/first -fina app/app/templates/create_category.html
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create Category - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>{{ _('category.create') }}</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">{{ _('category.name') }} *</label>
|
||||
<input type="text" id="name" name="name" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">{{ _('category.description') }}</label>
|
||||
<textarea id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="color">{{ _('category.color') }}</label>
|
||||
<input type="color" id="color" name="color" value="#6366f1">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">{{ _('common.cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary">{{ _('category.create') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
424
backup/first -fina app/app/templates/create_expense.html
Executable file
424
backup/first -fina app/app/templates/create_expense.html
Executable file
|
|
@ -0,0 +1,424 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Add Expense - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>💸 Add Expense to {{ category.name }}</h1>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description *</label>
|
||||
<input type="text" id="description" name="description" required autofocus placeholder="e.g., Grocery shopping">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount *</label>
|
||||
<input type="number" id="amount" name="amount" step="0.01" min="0.01" required placeholder="0.00">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="date">Date *</label>
|
||||
<input type="date" id="date" name="date" value="{{ today }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="paid_by">Paid By</label>
|
||||
<input type="text" id="paid_by" name="paid_by" placeholder="e.g., John, Cash, Credit Card">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags</label>
|
||||
<input type="text" id="tags" name="tags" placeholder="Type or click tags below">
|
||||
|
||||
{% if user_tags %}
|
||||
<div class="tag-suggestions">
|
||||
{% for tag in user_tags %}
|
||||
<button type="button" class="tag-btn" data-tag="{{ tag.name }}" style="background: {{ tag.color }}20; border: 1px solid {{ tag.color }}; color: {{ tag.color }};">
|
||||
🏷️ {{ tag.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small>Click tags to add them. Multiple tags can be separated by commas.</small>
|
||||
{% else %}
|
||||
<small>No tags yet. <a href="{{ url_for('settings.create_tag') }}" style="color: #a5b4fc;">Create tags in Settings</a></small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="file">{{ _('expense.receipt') }}</label>
|
||||
<div class="receipt-upload-container">
|
||||
<!-- Camera button for mobile -->
|
||||
<button type="button" class="btn btn-camera" id="cameraBtn" onclick="openCamera()">
|
||||
📸 {{ _('ocr.take_photo') }}
|
||||
</button>
|
||||
|
||||
<!-- File input -->
|
||||
<input type="file" id="file" name="file" accept="image/*,.pdf" capture="environment">
|
||||
|
||||
<!-- OCR processing indicator -->
|
||||
<div id="ocrProcessing" class="ocr-processing" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<span>{{ _('ocr.processing') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- OCR results -->
|
||||
<div id="ocrResults" class="ocr-results" style="display: none;"></div>
|
||||
</div>
|
||||
<small>{{ _('expense.receipt_hint') }} | 🤖 {{ _('ocr.ai_extraction') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('main.view_category', category_id=category.id) }}" class="btn btn-secondary">{{ _('common.cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary">💾 {{ _('expense.create') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tag-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tag-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tag-btn.active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tagsInput = document.getElementById('tags');
|
||||
const tagButtons = document.querySelectorAll('.tag-btn');
|
||||
|
||||
tagButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const tagName = this.getAttribute('data-tag');
|
||||
const currentTags = tagsInput.value;
|
||||
|
||||
// Check if tag already exists
|
||||
const tagsArray = currentTags.split(',').map(t => t.trim()).filter(t => t);
|
||||
|
||||
if (tagsArray.includes(tagName)) {
|
||||
// Remove tag
|
||||
const newTags = tagsArray.filter(t => t !== tagName);
|
||||
tagsInput.value = newTags.join(', ');
|
||||
this.classList.remove('active');
|
||||
} else {
|
||||
// Add tag
|
||||
if (currentTags.trim()) {
|
||||
tagsInput.value = currentTags.trim() + ', ' + tagName;
|
||||
} else {
|
||||
tagsInput.value = tagName;
|
||||
}
|
||||
this.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update button states based on input
|
||||
tagsInput.addEventListener('input', function() {
|
||||
const tagsArray = this.value.split(',').map(t => t.trim()).filter(t => t);
|
||||
tagButtons.forEach(button => {
|
||||
const tagName = button.getAttribute('data-tag');
|
||||
if (tagsArray.includes(tagName)) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// OCR functionality
|
||||
const fileInput = document.getElementById('file');
|
||||
const ocrProcessing = document.getElementById('ocrProcessing');
|
||||
const ocrResults = document.getElementById('ocrResults');
|
||||
const amountInput = document.getElementById('amount');
|
||||
const descriptionInput = document.getElementById('description');
|
||||
const dateInput = document.getElementById('date');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', async function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Only process images for OCR
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
|
||||
// Show processing indicator
|
||||
ocrProcessing.style.display = 'flex';
|
||||
ocrResults.style.display = 'none';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ocr/process', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
ocrProcessing.style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
displayOCRResults(data);
|
||||
} else {
|
||||
ocrResults.innerHTML = `<div class="ocr-error">❌ ${data.error || 'OCR failed'}</div>`;
|
||||
ocrResults.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
ocrProcessing.style.display = 'none';
|
||||
ocrResults.innerHTML = `<div class="ocr-error">❌ Error: ${error.message}</div>`;
|
||||
ocrResults.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displayOCRResults(data) {
|
||||
const confidenceClass = data.confidence === 'high' ? 'success' :
|
||||
data.confidence === 'medium' ? 'warning' : 'low';
|
||||
|
||||
let html = `<div class="ocr-result-box ${confidenceClass}">`;
|
||||
html += `<div class="ocr-header">🤖 AI Detected</div>`;
|
||||
|
||||
if (data.amount) {
|
||||
html += `<div class="ocr-field">
|
||||
<strong>Amount:</strong> ${data.amount.toFixed(2)}
|
||||
<button type="button" class="btn-apply" onclick="applyOCRAmount(${data.amount})">Use This</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (data.merchant) {
|
||||
html += `<div class="ocr-field">
|
||||
<strong>Merchant:</strong> ${data.merchant}
|
||||
<button type="button" class="btn-apply" onclick="applyOCRMerchant('${escapeHtml(data.merchant)}')">Use This</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (data.date) {
|
||||
html += `<div class="ocr-field">
|
||||
<strong>Date:</strong> ${data.date}
|
||||
<button type="button" class="btn-apply" onclick="applyOCRDate('${data.date}')">Use This</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const confidenceIcons = {
|
||||
'high': '✅',
|
||||
'medium': '⚠️',
|
||||
'low': '❌',
|
||||
'none': '❌'
|
||||
};
|
||||
|
||||
html += `<div class="ocr-confidence">${confidenceIcons[data.confidence]} Confidence: ${data.confidence.toUpperCase()}</div>`;
|
||||
html += `</div>`;
|
||||
|
||||
ocrResults.innerHTML = html;
|
||||
ocrResults.style.display = 'block';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
// Camera functionality
|
||||
function openCamera() {
|
||||
const fileInput = document.getElementById('file');
|
||||
if (fileInput) fileInput.click();
|
||||
}
|
||||
|
||||
// Apply OCR results
|
||||
function applyOCRAmount(amount) {
|
||||
const input = document.getElementById('amount');
|
||||
if (input) {
|
||||
input.value = amount.toFixed(2);
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function applyOCRMerchant(merchant) {
|
||||
const input = document.getElementById('description');
|
||||
if (input) {
|
||||
input.value = merchant;
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function applyOCRDate(date) {
|
||||
const input = document.getElementById('date');
|
||||
if (input) {
|
||||
input.value = date;
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Receipt upload styles */
|
||||
.receipt-upload-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-camera {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-camera:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.ocr-processing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.ocr-results {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ocr-result-box {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ocr-result-box.success {
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.ocr-result-box.warning {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
}
|
||||
|
||||
.ocr-result-box.low {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.ocr-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.ocr-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ocr-field:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-apply:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ocr-confidence {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ocr-error {
|
||||
padding: 0.75rem;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
border-radius: 8px;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
/* Mobile optimization */
|
||||
@media (max-width: 768px) {
|
||||
.btn-camera {
|
||||
font-size: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ocr-field {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
320
backup/first -fina app/app/templates/dashboard.html
Executable file
320
backup/first -fina app/app/templates/dashboard.html
Executable file
|
|
@ -0,0 +1,320 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - FINA{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard">
|
||||
|
||||
{% if categories and categories|length > 0 %}
|
||||
|
||||
<div class="metrics-section glass-card" style="margin-bottom: 2rem; margin-top: 0;">
|
||||
<div class="metrics-header">
|
||||
<h2>📈 {{ _('dashboard.metrics') }}</h2>
|
||||
<div class="metrics-controls">
|
||||
<select id="metricCategory" class="metric-select">
|
||||
<option value="all">{{ _('dashboard.all_categories') }}</option>
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat.id }}" data-color="{{ cat.color }}">{{ cat.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="metricYear" class="metric-select">
|
||||
{% for year in available_years %}
|
||||
<option value="{{ year }}" {% if year == current_year %}selected{% endif %}>{{ year }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-container">
|
||||
<div class="chart-box chart-box-pie">
|
||||
<h3>{{ _('dashboard.expenses_by_category') }}</h3>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="pieChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-box chart-box-bar">
|
||||
<h3>{{ _('dashboard.monthly_expenses') }}</h3>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="barChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-container">
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('dashboard.total_spent') }}</h3>
|
||||
<p class="stat-value">{{ total_spent|currency }}</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('dashboard.categories_section') }}</h3>
|
||||
<p class="stat-value">{{ (categories|default([]))|length }}</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('dashboard.total_expenses') }}</h3>
|
||||
<p class="stat-value">{{ total_expenses }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" style="margin-top: 2rem;">
|
||||
<h2>📁 {{ _('dashboard.categories_section') }}</h2>
|
||||
</div>
|
||||
<div class="categories-grid">
|
||||
{% for category in categories %}
|
||||
<a href="{{ url_for('main.view_category', category_id=category.id) }}" class="category-card glass-card" style="border-left: 4px solid {{ category.color }}">
|
||||
<div class="category-content">
|
||||
<h3>{{ category.name }}</h3>
|
||||
{% if category.description %}
|
||||
<p class="category-description">{{ category.description }}</p>
|
||||
{% endif %}
|
||||
<div class="category-amount">{{ category.get_total_spent()|currency }}</div>
|
||||
<div class="category-info">
|
||||
<span>{{ category.expenses|length }} {{ _('category.expenses') | lower }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="stats-container">
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('dashboard.total_spent') }}</h3>
|
||||
<p class="stat-value">{{ total_spent|currency }}</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('category.expenses') }}</h3>
|
||||
<p class="stat-value">{{ (categories|default([]))|length }}</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('dashboard.total_expenses') }}</h3>
|
||||
<p class="stat-value">{{ total_expenses }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card empty-state">
|
||||
<h2>{{ _('empty.welcome_title') }}</h2>
|
||||
<p>{{ _('empty.welcome_message') }}</p>
|
||||
<a href="{{ url_for('main.create_category') }}" class="btn btn-primary">{{ _('empty.create_category') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Upcoming Subscriptions Widget -->
|
||||
{% if upcoming_subscriptions or suggestions_count > 0 %}
|
||||
<div class="glass-card" style="margin-top: 2rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h2>🔄 {{ _('subscription.title') }}</h2>
|
||||
<a href="{{ url_for('subscriptions.index') }}" class="btn btn-secondary btn-sm">{{ _('dashboard.view_all') }}</a>
|
||||
</div>
|
||||
|
||||
{% if suggestions_count > 0 %}
|
||||
<div class="alert" style="background: rgba(245, 158, 11, 0.2); border: 1px solid #f59e0b; margin-bottom: 1rem; position: relative;">
|
||||
💡 <strong>{{ suggestions_count }}</strong> {{ _('subscription.suggestions') | lower }} -
|
||||
<a href="{{ url_for('subscriptions.index') }}" style="color: #fbbf24; text-decoration: underline;">{{ _('common.view') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if upcoming_subscriptions %}
|
||||
<div class="subscriptions-widget">
|
||||
{% for sub in upcoming_subscriptions %}
|
||||
<div class="subscription-widget-item" style="display: flex; justify-content: space-between; padding: 0.75rem 0; border-bottom: 1px solid var(--glass-border);">
|
||||
<div>
|
||||
<strong>{{ sub.name }}</strong>
|
||||
<br>
|
||||
<small style="color: var(--text-secondary);">
|
||||
{{ sub.amount|currency }} -
|
||||
{% if sub.next_due_date %}
|
||||
{% set days_until = (sub.next_due_date - today).days %}
|
||||
{% if days_until == 0 %}
|
||||
{{ _('subscription.today') }}
|
||||
{% elif days_until == 1 %}
|
||||
{{ _('subscription.tomorrow') }}
|
||||
{% elif days_until < 7 %}
|
||||
in {{ days_until }} {{ _('subscription.days') }}
|
||||
{% else %}
|
||||
{{ sub.next_due_date.strftime('%b %d') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
<span style="font-weight: 600;">{{ sub.amount|currency }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="color: var(--text-secondary); text-align: center; padding: 1rem;">
|
||||
{{ _('subscription.no_upcoming') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.charts-container {
|
||||
display: grid;
|
||||
grid-template-columns: 400px 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.chart-box-pie {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-box-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-box-pie .chart-wrapper {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.charts-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-box-pie .chart-wrapper {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
const chartData = {{ chart_data|tojson }};
|
||||
const categories = {{ categories_json|tojson }};
|
||||
const currentYear = {{ current_year }};
|
||||
const currencySymbol = '{{ current_user.currency }}';
|
||||
let pieChart, barChart;
|
||||
|
||||
function initCharts() {
|
||||
const pieCtx = document.getElementById('pieChart').getContext('2d');
|
||||
pieChart = new Chart(pieCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: chartData.map(d => d.name),
|
||||
datasets: [{
|
||||
data: chartData.map(d => d.value),
|
||||
backgroundColor: chartData.map(d => d.color),
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 1,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: { color: '#ffffff', padding: 15, font: { size: 12 } }
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.label + ': ' + formatCurrency(context.parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const barCtx = document.getElementById('barChart').getContext('2d');
|
||||
barChart = new Chart(barCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: []
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 2,
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { color: '#ffffff' }, grid: { color: 'rgba(255, 255, 255, 0.1)' } },
|
||||
x: { ticks: { color: '#ffffff' }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return formatCurrency(context.parsed.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateCharts('all', currentYear);
|
||||
}
|
||||
|
||||
function formatCurrency(amount) {
|
||||
const formatted = amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
|
||||
if (currencySymbol === 'RON') {
|
||||
return formatted + ' Lei';
|
||||
} else if (currencySymbol === 'EUR') {
|
||||
return '€' + formatted;
|
||||
} else if (currencySymbol === 'GBP') {
|
||||
return '£' + formatted;
|
||||
} else {
|
||||
return '$' + formatted;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCharts(categoryId, year) {
|
||||
fetch(`/api/metrics?category=${categoryId}&year=${year}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
barChart.data.datasets = [{
|
||||
label: data.category_name,
|
||||
data: data.monthly_data,
|
||||
backgroundColor: data.color || '#6366f1',
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
borderWidth: 1
|
||||
}];
|
||||
barChart.update();
|
||||
|
||||
if (categoryId === 'all') {
|
||||
pieChart.data.labels = data.pie_labels;
|
||||
pieChart.data.datasets[0].data = data.pie_data;
|
||||
pieChart.data.datasets[0].backgroundColor = data.pie_colors;
|
||||
pieChart.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('metricCategory').addEventListener('change', function() {
|
||||
updateCharts(this.value, document.getElementById('metricYear').value);
|
||||
});
|
||||
|
||||
document.getElementById('metricYear').addEventListener('change', function() {
|
||||
updateCharts(document.getElementById('metricCategory').value, this.value);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initCharts);
|
||||
</script>
|
||||
{% endblock %}
|
||||
69
backup/first -fina app/app/templates/edit_category.html
Executable file
69
backup/first -fina app/app/templates/edit_category.html
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit {{ category.name }} - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>✏️ Edit Category</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Category Name *</label>
|
||||
<input type="text" id="name" name="name" value="{{ category.name }}" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" rows="3">{{ category.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="color">Color</label>
|
||||
<input type="color" id="color" name="color" value="{{ category.color }}">
|
||||
</div>
|
||||
|
||||
<hr style="margin: 2rem 0; border: none; border-top: 1px solid var(--glass-border);">
|
||||
|
||||
<h3 style="margin-bottom: 1rem;">💰 Budget Settings</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="monthly_budget">{{ _('budget.monthly_limit') }} ({{ _('common.optional') }})</label>
|
||||
<input type="number" id="monthly_budget" name="monthly_budget" step="0.01" min="0"
|
||||
value="{{ category.monthly_budget if category.monthly_budget else '' }}"
|
||||
placeholder="e.g., 500">
|
||||
<small style="color: var(--text-secondary);">{{ _('budget.monthly_limit_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="budget_alert_threshold">{{ _('budget.alert_threshold') }} (%)</label>
|
||||
<input type="number" id="budget_alert_threshold" name="budget_alert_threshold"
|
||||
min="50" max="200" step="5"
|
||||
value="{{ (category.budget_alert_threshold * 100)|int if category.budget_alert_threshold else 100 }}">
|
||||
<small style="color: var(--text-secondary);">{{ _('budget.alert_threshold_desc') }}</small>
|
||||
</div>
|
||||
|
||||
{% if category.monthly_budget %}
|
||||
<div style="background: rgba(99, 102, 241, 0.1); padding: 1rem; border-radius: 8px; margin-top: 1rem;">
|
||||
{% set status = category.get_budget_status() %}
|
||||
<p style="margin: 0; font-size: 0.9rem;">
|
||||
<strong>{{ _('budget.current_month') }}:</strong><br>
|
||||
{{ _('budget.spent') }}: {{ status.spent|currency }}<br>
|
||||
{{ _('budget.budget') }}: {{ status.budget|currency }}<br>
|
||||
<span style="color: {% if status.over_budget %}#ef4444{% else %}#10b981{% endif %};">
|
||||
{{ _('budget.remaining') }}: {{ status.remaining|currency }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('main.view_category', category_id=category.id) }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">💾 Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
408
backup/first -fina app/app/templates/edit_expense.html
Executable file
408
backup/first -fina app/app/templates/edit_expense.html
Executable file
|
|
@ -0,0 +1,408 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Expense - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>✏️ Edit Expense</h1>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description *</label>
|
||||
<input type="text" id="description" name="description" value="{{ expense.description }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount *</label>
|
||||
<input type="number" id="amount" name="amount" step="0.01" min="0.01" value="{{ expense.amount }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="date">Date *</label>
|
||||
<input type="date" id="date" name="date" value="{{ expense.date.strftime('%Y-%m-%d') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="paid_by">Paid By</label>
|
||||
<input type="text" id="paid_by" name="paid_by" value="{{ expense.paid_by or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags</label>
|
||||
<input type="text" id="tags" name="tags" value="{{ expense.tags or '' }}" placeholder="Type or click tags below">
|
||||
|
||||
{% if user_tags %}
|
||||
<div class="tag-suggestions">
|
||||
{% for tag in user_tags %}
|
||||
<button type="button" class="tag-btn" data-tag="{{ tag.name }}" style="background: {{ tag.color }}20; border: 1px solid {{ tag.color }}; color: {{ tag.color }};">
|
||||
🏷️ {{ tag.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small>Click tags to add/remove them. Multiple tags can be separated by commas.</small>
|
||||
{% else %}
|
||||
<small>No tags yet. <a href="{{ url_for('settings.create_tag') }}" style="color: #a5b4fc;">Create tags in Settings</a></small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="file">{{ _('expense.receipt') }}</label>
|
||||
{% if expense.file_path %}
|
||||
<p style="color: var(--text-secondary); margin-bottom: 0.5rem;">
|
||||
Current: <a href="{{ url_for('main.download_file', expense_id=expense.id) }}" style="color: #a5b4fc;">{{ _('common.download') }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="receipt-upload-container">
|
||||
<button type="button" class="btn-camera" onclick="openCamera()">
|
||||
📸 {{ _('ocr.take_photo') }}
|
||||
</button>
|
||||
<input type="file" id="file" name="file" accept="image/*,.pdf" capture="environment">
|
||||
<div id="ocrProcessing" class="ocr-processing" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<span>{{ _('ocr.processing') }}</span>
|
||||
</div>
|
||||
<div id="ocrResults" class="ocr-results" style="display: none;"></div>
|
||||
</div>
|
||||
<small>{{ _('expense.receipt_hint') }} | 🤖 {{ _('ocr.ai_extraction') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('main.view_category', category_id=expense.category_id) }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">💾 Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tag-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tag-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tag-btn.active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tagsInput = document.getElementById('tags');
|
||||
const tagButtons = document.querySelectorAll('.tag-btn');
|
||||
|
||||
// Initialize active states based on existing tags
|
||||
const existingTags = tagsInput.value.split(',').map(t => t.trim()).filter(t => t);
|
||||
tagButtons.forEach(button => {
|
||||
const tagName = button.getAttribute('data-tag');
|
||||
if (existingTags.includes(tagName)) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
tagButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const tagName = this.getAttribute('data-tag');
|
||||
const currentTags = tagsInput.value;
|
||||
const tagsArray = currentTags.split(',').map(t => t.trim()).filter(t => t);
|
||||
|
||||
if (tagsArray.includes(tagName)) {
|
||||
const newTags = tagsArray.filter(t => t !== tagName);
|
||||
tagsInput.value = newTags.join(', ');
|
||||
this.classList.remove('active');
|
||||
} else {
|
||||
if (currentTags.trim()) {
|
||||
tagsInput.value = currentTags.trim() + ', ' + tagName;
|
||||
} else {
|
||||
tagsInput.value = tagName;
|
||||
}
|
||||
this.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tagsInput.addEventListener('input', function() {
|
||||
const tagsArray = this.value.split(',').map(t => t.trim()).filter(t => t);
|
||||
tagButtons.forEach(button => {
|
||||
const tagName = button.getAttribute('data-tag');
|
||||
if (tagsArray.includes(tagName)) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// OCR functionality (same as create form)
|
||||
const fileInput = document.getElementById('file');
|
||||
const ocrProcessing = document.getElementById('ocrProcessing');
|
||||
const ocrResults = document.getElementById('ocrResults');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', async function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file || !file.type.startsWith('image/')) return;
|
||||
|
||||
ocrProcessing.style.display = 'flex';
|
||||
ocrResults.style.display = 'none';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ocr/process', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
ocrProcessing.style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
displayOCRResults(data);
|
||||
} else {
|
||||
ocrResults.innerHTML = `<div class="ocr-error">❌ ${data.error || 'OCR failed'}</div>`;
|
||||
ocrResults.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
ocrProcessing.style.display = 'none';
|
||||
ocrResults.innerHTML = `<div class="ocr-error">❌ Error: ${error.message}</div>`;
|
||||
ocrResults.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displayOCRResults(data) {
|
||||
const confidenceClass = data.confidence === 'high' ? 'success' :
|
||||
data.confidence === 'medium' ? 'warning' : 'low';
|
||||
|
||||
let html = `<div class="ocr-result-box ${confidenceClass}">`;
|
||||
html += `<div class="ocr-header">🤖 AI Detected</div>`;
|
||||
|
||||
if (data.amount) {
|
||||
html += `<div class="ocr-field">
|
||||
<strong>Amount:</strong> ${data.amount.toFixed(2)}
|
||||
<button type="button" class="btn-apply" onclick="applyOCRAmount(${data.amount})">Use This</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (data.merchant) {
|
||||
html += `<div class="ocr-field">
|
||||
<strong>Merchant:</strong> ${data.merchant}
|
||||
<button type="button" class="btn-apply" onclick="applyOCRMerchant('${escapeHtml(data.merchant)}')">Use This</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (data.date) {
|
||||
html += `<div class="ocr-field">
|
||||
<strong>Date:</strong> ${data.date}
|
||||
<button type="button" class="btn-apply" onclick="applyOCRDate('${data.date}')">Use This</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `<div class="ocr-confidence">${data.confidence === 'high' ? '✅' : data.confidence === 'medium' ? '⚠️' : '❌'} Confidence: ${data.confidence.toUpperCase()}</div>`;
|
||||
html += `</div>`;
|
||||
|
||||
ocrResults.innerHTML = html;
|
||||
ocrResults.style.display = 'block';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
function openCamera() {
|
||||
const fileInput = document.getElementById('file');
|
||||
if (fileInput) fileInput.click();
|
||||
}
|
||||
|
||||
function applyOCRAmount(amount) {
|
||||
const input = document.getElementById('amount');
|
||||
if (input) {
|
||||
input.value = amount.toFixed(2);
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function applyOCRMerchant(merchant) {
|
||||
const input = document.getElementById('description');
|
||||
if (input) {
|
||||
input.value = merchant;
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function applyOCRDate(date) {
|
||||
const input = document.getElementById('date');
|
||||
if (input) {
|
||||
input.value = date;
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Receipt OCR styles (same as create form) */
|
||||
.receipt-upload-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-camera {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-camera:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.ocr-processing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.ocr-results {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ocr-result-box {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ocr-result-box.success {
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.ocr-result-box.warning {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
}
|
||||
|
||||
.ocr-result-box.low {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.ocr-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.ocr-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ocr-field:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-apply:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ocr-confidence {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ocr-error {
|
||||
padding: 0.75rem;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
border-radius: 8px;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.btn-camera {
|
||||
font-size: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ocr-field {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
30
backup/first -fina app/app/templates/login.html
Executable file
30
backup/first -fina app/app/templates/login.html
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="glass-card auth-card">
|
||||
<h1>{{ _('auth.welcome_back') }}</h1>
|
||||
<p class="subtitle">{{ _('auth.login') }}</p>
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{{ _('auth.username') }}</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">{{ _('auth.password') }}</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">{{ _('auth.sign_in') }}</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-link">{{ _('auth.no_account') }} <a href="{{ url_for('auth.register') }}">{{ _('auth.sign_up') }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
355
backup/first -fina app/app/templates/predictions.html
Normal file
355
backup/first -fina app/app/templates/predictions.html
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('predictions.title') }} - FINA{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h3 mb-2">
|
||||
<i class="fas fa-chart-line me-2"></i>
|
||||
{{ _('predictions.title') }}
|
||||
</h1>
|
||||
<p class="text-muted">{{ _('predictions.subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if predictions.total_months < 3 %}
|
||||
<!-- Not enough data warning -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
{{ _('predictions.no_data') }}
|
||||
</h5>
|
||||
<p class="mb-0">{{ _('predictions.no_data_desc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted mb-2">{{ _('predictions.total_predicted') }}</h6>
|
||||
<h3 class="mb-0">{{ predictions.total.amount|round(2) }} RON</h3>
|
||||
<small class="text-muted">
|
||||
{{ _('predictions.based_on', n=predictions.total_months) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted mb-2">{{ _('predictions.confidence') }}</h6>
|
||||
<h3 class="mb-0">
|
||||
{% if predictions.total.confidence == 'high' %}
|
||||
<span class="badge bg-success">{{ _('predictions.confidence_high') }}</span>
|
||||
{% elif predictions.total.confidence == 'medium' %}
|
||||
<span class="badge bg-warning">{{ _('predictions.confidence_medium') }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ _('predictions.confidence_low') }}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<small class="text-muted">{{ predictions.total.months_of_data }} {{ _('predictions.month') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted mb-2">{{ _('predictions.trend') }}</h6>
|
||||
<h3 class="mb-0">
|
||||
{% if predictions.total.trend == 'increasing' %}
|
||||
<i class="fas fa-arrow-up text-danger"></i>
|
||||
{{ _('predictions.trend_increasing') }}
|
||||
{% elif predictions.total.trend == 'decreasing' %}
|
||||
<i class="fas fa-arrow-down text-success"></i>
|
||||
{{ _('predictions.trend_decreasing') }}
|
||||
{% else %}
|
||||
<i class="fas fa-minus text-info"></i>
|
||||
{{ _('predictions.trend_stable') }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Smart Insights -->
|
||||
{% if insights %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
{{ _('predictions.insights') }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for insight in insights %}
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-check-circle text-success me-2"></i>
|
||||
{{ insight }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Predictions Chart -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{{ _('predictions.forecast') }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="predictionsChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Breakdown -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{{ _('predictions.by_category') }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _('common.category') }}</th>
|
||||
<th>{{ _('predictions.amount') }}</th>
|
||||
<th>{{ _('predictions.confidence') }}</th>
|
||||
<th>{{ _('predictions.trend') }}</th>
|
||||
<th>{{ _('common.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for category_name, prediction in predictions.by_category.items() %}
|
||||
<tr>
|
||||
<td>
|
||||
<i class="fas fa-tag me-2"></i>
|
||||
{{ category_name }}
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ prediction.predicted_amount|round(2) }} RON</strong>
|
||||
</td>
|
||||
<td>
|
||||
{% if prediction.confidence == 'high' %}
|
||||
<span class="badge bg-success">{{ _('predictions.confidence_high') }}</span>
|
||||
{% elif prediction.confidence == 'medium' %}
|
||||
<span class="badge bg-warning">{{ _('predictions.confidence_medium') }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ _('predictions.confidence_low') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if prediction.trend == 'increasing' %}
|
||||
<i class="fas fa-arrow-up text-danger"></i>
|
||||
{{ _('predictions.trend_increasing') }}
|
||||
{% elif prediction.trend == 'decreasing' %}
|
||||
<i class="fas fa-arrow-down text-success"></i>
|
||||
{{ _('predictions.trend_decreasing') }}
|
||||
{% else %}
|
||||
<i class="fas fa-minus text-info"></i>
|
||||
{{ _('predictions.trend_stable') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="showCategoryForecast({{ prediction.category_id }}, '{{ category_name }}')">
|
||||
{{ _('predictions.view_details') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Methodology Info -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
{{ _('predictions.methodology') }}
|
||||
</h6>
|
||||
<p class="mb-0 text-muted small">
|
||||
{{ _('predictions.methodology_desc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Category Forecast Modal -->
|
||||
<div class="modal fade" id="categoryForecastModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="categoryForecastTitle"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<canvas id="categoryForecastChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Predictions data
|
||||
const predictionsData = {{ predictions|tojson }};
|
||||
|
||||
// Main predictions chart
|
||||
const ctx = document.getElementById('predictionsChart');
|
||||
if (ctx && predictionsData.by_category) {
|
||||
const categories = Object.keys(predictionsData.by_category);
|
||||
const amounts = categories.map(cat => predictionsData.by_category[cat].predicted_amount);
|
||||
const colors = [
|
||||
'rgba(255, 99, 132, 0.7)',
|
||||
'rgba(54, 162, 235, 0.7)',
|
||||
'rgba(255, 206, 86, 0.7)',
|
||||
'rgba(75, 192, 192, 0.7)',
|
||||
'rgba(153, 102, 255, 0.7)',
|
||||
'rgba(255, 159, 64, 0.7)',
|
||||
'rgba(201, 203, 207, 0.7)'
|
||||
];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: categories,
|
||||
datasets: [{
|
||||
label: '{{ _("predictions.total_predicted") }}',
|
||||
data: amounts,
|
||||
backgroundColor: colors.slice(0, categories.length),
|
||||
borderColor: colors.slice(0, categories.length).map(c => c.replace('0.7', '1')),
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.parsed.y.toFixed(2) + ' RON';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + ' RON';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show category forecast
|
||||
async function showCategoryForecast(categoryId, categoryName) {
|
||||
try {
|
||||
const response = await fetch(`/api/predictions/category/${categoryId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update modal title
|
||||
document.getElementById('categoryForecastTitle').textContent =
|
||||
'{{ _("predictions.forecast") }}: ' + categoryName;
|
||||
|
||||
// Create chart
|
||||
const modalCtx = document.getElementById('categoryForecastChart');
|
||||
|
||||
// Destroy existing chart if any
|
||||
if (window.categoryChart) {
|
||||
window.categoryChart.destroy();
|
||||
}
|
||||
|
||||
window.categoryChart = new Chart(modalCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.forecast.map(f => f.month),
|
||||
datasets: [{
|
||||
label: '{{ _("predictions.amount") }}',
|
||||
data: data.forecast.map(f => f.amount),
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.parsed.y.toFixed(2) + ' RON';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + ' RON';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show modal
|
||||
new bootstrap.Modal(document.getElementById('categoryForecastModal')).show();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching forecast:', error);
|
||||
alert('{{ _("common.error") }}');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
41
backup/first -fina app/app/templates/register.html
Executable file
41
backup/first -fina app/app/templates/register.html
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Register - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="glass-card auth-card">
|
||||
<h1>{{ _('auth.create_account') }}</h1>
|
||||
<p class="subtitle">{{ _('auth.register') }}</p>
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{{ _('auth.username') }}</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{ _('auth.email') }}</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">{{ _('auth.password') }}</label>
|
||||
<input type="password" id="password" name="password" required minlength="8">
|
||||
<small>At least 8 characters</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">{{ _('auth.confirm_password') }}</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">{{ _('auth.sign_up') }}</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-link">{{ _('auth.have_account') }} <a href="{{ url_for('auth.login') }}">{{ _('auth.sign_in') }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
592
backup/first -fina app/app/templates/search.html
Normal file
592
backup/first -fina app/app/templates/search.html
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('search.title') }} - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="search-page">
|
||||
<div class="glass-card">
|
||||
<div class="search-header">
|
||||
<h1>🔍 {{ _('search.title') }}</h1>
|
||||
<p class="search-subtitle">{{ _('search.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Search Form -->
|
||||
<form method="GET" action="{{ url_for('main.search_page') }}" class="search-form">
|
||||
<div class="search-input-wrapper">
|
||||
<input type="text"
|
||||
name="q"
|
||||
id="searchInput"
|
||||
value="{{ query }}"
|
||||
placeholder="{{ _('search.placeholder') }}"
|
||||
autocomplete="off"
|
||||
autofocus>
|
||||
<button type="submit" class="btn-search">
|
||||
<span class="desktop-text">{{ _('search.button') }}</span>
|
||||
<span class="mobile-icon">🔍</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="searchSuggestions" class="search-suggestions"></div>
|
||||
</form>
|
||||
|
||||
{% if results %}
|
||||
<div class="search-results">
|
||||
<div class="results-summary">
|
||||
<h2>{{ _('search.results_for') }} "<strong>{{ query }}</strong>"</h2>
|
||||
<p class="results-count">{{ results.total }} {{ _('search.results_found') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Expenses Results -->
|
||||
{% if results.expenses %}
|
||||
<div class="result-section">
|
||||
<h3 class="section-title">💸 {{ _('search.expenses') }} ({{ results.expenses|length }})</h3>
|
||||
<div class="result-list">
|
||||
{% for expense in results.expenses %}
|
||||
<a href="{{ expense.url }}" class="result-item">
|
||||
<div class="result-icon" style="background: {{ expense.category_color }}20; color: {{ expense.category_color }};">
|
||||
💸
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<div class="result-title">{{ expense.description }}</div>
|
||||
<div class="result-meta">
|
||||
<span class="meta-badge" style="background: {{ expense.category_color }}20; color: {{ expense.category_color }};">
|
||||
{{ expense.category_name }}
|
||||
</span>
|
||||
<span>{{ expense.date }}</span>
|
||||
{% if expense.paid_by %}
|
||||
<span>👤 {{ expense.paid_by }}</span>
|
||||
{% endif %}
|
||||
{% if expense.tags %}
|
||||
<span>🏷️ {{ expense.tags }}</span>
|
||||
{% endif %}
|
||||
{% if expense.has_receipt %}
|
||||
<span>📎</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-amount">{{ expense.amount|currency }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Categories Results -->
|
||||
{% if results.categories %}
|
||||
<div class="result-section">
|
||||
<h3 class="section-title">📁 {{ _('search.categories') }} ({{ results.categories|length }})</h3>
|
||||
<div class="result-list">
|
||||
{% for category in results.categories %}
|
||||
<a href="{{ category.url }}" class="result-item">
|
||||
<div class="result-icon" style="background: {{ category.color }}20; color: {{ category.color }};">
|
||||
📁
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<div class="result-title">{{ category.name }}</div>
|
||||
<div class="result-meta">
|
||||
{% if category.description %}
|
||||
<span>{{ category.description[:50] }}{% if category.description|length > 50 %}...{% endif %}</span>
|
||||
{% endif %}
|
||||
<span>{{ category.expense_count }} {{ _('search.expenses_count') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-amount">{{ category.total_spent|currency }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Subscriptions Results -->
|
||||
{% if results.subscriptions %}
|
||||
<div class="result-section">
|
||||
<h3 class="section-title">🔄 {{ _('search.subscriptions') }} ({{ results.subscriptions|length }})</h3>
|
||||
<div class="result-list">
|
||||
{% for sub in results.subscriptions %}
|
||||
<a href="{{ sub.url }}" class="result-item">
|
||||
<div class="result-icon" style="background: #10b98120; color: #10b981;">
|
||||
🔄
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<div class="result-title">{{ sub.name }}</div>
|
||||
<div class="result-meta">
|
||||
<span class="meta-badge">{{ sub.frequency }}</span>
|
||||
{% if sub.next_due %}
|
||||
<span>📅 {{ sub.next_due }}</span>
|
||||
{% endif %}
|
||||
<span>{{ sub.category_name }}</span>
|
||||
{% if not sub.is_active %}
|
||||
<span class="inactive-badge">{{ _('search.inactive') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-amount">{{ sub.amount|currency }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tags Results -->
|
||||
{% if results.tags %}
|
||||
<div class="result-section">
|
||||
<h3 class="section-title">🏷️ {{ _('search.tags') }} ({{ results.tags|length }})</h3>
|
||||
<div class="result-list">
|
||||
{% for tag in results.tags %}
|
||||
<a href="{{ tag.url }}" class="result-item">
|
||||
<div class="result-icon" style="background: {{ tag.color }}20; color: {{ tag.color }};">
|
||||
🏷️
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<div class="result-title">{{ tag.name }}</div>
|
||||
<div class="result-meta">
|
||||
<span>{{ tag.expense_count }} {{ _('search.expenses_count') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if results.total == 0 %}
|
||||
<div class="no-results">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<h3>{{ _('search.no_results') }}</h3>
|
||||
<p>{{ _('search.no_results_message') }}</p>
|
||||
<ul class="search-tips">
|
||||
<li>{{ _('search.tip_spelling') }}</li>
|
||||
<li>{{ _('search.tip_keywords') }}</li>
|
||||
<li>{{ _('search.tip_date') }}</li>
|
||||
<li>{{ _('search.tip_amount') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif query %}
|
||||
<div class="no-results">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<h3>{{ _('search.no_results') }}</h3>
|
||||
<p>{{ _('search.no_results_message') }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="search-welcome">
|
||||
<div class="search-icon">🔍</div>
|
||||
<h2>{{ _('search.welcome_title') }}</h2>
|
||||
<p>{{ _('search.welcome_message') }}</p>
|
||||
<div class="search-examples">
|
||||
<h4>{{ _('search.examples_title') }}</h4>
|
||||
<div class="example-chips">
|
||||
<button type="button" class="example-chip" onclick="searchFor('groceries')">🛒 groceries</button>
|
||||
<button type="button" class="example-chip" onclick="searchFor('45.99')">💰 45.99</button>
|
||||
<button type="button" class="example-chip" onclick="searchFor('2024-12-15')">📅 2024-12-15</button>
|
||||
<button type="button" class="example-chip" onclick="searchFor('netflix')">🔄 netflix</button>
|
||||
<button type="button" class="example-chip" onclick="searchFor('restaurant')">🏷️ restaurant</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.search-page {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.search-header h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.search-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-input-wrapper input {
|
||||
flex: 1;
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-input-wrapper input:focus {
|
||||
border-color: #667eea;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 1rem 2rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-search .mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 80px;
|
||||
background: rgba(30, 30, 50, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
margin-top: 0.5rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-suggestions.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.results-summary {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.results-summary h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.result-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.meta-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.inactive-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.result-amount {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.no-results, .search-welcome {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
.empty-icon, .search-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.no-results h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.search-tips {
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.search-tips li {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.search-examples {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.search-examples h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.example-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.example-chip {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.example-chip:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.search-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.search-input-wrapper input {
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 0.875rem 1rem;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.btn-search .desktop-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-search .mobile-icon {
|
||||
display: inline;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
flex-wrap: wrap;
|
||||
padding: 0.875rem;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.result-amount {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.search-suggestions {
|
||||
right: 60px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let searchTimeout;
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const suggestionsBox = document.getElementById('searchSuggestions');
|
||||
|
||||
// Auto-suggest functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = this.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
suggestionsBox.classList.remove('active');
|
||||
suggestionsBox.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetch(`/api/search/suggestions?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.suggestions && data.suggestions.length > 0) {
|
||||
displaySuggestions(data.suggestions);
|
||||
} else {
|
||||
suggestionsBox.classList.remove('active');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Suggestions error:', error);
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Hide suggestions when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchInput.contains(e.target) && !suggestionsBox.contains(e.target)) {
|
||||
suggestionsBox.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displaySuggestions(suggestions) {
|
||||
let html = '';
|
||||
suggestions.forEach(sugg => {
|
||||
html += `<div class="suggestion-item" onclick="selectSuggestion('${escapeHtml(sugg.text)}')">`;
|
||||
html += `<span>${sugg.icon} ${escapeHtml(sugg.text)}</span>`;
|
||||
if (sugg.amount) {
|
||||
html += ` <span style="float: right; color: var(--text-secondary);">${sugg.amount.toFixed(2)}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
});
|
||||
suggestionsBox.innerHTML = html;
|
||||
suggestionsBox.classList.add('active');
|
||||
}
|
||||
|
||||
function selectSuggestion(text) {
|
||||
searchInput.value = text;
|
||||
suggestionsBox.classList.remove('active');
|
||||
searchInput.form.submit();
|
||||
}
|
||||
|
||||
function searchFor(query) {
|
||||
searchInput.value = query;
|
||||
searchInput.form.submit();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
30
backup/first -fina app/app/templates/settings/create_tag.html
Executable file
30
backup/first -fina app/app/templates/settings/create_tag.html
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create Tag - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>🏷️ Create Tag</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Tag Name *</label>
|
||||
<input type="text" id="name" name="name" required autofocus placeholder="e.g., urgent, monthly, personal">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="color">Color</label>
|
||||
<input type="color" id="color" name="color" value="#6366f1">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('settings.index') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">💾 Create Tag</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
backup/first -fina app/app/templates/settings/create_user.html
Executable file
41
backup/first -fina app/app/templates/settings/create_user.html
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create User - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>👤 Create User</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Username *</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email *</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password *</label>
|
||||
<input type="password" id="password" name="password" required minlength="6">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="is_admin"> Admin User
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('settings.index') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">💾 Create User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
77
backup/first -fina app/app/templates/settings/edit_profile.html
Executable file
77
backup/first -fina app/app/templates/settings/edit_profile.html
Executable file
|
|
@ -0,0 +1,77 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Profile - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>✏️ {{ _('settings.edit_profile') }}</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{{ _('auth.username') }}</label>
|
||||
<input type="text" id="username" name="username" value="{{ current_user.username }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{ _('auth.email') }}</label>
|
||||
<input type="email" id="email" name="email" value="{{ current_user.email }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="currency">{{ _('settings.currency') }}</label>
|
||||
<select id="currency" name="currency" class="metric-select">
|
||||
<option value="USD" {% if current_user.currency == 'USD' %}selected{% endif %}>$ USD</option>
|
||||
<option value="EUR" {% if current_user.currency == 'EUR' %}selected{% endif %}>€ EUR</option>
|
||||
<option value="RON" {% if current_user.currency == 'RON' %}selected{% endif %}>RON (Lei)</option>
|
||||
<option value="GBP" {% if current_user.currency == 'GBP' %}selected{% endif %}>£ GBP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="language">{{ _('settings.language') }}</label>
|
||||
<select id="language" name="language" class="metric-select">
|
||||
{% for lang in languages %}
|
||||
<option value="{{ lang.code }}" {% if current_user.language == lang.code %}selected{% endif %}>
|
||||
{{ lang.flag }} {{ lang.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_password">{{ _('auth.password') }} (leave blank to keep current)</label>
|
||||
<input type="password" id="new_password" name="new_password" minlength="6">
|
||||
</div>
|
||||
|
||||
<hr style="margin: 2rem 0; border: none; border-top: 1px solid var(--glass-border);">
|
||||
|
||||
<h3 style="margin-bottom: 1rem;">📧 {{ _('budget.alert_settings') }}</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" id="budget_alerts_enabled" name="budget_alerts_enabled"
|
||||
{% if current_user.budget_alerts_enabled %}checked{% endif %} style="width: auto;">
|
||||
<span>{{ _('budget.enable_alerts') }}</span>
|
||||
</label>
|
||||
<small style="color: var(--text-secondary);">{{ _('budget.enable_alerts_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alert_email">{{ _('budget.alert_email') }} ({{ _('common.optional') }})</label>
|
||||
<input type="email" id="alert_email" name="alert_email"
|
||||
value="{{ current_user.alert_email or '' }}"
|
||||
placeholder="{{ current_user.email }}">
|
||||
<small style="color: var(--text-secondary);">{{ _('budget.alert_email_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('settings.index') }}" class="btn btn-secondary">{{ _('settings.cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary">💾 {{ _('settings.save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
backup/first -fina app/app/templates/settings/edit_user.html
Executable file
41
backup/first -fina app/app/templates/settings/edit_user.html
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit User - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>✏️ Edit User: {{ user.username }}</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Username *</label>
|
||||
<input type="text" id="username" name="username" value="{{ user.username }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email *</label>
|
||||
<input type="email" id="email" name="email" value="{{ user.email }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_password">New Password (leave blank to keep current)</label>
|
||||
<input type="password" id="new_password" name="new_password" minlength="6">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}> Admin User
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('settings.index') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">💾 Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
218
backup/first -fina app/app/templates/settings/index.html
Executable file
218
backup/first -fina app/app/templates/settings/index.html
Executable file
|
|
@ -0,0 +1,218 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Settings - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings-container">
|
||||
<h1>⚙️ {{ _('settings.title') }}</h1>
|
||||
|
||||
<div class="settings-tabs">
|
||||
<button class="tab-btn active" onclick="openTab(event, 'profile')">{{ _('settings.profile') }}</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'security')">{{ _('settings.security') }}</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'tags')">{{ _('settings.tags') }}</button>
|
||||
<button class="tab-btn" onclick="openTab(event, 'import-export')">{{ _('settings.import_export') }}</button>
|
||||
{% if current_user.is_admin %}
|
||||
<button class="tab-btn" onclick="openTab(event, 'users')">{{ _('settings.users') }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Profile Tab -->
|
||||
<div id="profile" class="tab-content active">
|
||||
<div class="glass-card">
|
||||
<h2>👤 {{ _('settings.profile_settings') }}</h2>
|
||||
<form method="POST" action="{{ url_for('settings.edit_profile') }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">{{ _('settings.username') }}</label>
|
||||
<input type="text" id="username" name="username" value="{{ current_user.username }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{ _('settings.email') }}</label>
|
||||
<input type="email" id="email" name="email" value="{{ current_user.email }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="currency">{{ _('settings.currency') }}</label>
|
||||
<select id="currency" name="currency" class="metric-select">
|
||||
<option value="USD" {% if current_user.currency == 'USD' %}selected{% endif %}>$ USD</option>
|
||||
<option value="EUR" {% if current_user.currency == 'EUR' %}selected{% endif %}>€ EUR</option>
|
||||
<option value="RON" {% if current_user.currency == 'RON' %}selected{% endif %}>RON (Lei)</option>
|
||||
<option value="GBP" {% if current_user.currency == 'GBP' %}selected{% endif %}>£ GBP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_password">{{ _('settings.new_password') }}</label>
|
||||
<input type="password" id="new_password" name="new_password" placeholder="{{ _('settings.new_password_placeholder') }}">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">💾 {{ _('settings.save') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Tab (2FA) -->
|
||||
<div id="security" class="tab-content">
|
||||
<div class="glass-card">
|
||||
<h2>🔐 {{ _('settings.2fa_title') }}</h2>
|
||||
|
||||
{% if current_user.is_2fa_enabled %}
|
||||
<div style="padding: 1.5rem; background: rgba(16, 185, 129, 0.1); border: 1px solid var(--success); border-radius: 10px; margin-bottom: 1.5rem;">
|
||||
<p style="color: var(--success); font-weight: 600; margin-bottom: 0.5rem;">✅ {{ _('settings.2fa_enabled') }}</p>
|
||||
<p style="color: var(--text-secondary); margin: 0;">{{ _('settings.2fa_enabled_desc') }}</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('settings.disable_2fa') }}" onsubmit="return confirm('{{ _('settings.2fa_disable_confirm') }}');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-danger">🔓 {{ _('settings.disable_2fa') }}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div style="padding: 1.5rem; background: rgba(239, 68, 68, 0.1); border: 1px solid var(--danger); border-radius: 10px; margin-bottom: 1.5rem;">
|
||||
<p style="color: var(--danger); font-weight: 600; margin-bottom: 0.5rem;">⚠️ {{ _('settings.2fa_disabled') }}</p>
|
||||
<p style="color: var(--text-secondary); margin: 0;">{{ _('settings.2fa_disabled_desc') }}</p>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('settings.setup_2fa') }}" class="btn btn-primary">🔒 {{ _('settings.enable_2fa') }}</a>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-top: 2rem; padding: 1rem; background: rgba(99, 102, 241, 0.1); border-radius: 10px;">
|
||||
<h3 style="margin-top: 0;">{{ _('settings.2fa_what_is') }}</h3>
|
||||
<p style="color: var(--text-secondary); margin: 0;">
|
||||
{{ _('settings.2fa_what_is_desc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags Tab -->
|
||||
<div id="tags" class="tab-content">
|
||||
<div class="glass-card">
|
||||
<div class="section-header">
|
||||
<h2>🏷️ {{ _('settings.manage_tags') }}</h2>
|
||||
<a href="{{ url_for('settings.create_tag') }}" class="btn btn-primary">+ {{ _('settings.create_tag_btn') }}</a>
|
||||
</div>
|
||||
|
||||
{% if tags %}
|
||||
<div class="tags-grid">
|
||||
{% for tag in tags %}
|
||||
<div class="tag-item glass-card" style="border-left: 4px solid {{ tag.color }}">
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
<form method="POST" action="{{ url_for('settings.delete_tag', tag_id=tag.id) }}" style="display: inline;" onsubmit="return confirm('{{ _('settings.delete_tag_confirm').replace('{name}', tag.name) }}');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-small btn-danger">{{ _('common.delete') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-message">{{ _('empty.no_tags_message') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import/Export Tab -->
|
||||
<div id="import-export" class="tab-content">
|
||||
<div class="glass-card" style="margin-bottom: 2rem;">
|
||||
<h2>📤 {{ _('settings.export_title') }}</h2>
|
||||
<p style="margin-bottom: 1rem;">{{ _('settings.export_desc') }}</p>
|
||||
<a href="{{ url_for('settings.export_data') }}" class="btn btn-primary">⬇️ {{ _('settings.export_btn') }}</a>
|
||||
</div>
|
||||
|
||||
<div class="glass-card">
|
||||
<h2>📥 {{ _('settings.import_title') }}</h2>
|
||||
<p style="margin-bottom: 1rem;">{{ _('settings.import_desc') }}</p>
|
||||
<form method="POST" action="{{ url_for('settings.import_data') }}" enctype="multipart/form-data" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="file">{{ _('settings.import_file_label') }}</label>
|
||||
<input type="file" id="file" name="file" accept=".csv" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">⬆️ {{ _('settings.import_btn') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Tab (Admin Only) -->
|
||||
{% if current_user.is_admin %}
|
||||
<div id="users" class="tab-content">
|
||||
<div class="glass-card">
|
||||
<div class="section-header">
|
||||
<h2>👥 {{ _('settings.users_title') }}</h2>
|
||||
<a href="{{ url_for('settings.create_user') }}" class="btn btn-primary">+ {{ _('settings.create_user_btn') }}</a>
|
||||
</div>
|
||||
|
||||
{% if users %}
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _('settings.table_username') }}</th>
|
||||
<th>{{ _('settings.table_email') }}</th>
|
||||
<th>{{ _('settings.table_role') }}</th>
|
||||
<th>{{ _('settings.table_currency') }}</th>
|
||||
<th>{{ _('settings.table_2fa') }}</th>
|
||||
<th>{{ _('settings.table_actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
<span style="color: #fbbf24;">⭐ {{ _('settings.role_admin') }}</span>
|
||||
{% else %}
|
||||
<span>{{ _('settings.role_user') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.currency }}</td>
|
||||
<td>
|
||||
{% if user.is_2fa_enabled %}
|
||||
<span style="color: var(--success);">✅ {{ _('settings.2fa_status_enabled') }}</span>
|
||||
{% else %}
|
||||
<span style="color: var(--text-secondary);">❌ {{ _('settings.2fa_status_disabled') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('settings.edit_user', user_id=user.id) }}" class="btn btn-small btn-secondary">{{ _('common.edit') }}</a>
|
||||
{% if user.id != current_user.id %}
|
||||
<form method="POST" action="{{ url_for('settings.delete_user', user_id=user.id) }}" style="display: inline;" onsubmit="return confirm('{{ _('settings.delete_user_confirm').replace('{name}', user.username) }}');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-small btn-danger">{{ _('common.delete') }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-message">{{ _('settings.no_users') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openTab(evt, tabName) {
|
||||
var i, tabcontent, tablinks;
|
||||
tabcontent = document.getElementsByClassName("tab-content");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].classList.remove("active");
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tab-btn");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].classList.remove("active");
|
||||
}
|
||||
document.getElementById(tabName).classList.add("active");
|
||||
evt.currentTarget.classList.add("active");
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
78
backup/first -fina app/app/templates/settings/setup_2fa.html
Executable file
78
backup/first -fina app/app/templates/settings/setup_2fa.html
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Setup 2FA - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>🔐 Setup Two-Factor Authentication</h1>
|
||||
|
||||
<div style="margin: 2rem 0;">
|
||||
<h3>Step 1: Scan QR Code</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
|
||||
Scan this QR code with your authenticator app (Google Authenticator, Authy, Microsoft Authenticator, etc.)
|
||||
</p>
|
||||
|
||||
{% if qr_code %}
|
||||
<div style="text-align: center; padding: 2rem; background: white; border-radius: 15px; margin: 1rem 0;">
|
||||
<img src="data:image/png;base64,{{ qr_code }}" alt="2FA QR Code" style="max-width: 300px; display: block; margin: 0 auto;">
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="padding: 2rem; background: rgba(239, 68, 68, 0.1); border: 1px solid var(--danger); border-radius: 15px; margin: 1rem 0;">
|
||||
<p style="color: var(--danger); margin: 0;">❌ QR code generation failed. Please use manual entry below.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<details style="margin-top: 1rem;">
|
||||
<summary style="cursor: pointer; color: var(--text-secondary); font-weight: 500;">Can't scan? Enter manually</summary>
|
||||
<div style="margin-top: 1rem; padding: 1.5rem; background: rgba(255, 255, 255, 0.05); border-radius: 10px;">
|
||||
<p style="margin-bottom: 0.5rem;"><strong>Secret Key:</strong></p>
|
||||
<code style="font-size: 1.2rem; color: #10b981; word-break: break-all; display: block; padding: 1rem; background: rgba(0, 0, 0, 0.2); border-radius: 8px;">{{ secret }}</code>
|
||||
<p style="margin-top: 1rem; color: var(--text-secondary); font-size: 0.9rem;">
|
||||
Enter this key manually in your authenticator app under "Enter a setup key" or "Manual entry"
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div style="margin: 2rem 0;">
|
||||
<h3>Step 2: Verify Code</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
|
||||
Enter the 6-digit code from your authenticator app to complete setup
|
||||
</p>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="token">Authentication Code *</label>
|
||||
<input type="text" id="token" name="token" required autofocus
|
||||
pattern="[0-9]{6}" maxlength="6" placeholder="000000"
|
||||
style="font-size: 2rem; text-align: center; letter-spacing: 0.8rem; font-weight: 600;">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('settings.index') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">✅ Enable 2FA</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem; padding: 1.5rem; background: rgba(99, 102, 241, 0.1); border: 1px solid var(--primary); border-radius: 10px;">
|
||||
<p style="margin: 0;">
|
||||
<strong>💡 Important:</strong> Save your secret key in a secure location. You'll need it if you lose access to your authenticator app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
details summary:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
input[type="text"]#token {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
34
backup/first -fina app/app/templates/setup_2fa.html
Executable file
34
backup/first -fina app/app/templates/setup_2fa.html
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Setup 2FA - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="glass-card auth-card">
|
||||
<h1>🔐 Setup Two-Factor Authentication</h1>
|
||||
<p class="subtitle">Scan this QR code with your authenticator app</p>
|
||||
|
||||
<div class="qr-container">
|
||||
<img src="data:image/png;base64,{{ qr_code }}" alt="QR Code" class="qr-code">
|
||||
</div>
|
||||
|
||||
<div class="secret-container">
|
||||
<p><strong>Manual Entry Key:</strong></p>
|
||||
<code class="secret-key">{{ secret }}</code>
|
||||
</div>
|
||||
|
||||
<p class="info-text">Use Google Authenticator, Authy, or any TOTP app</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.verify_2fa') }}" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="token">Enter 6-digit code</label>
|
||||
<input type="text" id="token" name="token" required pattern="[0-9]{6}" maxlength="6" autofocus>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Verify & Complete Setup</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
105
backup/first -fina app/app/templates/subscriptions/create.html
Normal file
105
backup/first -fina app/app/templates/subscriptions/create.html
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('subscription.add') }} - FINA{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>➕ {{ _('subscription.add') }}</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">{{ _('subscription.name') }}</label>
|
||||
<input type="text" id="name" name="name" required placeholder="Netflix, Spotify, etc.">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">{{ _('expense.amount') }}</label>
|
||||
<input type="number" id="amount" name="amount" step="0.01" min="0" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="frequency">{{ _('subscription.frequency') }}</label>
|
||||
<select id="frequency" name="frequency" required onchange="toggleCustomInterval()">
|
||||
<option value="weekly">{{ _('subscription.freq_weekly') }}</option>
|
||||
<option value="biweekly">{{ _('subscription.freq_biweekly') }}</option>
|
||||
<option value="monthly" selected>{{ _('subscription.freq_monthly') }}</option>
|
||||
<option value="quarterly">{{ _('subscription.freq_quarterly') }}</option>
|
||||
<option value="yearly">{{ _('subscription.freq_yearly') }}</option>
|
||||
<option value="custom">{{ _('subscription.freq_custom') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="custom-interval-group" style="display: none;">
|
||||
<label for="custom_interval_days">{{ _('subscription.custom_interval') }}</label>
|
||||
<input type="number" id="custom_interval_days" name="custom_interval_days" min="1" placeholder="e.g., 45 days">
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.custom_interval_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category_id">{{ _('category.name') }}</label>
|
||||
<select id="category_id" name="category_id" required>
|
||||
<option value="">{{ _('common.select') }}</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}">{{ category.icon }} {{ category.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start_date">{{ _('subscription.start_date') }}</label>
|
||||
<input type="date" id="start_date" name="start_date" value="{{ today }}" required>
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.start_date_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="end_date">{{ _('subscription.end_date') }} ({{ _('common.optional') }})</label>
|
||||
<input type="date" id="end_date" name="end_date">
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.end_date_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="total_occurrences">{{ _('subscription.total_occurrences') }} ({{ _('common.optional') }})</label>
|
||||
<input type="number" id="total_occurrences" name="total_occurrences" min="1" placeholder="e.g., 12">
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.total_occurrences_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" id="auto_create_expense" name="auto_create_expense" style="width: auto;">
|
||||
<span>{{ _('subscription.auto_create') }}</span>
|
||||
</label>
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.auto_create_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">{{ _('subscription.notes') }}</label>
|
||||
<textarea id="notes" name="notes" rows="3" placeholder="{{ _('subscription.notes_placeholder') }}"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('subscriptions.index') }}" class="btn btn-secondary">{{ _('common.cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary">{{ _('common.save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleCustomInterval() {
|
||||
const frequency = document.getElementById('frequency').value;
|
||||
const customGroup = document.getElementById('custom-interval-group');
|
||||
const customInput = document.getElementById('custom_interval_days');
|
||||
|
||||
if (frequency === 'custom') {
|
||||
customGroup.style.display = 'block';
|
||||
customInput.required = true;
|
||||
} else {
|
||||
customGroup.style.display = 'none';
|
||||
customInput.required = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
101
backup/first -fina app/app/templates/subscriptions/edit.html
Normal file
101
backup/first -fina app/app/templates/subscriptions/edit.html
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('common.edit') }} {{ subscription.name }} - FINA{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="glass-card form-card">
|
||||
<h1>✏️ {{ _('common.edit') }} {{ _('subscription.title') }}</h1>
|
||||
|
||||
<form method="POST" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">{{ _('subscription.name') }}</label>
|
||||
<input type="text" id="name" name="name" value="{{ subscription.name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">{{ _('expense.amount') }}</label>
|
||||
<input type="number" id="amount" name="amount" step="0.01" min="0" value="{{ subscription.amount }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="frequency">{{ _('subscription.frequency') }}</label>
|
||||
<select id="frequency" name="frequency" required onchange="toggleCustomInterval()">
|
||||
<option value="weekly" {% if subscription.frequency == 'weekly' %}selected{% endif %}>{{ _('subscription.freq_weekly') }}</option>
|
||||
<option value="biweekly" {% if subscription.frequency == 'biweekly' %}selected{% endif %}>{{ _('subscription.freq_biweekly') }}</option>
|
||||
<option value="monthly" {% if subscription.frequency == 'monthly' %}selected{% endif %}>{{ _('subscription.freq_monthly') }}</option>
|
||||
<option value="quarterly" {% if subscription.frequency == 'quarterly' %}selected{% endif %}>{{ _('subscription.freq_quarterly') }}</option>
|
||||
<option value="yearly" {% if subscription.frequency == 'yearly' %}selected{% endif %}>{{ _('subscription.freq_yearly') }}</option>
|
||||
<option value="custom" {% if subscription.frequency == 'custom' %}selected{% endif %}>{{ _('subscription.freq_custom') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="custom-interval-group" style="display: {% if subscription.frequency == 'custom' %}block{% else %}none{% endif %};">
|
||||
<label for="custom_interval_days">{{ _('subscription.custom_interval') }}</label>
|
||||
<input type="number" id="custom_interval_days" name="custom_interval_days" min="1" value="{{ subscription.custom_interval_days or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category_id">{{ _('category.name') }}</label>
|
||||
<select id="category_id" name="category_id" required>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}" {% if subscription.category_id == category.id %}selected{% endif %}>{{ category.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="formend_date">{{ _('subscription.end_date') }} ({{ _('common.optional') }})</label>
|
||||
<input type="date" id="end_date" name="end_date" value="{{ subscription.end_date.strftime('%Y-%m-%d') if subscription.end_date else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="total_occurrences">{{ _('subscription.total_occurrences') }} ({{ _('common.optional') }})</label>
|
||||
<input type="number" id="total_occurrences" name="total_occurrences" min="1" value="{{ subscription.total_occurrences or '' }}">
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.occurrences_remaining') }}: {{ (subscription.total_occurrences - subscription.occurrences_count) if subscription.total_occurrences else '∞' }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" id="auto_create_expense" name="auto_create_expense" {% if subscription.auto_create_expense %}checked{% endif %} style="width: auto;">
|
||||
<span>{{ _('subscription.auto_create') }}</span>
|
||||
</label>
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.auto_create_desc') }}</small>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleCustomInterval() {
|
||||
const frequency = document.getElementById('frequency').value;
|
||||
const customGroup = document.getElementById('custom-interval-group');
|
||||
const customInput = document.getElementById('custom_interval_days');
|
||||
|
||||
if (frequency === 'custom') {
|
||||
customGroup.style.display = 'block';
|
||||
customInput.required = true;
|
||||
} else {
|
||||
customGroup.style.display = 'none';
|
||||
customInput.required = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="-group">
|
||||
<label for="next_due_date">{{ _('subscription.next_payment') }}</label>
|
||||
<input type="date" id="next_due_date" name="next_due_date" value="{{ subscription.next_due_date.strftime('%Y-%m-%d') if subscription.next_due_date else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">{{ _('subscription.notes') }}</label>
|
||||
<textarea id="notes" name="notes" rows="3">{{ subscription.notes or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('subscriptions.index') }}" class="btn btn-secondary">{{ _('common.cancel') }}</a>
|
||||
<button type="submit" class="btn btn-primary">{{ _('common.save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
207
backup/first -fina app/app/templates/subscriptions/index.html
Normal file
207
backup/first -fina app/app/templates/subscriptions/index.html
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ _('subscription.title') }} - FINA{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="subscriptions-page">
|
||||
<div class="page-header">
|
||||
<h1>🔄 {{ _('subscription.title') }}</h1>
|
||||
<div class="header-actions">
|
||||
<form method="POST" action="{{ url_for('subscriptions.auto_create_expenses') }}" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-secondary" title="{{ _('subscription.auto_create_tooltip') }}">⚡ {{ _('subscription.create_due') }}</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('subscriptions.detect') }}" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-secondary">🔍 {{ _('subscription.detect') }}</button>
|
||||
</form>
|
||||
<a href="{{ url_for('subscriptions.create') }}" class="btn btn-primary">➕ {{ _('subscription.add') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="stats-container" style="margin-bottom: 2rem;">
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('subscription.active') }}</h3>
|
||||
<p class="stat-value">{{ subscriptions|length }}</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('subscription.monthly_cost') }}</h3>
|
||||
<p class="stat-value">{{ monthly_cost|currency }}</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-card stat-card">
|
||||
<h3>{{ _('subscription.yearly_cost') }}</h3>
|
||||
<p class="stat-value">{{ yearly_cost|currency }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Suggestions -->
|
||||
{% if suggestions %}
|
||||
<div class="glass-card suggestions-section" style="margin-bottom: 2rem;">
|
||||
<h2>💡 {{ _('subscription.suggestions') }}</h2>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1rem;">
|
||||
{{ _('subscription.suggestions_desc') }}
|
||||
</p>
|
||||
|
||||
{% for suggestion in suggestions %}
|
||||
<div class="suggestion-card glass-card" style="margin-bottom: 1rem; padding: 1rem; border-left: 3px solid #f59e0b;">
|
||||
<div class="suggestion-content">
|
||||
<div class="suggestion-header">
|
||||
<h3>{{ suggestion.suggested_name }}</h3>
|
||||
<span class="confidence-badge" style="background: rgba(245, 158, 11, 0.2); padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.85rem;">
|
||||
{{ suggestion.confidence_score|round(0)|int }}% {{ _('subscription.confidence') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="suggestion-details" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin: 1rem 0;">
|
||||
<div>
|
||||
<small style="color: var(--text-secondary);">{{ _('expense.amount') }}</small>
|
||||
<p style="font-weight: 600;">{{ suggestion.average_amount|currency }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.frequency') }}</small>
|
||||
<p style="font-weight: 600;">{{ _(('subscription.freq_' + suggestion.detected_frequency)) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.occurrences') }}</small>
|
||||
<p style="font-weight: 600;">{{ suggestion.occurrence_count }} {{ _('subscription.times') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<small style="color: var(--text-secondary);">{{ _('subscription.period') }}</small>
|
||||
<p style="font-weight: 600;">{{ suggestion.first_occurrence.strftime('%b %Y') }} - {{ suggestion.last_occurrence.strftime('%b %Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="suggestion-actions" style="display: flex; gap: 0.5rem;">
|
||||
<form method="POST" action="{{ url_for('subscriptions.accept_suggestion', pattern_id=suggestion.id) }}" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm">✅ {{ _('subscription.accept') }}</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('subscriptions.dismiss_suggestion', pattern_id=suggestion.id) }}" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-secondary btn-sm">❌ {{ _('subscription.dismiss') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Active Subscriptions -->
|
||||
{% if subscriptions %}
|
||||
<div class="glass-card">
|
||||
<h2>{{ _('subscription.active_list') }}</h2>
|
||||
|
||||
<div class="subscriptions-list">
|
||||
{% for sub in subscriptions %}
|
||||
<div class="subscription-item" style="display: flex; justify-content: space-between; align-items: center; padding: 1rem; border-bottom: 1px solid var(--glass-border);">
|
||||
<div class="subscription-info" style="flex: 1;">
|
||||
<h3 style="margin: 0;">
|
||||
{{ sub.name }}
|
||||
{% if sub.auto_create_expense %}
|
||||
<span style="background: rgba(34, 197, 94, 0.2); color: #4ade80; padding: 0.2rem 0.5rem; border-radius: 5px; font-size: 0.75rem; margin-left: 0.5rem;" title="{{ _('subscription.auto_create_tooltip') }}">⚡ {{ _('subscription.auto') }}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div style="display: flex; gap: 2rem; margin-top: 0.5rem; color: var(--text-secondary); font-size: 0.9rem; flex-wrap: wrap;">
|
||||
<span>💰 {{ sub.amount|currency }} /
|
||||
{% if sub.frequency == 'custom' %}
|
||||
{{ _('subscription.every') }} {{ sub.custom_interval_days }} {{ _('subscription.days') }}
|
||||
{% else %}
|
||||
{{ _(('subscription.freq_' + sub.frequency)) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if sub.next_due_date %}
|
||||
<span>📅 {{ _('subscription.next_payment') }}: {{ sub.next_due_date.strftime('%b %d, %Y') }}</span>
|
||||
{% endif %}
|
||||
<span>📊 {{ _('subscription.annual') }}: {{ sub.get_annual_cost()|currency }}</span>
|
||||
{% if sub.total_occurrences %}
|
||||
<span>🔢 {{ sub.occurrences_count }}/{{ sub.total_occurrences }} {{ _('subscription.times') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if sub.notes %}
|
||||
<p style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-secondary);">{{ sub.notes }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="subscription-actions" style="display: flex; gap: 0.5rem;">
|
||||
<a href="{{ url_for('subscriptions.edit', subscription_id=sub.id) }}" class="btn btn-secondary btn-sm">{{ _('common.edit') }}</a>
|
||||
<form method="POST" action="{{ url_for('subscriptions.toggle', subscription_id=sub.id) }}" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-secondary btn-sm">
|
||||
{% if sub.is_active %}⏸️{% else %}▶️{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('subscriptions.delete', subscription_id=sub.id) }}" onsubmit="return confirm('{{ _('subscription.delete_confirm') }}');" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-secondary btn-sm">🗑️</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="glass-card empty-state">
|
||||
<h2>{{ _('subscription.no_subscriptions') }}</h2>
|
||||
<p>{{ _('subscription.no_subscriptions_desc') }}</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 1rem;">
|
||||
<form method="POST" action="{{ url_for('subscriptions.detect') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary">🔍 {{ _('subscription.detect') }}</button>
|
||||
</form>
|
||||
<a href="{{ url_for('subscriptions.create') }}" class="btn btn-secondary">➕ {{ _('subscription.add_manual') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.subscriptions-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.subscription-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.subscription-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.subscription-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
25
backup/first -fina app/app/templates/verify_login.html
Executable file
25
backup/first -fina app/app/templates/verify_login.html
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Verify Login - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="glass-card auth-card">
|
||||
<h1>🔐 Two-Factor Authentication</h1>
|
||||
<p class="subtitle">Enter the code from your authenticator app</p>
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="token">6-digit code</label>
|
||||
<input type="text" id="token" name="token" required pattern="[0-9]{6}" maxlength="6" autofocus>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Verify</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-link"><a href="{{ url_for('auth.login') }}">Back to login</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
193
backup/first -fina app/app/templates/view_category.html
Executable file
193
backup/first -fina app/app/templates/view_category.html
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ category.name }} - Finance Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="category-view">
|
||||
<div class="category-header glass-card" style="border-left: 4px solid {{ category.color }}">
|
||||
<div>
|
||||
<h1>{{ category.name }}</h1>
|
||||
{% if category.description %}
|
||||
<p class="category-description">{{ category.description }}</p>
|
||||
{% endif %}
|
||||
<p class="category-total">Total: <strong>{{ total_spent|currency }}</strong></p>
|
||||
</div>
|
||||
<div class="category-actions">
|
||||
<a href="{{ url_for('main.create_expense', category_id=category.id) }}" class="btn btn-primary">+ Add Expense</a>
|
||||
<a href="{{ url_for('main.edit_category', category_id=category.id) }}" class="btn btn-secondary">Edit</a>
|
||||
<form method="POST" action="{{ url_for('main.delete_category', category_id=category.id) }}" style="display: inline;" onsubmit="return confirm('Delete category and all expenses?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if expenses %}
|
||||
<div class="glass-card">
|
||||
<h2>Expenses</h2>
|
||||
<table class="expenses-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Paid By</th>
|
||||
<th>Tags</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in expenses %}
|
||||
<tr>
|
||||
<td>{{ expense.date.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ expense.description }}</td>
|
||||
<td><strong>{{ expense.amount|currency }}</strong></td>
|
||||
<td>{{ expense.paid_by or '-' }}</td>
|
||||
<td>{{ expense.tags or '-' }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('main.edit_expense', expense_id=expense.id) }}" class="btn btn-small btn-secondary">Edit</a>
|
||||
{% if expense.file_path %}
|
||||
<button onclick="viewAttachment('{{ url_for('main.view_file', expense_id=expense.id) }}', '{{ expense.file_path }}')" class="btn btn-small btn-secondary">👁️ View</button>
|
||||
<a href="{{ url_for('main.download_file', expense_id=expense.id) }}" class="btn btn-small btn-secondary">⬇️</a>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('main.delete_expense', expense_id=expense.id) }}" style="display: inline;" onsubmit="return confirm('Delete this expense?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-small btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="glass-card empty-state">
|
||||
<h2>{{ _('empty.no_expenses_title') }}</h2>
|
||||
<p>{{ _('empty.no_expenses_message') }}</p>
|
||||
<a href="{{ url_for('main.create_expense', category_id=category.id) }}" class="btn btn-primary">+ {{ _('empty.add_expense') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Attachment Preview Modal -->
|
||||
<div id="attachmentModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeModal()">×</span>
|
||||
<div id="attachmentViewer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.category-view { max-width: 1200px; margin: 0 auto; }
|
||||
.category-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 2rem; flex-wrap: wrap; gap: 1rem; }
|
||||
.category-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.category-total { font-size: 1.2rem; margin-top: 1rem; }
|
||||
.expenses-table { width: 100%; border-collapse: collapse; margin-top: 1.5rem; }
|
||||
.expenses-table th, .expenses-table td { padding: 1rem; text-align: left; border-bottom: 1px solid var(--glass-border); }
|
||||
.expenses-table th { font-weight: 600; color: var(--text-secondary); }
|
||||
.expenses-table tr:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
margin: 2% auto;
|
||||
padding: 2rem;
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
background: rgba(59, 7, 100, 0.95);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1.5rem;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
#attachmentViewer img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#attachmentViewer iframe {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function viewAttachment(url, filePath) {
|
||||
const modal = document.getElementById('attachmentModal');
|
||||
const viewer = document.getElementById('attachmentViewer');
|
||||
|
||||
// Determine file type
|
||||
const fileExt = filePath.split('.').pop().toLowerCase();
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) {
|
||||
// Image preview
|
||||
viewer.innerHTML = '<img src="' + url + '" alt="Attachment">';
|
||||
} else if (fileExt === 'pdf') {
|
||||
// PDF viewer
|
||||
viewer.innerHTML = '<iframe src="' + url + '"></iframe>';
|
||||
} else {
|
||||
// Fallback for other files
|
||||
viewer.innerHTML = '<p style="text-align: center; padding: 2rem;">Preview not available. <a href="' + url + '" download class="btn btn-primary">Download File</a></p>';
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('attachmentModal');
|
||||
modal.style.display = 'none';
|
||||
document.getElementById('attachmentViewer').innerHTML = '';
|
||||
}
|
||||
|
||||
// Close modal on click outside
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('attachmentModal');
|
||||
if (event.target == modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal on Escape key
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue