Documentation v1.0 - Pour vente

This commit is contained in:
h3r7
2026-03-01 17:45:05 +01:00
parent d3b9f1c664
commit 6d3ef9dec3
5 changed files with 225 additions and 293 deletions

157
DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,157 @@
# 💸 Dépenses Trello - Application de Gestion des Dépenses
## 📝 Description
**Dépenses Trello** est une application web de gestion des dépenses personnelles avec synchronisation automatique vers Trello. Créée pour simplifier le suivi des dépenses au quotidien.
---
## ✨ Fonctionnalités
### Gestion des Dépenses
- **Ajout rapide** de dépenses (prénom, date, libellé, montant, catégorie)
- ✏️ **Modification** des dépenses existantes
- 🗑️ **Suppression** de dépenses
- 📤 **Export CSV** pour Excel/comptabilité
- 📥 **Import CSV** pour récupérer des données
- 📄 **Export PDF** pour les relevés
### Catégories Automatiques
- 🚗 **Transport** (essence, gazole, parking)
- 🛒 **Courses** (Carrefour, Lidl, Auchan)
- 🎰 **Loisirs** (loto, bar, café, cinéma)
- 🏠 **Maison** (lumière, loyer, brico)
- ❤️ **Santé** (pharmacie, contrôle technique)
- 📦 **Autre** (divers)
### Graphiques & Statistiques
- 📊 **Bar chart** - visualisation classique
- 🥧 **Camembert** - répartition par catégorie
- 👤 **Par personne** - suivi par membre du foyer
- 📅 **Par mois** - évolution dans le temps
### Budget
- 💰 **Budget mensuel** configurable
- ⚠️ **Alerte** automatique quand le budget est dépassé
### Dépenses Récurrentes
- 🔄 **Loyer**, EDF, téléphone - automatiquement listés
### Synchronisation Trello
- 🟦 **Envoi automatique** des dépenses vers une liste Trello
-**Suivi du statut** (En attente / Envoyé)
---
## 🛠️ Stack Technique
| Composant | Technologie |
|-----------|--------------|
| **Backend** | Python Flask |
| **Base de données** | SQLite |
| **Frontend** | HTML5, Vanilla JavaScript |
| **Graphiques** | Chart.js |
| **PDF** | jsPDF |
| **Hébergement** | VPS (Linux) |
---
## 📋 Spécifications
- **Port**: 8769
- **URL**: http://178.18.250.53:8769/
- **API REST**: /api/depenses, /api/config, /api/budget, /api/recurring
- **Données**: 21+ dépenses en base (extensible)
- **Utilisateurs**: Multi-utilisateurs (via prénom)
---
## 🚀 Installation
```bash
# Cloner le projet
git clone http://178.18.250.53:3000/admin/Perso.git
# Installer les dépendances
pip install flask requests
# Lancer l'application
python app.py
```
---
## 💼 Potentiel Commercial
### Cible
- 👨‍👩‍👧 **Particuliers** gestion budget familial
- 💼 **Auto-entrepreneurs** frais professionnels
- 🏢 **Petites entreprises** suivi dépenses
### Arguments de Vente
1.**Simple** - interface épurée, pas de formation
2.**Complet** - catégories, graphiques, budget, PDF
3.**Automatisé** - synchronisation Trello
4.**Pas d'abonnement** - hébergement propre
5.**Open Source** - customisable
### Prix Recommandés
| Offre | Prix |
|-------|------|
| Usage personnel | 19€ |
| Usage pro | 49€ |
| Installation + config | 29€ |
| Support mensuel | 9€/mois |
---
## 📱 Captures d'Écran
### Page Saisie
- Formulaire rapide avec catégories auto
- Liste des dernières dépenses
- Boutons Envoyer tout / Export
### Page Dashboard
- Graphiques Bar / Camembert
- Filtres Mois / Personne / Catégorie
- Total en temps réel
### Page Config
- Gestion des prénoms
- Personnalisation des catégories
- Configuration Trello (API Key, Token, List ID)
- Budget mensuel
---
## 🔧 Configuration Trello
1. Créer un Power-Up sur https://trello.com/power-ups/admin
2. Générer une API Key
3. Générer un Token (avec permissions write)
4. Copier le List ID cible
5. Coller dans Config
---
## 📝 Roadmap Future
- [ ] Application mobile (PWA)
- [ ] Mode hors-ligne
- [ ] Catégories personnalisées illimitées
- [ ] Rapports mensuels par email
- [ ] Intégration Slack/Discord
- [ ] Multi-devises
---
## 📞 Contact
**Développé par**: H3R7Tech
**Date**: Mars 2026
**Version**: 1.6
---
*Document généré automatiquement - Dépenses Trello*

