v1.5 - Dashboard avec filtres (Mois/Personne/Catégorie)
This commit is contained in:
@@ -3,8 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>💸 Dépenses - Dashboard</title>
|
<title>📊 Dépenses Dashboard</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
<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>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<style>
|
<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; }
|
: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; }
|
.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; }
|
.card { background: var(--bg-card); border-radius: 15px; padding: 15px; margin-bottom: 15px; }
|
||||||
h2 { font-size: 1em; margin-bottom: 12px; color: var(--accent); }
|
h2 { font-size: 1em; margin-bottom: 12px; color: var(--accent); }
|
||||||
|
.filter-bar { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
|
||||||
/* Stats Grid */
|
.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; }
|
||||||
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 15px; }
|
.filter-chip:hover { background: var(--bg-card); }
|
||||||
.stat-box { background: var(--bg-input); padding: 15px; border-radius: 12px; text-align: center; }
|
.filter-chip.active { background: var(--primary); color: #fff; border-color: var(--accent); }
|
||||||
.stat-box .label { font-size: 0.75em; color: var(--text-dim); text-transform: uppercase; }
|
.chart-container { height: 250px; position: relative; }
|
||||||
.stat-box .value { font-size: 1.5em; font-weight: bold; color: var(--accent); }
|
.total { text-align: right; font-size: 1.3em; margin: 15px 0; padding: 15px; background: rgba(0,217,255,0.1); border-radius: 10px; }
|
||||||
.stat-box .value.danger { color: var(--danger); }
|
.expense-list { max-height: 400px; overflow-y: auto; }
|
||||||
.stat-box .value.success { color: var(--success); }
|
.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; }
|
||||||
/* Charts */
|
.expense-prenom { background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 3px 10px; border-radius: 15px; font-size: 0.75em; }
|
||||||
.charts-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 15px; }
|
.expense-montant { font-weight: bold; color: var(--accent); }
|
||||||
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>📊 Dashboard Dépenses</h1>
|
<h1>📊 Dépenses Dashboard</h1>
|
||||||
|
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a href="?page=saisie" id="nav-saisie">✏️ Saisie</a>
|
<a href="/?page=saisie" id="nav-saisie">✏️ Saisie</a>
|
||||||
<a href="?page=dashboard" id="nav-dashboard" class="active">📊 Dashboard</a>
|
<a href="/dashboard" id="nav-dashboard" class="active">📊 Dashboard</a>
|
||||||
<a href="?page=config" id="nav-config">⚙️ Config</a>
|
<a href="?page=config" id="nav-config">⚙️ Config</a>
|
||||||
</div>
|
</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">
|
<div class="card">
|
||||||
<h2>💰 Budget Mensuel (500€)</h2>
|
<h2>📋 Liste des dépenses</h2>
|
||||||
<div class="progress-container">
|
<div id="expense-list" class="expense-list"></div>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Charts -->
|
<div class="card">
|
||||||
<div class="charts-grid">
|
<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">
|
<div class="chart-container">
|
||||||
<h3>📈 Évolution quotidienne</h3>
|
<canvas id="mainChart"></canvas>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var allDepenses = [];
|
var depenses = [];
|
||||||
var currentMonth = new Date();
|
var currentFilter = { type: 'all', value: null };
|
||||||
|
var chart = null;
|
||||||
function formatDate(d) { if(!d) return ""; return d.split("-").reverse().join("/"); }
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
var r = await fetch('/api/depenses');
|
var r = await fetch('/api/depenses');
|
||||||
allDepenses = await r.json();
|
depenses = await r.json();
|
||||||
updateDashboard();
|
renderFilters();
|
||||||
checkPage();
|
renderExpenses();
|
||||||
|
updateChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPage() {
|
function renderFilters() {
|
||||||
var params = new URLSearchParams(window.location.search);
|
var bars = [
|
||||||
var page = params.get('page') || 'saisie';
|
{ id: 'byMonth', label: 'Mois', type: 'month' },
|
||||||
showPage(page);
|
{ id: 'byPerson', label: 'Personne', type: 'person' },
|
||||||
}
|
{ id: 'byCategory', label: 'Catégorie', type: 'category' }
|
||||||
|
];
|
||||||
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 = '';
|
var html = '';
|
||||||
topList.forEach(function(entry) {
|
for (var i = 0; i < bars.length; i++) {
|
||||||
html += '<div class="top-item">';
|
html += '<div class="filter-chip' + (currentFilter.type === bars[i].type ? ' active' : '') + '" onclick="setFilter(\'' + bars[i].type + '\')">' + bars[i].label + '</div>';
|
||||||
html += '<span class="name">' + entry[0] + '</span>';
|
}
|
||||||
html += '<span class="amount" style="color:var(--accent)">' + entry[1].toFixed(2) + '€</span>';
|
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>';
|
html += '</div>';
|
||||||
});
|
html += '<div style="flex:1; padding: 0 15px; font-size: 0.9em;">' + d.libelle + '</div>';
|
||||||
document.getElementById('top-spenders').innerHTML = html || '<p style="color:var(--text-dim)">Aucune dépense</p>';
|
html += '<div style="display:flex; align-items:center; gap:10px">';
|
||||||
|
html += '<span style="color:var(--text-dim); font-size:0.8em;">' + d.date + '</span>';
|
||||||
// Table
|
html += '<span class="expense-montant">' + parseFloat(d.montant).toFixed(2) + '€</span>';
|
||||||
var sorted = monthDepenses.slice().sort(function(a, b) { return b.date.localeCompare(a.date); });
|
html += '</div></div>';
|
||||||
html = '';
|
}
|
||||||
sorted.forEach(function(d) {
|
document.getElementById('expense-list').innerHTML = html || '<p style="color:var(--text-dim)">Aucune dépense trouvée</p>';
|
||||||
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 updateChart() {
|
||||||
|
var ctx = document.getElementById('mainChart').getContext('2d');
|
||||||
function renderDailyChart(depenses) {
|
var filtered = getFilteredData();
|
||||||
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 labels = [];
|
||||||
var data = [];
|
var data = [];
|
||||||
for (var i = 1; i <= maxDay; i++) {
|
var backgroundColors = [];
|
||||||
labels.push(i);
|
|
||||||
data.push(days[i]);
|
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 (chart) chart.destroy();
|
||||||
if (dailyChart) dailyChart.destroy();
|
chart = new Chart(ctx, {
|
||||||
dailyChart = new Chart(ctx, {
|
type: 'bar',
|
||||||
type: 'line',
|
|
||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Dépenses',
|
label: 'Montant (€)',
|
||||||
data: data,
|
data: data,
|
||||||
borderColor: '#00d9ff',
|
backgroundColor: backgroundColors,
|
||||||
backgroundColor: 'rgba(0, 217, 255, 0.1)',
|
borderColor: backgroundColors.map(c => c.replace('0.7', '1')),
|
||||||
fill: true,
|
borderWidth: 1
|
||||||
tension: 0.4
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
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: {
|
scales: {
|
||||||
x: { grid: { color: '#1a1a35' }, ticks: { color: '#888' } },
|
y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#888' } },
|
||||||
y: { grid: { color: '#1a1a35' }, 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();
|
load();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user