320 lines
11 KiB
HTML
Executable file
320 lines
11 KiB
HTML
Executable file
{% 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 %}
|