76
app.py
View File

@@ -332,81 +332,5 @@ def save_trello():
conn.close() conn.close()
return redirect('/?page=config') return redirect('/?page=config')
# Get budget
@app.route('/api/budget')
def get_budget():
conn = get_db()
c = conn.cursor()
c.execute("SELECT value FROM config WHERE key='budget'")
row = c.fetchone()
conn.close()
return jsonify({'budget': float(row[0]) if row and row[0] else 0})
# Set budget
@app.route('/api/budget', methods=['POST'])
def set_budget():
conn = get_db()
c = conn.cursor()
budget = request.form.get('budget', 0)
c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('budget', budget))
conn.commit()
conn.close()
return redirect('/?page=config')
# Get recurring
@app.route('/api/recurring')
def get_recurring():
conn = get_db()
c = conn.cursor()
c.execute('SELECT id, libelle, montant, category, jour FROM recurring')
rows = c.fetchall()
conn.close()
return jsonify([{'id': r[0], 'libelle': r[1], 'montant': r[2], 'category': r[3], 'jour': r[4]} for r in rows])
# Add recurring
@app.route('/api/recurring/add', methods=['POST'])
def add_recurring():
conn = get_db()
c = conn.cursor()
c.execute('INSERT INTO recurring (libelle, montant, category, jour) VALUES (?, ?, ?, ?)',
(request.form.get('libelle'), float(request.form.get('montant', 0)),
request.form.get('category', 'Autre'), int(request.form.get('jour', 1))))
conn.commit()
conn.close()
return redirect('/?page=config')
# Del recurring
@app.route('/api/recurring/del/<int:id>')
def del_recurring(id):
conn = get_db()
c = conn.cursor()
c.execute('DELETE FROM recurring WHERE id=?', (id,))
conn.commit()
conn.close()
return redirect('/?page=config')
# Get monthly stats
@app.route('/api/stats/monthly')
def get_monthly_stats():
conn = get_db()
c = conn.cursor()
c.execute('''SELECT strftime('%Y-%m', date) as month, SUM(montant) as total
FROM depenses GROUP BY month ORDER BY month''')
rows = c.fetchall()
conn.close()
return jsonify([{'month': r[0], 'total': r[1]} for r in rows])
# Get stats by category
@app.route('/api/stats/category')
def get_stats_category():
conn = get_db()
c = conn.cursor()
c.execute('''SELECT category, SUM(montant) as total FROM depenses GROUP BY category''')
rows = c.fetchall()
conn.close()
return jsonify([{'category': r[0] or 'Autre', 'total': r[1]} for r in rows])
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8769, debug=False) app.run(host='0.0.0.0', port=8769, debug=False)

Binary file not shown.

View File

