Compare commits

...

2 Commits

Author SHA1 Message Date
h3r7
a3d1a33f13 Merge feature/HRT-143-depenses-redesign -> master
Refonte glassmorphism /depenses/ validee CTO HRT-143
- index.html 1444 lignes : 3-col layout, 4 KPI cards, donut chart, modales
- dashboard.html 731 lignes : 6 glass cards, multi-chart, sparkline
- Palette #020617/#00d9ff/#a855f7/#00ff88/#f43f5e conforme specs
- 0 regression API, app.py inchange, backups presents

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-11 16:07:59 +02:00
Paperclip
7f0688d00a feat(HRT-143): Refonte design /depenses/ glassmorphism + modern UI
- Refonte complète index.html (1444 lignes) :
  layout 3 colonnes desktop / mobile responsive
  Header sticky glassmorphism avec nav tabs
  4 KPI cards animées (total, budget restant, nb, max)
  Donut chart répartition catégories en sidebar droite
  Top 5 dépenses avec progress bars
  Formulaire avec labels uppercase + inputs focus glow
  Liste dépenses avec badges colorés par catégorie/statut
  Modales overlay backdrop-blur + animation spring
  Budget bar gradient animée (matrix → rose selon %)

- Refonte complète dashboard.html (731 lignes) :
  Grid 3 colonnes avec cards glassmorphism
  4 KPI cards (total, count, moyenne, catégories)
  Chart principal multi-mode (bar/line/pie)
  Donut répartition + table catégories avec %
  Résumé par personne avec progress bars
  Top 7 dépenses avec progress bars
  Sparkline tendance 6 derniers mois

- Stack : HTML/CSS/JS vanilla + Tailwind CDN + JetBrains Mono
- Palette : --bg-primary #020617, --cyan #00d9ff, --violet #a855f7
- 0 modification app.py, toutes APIs inchangées

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-11 16:01:58 +02:00
2 changed files with 2134 additions and 620 deletions

View File

