Documentation v1.0 - Pour vente
This commit is contained in:
@@ -7,9 +7,9 @@
|
||||
<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; --success: #00ff88; --danger: #ff4757; }
|
||||
:root { --bg-dark: #0d0d1a; --bg-card: #16162a; --bg-input: #1a1a35; --primary: #e94560; --secondary: #7b2cbf; --accent: #00d9ff; --text: #fff; --text-dim: #888; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Outfit', sans-serif; background: var(--bg-dark); color: var(--text); padding: 15px; min-height: 100vh; }
|
||||
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; }
|
||||
@@ -17,16 +17,10 @@
|
||||
.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; border: 2px solid transparent; transition: all 0.2s; }
|
||||
.filter-chip:hover { background: var(--bg-card); }
|
||||
.filter-chip.active { background: var(--primary); color: #fff; border-color: var(--accent); }
|
||||
.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; }
|
||||
.expense-list { max-height: 400px; overflow-y: auto; }
|
||||
.expense-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: var(--bg-input); border-radius: 8px; margin-bottom: 8px; }
|
||||
.expense-cat { background: var(--secondary); padding: 3px 8px; border-radius: 5px; font-size: 0.75em; }
|
||||
.expense-prenom { background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 3px 10px; border-radius: 15px; font-size: 0.75em; }
|
||||
.expense-montant { font-weight: bold; color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -38,23 +32,21 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📋 Liste des dépenses</h2>
|
||||
<div id="expense-list" class="expense-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>💰 Total</h2>
|
||||
<div id="total-display" class="total">0.00€</div>
|
||||
<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>
|
||||
<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" style="margin-bottom:15px; font-size:0.85em">
|
||||
<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>
|
||||
@@ -65,105 +57,51 @@
|
||||
|
||||
<script>
|
||||
var depenses = [];
|
||||
var currentFilter = { type: 'all', value: null };
|
||||
var currentFilter = { type: 'month', value: null };
|
||||
var chartMode = 'bar';
|
||||
var chart = null;
|
||||
|
||||
async function load() {
|
||||
var r = await fetch('/api/depenses');
|
||||
depenses = await r.json();
|
||||
renderFilters();
|
||||
renderExpenses();
|
||||
updateChart();
|
||||
}
|
||||
|
||||
function setChartMode(mode) {
|
||||
chartMode = mode;
|
||||
document.getElementById('chart-filters').innerHTML = '<div class="filter-chip' + (mode === 'bar' ? ' active' : '') + '" onclick="setChartMode('\''+mode+'\'')">Bar chart</div><div class="filter-chip' + (mode === 'pie' ? ' active' : '') + '" onclick="setChartMode('\''+mode+'\'')">Camembert</div>';
|
||||
updateChart();
|
||||
}
|
||||
|
||||
function renderFilters() {
|
||||
var bars = [
|
||||
{ id: 'byMonth', label: 'Mois', type: 'month' },
|
||||
{ id: 'byPerson', label: 'Personne', type: 'person' },
|
||||
{ id: 'byCategory', label: 'Catégorie', type: 'category' }
|
||||
];
|
||||
var html = '';
|
||||
for (var i = 0; i < bars.length; i++) {
|
||||
html += '<div class="filter-chip' + (currentFilter.type === bars[i].type ? ' active' : '') + '" onclick="setFilter(\'' + bars[i].type + '\')">' + bars[i].label + '</div>';
|
||||
}
|
||||
document.getElementById('filter-bar').innerHTML = html;
|
||||
function getFilteredData() {
|
||||
if (!currentFilter.value) return depenses;
|
||||
if (currentFilter.type === 'month') return depenses.filter(d => d.date && d.date.startsWith(currentFilter.value));
|
||||
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;
|
||||
renderFilters();
|
||||
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();
|
||||
}
|
||||
if (type === 'month') values = [...new Set(depenses.map(d => d.date.split('-')[0] + '-' + d.date.split('-')[1]))].sort().reverse();
|
||||
else if (type === 'person') values = [...new Set(depenses.map(d => d.prenom))].sort();
|
||||
else if (type === 'category') values = [...new Set(depenses.map(d => d.category || 'Autre'))].sort();
|
||||
|
||||
currentFilter.value = values[0] || null;
|
||||
renderValues();
|
||||
renderExpenses();
|
||||
renderFilters();
|
||||
updateChart();
|
||||
}
|
||||
|
||||
function renderValues() {
|
||||
var html = '';
|
||||
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();
|
||||
}
|
||||
|
||||
html += '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="setFilter(\'' + currentFilter.type + '\')">Tous</div>';
|
||||
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] + '\'; renderValues(); renderExpenses(); updateChart();">' + values[i] + '</div>';
|
||||
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 getFilteredData() {
|
||||
if (!currentFilter.value) return depenses;
|
||||
if (currentFilter.type === 'month') {
|
||||
return depenses.filter(d => d.date && d.date.startsWith(currentFilter.value));
|
||||
} else if (currentFilter.type === 'person') {
|
||||
return depenses.filter(d => d.prenom === currentFilter.value);
|
||||
} else if (currentFilter.type === 'category') {
|
||||
return depenses.filter(d => (d.category || 'Autre') === currentFilter.value);
|
||||
}
|
||||
return depenses;
|
||||
}
|
||||
|
||||
function renderExpenses() {
|
||||
var filtered = getFilteredData();
|
||||
var total = filtered.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
|
||||
document.getElementById('total-display').textContent = total.toFixed(2) + '€';
|
||||
|
||||
var html = '';
|
||||
for (var i = 0; i < Math.min(filtered.length, 50); i++) {
|
||||
var d = filtered[i];
|
||||
html += '<div class="expense-item">';
|
||||
html += '<div>';
|
||||
html += '<span class="expense-prenom">' + (d.prenom || '??') + '</span> ';
|
||||
html += '<span class="expense-cat">' + (d.category || 'Autre') + '</span>';
|
||||
html += '</div>';
|
||||
html += '<div style="flex:1; padding: 0 15px; font-size: 0.9em;">' + d.libelle + '</div>';
|
||||
html += '<div style="display:flex; align-items:center; gap:10px">';
|
||||
html += '<span style="color:var(--text-dim); font-size:0.8em;">' + d.date + '</span>';
|
||||
html += '<span class="expense-montant">' + parseFloat(d.montant).toFixed(2) + '€</span>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
document.getElementById('expense-list').innerHTML = html || '<p style="color:var(--text-dim)">Aucune dépense trouvée</p>';
|
||||
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 updateChart() {
|
||||
@@ -172,131 +110,44 @@
|
||||
var labels = [];
|
||||
var data = [];
|
||||
var backgroundColors = [];
|
||||
var borderColors = [];
|
||||
|
||||
if (chartMode === 'pie') {
|
||||
if (currentFilter.type === 'person') {
|
||||
var persons = {};
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var d = filtered[i];
|
||||
var p = d.prenom || 'Inconnu';
|
||||
if (!persons[p]) persons[p] = 0;
|
||||
persons[p] += parseFloat(d.montant) || 0;
|
||||
}
|
||||
Object.keys(persons).sort().forEach(function(p) {
|
||||
labels.push(p);
|
||||
data.push(persons[p]);
|
||||
backgroundColors.push('rgba(233, 69, 96, 0.7)');
|
||||
borderColors.push('rgba(233, 69, 96, 1)');
|
||||
});
|
||||
} else if (currentFilter.type === 'category') {
|
||||
var categories = {};
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var d = filtered[i];
|
||||
var c = d.category || 'Autre';
|
||||
if (!categories[c]) categories[c] = 0;
|
||||
categories[c] += parseFloat(d.montant) || 0;
|
||||
}
|
||||
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)', 'rgba(255, 71, 87, 0.7)', 'rgba(153, 68, 204, 0.7)'
|
||||
];
|
||||
Object.keys(categories).sort().forEach(function(c, idx) {
|
||||
labels.push(c);
|
||||
data.push(categories[c]);
|
||||
backgroundColors.push(colors[idx % colors.length]);
|
||||
borderColors.push(colors[idx % colors.length].replace('0.7', '1'));
|
||||
});
|
||||
} else {
|
||||
labels.push('Mois actuel');
|
||||
data.push(filtered.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0));
|
||||
backgroundColors.push('rgba(0, 217, 255, 0.7)');
|
||||
borderColors.push('rgba(0, 217, 255, 1)');
|
||||
}
|
||||
} else {
|
||||
if (currentFilter.type === 'month') {
|
||||
var months = {};
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var d = filtered[i];
|
||||
var m = d.date.split('-')[0] + '-' + d.date.split('-')[1];
|
||||
if (!months[m]) months[m] = 0;
|
||||
months[m] += parseFloat(d.montant) || 0;
|
||||
}
|
||||
Object.keys(months).sort().reverse().forEach(function(m) {
|
||||
labels.push(m);
|
||||
data.push(months[m]);
|
||||
backgroundColors.push('rgba(0, 217, 255, 0.7)');
|
||||
borderColors.push('rgba(0, 217, 255, 1)');
|
||||
});
|
||||
} else if (currentFilter.type === 'person') {
|
||||
var persons = {};
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var d = filtered[i];
|
||||
var p = d.prenom || 'Inconnu';
|
||||
if (!persons[p]) persons[p] = 0;
|
||||
persons[p] += parseFloat(d.montant) || 0;
|
||||
}
|
||||
Object.keys(persons).sort().forEach(function(p) {
|
||||
labels.push(p);
|
||||
data.push(persons[p]);
|
||||
backgroundColors.push('rgba(233, 69, 96, 0.7)');
|
||||
borderColors.push('rgba(233, 69, 96, 1)');
|
||||
});
|
||||
} else if (currentFilter.type === 'category') {
|
||||
var categories = {};
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var d = filtered[i];
|
||||
var c = d.category || 'Autre';
|
||||
if (!categories[c]) categories[c] = 0;
|
||||
categories[c] += parseFloat(d.montant) || 0;
|
||||
}
|
||||
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)', 'rgba(255, 71, 87, 0.7)', 'rgba(153, 68, 204, 0.7)'
|
||||
];
|
||||
Object.keys(categories).sort().forEach(function(c, idx) {
|
||||
labels.push(c);
|
||||
data.push(categories[c]);
|
||||
backgroundColors.push(colors[idx % colors.length]);
|
||||
borderColors.push(colors[idx % colors.length].replace('0.7', '1'));
|
||||
});
|
||||
}
|
||||
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();
|
||||
chart = new Chart(ctx, {
|
||||
type: chartMode,
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Montant (€)',
|
||||
data: data,
|
||||
backgroundColor: backgroundColors,
|
||||
borderColor: borderColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#fff', padding: 15, font: { size: 12 } }
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: chartMode === 'pie' ? 'Répartition par ' + (currentFilter.type === 'month' ? 'Mois' : currentFilter.type === 'person' ? 'Personne' : 'Catégorie') : ''
|
||||
}
|
||||
},
|
||||
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: 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>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user