@@ -7,9 +7,9 @@
<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=Outfit:wght@400;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> <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; } * { 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; } 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 { 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 { 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; } .card { background: var(--bg-card); border-radius: 15px; padding: 15px; margin-bottom: 15px; }
h2 { font-size: 1em; margin-bottom: 12px; color: var(--accent); } h2 { font-size: 1em; margin-bottom: 12px; color: var(--accent); }
.filter-bar { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; } .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 { padding: 10px 20px; background: var(--bg-input); border-radius: 25px; font-size: 0.9em; cursor: pointer; }
.filter-chip:hover { background: var(--bg-card); } .filter-chip.active { background: var(--primary); color: #fff; }
.filter-chip.active { background: var(--primary); color: #fff; border-color: var(--accent); }
.chart-container { height: 250px; position: relative; } .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; } .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> </style>
</head> </head>
<body> <body>
@@ -38,23 +32,21 @@
</div> </div>
<div class="card"> <div class="card">
<h2>📋 Liste des dépenses</h2> <h2>💰 Total: <span id="total-display">0.00€</span></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>
</div> </div>
<div class="card"> <div class="card">
<h2>📊 Filtres</h2> <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>
<div class="card"> <div class="card">
<h2>📈 Graphiques</h2> <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 active" onclick="setChartMode('bar')">Bar chart</div>
<div class="filter-chip" onclick="setChartMode('pie')">Camembert</div> <div class="filter-chip" onclick="setChartMode('pie')">Camembert</div>
</div> </div>
@@ -65,105 +57,51 @@
<script> <script>
var depenses = []; var depenses = [];
var currentFilter = { type: 'all', value: null }; var currentFilter = { type: 'month', value: null };
var chartMode = 'bar'; var chartMode = 'bar';
var chart = null; var chart = null;
async function load() { function getFilteredData() {
var r = await fetch('/api/depenses'); if (!currentFilter.value) return depenses;
depenses = await r.json(); if (currentFilter.type === 'month') return depenses.filter(d => d.date && d.date.startsWith(currentFilter.value));
renderFilters(); if (currentFilter.type === 'person') return depenses.filter(d => d.prenom === currentFilter.value);
renderExpenses(); if (currentFilter.type === 'category') return depenses.filter(d => (d.category || 'Autre') === currentFilter.value);
updateChart(); return depenses;
}
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 setFilter(type) { function setFilter(type) {
currentFilter.type = type; currentFilter.type = type;
renderFilters(); currentFilter.value = null;
setChartMode('bar'); setChartMode('bar');
var values = []; var values = [];
if (type === 'month') { if (type === 'month') values = [...new Set(depenses.map(d => d.date.split('-')[0] + '-' + d.date.split('-')[1]))].sort().reverse();
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 === 'person') { else if (type === 'category') values = [...new Set(depenses.map(d => d.category || 'Autre'))].sort();
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; currentFilter.value = values[0] || null;
renderValues(); renderFilters();
renderExpenses();
updateChart(); updateChart();
} }
function renderValues() { function renderFilters() {
var html = ''; var html = '<div class="filter-chip' + (!currentFilter.value ? ' active' : '') + '" onclick="setFilter(\'' + currentFilter.type + '\')">Tous</div>';
var values = [];
if (currentFilter.type === 'month') { var values = [];
values = [...new Set(depenses.map(d => d.date.split('-')[0] + '-' + d.date.split('-')[1]))].sort().reverse(); 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') { else if (currentFilter.type === 'person') values = [...new Set(depenses.map(d => d.prenom))].sort();
values = [...new Set(depenses.map(d => d.prenom))].sort(); else if (currentFilter.type === 'category') values = [...new Set(depenses.map(d => d.category || 'Autre'))].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>';
for (var i = 0; i < values.length; i++) { 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; document.getElementById('filter-bar').innerHTML = html;
} }
function getFilteredData() { function setChartMode(mode) {
if (!currentFilter.value) return depenses; chartMode = mode;
if (currentFilter.type === 'month') { 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>';
return depenses.filter(d => d.date && d.date.startsWith(currentFilter.value)); updateChart();
} 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 updateChart() { function updateChart() {
@@ -172,131 +110,44 @@
var labels = []; var labels = [];
var data = []; var data = [];
var backgroundColors = []; 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') { if (currentFilter.type === 'month') {
var months = {}; var months = {};
for (var i = 0; i < filtered.length; i++) { filtered.forEach(function(d) { var m = d.date.split('-')[0] + '-' + d.date.split('-')[1]; months[m] = (months[m] || 0) + (parseFloat(d.montant) || 0); });
var d = filtered[i]; 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];
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') { } else if (currentFilter.type === 'person') {
var persons = {}; var persons = {};
for (var i = 0; i < filtered.length; i++) { filtered.forEach(function(d) { var p = d.prenom || 'Inconnu'; persons[p] = (persons[p] || 0) + (parseFloat(d.montant) || 0); });
var d = filtered[i]; 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';
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') { } else if (currentFilter.type === 'category') {
var categories = {}; var categories = {};
for (var i = 0; i < filtered.length; i++) { filtered.forEach(function(d) { var c = d.category || 'Autre'; categories[c] = (categories[c] || 0) + (parseFloat(d.montant) || 0); });
var d = filtered[i]; 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]); });
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'));
});
}
} }
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(); if (chart) chart.destroy();
chart = new Chart(ctx, { chart = new Chart(ctx, {
type: chartMode, type: chartMode,
data: { data: { labels: labels, datasets: [{ label: 'Montant (€)', data: data, backgroundColor: backgroundColors, borderColor: backgroundColors.map(c => c.replace('0.7', '1')), borderWidth: 1 }] },
labels: labels, 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' } } } }
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' } }
}
}
}); });
} }
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(); load();
</script> </script>
</body> </body>

View File

@@ -61,7 +61,7 @@
<a href="?page=config" id="nav-config">⚙️ Config</a> <a href="?page=config" id="nav-config">⚙️ Config</a>
</div> </div>
<div id="saisie"> <div id="saisie" style="display:none">
<div class="card"> <div class="card">
<h2> Nouvelle dépense</h2> <h2> Nouvelle dépense</h2>
<form method="POST" action="/api/add" id="form-add"> <form method="POST" action="/api/add" id="form-add">