Files
turf_saas/boite_a_idees_dashboard.html
2026-04-25 17:18:43 +02:00

201 lines
12 KiB
HTML
Executable File

<!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; }
.home-btn{position:fixed;top:10px;left:10px;z-index:9999;background:linear-gradient(135deg,#00d9ff,#7b2cbf);color:#fff;border:none;border-radius:8px;padding:10px 15px;font-size:14px;cursor:pointer;text-decoration:none;display:flex;align-items:center;gap:8px;box-shadow:0 2px 10px rgba(0,217,255,0.3);transition:all 0.3s;font-family:-apple-system,BlinkMacSystemFont,sans-serif}.home-btn:hover{transform:translateY(-2px);box-shadow:0 4px 15px rgba(0,217,255,0.5)}
</style>
</head>
<body>
<a href="https://portal-kolifee.duckdns.org/" class="home-btn" title="Retour au portail"><span style="font-size:18px">🏠</span><span>Accueil</span></a>
<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="dashboard?page=config" id="nav-config">⚙️ Config</a>
</div>
<div class="card">
<h2>💰 Total: <span id="total-display">0.00€</span></h2>
</div>
<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>
</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>
<script>
var depenses = [];
var currentFilter = { type: 'month', value: null };
var chartMode = 'bar';
var chart = null;
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;
}
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();
}
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>';
}
document.getElementById('filter-bar').innerHTML = html;
}
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 updateSummaries() {
var filtered = getFilteredData();
var cats = {};
var pers = {};
filtered.forEach(function(d) {
var c = d.category || 'Autre';
var p = d.prenom || 'Inconnu';
cats[c] = (cats[c] || 0) + (parseFloat(d.montant) || 0);
pers[p] = (pers[p] || 0) + (parseFloat(d.montant) || 0);
});
var catHtml = '<table style="width:100%;text-align:left"><tr><th>Catégorie</th><th>Total</th></tr>';
Object.keys(cats).sort().forEach(function(c) { catHtml += '<tr><td>'+c+'</td><td style="color:var(--primary)">'+cats[c].toFixed(2)+'€</td></tr>'; });
catHtml += '</table>';
document.getElementById("category-summary").innerHTML = catHtml;
var persHtml = '<table style="width:100%;text-align:left"><tr><th>Personne</th><th>Total</th></tr>';
Object.keys(pers).sort().forEach(function(p) { persHtml += '<tr><td>'+p+'</td><td style="color:var(--secondary)">'+pers[p].toFixed(2)+'€</td></tr>'; });
persHtml += '</table>';
document.getElementById("person-summary").innerHTML = persHtml;
}
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 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();
// Category chart
var catCtx = document.getElementById('categoryChart');
if (catCtx) {
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 catColors = ['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)', 'rgba(255, 99, 132, 0.7)'];
new Chart(catCtx, {
type: 'doughnut',
data: { labels: catLabels, datasets: [{ data: catValues, backgroundColor: catColors }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#fff' } } } }
});
}
updateSummaries(); 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';
}
}
load();
</script>
<div class="card">
<h2>📊 Par Catégorie</h2>
<div class="chart-container" style="height:200px">
<canvas id="categoryChart"></canvas>
</div>
<div id="category-summary"></div>
</div>
<div class="card"><h2>👤 Par Utilisateur</h2><div id="person-summary"></div></div>
</body>
</html>