v1.2 - Dashboard with charts, Import/Export CSV, 17 dépenses
This commit is contained in:
5
app.py
5
app.py
@@ -30,6 +30,11 @@ def index():
|
|||||||
with open('/home/h3r7/depenses_trello/templates/index.html', 'r') as f:
|
with open('/home/h3r7/depenses_trello/templates/index.html', 'r') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
@app.route('/dashboard')
|
||||||
|
def dashboard():
|
||||||
|
with open('/home/h3r7/depenses_trello/templates/dashboard.html', 'r') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
# API Config
|
# API Config
|
||||||
@app.route('/api/config')
|
@app.route('/api/config')
|
||||||
def get_config():
|
def get_config():
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"prenom": "HERY",
|
"prenom": "HERY",
|
||||||
"date": "2026-02-24",
|
"date": "2026-02-24",
|
||||||
"IDL GAUFFlibelle": "LRIER",
|
"libelle": "LIDL GAUFFRIER",
|
||||||
"montant": 24.98,
|
"montant": 24.98,
|
||||||
"status": "Envoy\u00e9 \u2705"
|
"status": "Envoy\u00e9 \u2705"
|
||||||
},
|
},
|
||||||
@@ -139,7 +139,9 @@
|
|||||||
],
|
],
|
||||||
"format": "{prenom} - {date} - {libelle} - {montant}\u20ac",
|
"format": "{prenom} - {date} - {libelle} - {montant}\u20ac",
|
||||||
"prenoms": [
|
"prenoms": [
|
||||||
"HERY"
|
"HERY",
|
||||||
|
"Papa",
|
||||||
|
"Maman"
|
||||||
],
|
],
|
||||||
"trello": {
|
"trello": {
|
||||||
"api_key": "0e86fa879fe9cc1bd5bff8a4b32bceff",
|
"api_key": "0e86fa879fe9cc1bd5bff8a4b32bceff",
|
||||||
|
|||||||
358
templates/dashboard.html
Normal file
358
templates/dashboard.html
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
<!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@300;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; }
|
||||||
|
* { 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; }
|
||||||
|
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); }
|
||||||
|
|
||||||
|
/* Stats Grid */
|
||||||
|
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 15px; }
|
||||||
|
.stat-box { background: var(--bg-input); padding: 15px; border-radius: 12px; text-align: center; }
|
||||||
|
.stat-box .label { font-size: 0.75em; color: var(--text-dim); text-transform: uppercase; }
|
||||||
|
.stat-box .value { font-size: 1.5em; font-weight: bold; color: var(--accent); }
|
||||||
|
.stat-box .value.danger { color: var(--danger); }
|
||||||
|
.stat-box .value.success { color: var(--success); }
|
||||||
|
|
||||||
|
/* Charts */
|
||||||
|
.charts-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 15px; }
|
||||||
|
.chart-container { background: var(--bg-input); padding: 15px; border-radius: 12px; }
|
||||||
|
.chart-container h3 { font-size: 0.85em; color: var(--text-dim); margin-bottom: 10px; }
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.table-container { overflow-x: auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 0.85em; }
|
||||||
|
th { text-align: left; padding: 10px; background: var(--bg-input); color: var(--text-dim); font-weight: 600; }
|
||||||
|
td { padding: 10px; border-bottom: 1px solid var(--bg-input); }
|
||||||
|
tr:hover { background: rgba(255,255,255,0.02); }
|
||||||
|
|
||||||
|
.amount { font-weight: bold; color: var(--accent); }
|
||||||
|
.amount.negative { color: var(--danger); }
|
||||||
|
.status { padding: 3px 8px; border-radius: 10px; font-size: 0.75em; }
|
||||||
|
.status.pending { background: rgba(255,200,0,0.2); color: #ffc800; }
|
||||||
|
.status.sent { background: rgba(0,255,136,0.2); color: var(--success); }
|
||||||
|
|
||||||
|
/* Filters */
|
||||||
|
.filters { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px; }
|
||||||
|
.filters select, .filters input { padding: 8px 12px; background: var(--bg-input); border: 2px solid transparent; border-radius: 8px; color: var(--text); }
|
||||||
|
|
||||||
|
/* Month selector */
|
||||||
|
.month-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||||
|
.month-nav button { background: var(--bg-card); border: none; padding: 8px 15px; border-radius: 8px; color: var(--text); cursor: pointer; }
|
||||||
|
.month-nav button:hover { background: var(--bg-input); }
|
||||||
|
.month-title { font-size: 1.2em; font-weight: bold; }
|
||||||
|
|
||||||
|
/* Top spenders */
|
||||||
|
.top-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.top-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: var(--bg-input); border-radius: 8px; }
|
||||||
|
.top-item .name { font-weight: 600; }
|
||||||
|
.top-item .amount { font-weight: bold; }
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
.progress-container { margin-top: 10px; }
|
||||||
|
.progress-bar { height: 8px; background: var(--bg-input); border-radius: 4px; overflow: hidden; }
|
||||||
|
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
|
||||||
|
.progress-fill.good { background: var(--success); }
|
||||||
|
.progress-fill.warning { background: #ffc800; }
|
||||||
|
.progress-fill.danger { background: var(--danger); }
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.btn { padding: 10px 20px; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; }
|
||||||
|
.btn-primary { background: linear-gradient(135deg, var(--accent), #0099cc); color: var(--bg-dark); }
|
||||||
|
.btn-export { background: linear-gradient(135deg, #9944cc, #6622aa); color: #fff; }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.stats-grid, .charts-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📊 Dashboard Dépenses</h1>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
<a href="?page=saisie" id="nav-saisie">✏️ Saisie</a>
|
||||||
|
<a href="?page=dashboard" id="nav-dashboard" class="active">📊 Dashboard</a>
|
||||||
|
<a href="?page=config" id="nav-config">⚙️ Config</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Month Navigation -->
|
||||||
|
<div class="month-nav">
|
||||||
|
<button onclick="changeMonth(-1)">◀</button>
|
||||||
|
<span class="month-title" id="month-title">Février 2026</span>
|
||||||
|
<button onclick="changeMonth(1)">▶</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Overview -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="label">Total mois</div>
|
||||||
|
<div class="value" id="stat-total">0€</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="label">Moyenne/jour</div>
|
||||||
|
<div class="value" id="stat-avg">0€</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="label">Nb dépenses</div>
|
||||||
|
<div class="value" id="stat-count">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="label">Budget restant</div>
|
||||||
|
<div class="value" id="stat-budget">0€</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Budget Progress -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>💰 Budget Mensuel (500€)</h2>
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="budget-bar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between;margin-top:5px;font-size:0.8em;color:var(--text-dim)">
|
||||||
|
<span id="budget-spent">0€ dépensés</span>
|
||||||
|
<span id="budget-remaining">500€ restants</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts -->
|
||||||
|
<div class="charts-grid">
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>📈 Évolution quotidienne</h3>
|
||||||
|
<canvas id="chart-daily"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<h3>👥 Par personne</h3>
|
||||||
|
<canvas id="chart-person"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Spenders -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>🏆 Top Dépenseurs</h2>
|
||||||
|
<div class="top-list" id="top-spenders"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Expenses -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>💳 Dernières dépenses</h2>
|
||||||
|
<div class="table-container">
|
||||||
|
<table id="expenses-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Personne</th>
|
||||||
|
<th>Libellé</th>
|
||||||
|
<th>Montant</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="expenses-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-export" onclick="exportCSV()">📥 Exporter CSV</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var allDepenses = [];
|
||||||
|
var currentMonth = new Date();
|
||||||
|
|
||||||
|
function formatDate(d) { if(!d) return ""; return d.split("-").reverse().join("/"); }
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
var r = await fetch('/api/depenses');
|
||||||
|
allDepenses = await r.json();
|
||||||
|
updateDashboard();
|
||||||
|
checkPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPage() {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
var page = params.get('page') || 'saisie';
|
||||||
|
showPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPage(page) {
|
||||||
|
document.getElementById('nav-saisie').className = page === 'saisie' ? 'nav a' : 'nav a';
|
||||||
|
document.getElementById('nav-dashboard').className = page === 'dashboard' ? 'nav a active' : 'nav a';
|
||||||
|
document.getElementById('nav-config').className = page === 'config' ? 'nav a active' : 'nav a';
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMonth(delta) {
|
||||||
|
currentMonth.setMonth(currentMonth.getMonth() + delta);
|
||||||
|
updateDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthKey(date) {
|
||||||
|
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByMonth(depenses, date) {
|
||||||
|
var monthKey = getMonthKey(date);
|
||||||
|
return depenses.filter(function(d) {
|
||||||
|
return d.date && d.date.startsWith(monthKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDashboard() {
|
||||||
|
var monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||||
|
document.getElementById('month-title').textContent = monthNames[currentMonth.getMonth()] + ' ' + currentMonth.getFullYear();
|
||||||
|
|
||||||
|
var monthDepenses = filterByMonth(allDepenses, currentMonth);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
var total = monthDepenses.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
|
||||||
|
var count = monthDepenses.length;
|
||||||
|
var daysInMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate();
|
||||||
|
var avg = count > 0 ? total / daysInMonth : 0;
|
||||||
|
var budget = 500;
|
||||||
|
var remaining = budget - total;
|
||||||
|
|
||||||
|
document.getElementById('stat-total').textContent = total.toFixed(2) + '€';
|
||||||
|
document.getElementById('stat-total').className = 'value ' + (total > budget ? 'danger' : '');
|
||||||
|
document.getElementById('stat-avg').textContent = avg.toFixed(2) + '€';
|
||||||
|
document.getElementById('stat-count').textContent = count;
|
||||||
|
document.getElementById('stat-budget').textContent = remaining.toFixed(2) + '€';
|
||||||
|
document.getElementById('stat-budget').className = 'value ' + (remaining < 0 ? 'danger' : 'success');
|
||||||
|
|
||||||
|
// Budget bar
|
||||||
|
var percent = Math.min((total / budget) * 100, 100);
|
||||||
|
var bar = document.getElementById('budget-bar');
|
||||||
|
bar.style.width = percent + '%';
|
||||||
|
bar.className = 'progress-fill ' + (percent > 90 ? 'danger' : percent > 70 ? 'warning' : 'good');
|
||||||
|
document.getElementById('budget-spent').textContent = total.toFixed(2) + '€ dépensés';
|
||||||
|
document.getElementById('budget-remaining').textContent = Math.max(0, remaining).toFixed(2) + '€ restants';
|
||||||
|
|
||||||
|
// Charts
|
||||||
|
renderDailyChart(monthDepenses);
|
||||||
|
renderPersonChart(monthDepenses);
|
||||||
|
|
||||||
|
// Top spenders
|
||||||
|
var byPerson = {};
|
||||||
|
monthDepenses.forEach(function(d) {
|
||||||
|
byPerson[d.prenom] = (byPerson[d.prenom] || 0) + (parseFloat(d.montant) || 0);
|
||||||
|
});
|
||||||
|
var topList = Object.entries(byPerson).sort(function(a, b) { return b[1] - a[1]; });
|
||||||
|
var html = '';
|
||||||
|
topList.forEach(function(entry) {
|
||||||
|
html += '<div class="top-item">';
|
||||||
|
html += '<span class="name">' + entry[0] + '</span>';
|
||||||
|
html += '<span class="amount" style="color:var(--accent)">' + entry[1].toFixed(2) + '€</span>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
document.getElementById('top-spenders').innerHTML = html || '<p style="color:var(--text-dim)">Aucune dépense</p>';
|
||||||
|
|
||||||
|
// Table
|
||||||
|
var sorted = monthDepenses.slice().sort(function(a, b) { return b.date.localeCompare(a.date); });
|
||||||
|
html = '';
|
||||||
|
sorted.forEach(function(d) {
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td>' + formatDate(d.date) + '</td>';
|
||||||
|
html += '<td>' + d.prenom + '</td>';
|
||||||
|
html += '<td>' + d.libelle + '</td>';
|
||||||
|
html += '<td class="amount">' + parseFloat(d.montant).toFixed(2) + '€</td>';
|
||||||
|
var statusClass = d.status === 'Envoyé ✅' ? 'sent' : 'pending';
|
||||||
|
html += '<td><span class="status ' + statusClass + '">' + (d.status || 'En attente') + '</span></td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
document.getElementById('expenses-tbody').innerHTML = html || '<tr><td colspan="5" style="text-align:center;color:var(--text-dim)">Aucune dépense ce mois</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var dailyChart, personChart;
|
||||||
|
|
||||||
|
function renderDailyChart(depenses) {
|
||||||
|
var days = {};
|
||||||
|
var maxDay = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate();
|
||||||
|
for (var i = 1; i <= maxDay; i++) { days[i] = 0; }
|
||||||
|
|
||||||
|
depenses.forEach(function(d) {
|
||||||
|
if (d.date) {
|
||||||
|
var day = parseInt(d.date.split('-')[2]);
|
||||||
|
days[day] = (days[day] || 0) + (parseFloat(d.montant) || 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var labels = [];
|
||||||
|
var data = [];
|
||||||
|
for (var i = 1; i <= maxDay; i++) {
|
||||||
|
labels.push(i);
|
||||||
|
data.push(days[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx = document.getElementById('chart-daily').getContext('2d');
|
||||||
|
if (dailyChart) dailyChart.destroy();
|
||||||
|
dailyChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Dépenses',
|
||||||
|
data: data,
|
||||||
|
borderColor: '#00d9ff',
|
||||||
|
backgroundColor: 'rgba(0, 217, 255, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { color: '#1a1a35' }, ticks: { color: '#888' } },
|
||||||
|
y: { grid: { color: '#1a1a35' }, ticks: { color: '#888' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPersonChart(depenses) {
|
||||||
|
var byPerson = {};
|
||||||
|
depenses.forEach(function(d) {
|
||||||
|
byPerson[d.prenom] = (byPerson[d.prenom] || 0) + (parseFloat(d.montant) || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var ctx = document.getElementById('chart-person').getContext('2d');
|
||||||
|
if (personChart) personChart.destroy();
|
||||||
|
personChart = new Chart(ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: Object.keys(byPerson),
|
||||||
|
datasets: [{
|
||||||
|
data: Object.values(byPerson),
|
||||||
|
backgroundColor: ['#e94560', '#7b2cbf', '#00d9ff', '#00ff88', '#ffc800']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { position: 'bottom', labels: { color: '#fff' } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCSV() {
|
||||||
|
window.location.href = '/api/export';
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
|
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a href="?page=saisie" id="nav-saisie">✏️ Saisie</a>
|
<a href="?page=saisie" id="nav-saisie">✏️ Saisie</a>
|
||||||
|
<a href="/dashboard" id="nav-dashboard">📊 Dashboard</a>
|
||||||
<a href="?page=config" id="nav-config">⚙️ Config</a>
|
<a href="?page=config" id="nav-config">⚙️ Config</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user