v1.2 - Dashboard with charts, Import/Export CSV, 17 dépenses

This commit is contained in:
h3r7
2026-02-28 08:53:48 +01:00
parent 64c7a976f7
commit b40bf56c97
4 changed files with 368 additions and 2 deletions

358
templates/dashboard.html Normal file
View File

@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="fr">
<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">
<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; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Outfit', sans-serif; background: var(--bg-dark); color: var(--text); padding: 15px; min-height: 100vh; }
h1 { text-align: center; font-size: 1.6em; background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.nav { display: flex; gap: 10px; margin: 15px 0; }
.nav a { flex: 1; padding: 12px; background: var(--bg-card); border-radius: 10px; text-align: center; color: var(--text-dim); text-decoration: none; }
.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; }
}
</style>
</head>
<body>
<h1>📊 Dashboard Dépenses</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=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>
</div>
<!-- Charts -->
<div class="charts-grid">
<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>
</div>
</div>
<script>
var allDepenses = [];
var currentMonth = new Date();
function formatDate(d) { if(!d) return ""; return d.split("-").reverse().join("/"); }
async function load() {
var r = await fetch('/api/depenses');
allDepenses = await r.json();
updateDashboard();
checkPage();
}
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]; });
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>';
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>';
}
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);
}
});
var labels = [];
var data = [];
for (var i = 1; i <= maxDay; i++) {
labels.push(i);
data.push(days[i]);
}
var ctx = document.getElementById('chart-daily').getContext('2d');
if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Dépenses',
data: data,
borderColor: '#00d9ff',
backgroundColor: 'rgba(0, 217, 255, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: '#1a1a35' }, ticks: { color: '#888' } },
y: { grid: { color: '#1a1a35' }, 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>
</html>

View File

@@ -48,6 +48,7 @@
<div class="nav">
<a href="?page=saisie" id="nav-saisie">✏️ Saisie</a>
<a href="/dashboard" id="nav-dashboard">📊 Dashboard</a>
<a href="?page=config" id="nav-config">⚙️ Config</a>
</div>