v1.5 - Dashboard avec filtres (Mois/Personne/Catégorie)

This commit is contained in:
h3r7
2026-03-01 07:54:55 +01:00
parent 31e5a6bc29
commit 83a850d520

View File

@@ -3,8 +3,8 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💸 Dépenses - Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<title>📊 Dépenses Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root { --bg-dark: #0d0d1a; --bg-card: #16162a; --bg-input: #1a1a35; --primary: #e94560; --secondary: #7b2cbf; --accent: #00d9ff; --text: #fff; --text-dim: #888; --success: #00ff88; --danger: #ff4757; }
@@ -16,342 +16,226 @@
.nav a.active { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; }
.card { background: var(--bg-card); border-radius: 15px; padding: 15px; margin-bottom: 15px; }
h2 { font-size: 1em; margin-bottom: 12px; color: var(--accent); }
/* Stats Grid */
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 15px; }
.stat-box { background: var(--bg-input); padding: 15px; border-radius: 12px; text-align: center; }
.stat-box .label { font-size: 0.75em; color: var(--text-dim); text-transform: uppercase; }
.stat-box .value { font-size: 1.5em; font-weight: bold; color: var(--accent); }
.stat-box .value.danger { color: var(--danger); }
.stat-box .value.success { color: var(--success); }
/* Charts */
.charts-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 15px; }
.chart-container { background: var(--bg-input); padding: 15px; border-radius: 12px; }
.chart-container h3 { font-size: 0.85em; color: var(--text-dim); margin-bottom: 10px; }
/* Table */
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.85em; }
th { text-align: left; padding: 10px; background: var(--bg-input); color: var(--text-dim); font-weight: 600; }
td { padding: 10px; border-bottom: 1px solid var(--bg-input); }
tr:hover { background: rgba(255,255,255,0.02); }
.amount { font-weight: bold; color: var(--accent); }
.amount.negative { color: var(--danger); }
.status { padding: 3px 8px; border-radius: 10px; font-size: 0.75em; }
.status.pending { background: rgba(255,200,0,0.2); color: #ffc800; }
.status.sent { background: rgba(0,255,136,0.2); color: var(--success); }
/* Filters */
.filters { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px; }
.filters select, .filters input { padding: 8px 12px; background: var(--bg-input); border: 2px solid transparent; border-radius: 8px; color: var(--text); }
/* Month selector */
.month-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
.month-nav button { background: var(--bg-card); border: none; padding: 8px 15px; border-radius: 8px; color: var(--text); cursor: pointer; }
.month-nav button:hover { background: var(--bg-input); }
.month-title { font-size: 1.2em; font-weight: bold; }
/* Top spenders */
.top-list { display: flex; flex-direction: column; gap: 8px; }
.top-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: var(--bg-input); border-radius: 8px; }
.top-item .name { font-weight: 600; }
.top-item .amount { font-weight: bold; }
/* Progress bar */
.progress-container { margin-top: 10px; }
.progress-bar { height: 8px; background: var(--bg-input); border-radius: 4px; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
.progress-fill.good { background: var(--success); }
.progress-fill.warning { background: #ffc800; }
.progress-fill.danger { background: var(--danger); }
/* Actions */
.actions { display: flex; gap: 10px; flex-wrap: wrap; }
.btn { padding: 10px 20px; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; }
.btn-primary { background: linear-gradient(135deg, var(--accent), #0099cc); color: var(--bg-dark); }
.btn-export { background: linear-gradient(135deg, #9944cc, #6622aa); color: #fff; }
@media (max-width: 600px) {
.stats-grid, .charts-grid { grid-template-columns: 1fr; }
}
.filter-bar { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
.filter-chip { padding: 10px 20px; background: var(--bg-input); border-radius: 25px; font-size: 0.9em; cursor: pointer; border: 2px solid transparent; transition: all 0.2s; }
.filter-chip:hover { background: var(--bg-card); }
.filter-chip.active { background: var(--primary); color: #fff; border-color: var(--accent); }
.chart-container { height: 250px; position: relative; }
.total { text-align: right; font-size: 1.3em; margin: 15px 0; padding: 15px; background: rgba(0,217,255,0.1); border-radius: 10px; }
.expense-list { max-height: 400px; overflow-y: auto; }
.expense-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: var(--bg-input); border-radius: 8px; margin-bottom: 8px; }
.expense-cat { background: var(--secondary); padding: 3px 8px; border-radius: 5px; font-size: 0.75em; }
.expense-prenom { background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 3px 10px; border-radius: 15px; font-size: 0.75em; }
.expense-montant { font-weight: bold; color: var(--accent); }
</style>
</head>
<body>
<h1>📊 Dashboard Dépenses</h1>
<h1>📊 Dépenses Dashboard</h1>
<div class="nav">
<a href="?page=saisie" id="nav-saisie">✏️ Saisie</a>
<a href="?page=dashboard" id="nav-dashboard" class="active">📊 Dashboard</a>
<a href="/?page=saisie" id="nav-saisie">✏️ Saisie</a>
<a href="/dashboard" id="nav-dashboard" class="active">📊 Dashboard</a>
<a href="?page=config" id="nav-config">⚙️ Config</a>
</div>
<!-- Month Navigation -->
<div class="month-nav">
<button onclick="changeMonth(-1)"></button>
<span class="month-title" id="month-title">Février 2026</span>
<button onclick="changeMonth(1)"></button>
</div>
<!-- Stats Overview -->
<div class="stats-grid">
<div class="stat-box">
<div class="label">Total mois</div>
<div class="value" id="stat-total">0€</div>
</div>
<div class="stat-box">
<div class="label">Moyenne/jour</div>
<div class="value" id="stat-avg">0€</div>
</div>
<div class="stat-box">
<div class="label">Nb dépenses</div>
<div class="value" id="stat-count">0</div>
</div>
<div class="stat-box">
<div class="label">Budget restant</div>
<div class="value" id="stat-budget">0€</div>
</div>
</div>
<!-- Budget Progress -->
<div class="card">
<h2>💰 Budget Mensuel (500€)</h2>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="budget-bar" style="width: 0%"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:5px;font-size:0.8em;color:var(--text-dim)">
<span id="budget-spent">0€ dépensés</span>
<span id="budget-remaining">500€ restants</span>
</div>
</div>
<h2>📋 Liste des dépenses</h2>
<div id="expense-list" class="expense-list"></div>
</div>
<!-- Charts -->
<div class="charts-grid">
<div class="card">
<h2>💰 Total</h2>
<div id="total-display" class="total">0.00€</div>
</div>
<div class="card">
<h2>📊 Filtres</h2>
<div class="filter-bar" id="filter-bar"></div>
</div>
<div class="card">
<h2>📈 Graphique</h2>
<div class="chart-container">
<h3>📈 Évolution quotidienne</h3>
<canvas id="chart-daily"></canvas>
</div>
<div class="chart-container">
<h3>👥 Par personne</h3>
<canvas id="chart-person"></canvas>
</div>
</div>
<!-- Top Spenders -->
<div class="card">
<h2>🏆 Top Dépenseurs</h2>
<div class="top-list" id="top-spenders"></div>
</div>
<!-- Recent Expenses -->
<div class="card">
<h2>💳 Dernières dépenses</h2>
<div class="table-container">
<table id="expenses-table">
<thead>
<tr>
<th>Date</th>
<th>Personne</th>
<th>Libellé</th>
<th>Montant</th>
<th>Status</th>
</tr>
</thead>
<tbody id="expenses-tbody"></tbody>
</table>
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="actions">
<button class="btn btn-export" onclick="exportCSV()">📥 Exporter CSV</button>
<canvas id="mainChart"></canvas>
</div>
</div>
<script>
var allDepenses = [];
var currentMonth = new Date();
function formatDate(d) { if(!d) return ""; return d.split("-").reverse().join("/"); }
var depenses = [];
var currentFilter = { type: 'all', value: null };
var chart = null;
async function load() {
var r = await fetch('/api/depenses');
allDepenses = await r.json();
updateDashboard();
checkPage();
depenses = await r.json();
renderFilters();
renderExpenses();
updateChart();
}
function checkPage() {
var params = new URLSearchParams(window.location.search);
var page = params.get('page') || 'saisie';
showPage(page);
}
function showPage(page) {
document.getElementById('nav-saisie').className = page === 'saisie' ? 'nav a' : 'nav a';
document.getElementById('nav-dashboard').className = page === 'dashboard' ? 'nav a active' : 'nav a';
document.getElementById('nav-config').className = page === 'config' ? 'nav a active' : 'nav a';
}
function changeMonth(delta) {
currentMonth.setMonth(currentMonth.getMonth() + delta);
updateDashboard();
}
function getMonthKey(date) {
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0');
}
function filterByMonth(depenses, date) {
var monthKey = getMonthKey(date);
return depenses.filter(function(d) {
return d.date && d.date.startsWith(monthKey);
});
}
function updateDashboard() {
var monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
document.getElementById('month-title').textContent = monthNames[currentMonth.getMonth()] + ' ' + currentMonth.getFullYear();
var monthDepenses = filterByMonth(allDepenses, currentMonth);
// Stats
var total = monthDepenses.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
var count = monthDepenses.length;
var daysInMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate();
var avg = count > 0 ? total / daysInMonth : 0;
var budget = 500;
var remaining = budget - total;
document.getElementById('stat-total').textContent = total.toFixed(2) + '€';
document.getElementById('stat-total').className = 'value ' + (total > budget ? 'danger' : '');
document.getElementById('stat-avg').textContent = avg.toFixed(2) + '€';
document.getElementById('stat-count').textContent = count;
document.getElementById('stat-budget').textContent = remaining.toFixed(2) + '€';
document.getElementById('stat-budget').className = 'value ' + (remaining < 0 ? 'danger' : 'success');
// Budget bar
var percent = Math.min((total / budget) * 100, 100);
var bar = document.getElementById('budget-bar');
bar.style.width = percent + '%';
bar.className = 'progress-fill ' + (percent > 90 ? 'danger' : percent > 70 ? 'warning' : 'good');
document.getElementById('budget-spent').textContent = total.toFixed(2) + '€ dépensés';
document.getElementById('budget-remaining').textContent = Math.max(0, remaining).toFixed(2) + '€ restants';
// Charts
renderDailyChart(monthDepenses);
renderPersonChart(monthDepenses);
// Top spenders
var byPerson = {};
monthDepenses.forEach(function(d) {
byPerson[d.prenom] = (byPerson[d.prenom] || 0) + (parseFloat(d.montant) || 0);
});
var topList = Object.entries(byPerson).sort(function(a, b) { return b[1] - a[1]; });
function renderFilters() {
var bars = [
{ id: 'byMonth', label: 'Mois', type: 'month' },
{ id: 'byPerson', label: 'Personne', type: 'person' },
{ id: 'byCategory', label: 'Catégorie', type: 'category' }
];
var html = '';
topList.forEach(function(entry) {
html += '<div class="top-item">';
html += '<span class="name">' + entry[0] + '</span>';
html += '<span class="amount" style="color:var(--accent)">' + entry[1].toFixed(2) + '€</span>';
for (var i = 0; i < bars.length; i++) {
html += '<div class="filter-chip' + (currentFilter.type === bars[i].type ? ' active' : '') + '" onclick="setFilter(\'' + bars[i].type + '\')">' + bars[i].label + '</div>';
}
document.getElementById('filter-bar').innerHTML = html;
}
function setFilter(type) {
currentFilter.type = type;
renderFilters();
var values = [];
if (type === 'month') {
values = [...new Set(depenses.map(d => d.date.split('-')[0] + '-' + d.date.split('-')[1]))].sort().reverse();
} else if (type === 'person') {
values = [...new Set(depenses.map(d => d.prenom))].sort();
} else if (type === 'category') {
values = [...new Set(depenses.map(d => d.category || 'Autre'))].sort();
}
currentFilter.value = values[0] || null;
renderValues();
renderExpenses();
updateChart();
}
function renderValues() {
var html = '';
var values = [];
if (currentFilter.type === 'month') {
values = [...new Set(depenses.map(d => d.date.split('-')[0] + '-' + d.date.split('-')[1]))].sort().reverse();
} else if (currentFilter.type === 'person') {
values = [...new Set(depenses.map(d => d.prenom))].sort();
} else if (currentFilter.type === 'category') {
values = [...new Set(depenses.map(d => d.category || 'Autre'))].sort();
}
html += '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="setFilter(\'' + currentFilter.type + '\')">Tous</div>';
for (var i = 0; i < values.length; i++) {
html += '<div class="filter-chip' + (currentFilter.value === values[i] ? ' active' : '') + '" onclick="currentFilter.value = \'' + values[i] + '\'; renderValues(); renderExpenses(); updateChart();">' + values[i] + '</div>';
}
document.getElementById('filter-bar').innerHTML = html;
}
function getFilteredData() {
if (!currentFilter.value) return depenses;
if (currentFilter.type === 'month') {
return depenses.filter(d => d.date && d.date.startsWith(currentFilter.value));
} else if (currentFilter.type === 'person') {
return depenses.filter(d => d.prenom === currentFilter.value);
} else if (currentFilter.type === 'category') {
return depenses.filter(d => (d.category || 'Autre') === currentFilter.value);
}
return depenses;
}
function renderExpenses() {
var filtered = getFilteredData();
var total = filtered.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
document.getElementById('total-display').textContent = total.toFixed(2) + '€';
var html = '';
for (var i = 0; i < Math.min(filtered.length, 50); i++) {
var d = filtered[i];
html += '<div class="expense-item">';
html += '<div>';
html += '<span class="expense-prenom">' + (d.prenom || '??') + '</span> ';
html += '<span class="expense-cat">' + (d.category || 'Autre') + '</span>';
html += '</div>';
});
document.getElementById('top-spenders').innerHTML = html || '<p style="color:var(--text-dim)">Aucune dépense</p>';
// Table
var sorted = monthDepenses.slice().sort(function(a, b) { return b.date.localeCompare(a.date); });
html = '';
sorted.forEach(function(d) {
html += '<tr>';
html += '<td>' + formatDate(d.date) + '</td>';
html += '<td>' + d.prenom + '</td>';
html += '<td>' + d.libelle + '</td>';
html += '<td class="amount">' + parseFloat(d.montant).toFixed(2) + '€</td>';
var statusClass = d.status === 'Envoyé ✅' ? 'sent' : 'pending';
html += '<td><span class="status ' + statusClass + '">' + (d.status || 'En attente') + '</span></td>';
html += '</tr>';
});
document.getElementById('expenses-tbody').innerHTML = html || '<tr><td colspan="5" style="text-align:center;color:var(--text-dim)">Aucune dépense ce mois</td></tr>';
html += '<div style="flex:1; padding: 0 15px; font-size: 0.9em;">' + d.libelle + '</div>';
html += '<div style="display:flex; align-items:center; gap:10px">';
html += '<span style="color:var(--text-dim); font-size:0.8em;">' + d.date + '</span>';
html += '<span class="expense-montant">' + parseFloat(d.montant).toFixed(2) + '€</span>';
html += '</div></div>';
}
document.getElementById('expense-list').innerHTML = html || '<p style="color:var(--text-dim)">Aucune dépense trouvée</p>';
}
var dailyChart, personChart;
function renderDailyChart(depenses) {
var days = {};
var maxDay = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate();
for (var i = 1; i <= maxDay; i++) { days[i] = 0; }
depenses.forEach(function(d) {
if (d.date) {
var day = parseInt(d.date.split('-')[2]);
days[day] = (days[day] || 0) + (parseFloat(d.montant) || 0);
}
});
function updateChart() {
var ctx = document.getElementById('mainChart').getContext('2d');
var filtered = getFilteredData();
var labels = [];
var data = [];
for (var i = 1; i <= maxDay; i++) {
labels.push(i);
data.push(days[i]);
var backgroundColors = [];
if (currentFilter.type === 'month') {
var months = {};
for (var i = 0; i < filtered.length; i++) {
var d = filtered[i];
var m = d.date.split('-')[0] + '-' + d.date.split('-')[1];
if (!months[m]) months[m] = 0;
months[m] += parseFloat(d.montant) || 0;
}
Object.keys(months).sort().reverse().forEach(function(m) {
labels.push(m);
data.push(months[m]);
backgroundColors.push('rgba(0, 217, 255, 0.7)');
});
} else if (currentFilter.type === 'person') {
var persons = {};
for (var i = 0; i < filtered.length; i++) {
var d = filtered[i];
var p = d.prenom || 'Inconnu';
if (!persons[p]) persons[p] = 0;
persons[p] += parseFloat(d.montant) || 0;
}
Object.keys(persons).sort().forEach(function(p) {
labels.push(p);
data.push(persons[p]);
backgroundColors.push('rgba(233, 69, 96, 0.7)');
});
} else if (currentFilter.type === 'category') {
var categories = {};
for (var i = 0; i < filtered.length; i++) {
var d = filtered[i];
var c = d.category || 'Autre';
if (!categories[c]) categories[c] = 0;
categories[c] += parseFloat(d.montant) || 0;
}
Object.keys(categories).sort().forEach(function(c) {
labels.push(c);
data.push(categories[c]);
backgroundColors.push('rgba(123, 44, 191, 0.7)');
});
}
var ctx = document.getElementById('chart-daily').getContext('2d');
if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(ctx, {
type: 'line',
if (chart) chart.destroy();
chart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Dépenses',
label: 'Montant (€)',
data: data,
borderColor: '#00d9ff',
backgroundColor: 'rgba(0, 217, 255, 0.1)',
fill: true,
tension: 0.4
backgroundColor: backgroundColors,
borderColor: backgroundColors.map(c => c.replace('0.7', '1')),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: {
display: true,
text: currentFilter.type === 'month' ? 'Dépenses par mois' :
currentFilter.type === 'person' ? 'Dépenses par personne' : 'Dépenses par catégorie'
}
},
scales: {
x: { grid: { color: '#1a1a35' }, ticks: { color: '#888' } },
y: { grid: { color: '#1a1a35' }, ticks: { color: '#888' } }
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#888' } },
x: { grid: { display: false }, ticks: { color: '#888' } }
}
}
});
}
function renderPersonChart(depenses) {
var byPerson = {};
depenses.forEach(function(d) {
byPerson[d.prenom] = (byPerson[d.prenom] || 0) + (parseFloat(d.montant) || 0);
});
var ctx = document.getElementById('chart-person').getContext('2d');
if (personChart) personChart.destroy();
personChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: Object.keys(byPerson),
datasets: [{
data: Object.values(byPerson),
backgroundColor: ['#e94560', '#7b2cbf', '#00d9ff', '#00ff88', '#ffc800']
}]
},
options: {
responsive: true,
plugins: { legend: { position: 'bottom', labels: { color: '#fff' } } }
}
});
}
function exportCSV() {
window.location.href = '/api/export';
}
load();
</script>
</body>