v1.2 - Dashboard with charts, Import/Export CSV, 17 dépenses
This commit is contained in:
358
templates/dashboard.html
Normal file
358
templates/dashboard.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user