Compare commits
2 Commits
6d3ef9dec3
...
a3d1a33f13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3d1a33f13 | ||
|
|
7f0688d00a |
@@ -1,154 +1,731 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<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>📊 Dashboard Dépenses — H3R7</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
|
<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.jsdelivr.net/npm/chart.js"></script>
|
||||||
<style>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
:root { --bg-dark: #0d0d1a; --bg-card: #16162a; --bg-input: #1a1a35; --primary: #e94560; --secondary: #7b2cbf; --accent: #00d9ff; --text: #fff; --text-dim: #888; }
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
:root {
|
||||||
body { font-family: 'Outfit', sans-serif; background: var(--bg-dark); color: var(--text); padding: 15px; }
|
--bg-primary: #020617;
|
||||||
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; }
|
--bg-card: rgba(15, 23, 42, 0.8);
|
||||||
.nav { display: flex; gap: 10px; margin: 15px 0; }
|
--bg-input: rgba(30, 41, 59, 0.6);
|
||||||
.nav a { flex: 1; padding: 12px; background: var(--bg-card); border-radius: 10px; text-align: center; color: var(--text-dim); text-decoration: none; }
|
--cyan: #00d9ff;
|
||||||
.nav a.active { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; }
|
--violet: #a855f7;
|
||||||
.card { background: var(--bg-card); border-radius: 15px; padding: 15px; margin-bottom: 15px; }
|
--matrix: #00ff88;
|
||||||
h2 { font-size: 1em; margin-bottom: 12px; color: var(--accent); }
|
--rose: #f43f5e;
|
||||||
.filter-bar { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
|
--amber: #f59e0b;
|
||||||
.filter-chip { padding: 10px 20px; background: var(--bg-input); border-radius: 25px; font-size: 0.9em; cursor: pointer; }
|
--text: #f1f5f9;
|
||||||
.filter-chip.active { background: var(--primary); color: #fff; }
|
--text-dim: #64748b;
|
||||||
.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>
|
* { 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>
|
</head>
|
||||||
<body>
|
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
<!-- HEADER -->
|
||||||
<h2>💰 Total: <span id="total-display">0.00€</span></h2>
|
<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>
|
||||||
|
<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">
|
<!-- KPI BAR -->
|
||||||
<h2>📊 Filtres</h2>
|
<div style="padding:12px 16px;position:relative;z-index:1">
|
||||||
<div class="filter-bar" id="filter-bar">
|
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;max-width:1400px;margin:0 auto">
|
||||||
<div class="filter-chip active" onclick="setFilter('month')">Mois</div>
|
<div class="kpi-card" style="--kpi-color:var(--rose)">
|
||||||
<div class="filter-chip" onclick="setFilter('person')">Personne</div>
|
<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="filter-chip" onclick="setFilter('category')">Catégorie</div>
|
<div class="kpi-value" id="kpi-total" style="color:var(--rose)">0.00€</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- MAIN DASHBOARD GRID -->
|
||||||
|
<div class="dash-grid">
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<!-- Main chart — 2 columns -->
|
||||||
<h2>📈 Graphiques</h2>
|
<div class="glass-card span-2" style="padding:18px">
|
||||||
<div class="filter-bar" id="chart-filters">
|
<div class="section-title">
|
||||||
<div class="filter-chip active" onclick="setChartMode('bar')">Bar chart</div>
|
📈 Évolution
|
||||||
<div class="filter-chip" onclick="setChartMode('pie')">Camembert</div>
|
<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 class="chart-container">
|
</div>
|
||||||
|
<div style="height:280px;position:relative">
|
||||||
<canvas id="mainChart"></canvas>
|
<canvas id="mainChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- 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 depenses = [];
|
||||||
var currentFilter = { type: 'month', value: null };
|
var currentFilter = { type: 'month', value: null };
|
||||||
var chartMode = 'bar';
|
var chartMode = 'bar';
|
||||||
var chart = null;
|
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() {
|
function getFilteredData() {
|
||||||
if (!currentFilter.value) return depenses;
|
if (!currentFilter.value) return depenses;
|
||||||
if (currentFilter.type === 'month') return depenses.filter(d => d.date && d.date.startsWith(currentFilter.value));
|
if (currentFilter.type === 'month') return depenses.filter(function(d) { return d.date && d.date.startsWith(currentFilter.value); });
|
||||||
if (currentFilter.type === 'person') return depenses.filter(d => d.prenom === currentFilter.value);
|
if (currentFilter.type === 'person') return depenses.filter(function(d) { return d.prenom === currentFilter.value; });
|
||||||
if (currentFilter.type === 'category') return depenses.filter(d => (d.category || 'Autre') === currentFilter.value);
|
if (currentFilter.type === 'category') return depenses.filter(function(d) { return (d.category || 'Autre') === currentFilter.value; });
|
||||||
return depenses;
|
return depenses;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFilter(type) {
|
function setFilter(type) {
|
||||||
currentFilter.type = type;
|
currentFilter.type = type;
|
||||||
currentFilter.value = null;
|
currentFilter.value = null;
|
||||||
setChartMode('bar');
|
|
||||||
|
// 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 = [];
|
var values = [];
|
||||||
if (type === 'month') values = [...new Set(depenses.map(d => d.date.split('-')[0] + '-' + d.date.split('-')[1]))].sort().reverse();
|
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(d => d.prenom))].sort();
|
else if (type === 'person') values = [...new Set(depenses.map(function(d) { return d.prenom; }))].sort();
|
||||||
else if (type === 'category') values = [...new Set(depenses.map(d => d.category || 'Autre'))].sort();
|
else if (type === 'category') values = [...new Set(depenses.map(function(d) { return d.category || 'Autre'; }))].sort();
|
||||||
|
|
||||||
currentFilter.value = values[0] || null;
|
currentFilter.value = values[0] || null;
|
||||||
renderFilters();
|
renderFilters(values);
|
||||||
updateChart();
|
updateAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFilters() {
|
function renderFilters(values) {
|
||||||
var html = '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="setFilter(\'' + currentFilter.type + '\')">Tous</div>';
|
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 values = [];
|
var html = '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="selectValue(null)">Tous</div>';
|
||||||
if (currentFilter.type === 'month') values = [...new Set(depenses.map(d => d.date.split('-')[0] + '-' + d.date.split('-')[1]))].sort().reverse();
|
for (var i=0; i<values.length; i++) {
|
||||||
else if (currentFilter.type === 'person') values = [...new Set(depenses.map(d => d.prenom))].sort();
|
html += '<div class="filter-chip' + (currentFilter.value === values[i] ? ' active' : '') + '" onclick="selectValue(\'' + values[i] + '\')">' + values[i] + '</div>';
|
||||||
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>';
|
|
||||||
}
|
}
|
||||||
document.getElementById('filter-bar').innerHTML = html;
|
document.getElementById('filter-bar').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setChartMode(mode) {
|
function selectValue(val) {
|
||||||
chartMode = mode;
|
currentFilter.value = val;
|
||||||
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>';
|
renderFilters();
|
||||||
updateChart();
|
updateAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChart() {
|
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 ctx = document.getElementById('mainChart').getContext('2d');
|
||||||
var filtered = getFilteredData();
|
var filtered = getFilteredData();
|
||||||
var labels = [];
|
|
||||||
var data = [];
|
var labels = [], data = [], bgColors = [];
|
||||||
var backgroundColors = [];
|
|
||||||
|
|
||||||
if (currentFilter.type === 'month') {
|
if (currentFilter.type === 'month') {
|
||||||
var months = {};
|
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); });
|
filtered.forEach(function(d) {
|
||||||
Object.keys(months).sort().reverse().forEach(function(m) { labels.push(m); data.push(months[m]); backgroundColors.push('rgba(0, 217, 255, 0.7)'); });
|
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') {
|
} else if (currentFilter.type === 'person') {
|
||||||
var persons = {};
|
var persons = {};
|
||||||
filtered.forEach(function(d) { var p = d.prenom || 'Inconnu'; persons[p] = (persons[p] || 0) + (parseFloat(d.montant) || 0); });
|
filtered.forEach(function(d) {
|
||||||
Object.keys(persons).sort().forEach(function(p) { labels.push(p); data.push(persons[p]); backgroundColors.push('rgba(233, 69, 96, 0.7)'); });
|
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') {
|
} else if (currentFilter.type === 'category') {
|
||||||
var categories = {};
|
var categories = {};
|
||||||
filtered.forEach(function(d) { var c = d.category || 'Autre'; categories[c] = (categories[c] || 0) + (parseFloat(d.montant) || 0); });
|
filtered.forEach(function(d) {
|
||||||
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)'];
|
var c = d.category||'Autre';
|
||||||
Object.keys(categories).sort().forEach(function(c, i) { labels.push(c); data.push(categories[c]); backgroundColors.push(colors[i % colors.length]); });
|
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]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var total = filtered.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
|
if (mainChart) mainChart.destroy();
|
||||||
document.getElementById('total-display').textContent = total.toFixed(2) + '€';
|
|
||||||
|
|
||||||
if (chart) chart.destroy();
|
var type = chartMode === 'line' ? 'line' : (chartMode === 'pie' ? 'doughnut' : 'bar');
|
||||||
chart = new Chart(ctx, {
|
|
||||||
type: chartMode,
|
mainChart = new Chart(ctx, {
|
||||||
data: { labels: labels, datasets: [{ label: 'Montant (€)', data: data, backgroundColor: backgroundColors, borderColor: backgroundColors.map(c => c.replace('0.7', '1')), borderWidth: 1 }] },
|
type: type,
|
||||||
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' } } } }
|
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) + ' €'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 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);
|
||||||
|
|
||||||
|
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 + '%)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
var r = await fetch('/api/depenses');
|
var r = await fetch('api/depenses');
|
||||||
depenses = await r.json();
|
depenses = await r.json();
|
||||||
setFilter('month');
|
setFilter('month');
|
||||||
} catch (e) {
|
} catch(e) {
|
||||||
console.error('Erreur:', e);
|
console.error('Erreur chargement:', e);
|
||||||
document.getElementById('total-display').textContent = 'Erreur de chargement';
|
document.getElementById('kpi-total').textContent = 'Erreur';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
1423
templates/index.html
1423
templates/index.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user