@@ -1,154 +1,731 @@
<!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@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; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Outfit', sans-serif; background: var(--bg-dark); color: var(--text); padding: 15px; }
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); }
.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; }
.filter-chip.active { background: var(--primary); color: #fff; }
.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; }
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📊 Dashboard Dépenses — H3R7</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--bg-primary: #020617;
--bg-card: rgba(15, 23, 42, 0.8);
--bg-input: rgba(30, 41, 59, 0.6);
--cyan: #00d9ff;
--violet: #a855f7;
--matrix: #00ff88;
--rose: #f43f5e;
--amber: #f59e0b;
--text: #f1f5f9;
--text-dim: #64748b;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg-primary);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* Grid background */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(0, 217, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 217, 255, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
top: -200px;
left: 50%;
transform: translateX(-50%);
width: 800px;
height: 400px;
background: radial-gradient(ellipse, rgba(168,85,247,0.06) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
/* Glass card */
.glass-card {
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(0, 217, 255, 0.1);
border-radius: 16px;
transition: border-color 0.3s, box-shadow 0.3s;
}
.glass-card:hover {
border-color: rgba(0, 217, 255, 0.2);
}
/* Header */
header {
position: sticky;
top: 0;
z-index: 50;
background: rgba(2, 6, 23, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(168, 85, 247, 0.2);
padding: 12px 20px;
}
.logo-text {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 1.1rem;
background: linear-gradient(135deg, var(--violet), var(--cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Nav tabs */
.nav-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-dim);
text-decoration: none;
transition: all 0.2s;
border: 1px solid transparent;
}
.nav-tab:hover { color: var(--text); background: rgba(168,85,247,0.08); border-color: rgba(168,85,247,0.15); }
.nav-tab.active { color: var(--violet); background: rgba(168,85,247,0.12); border-color: rgba(168,85,247,0.3); }
/* KPI cards */
.kpi-card {
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(0, 217, 255, 0.1);
border-radius: 14px;
padding: 18px 20px;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.kpi-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--kpi-color, var(--cyan)), transparent);
}
.kpi-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
border-color: rgba(0, 217, 255, 0.2);
}
.kpi-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.6rem;
font-weight: 700;
}
/* Filter chips */
.filter-chip {
padding: 7px 16px;
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(100, 116, 139, 0.2);
border-radius: 20px;
font-size: 0.82rem;
cursor: pointer;
transition: all 0.2s;
color: var(--text-dim);
white-space: nowrap;
}
.filter-chip:hover { border-color: rgba(168,85,247,0.3); color: var(--text); }
.filter-chip.active { background: rgba(168,85,247,0.15); border-color: rgba(168,85,247,0.4); color: var(--violet); }
.filter-chip.mode-active { background: rgba(0,217,255,0.12); border-color: rgba(0,217,255,0.35); color: var(--cyan); }
/* Section title */
.section-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-dim);
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title::after {
content: '';
flex: 1;
height: 1px;
background: rgba(100, 116, 139, 0.2);
}
/* Summary tables */
.summary-table { width: 100%; border-collapse: collapse; }
.summary-table th {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
padding: 6px 0;
text-align: left;
border-bottom: 1px solid rgba(100,116,139,0.15);
}
.summary-table td {
padding: 8px 0;
font-size: 0.85rem;
border-bottom: 1px solid rgba(100,116,139,0.07);
}
.summary-table tr:last-child td { border-bottom: none; }
/* Progress bar */
.prog-bar {
height: 5px;
background: rgba(100,116,139,0.2);
border-radius: 3px;
overflow: hidden;
margin-top: 4px;
}
.prog-fill {
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, var(--violet), var(--cyan));
}
/* Home button */
.home-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
background: rgba(168, 85, 247, 0.1);
border: 1px solid rgba(168, 85, 247, 0.25);
border-radius: 8px;
color: var(--violet);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s;
}
.home-btn:hover { background: rgba(168, 85, 247, 0.18); box-shadow: 0 2px 12px rgba(168,85,247,0.2); }
/* Scrollbar */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(100,116,139,0.3); border-radius: 3px; }
/* Grid layout */
.dash-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
padding: 16px;
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
}
.span-2 { grid-column: span 2; }
.span-3 { grid-column: span 3; }
@media (max-width: 1024px) {
.dash-grid { grid-template-columns: 1fr 1fr; }
.span-3 { grid-column: span 2; }
}
@media (max-width: 640px) {
.dash-grid { grid-template-columns: 1fr; }
.span-2, .span-3 { grid-column: span 1; }
}
/* Animation */
@keyframes slideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.glass-card { animation: slideIn 0.3s ease-out; }
/* Total badge */
.total-badge {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 1.8rem;
background: linear-gradient(135deg, var(--rose), var(--violet));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>
</head>
<body>
<h1>📊 Dépenses Dashboard</h1>
<div class="nav">
<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>
<!-- HEADER -->
<header>
<div style="display:flex;align-items:center;justify-content:space-between;max-width:1400px;margin:0 auto">
<div style="display:flex;align-items:center;gap:16px">
<a href="https://portal-kolifee.duckdns.org/" class="home-btn">🏠 Portail</a>
<div class="logo-text">H3R7 / Dashboard Dépenses</div>
</div>
<div class="card">
<h2>💰 Total: <span id="total-display">0.00€</span></h2>
<nav style="display:flex;gap:6px">
<a href="/depenses/?page=saisie" class="nav-tab">✏️ Saisie</a>
<a href="/depenses/dashboard" class="nav-tab active" id="nav-dashboard">📊 Dashboard</a>
<a href="/depenses/?page=config" class="nav-tab">⚙️ Config</a>
</nav>
<div style="font-family:'JetBrains Mono',monospace;font-size:0.78rem;color:var(--text-dim);text-align:right">
<div style="color:var(--violet)" id="header-period"></div>
<div id="header-total-small" style="color:var(--rose);font-weight:600">0.00€</div>
</div>
</div>
</header>
<div class="card">
<h2>📊 Filtres</h2>
<div class="filter-bar" id="filter-bar">
<div class="filter-chip active" onclick="setFilter('month')">Mois</div>
<div class="filter-chip" onclick="setFilter('person')">Personne</div>
<div class="filter-chip" onclick="setFilter('category')">Catégorie</div>
</div>
<!-- KPI BAR -->
<div style="padding:12px 16px;position:relative;z-index:1">
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;max-width:1400px;margin:0 auto">
<div class="kpi-card" style="--kpi-color:var(--rose)">
<div style="font-size:0.72rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-dim);margin-bottom:6px">Total période</div>
<div class="kpi-value" id="kpi-total" style="color:var(--rose)">0.00€</div>
</div>
<div class="card">
<h2>📈 Graphiques</h2>
<div class="filter-bar" id="chart-filters">
<div class="filter-chip active" onclick="setChartMode('bar')">Bar chart</div>
<div class="filter-chip" onclick="setChartMode('pie')">Camembert</div>
</div>
<div class="chart-container">
<canvas id="mainChart"></canvas>
</div>
<div class="kpi-card" style="--kpi-color:var(--cyan)">
<div style="font-size:0.72rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-dim);margin-bottom:6px">Nb dépenses</div>
<div class="kpi-value" id="kpi-count" style="color:var(--cyan)">0</div>
</div>
<div class="kpi-card" style="--kpi-color:var(--violet)">
<div style="font-size:0.72rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-dim);margin-bottom:6px">Moyenne</div>
<div class="kpi-value" id="kpi-avg" style="color:var(--violet)">0.00€</div>
</div>
<div class="kpi-card" style="--kpi-color:var(--amber)">
<div style="font-size:0.72rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-dim);margin-bottom:6px">Catégories actives</div>
<div class="kpi-value" id="kpi-cats" style="color:var(--amber)">0</div>
</div>
</div>
</div>
<script>
var depenses = [];
var currentFilter = { type: 'month', value: null };
var chartMode = 'bar';
var chart = null;
<!-- MAIN DASHBOARD GRID -->
<div class="dash-grid">
function getFilteredData() {
if (!currentFilter.value) return depenses;
if (currentFilter.type === 'month') return depenses.filter(d => d.date && d.date.startsWith(currentFilter.value));
if (currentFilter.type === 'person') return depenses.filter(d => d.prenom === currentFilter.value);
if (currentFilter.type === 'category') return depenses.filter(d => (d.category || 'Autre') === currentFilter.value);
return depenses;
}
<!-- Filter controls — full width -->
<div class="glass-card span-3" style="padding:14px 18px">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
<div style="font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-dim);white-space:nowrap">Vue par</div>
<div style="display:flex;gap:8px">
<div class="filter-chip active" id="fb-month" onclick="setFilter('month')">📅 Mois</div>
<div class="filter-chip" id="fb-person" onclick="setFilter('person')">👤 Personne</div>
<div class="filter-chip" id="fb-category" onclick="setFilter('category')">📂 Catégorie</div>
</div>
<div style="width:1px;height:20px;background:rgba(100,116,139,0.2)"></div>
<div style="font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-dim);white-space:nowrap">Valeur</div>
<div style="display:flex;gap:8px;overflow-x:auto;flex:1" id="filter-bar"></div>
</div>
</div>
function setFilter(type) {
currentFilter.type = type;
currentFilter.value = null;
setChartMode('bar');
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;
renderFilters();
updateChart();
}
<!-- Main chart — 2 columns -->
<div class="glass-card span-2" style="padding:18px">
<div class="section-title">
📈 Évolution
<div style="margin-left:auto;display:flex;gap:6px">
<div class="filter-chip mode-active" id="cm-bar" onclick="setChartMode('bar')" style="padding:4px 12px;font-size:0.75rem">Bar</div>
<div class="filter-chip" id="cm-line" onclick="setChartMode('line')" style="padding:4px 12px;font-size:0.75rem">Line</div>
<div class="filter-chip" id="cm-pie" onclick="setChartMode('pie')" style="padding:4px 12px;font-size:0.75rem">Pie</div>
</div>
</div>
<div style="height:280px;position:relative">
<canvas id="mainChart"></canvas>
</div>
</div>
function renderFilters() {
var html = '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="setFilter(\'' + currentFilter.type + '\')">Tous</div>';
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();
for (var i = 0; i < values.length; i++) {
html += '<div class="filter-chip' + (currentFilter.value === values[i] ? ' active' : '') + '" onclick="currentFilter.value = \'' + values[i] + '\'; renderFilters(); updateChart();">' + values[i] + '</div>';
<!-- Donut chart — 1 column -->
<div class="glass-card" style="padding:18px">
<div class="section-title">🍩 Répartition catégories</div>
<div style="height:180px;position:relative">
<canvas id="categoryChart"></canvas>
</div>
<div id="category-summary" style="margin-top:12px"></div>
</div>
<!-- Per person — 1 column -->
<div class="glass-card" style="padding:18px">
<div class="section-title">👤 Par personne</div>
<div id="person-summary"></div>
</div>
<!-- Top expenses — 1 column -->
<div class="glass-card" style="padding:18px">
<div class="section-title">🏆 Top dépenses</div>
<div id="top-expenses"></div>
</div>
<!-- Monthly trend — 1 column -->
<div class="glass-card" style="padding:18px">
<div class="section-title">📆 Tendance mensuelle</div>
<div style="height:160px;position:relative">
<canvas id="trendChart"></canvas>
</div>
</div>
</div>
<script>
var depenses = [];
var currentFilter = { type: 'month', value: null };
var chartMode = 'bar';
var mainChart = null;
var categoryChart = null;
var trendChart = null;
Chart.defaults.color = '#64748b';
Chart.defaults.borderColor = 'rgba(100, 116, 139, 0.08)';
var COLORS = ['#00d9ff','#a855f7','#00ff88','#f43f5e','#f59e0b','#38bdf8','#fb7185','#34d399','#fbbf24','#60a5fa'];
function getFilteredData() {
if (!currentFilter.value) return depenses;
if (currentFilter.type === 'month') return depenses.filter(function(d) { return d.date && d.date.startsWith(currentFilter.value); });
if (currentFilter.type === 'person') return depenses.filter(function(d) { return d.prenom === currentFilter.value; });
if (currentFilter.type === 'category') return depenses.filter(function(d) { return (d.category || 'Autre') === currentFilter.value; });
return depenses;
}
function setFilter(type) {
currentFilter.type = type;
currentFilter.value = null;
// Update filter type buttons
['month','person','category'].forEach(function(t) {
var el = document.getElementById('fb-'+t);
if (el) el.className = 'filter-chip' + (t === type ? ' active' : '');
});
var values = [];
if (type === 'month') values = [...new Set(depenses.map(function(d) { return d.date.split('-')[0]+'-'+d.date.split('-')[1]; }))].sort().reverse();
else if (type === 'person') values = [...new Set(depenses.map(function(d) { return d.prenom; }))].sort();
else if (type === 'category') values = [...new Set(depenses.map(function(d) { return d.category || 'Autre'; }))].sort();
currentFilter.value = values[0] || null;
renderFilters(values);
updateAll();
}
function renderFilters(values) {
if (!values) {
values = [];
if (currentFilter.type === 'month') values = [...new Set(depenses.map(function(d) { return d.date.split('-')[0]+'-'+d.date.split('-')[1]; }))].sort().reverse();
else if (currentFilter.type === 'person') values = [...new Set(depenses.map(function(d) { return d.prenom; }))].sort();
else if (currentFilter.type === 'category') values = [...new Set(depenses.map(function(d) { return d.category || 'Autre'; }))].sort();
}
var html = '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="selectValue(null)">Tous</div>';
for (var i=0; i<values.length; i++) {
html += '<div class="filter-chip' + (currentFilter.value === values[i] ? ' active' : '') + '" onclick="selectValue(\'' + values[i] + '\')">' + values[i] + '</div>';
}
document.getElementById('filter-bar').innerHTML = html;
}
function selectValue(val) {
currentFilter.value = val;
renderFilters();
updateAll();
}
function setChartMode(mode) {
chartMode = mode;
['bar','line','pie'].forEach(function(m) {
var el = document.getElementById('cm-'+m);
if (el) el.className = 'filter-chip' + (m === mode ? ' mode-active' : '');
});
updateMainChart();
}
function updateAll() {
updateKPIs();
updateMainChart();
updateCategoryChart();
updatePersonSummary();
updateTopExpenses();
updateTrendChart();
}
function updateKPIs() {
var filtered = getFilteredData();
var total = filtered.reduce(function(s,d) { return s+(parseFloat(d.montant)||0); }, 0);
var cats = new Set(filtered.map(function(d) { return d.category || 'Autre'; }));
var avg = filtered.length > 0 ? total / filtered.length : 0;
document.getElementById('kpi-total').textContent = total.toFixed(2) + '€';
document.getElementById('kpi-count').textContent = filtered.length;
document.getElementById('kpi-avg').textContent = avg.toFixed(2) + '€';
document.getElementById('kpi-cats').textContent = cats.size;
document.getElementById('header-total-small').textContent = total.toFixed(2) + '€';
var period = currentFilter.value || 'Toutes périodes';
document.getElementById('header-period').textContent = period;
}
function updateMainChart() {
var ctx = document.getElementById('mainChart').getContext('2d');
var filtered = getFilteredData();
var labels = [], data = [], bgColors = [];
if (currentFilter.type === 'month') {
var months = {};
filtered.forEach(function(d) {
var m = d.date.split('-')[0]+'-'+d.date.split('-')[1];
months[m] = (months[m]||0) + (parseFloat(d.montant)||0);
});
Object.keys(months).sort().reverse().forEach(function(m, i) {
labels.push(m); data.push(months[m]); bgColors.push(COLORS[i % COLORS.length]);
});
} else if (currentFilter.type === 'person') {
var persons = {};
filtered.forEach(function(d) {
var p = d.prenom||'Inconnu';
persons[p] = (persons[p]||0) + (parseFloat(d.montant)||0);
});
Object.keys(persons).sort().forEach(function(p, i) {
labels.push(p); data.push(persons[p]); bgColors.push(COLORS[i % COLORS.length]);
});
} else if (currentFilter.type === 'category') {
var categories = {};
filtered.forEach(function(d) {
var c = d.category||'Autre';
categories[c] = (categories[c]||0) + (parseFloat(d.montant)||0);
});
Object.keys(categories).sort().forEach(function(c, i) {
labels.push(c); data.push(categories[c]); bgColors.push(COLORS[i % COLORS.length]);
});
}
if (mainChart) mainChart.destroy();
var type = chartMode === 'line' ? 'line' : (chartMode === 'pie' ? 'doughnut' : 'bar');
mainChart = new Chart(ctx, {
type: type,
data: {
labels: labels,
datasets: [{
label: 'Montant (€)',
data: data,
backgroundColor: type === 'bar' ? bgColors.map(function(c) { return c + '99'; }) : bgColors,
borderColor: bgColors,
borderWidth: type === 'bar' ? 0 : 2,
borderRadius: type === 'bar' ? 6 : 0,
fill: type === 'line' ? { target: 'origin', above: 'rgba(0,217,255,0.08)' } : undefined,
tension: 0.4,
pointBackgroundColor: bgColors,
pointRadius: type === 'line' ? 4 : 0,
cutout: type === 'doughnut' ? '65%' : undefined,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: type === 'doughnut',
position: 'right',
labels: { color: '#94a3b8', font: { family: 'JetBrains Mono', size: 10 }, boxWidth: 10, padding: 8 }
},
tooltip: {
backgroundColor: 'rgba(15,23,42,0.95)',
borderColor: 'rgba(0,217,255,0.2)',
borderWidth: 1,
titleColor: '#94a3b8',
bodyColor: '#f1f5f9',
callbacks: {
label: function(ctx) { return ' ' + (parseFloat(ctx.raw)||0).toFixed(2) + ' €'; }
}
document.getElementById('filter-bar').innerHTML = html;
}
}
},
scales: type !== 'doughnut' ? {
y: {
beginAtZero: true,
grid: { color: 'rgba(100,116,139,0.08)' },
ticks: { color: '#64748b', font: { family: 'JetBrains Mono', size: 10 }, callback: function(v) { return v + '€'; } }
},
x: {
grid: { display: false },
ticks: { color: '#64748b', font: { size: 11 } }
}
} : {}
}
});
}
function setChartMode(mode) {
chartMode = mode;
document.getElementById('chart-filters').innerHTML = '<div class="filter-chip' + (mode === 'bar' ? ' active' : '') + '" onclick="setChartMode(\'bar\')">Bar chart</div><div class="filter-chip' + (mode === 'pie' ? ' active' : '') + '" onclick="setChartMode(\'pie\')">Camembert</div>';
updateChart();
}
function updateCategoryChart() {
var filtered = getFilteredData();
var catData = {};
filtered.forEach(function(d) {
var c = d.category || 'Autre';
catData[c] = (catData[c]||0) + (parseFloat(d.montant)||0);
});
var catLabels = Object.keys(catData).sort();
var catValues = catLabels.map(function(c) { return catData[c]; });
var total = catValues.reduce(function(a,b) { return a+b; }, 0);
function updateChart() {
var ctx = document.getElementById('mainChart').getContext('2d');
var filtered = getFilteredData();
var labels = [];
var data = [];
var backgroundColors = [];
if (currentFilter.type === 'month') {
var months = {};
filtered.forEach(function(d) { var m = d.date.split('-')[0] + '-' + d.date.split('-')[1]; months[m] = (months[m] || 0) + (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 = {};
filtered.forEach(function(d) { var p = d.prenom || 'Inconnu'; persons[p] = (persons[p] || 0) + (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 = {};
filtered.forEach(function(d) { var c = d.category || 'Autre'; categories[c] = (categories[c] || 0) + (parseFloat(d.montant) || 0); });
var colors = ['rgba(0, 217, 255, 0.7)', 'rgba(233, 69, 96, 0.7)', 'rgba(123, 44, 191, 0.7)', 'rgba(0, 255, 136, 0.7)', 'rgba(255, 200, 0, 0.7)'];
Object.keys(categories).sort().forEach(function(c, i) { labels.push(c); data.push(categories[c]); backgroundColors.push(colors[i % colors.length]); });
var ctx = document.getElementById('categoryChart').getContext('2d');
if (categoryChart) categoryChart.destroy();
categoryChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: catLabels,
datasets: [{ data: catValues, backgroundColor: COLORS.slice(0, catLabels.length), borderWidth: 0 }]
},
options: {
responsive: true, maintainAspectRatio: false, cutout: '70%',
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(15,23,42,0.95)',
borderColor: 'rgba(168,85,247,0.2)',
borderWidth: 1,
titleColor: '#94a3b8',
bodyColor: '#f1f5f9',
callbacks: {
label: function(ctx) {
var pct = total > 0 ? ((ctx.raw/total)*100).toFixed(1) : 0;
return ' ' + ctx.raw.toFixed(2) + '€ (' + pct + '%)';
}
}
var total = filtered.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
document.getElementById('total-display').textContent = total.toFixed(2) + '€';
if (chart) chart.destroy();
chart = new Chart(ctx, {
type: chartMode,
data: { labels: labels, datasets: [{ label: 'Montant (€)', data: data, backgroundColor: backgroundColors, borderColor: backgroundColors.map(c => c.replace('0.7', '1')), borderWidth: 1 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#fff' } } }, scales: chartMode === 'pie' ? {} : { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#888' } }, x: { grid: { display: false }, ticks: { color: '#888' } } } }
});
}
}
}
});
async function load() {
try {
var r = await fetch('/api/depenses');
depenses = await r.json();
setFilter('month');
} catch (e) {
console.error('Erreur:', e);
document.getElementById('total-display').textContent = 'Erreur de chargement';
}
// Category summary table
var html = '<table class="summary-table">';
html += '<tr><th>Catégorie</th><th>Total</th><th>%</th></tr>';
catLabels.forEach(function(c, i) {
var pct = total > 0 ? (catData[c]/total*100).toFixed(1) : 0;
html += '<tr>';
html += '<td style="display:flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:50%;background:'+COLORS[i % COLORS.length]+';display:inline-block"></span>'+c+'</td>';
html += '<td style="color:var(--rose);font-family:\'JetBrains Mono\',monospace;font-size:0.8rem">'+catData[c].toFixed(2)+'€</td>';
html += '<td style="color:var(--text-dim);font-size:0.78rem">'+pct+'%</td>';
html += '</tr>';
});
html += '</table>';
document.getElementById('category-summary').innerHTML = html;
}
function updatePersonSummary() {
var filtered = getFilteredData();
var pers = {};
filtered.forEach(function(d) {
var p = d.prenom || 'Inconnu';
pers[p] = (pers[p]||0) + (parseFloat(d.montant)||0);
});
var total = Object.values(pers).reduce(function(a,b) { return a+b; }, 0);
var sorted = Object.keys(pers).sort(function(a,b) { return pers[b]-pers[a]; });
var html = '';
sorted.forEach(function(p, i) {
var pct = total > 0 ? (pers[p]/total*100) : 0;
html += '<div style="margin-bottom:12px">';
html += '<div style="display:flex;justify-content:space-between;margin-bottom:4px">';
html += '<span style="color:var(--text);font-size:0.85rem;display:flex;align-items:center;gap:6px">';
html += '<span style="width:8px;height:8px;border-radius:50%;background:'+COLORS[i % COLORS.length]+';display:inline-block"></span>'+p+'</span>';
html += '<span style="font-family:\'JetBrains Mono\',monospace;color:var(--violet);font-size:0.82rem">'+pers[p].toFixed(2)+'€</span>';
html += '</div>';
html += '<div class="prog-bar"><div class="prog-fill" style="width:'+pct.toFixed(1)+'%;background:'+COLORS[i % COLORS.length]+'"></div></div>';
html += '</div>';
});
if (!html) html = '<div style="color:var(--text-dim);font-size:0.85rem">Aucune donnée</div>';
document.getElementById('person-summary').innerHTML = html;
}
function updateTopExpenses() {
var filtered = getFilteredData();
var sorted = filtered.slice().sort(function(a,b) { return (parseFloat(b.montant)||0) - (parseFloat(a.montant)||0); });
var top = sorted.slice(0, 7);
var maxVal = top.length > 0 ? parseFloat(top[0].montant)||1 : 1;
var html = '';
top.forEach(function(d, i) {
var pct = ((parseFloat(d.montant)||0) / maxVal * 100).toFixed(0);
html += '<div style="margin-bottom:10px">';
html += '<div style="display:flex;justify-content:space-between;margin-bottom:4px;gap:8px">';
html += '<span style="color:var(--text);font-size:0.82rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">'+i+'<span style="color:var(--text-dim)">.&nbsp;</span>'+(d.libelle||'—')+'</span>';
html += '<span style="font-family:\'JetBrains Mono\',monospace;color:var(--rose);font-size:0.78rem;white-space:nowrap">'+parseFloat(d.montant).toFixed(2)+'€</span>';
html += '</div>';
html += '<div class="prog-bar"><div class="prog-fill" style="width:'+pct+'%;background:'+COLORS[i % COLORS.length]+'"></div></div>';
html += '</div>';
});
if (!html) html = '<div style="color:var(--text-dim);font-size:0.85rem;text-align:center;padding:20px">Aucune dépense</div>';
document.getElementById('top-expenses').innerHTML = html;
}
function updateTrendChart() {
// Always show the last 6 months trend regardless of filter
var monthData = {};
depenses.forEach(function(d) {
if (!d.date) return;
var m = d.date.split('-')[0]+'-'+d.date.split('-')[1];
monthData[m] = (monthData[m]||0) + (parseFloat(d.montant)||0);
});
var months = Object.keys(monthData).sort().slice(-6);
var values = months.map(function(m) { return monthData[m]; });
var ctx = document.getElementById('trendChart').getContext('2d');
if (trendChart) trendChart.destroy();
trendChart = new Chart(ctx, {
type: 'line',
data: {
labels: months,
datasets: [{
label: 'Total mensuel',
data: values,
borderColor: '#a855f7',
backgroundColor: 'rgba(168,85,247,0.08)',
fill: true,
tension: 0.4,
pointBackgroundColor: '#a855f7',
pointRadius: 4,
borderWidth: 2,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: {
backgroundColor: 'rgba(15,23,42,0.95)',
borderColor: 'rgba(168,85,247,0.2)',
borderWidth: 1,
titleColor: '#94a3b8',
bodyColor: '#f1f5f9',
callbacks: { label: function(ctx) { return ' ' + ctx.raw.toFixed(2) + '€'; } }
}},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(100,116,139,0.08)' }, ticks: { color: '#64748b', font: { family: 'JetBrains Mono', size: 9 }, callback: function(v) { return v+'€'; } } },
x: { grid: { display: false }, ticks: { color: '#64748b', font: { size: 10 } } }
}
}
});
}
load();
</script>
async function load() {
try {
var r = await fetch('api/depenses');
depenses = await r.json();
setFilter('month');
} catch(e) {
console.error('Erreur chargement:', e);
document.getElementById('kpi-total').textContent = 'Erreur';
}
}
load();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff