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>
This commit is contained in:
@@ -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');
|
||||
<!-- 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>
|
||||
|
||||
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();
|
||||
<!-- 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>
|
||||
|
||||
currentFilter.value = values[0] || null;
|
||||
renderFilters();
|
||||
updateChart();
|
||||
}
|
||||
<!-- Per person — 1 column -->
|
||||
<div class="glass-card" style="padding:18px">
|
||||
<div class="section-title">👤 Par personne</div>
|
||||
<div id="person-summary"></div>
|
||||
</div>
|
||||
|
||||
function renderFilters() {
|
||||
var html = '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="setFilter(\'' + currentFilter.type + '\')">Tous</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>
|
||||
|
||||
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();
|
||||
<!-- 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>
|
||||
|
||||
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>';
|
||||
</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)">. </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>
|
||||
|
||||
1879
templates/index.html
1879
templates/index.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user