v1.4 - Catégories auto + Budget + Récurrent + PDF + Tests

This commit is contained in:
h3r7
2026-03-01 07:31:05 +01:00
parent 38616e6823
commit 31e5a6bc29
4 changed files with 384 additions and 84 deletions

Binary file not shown.

88
app.py
View File

@@ -80,11 +80,17 @@ def get_config():
import json import json
trello = json.loads(trello_row[0]) trello = json.loads(trello_row[0])
# Get categories
c.execute("SELECT value FROM config WHERE key='categories'")
cat_row = c.fetchone()
categories = cat_row[0].split(',') if cat_row else ['Courses', 'Essence', 'Loisirs', 'Maison', 'Santé', 'Transport', 'Autre']
conn.close() conn.close()
return jsonify({ return jsonify({
'format': format_val, 'format': format_val,
'prenoms': prenoms, 'prenoms': prenoms,
'categories': categories,
'trello': trello 'trello': trello
}) })
@@ -93,9 +99,9 @@ def get_config():
def add_depense(): def add_depense():
conn = get_db() conn = get_db()
c = conn.cursor() c = conn.cursor()
c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status) VALUES (?, ?, ?, ?, ?)', c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status, category) VALUES (?, ?, ?, ?, ?, ?)',
(request.form.get('prenom'), parse_date(request.form.get('date', '')), (request.form.get('prenom'), parse_date(request.form.get('date', '')),
request.form.get('libelle'), float(request.form.get('montant', 0)), 'En attente')) request.form.get('libelle'), float(request.form.get('montant', 0)), 'En attente', request.form.get('category', 'Autre')))
conn.commit() conn.commit()
conn.close() conn.close()
return redirect('/?page=saisie') return redirect('/?page=saisie')
@@ -105,7 +111,7 @@ def add_depense():
def get_depenses(): def get_depenses():
conn = get_db() conn = get_db()
c = conn.cursor() c = conn.cursor()
c.execute('SELECT id, prenom, date, libelle, montant, status FROM depenses ORDER BY date DESC') c.execute('SELECT id, prenom, date, libelle, montant, status, category FROM depenses ORDER BY date DESC')
rows = c.fetchall() rows = c.fetchall()
conn.close() conn.close()
return jsonify([dict(row) for row in rows]) return jsonify([dict(row) for row in rows])
@@ -326,5 +332,81 @@ 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

@@ -5,8 +5,10 @@
<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 Trello</title> <title>💸 Dépenses Trello</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=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.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; --success: #00ff88; --danger: #ff4757; --warning: #ffc800; }
* { 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; 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; } 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; }
@@ -14,58 +16,72 @@
.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; }
.nav a.active { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; } .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; } .card { background: var(--bg-card); border-radius: 15px; padding: 15px; margin-bottom: 15px; }
h2 { font-size: 1em; margin-bottom: 12px; } h2 { font-size: 1em; margin-bottom: 12px; color: var(--accent); }
.form-group { margin-bottom: 10px; } .form-group { margin-bottom: 10px; }
.form-group label { display: block; color: var(--text-dim); font-size: 0.85em; margin-bottom: 4px; } .form-group label { display: block; color: var(--text-dim); font-size: 0.85em; margin-bottom: 4px; }
.form-group input, .form-group select { width: 100%; padding: 12px; background: var(--bg-input); border: 2px solid transparent; border-radius: 10px; color: var(--text); font-size: 1em; } .form-group input, .form-group select { width: 100%; padding: 12px; background: var(--bg-input); border: 2px solid transparent; border-radius: 10px; color: var(--text); font-size: 1em; }
.form-group input:focus { border-color: var(--accent); outline: none; } .btn { width: 100%; padding: 14px; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; }
.btn { width: 100%; padding: 14px; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; margin-top: 10px; }
.btn-sm { width: auto; padding: 8px 12px; font-size: 0.85em; } .btn-sm { width: auto; padding: 8px 12px; font-size: 0.85em; }
.btn-primary { background: linear-gradient(135deg, var(--accent), #0099cc); color: var(--bg-dark); } .btn-primary { background: linear-gradient(135deg, var(--accent), #0099cc); color: var(--bg-dark); }
.btn-success { background: linear-gradient(135deg, var(--success), #00cc66); color: var(--bg-dark); } .btn-success { background: linear-gradient(135deg, var(--success), #00cc66); color: var(--bg-dark); }
.btn-danger { background: var(--danger); color: #fff; } .btn-danger { background: var(--danger); color: #fff; }
.btn-send { background: linear-gradient(135deg, #0088cc, #006699); color: #fff; } .btn-warning { background: var(--warning); color: var(--bg-dark); }
.btn-export { background: linear-gradient(135deg, #9944cc, #6622aa); color: #fff; } .btn-export { background: linear-gradient(135deg, #9944cc, #6622aa); color: #fff; }
.prenom-tags { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; } .prenom-tags { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.prenom-tag { display: flex; align-items: center; gap: 5px; background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 8px 12px; border-radius: 20px; font-size: 0.85em; } .prenom-tag { display: flex; align-items: center; gap: 5px; background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 8px 12px; border-radius: 20px; font-size: 0.85em; }
.expense-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; background: var(--bg-input); border-radius: 10px; margin-bottom: 8px; flex-wrap: wrap; gap: 5px; } .expense-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; background: var(--bg-input); border-radius: 10px; margin-bottom: 8px; }
.expense-prenom { background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 4px 10px; border-radius: 15px; font-size: 0.75em; white-space: nowrap; } .expense-prenom { background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 4px 10px; border-radius: 15px; font-size: 0.75em; }
.expense-montant { font-weight: 700; color: var(--accent); } .expense-cat { background: var(--secondary); padding: 4px 8px; border-radius: 8px; font-size: 0.7em; }
.expense-status { font-size: 0.75em; padding: 3px 8px; border-radius: 10px; } .expense-montant { font-weight: bold; color: var(--accent); }
.status-pending { background: rgba(255,200,0,0.2); color: #ffc800; } .status { padding: 3px 8px; border-radius: 10px; font-size: 0.75em; }
.status-sent { background: rgba(0,255,136,0.2); color: var(--success); } .status.pending { background: rgba(255,200,0,0.2); color: #ffc800; }
.status.sent { background: rgba(0,255,136,0.2); color: var(--success); }
.total { text-align: right; font-size: 1.2em; margin: 10px 0; padding: 12px; background: rgba(0,217,255,0.1); border-radius: 10px; } .total { text-align: right; font-size: 1.2em; margin: 10px 0; padding: 12px; background: rgba(0,217,255,0.1); border-radius: 10px; }
.total span { font-weight: 700; color: var(--accent); }
.preview { background: var(--bg-input); padding: 12px; border-radius: 10px; white-space: pre-wrap; color: var(--success); font-family: monospace; margin-top: 10px; }
.actions { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; } .actions { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.actions-row { display: flex; gap: 10px; } .actions-row { display: flex; gap: 10px; }
.actions-row .btn { margin-top: 0; } .actions-row .btn { margin-top: 0; }
.file-input { display: none; } .file-input { display: none; }
.filter-bar { display: flex; gap: 10px; margin-bottom: 10px; overflow-x: auto; }
.filter-chip { padding: 8px 16px; background: var(--bg-input); border-radius: 20px; font-size: 0.85em; white-space: nowrap; cursor: pointer; }
.filter-chip.active { background: var(--primary); color: #fff; }
.budget-alert { background: rgba(255,71,87,0.2); border: 1px solid var(--danger); padding: 10px; border-radius: 10px; margin-bottom: 10px; display: none; }
.recurring-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: var(--bg-input); border-radius: 8px; margin-bottom: 8px; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; }
.modal-content { background: var(--bg-card); padding: 20px; border-radius: 15px; max-width: 400px; margin: 50px auto; }
.modal h2 { margin-bottom: 15px; }
.modal-buttons { display: flex; gap: 10px; margin-top: 15px; }
.chart-container { height: 200px; margin: 15px 0; }
</style> </style>
</head> </head>
<body> <body>
<h1>💸 Dépenses Trello</h1> <h1>💸 Dépenses Trello</h1>
<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="/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>
<div id="saisie" style="display:none"> <div id="saisie">
<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">
<div class="form-group"><label>👤 Prénom</label><select name="prenom" id="prenom-select" required></select></div> <div class="form-group"><label>👤 Prénom</label><select name="prenom" id="prenom-select" required></select></div>
<div class="form-group"><label>📂 Catégorie</label><select name="category" id="category-select"></select></div>
<div class="form-group"><label>📅 Date</label><input type="text" name="date" id="depense-date" placeholder="JJ/MM/AAAA" required></div> <div class="form-group"><label>📅 Date</label><input type="text" name="date" id="depense-date" placeholder="JJ/MM/AAAA" required></div>
<div class="form-group"><label>📝 Libellé</label><input type="text" name="libelle" placeholder="Courses, Essence..." required></div> <div class="form-group"><label>📝 Libellé</label><input type="text" name="libelle" id="libelle-input" placeholder="Courses, Essence..." required></div>
<div class="form-group"><label>💰 Montant (€)</label><input type="number" name="montant" placeholder="0.00" step="0.01" required></div> <div class="form-group"><label>💰 Montant (€)</label><input type="number" name="montant" placeholder="0.00" step="0.01" required></div>
<button type="submit" class="btn btn-primary"> Ajouter</button> <button type="submit" class="btn btn-primary">💾 Enregistrer</button>
<button type="button" class="btn btn-danger" onclick="resetForm()" style="margin-top:8px">🧹 Effacer</button>
</form> </form>
</div> </div>
<div class="card"> <div class="card">
<div id="budget-alert" class="budget-alert">⚠️ Attention: Vous avez depasse le budget mensuel!</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h2>📋 Dépenses (<span id="count">0</span>)</h2> <h2>📋 Dépenses (<span id="count">0</span>)</h2>
<button class="btn btn-sm" style="background:var(--secondary)" onclick="loadDepenses()">🔄</button>
</div>
<div class="filter-bar" id="filter-bar"></div>
<div id="expenses"></div> <div id="expenses"></div>
<div class="total">Total: <span id="total">0.00€</span></div> <div class="total">Total: <span id="total">0.00€</span></div>
<div class="actions"> <div class="actions">
@@ -74,16 +90,40 @@
<button class="btn btn-success" onclick="sendAll()">🟦 Tout envoyer</button> <button class="btn btn-success" onclick="sendAll()">🟦 Tout envoyer</button>
</div> </div>
<div class="actions-row"> <div class="actions-row">
<button class="btn btn-export" onclick="exportCSV()">📥 Exporter CSV</button> <button class="btn btn-export" onclick="exportCSV()">📥 CSV</button>
<button class="btn btn-export" onclick="document.getElementById('file-import').click()">📤 Importer CSV</button> <button class="btn btn-export" onclick="exportPDF()">📥 PDF</button>
<button class="btn btn-export" onclick="document.getElementById('file-import').click()">📤</button>
<input type="file" id="file-import" class="file-input" accept=".csv" onchange="importCSV(this)"> <input type="file" id="file-import" class="file-input" accept=".csv" onchange="importCSV(this)">
</div> </div>
<button class="btn btn-danger" onclick="clearAll()">🗑️ Tout effacer</button>
</div> </div>
</div> </div>
</div> </div>
<div id="config" style="display:none"> <div id="config" style="display:none">
<div class="card">
<h2>💰 Budget Mensuel</h2>
<form method="POST" action="/api/budget" id="form-budget">
<div class="form-group"><label>Budget (€)</label><input type="number" name="budget" id="budget-input" placeholder="0.00" step="0.01"></div>
<button type="submit" class="btn btn-primary">💾 Sauvegarder</button>
</form>
</div>
<div class="card">
<h2>🔄 Dépenses Récurrentes</h2>
<div id="recurring-list"></div>
<form method="POST" action="/api/recurring/add" id="form-recurring" style="margin-top:10px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="form-group"><label>Libellé</label><input type="text" name="libelle" placeholder="Loyer, EDF..." required></div>
<div class="form-group"><label>Montant (€)</label><input type="number" name="montant" placeholder="0.00" step="0.01" required></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="form-group"><label>Catégorie</label><select name="category" id="recurring-cat"></select></div>
<div class="form-group"><label>Jour du mois</label><input type="number" name="jour" value="1" min="1" max="31"></div>
</div>
<button type="submit" class="btn btn-primary"> Ajouter</button>
</form>
</div>
<div class="card"> <div class="card">
<h2>👥 Prénoms</h2> <h2>👥 Prénoms</h2>
<div id="prenom-tags"></div> <div id="prenom-tags"></div>
@@ -95,13 +135,23 @@
</form> </form>
</div> </div>
<div class="card">
<h2>📂 Catégories</h2>
<div id="category-tags"></div>
<form method="POST" action="/api/category/add" id="form-category">
<div style="display:flex;gap:10px;margin-top:10px">
<input type="text" name="category" placeholder="Nouvelle catégorie..." style="flex:1">
<button type="submit" class="btn btn-primary" style="width:auto"></button>
</div>
</form>
</div>
<div class="card"> <div class="card">
<h2>📄 Format du texte</h2> <h2>📄 Format du texte</h2>
<form method="POST" action="/api/format" id="form-format"> <form method="POST" action="/api/format" id="form-format">
<div class="form-group"><input type="text" name="format" id="format-input"></div> <div class="form-group"><input type="text" name="format" id="format-input"></div>
<button type="submit" class="btn btn-primary">💾 Sauvegarder</button> <button type="submit" class="btn btn-primary">💾 Sauvegarder</button>
</form> </form>
<div class="preview" id="format-preview"></div>
</div> </div>
<div class="card"> <div class="card">
@@ -115,19 +165,102 @@
</div> </div>
</div> </div>
<!-- Edit Modal -->
<div id="edit-modal" class="modal">
<div class="modal-content">
<h2>✏️ Modifier la dépense</h2>
<form id="form-edit">
<input type="hidden" id="edit-id">
<div class="form-group"><label>👤 Prénom</label><select name="prenom" id="edit-prenom"></select></div>
<div class="form-group"><label>📂 Catégorie</label><select name="category" id="edit-category"></select></div>
<div class="form-group"><label>📅 Date</label><input type="text" name="date" id="edit-date" placeholder="JJ/MM/AAAA"></div>
<div class="form-group"><label>📝 Libellé</label><input type="text" name="libelle" id="edit-libelle"></div>
<div class="form-group"><label>💰 Montant (€)</label><input type="number" name="montant" id="edit-montant" step="0.01"></div>
<div class="modal-buttons">
<button type="submit" class="btn btn-primary">💾 Sauvegarder</button>
<button type="button" class="btn btn-danger" onclick="closeModal()">Annuler</button>
</div>
</form>
</div>
</div>
<script> <script>
var config = { prenoms: [], format: '', trello: {} }; var config = { prenoms: [], categories: [], format: '', trello: {} };
var depenses = []; var depenses = [];
var budget = 0;
var filterCategory = '';
function formatDate(d) { if(!d) return ""; return d.split("-").reverse().join("/"); } function formatDate(d) { if(!d) return ""; return d.split("-").reverse().join("/"); }
function parseDate(d) { if(!d) return ""; var p = d.split("/"); if(p.length===3) return p[2]+"-"+p[1]+"-"+p[0]; return d; }
async function load() { async function load() {
var r = await fetch('/api/config'); var r = await fetch('/api/config');
config = await r.json(); config = await r.json();
r = await fetch('/api/depenses'); r = await fetch('/api/depenses');
depenses = await r.json(); depenses = await r.json();
r = await fetch('/api/budget');
budget = await r.json();
render(); render();
checkPage(); checkPage();
initAutoCategory();
renderFilters();
loadRecurring();
checkBudget();
}
function checkBudget() {
var now = new Date();
var currentMonth = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0');
var monthTotal = depenses.filter(function(d) { return d.date && d.date.startsWith(currentMonth); })
.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
var alert = document.getElementById('budget-alert');
if (budget.budget > 0 && monthTotal > budget.budget) {
alert.textContent = "⚠️ Attention: Vous avez depasse le budget mensuel! (" + monthTotal.toFixed(2) + "€ / " + budget.budget + "€)";
alert.style.display = 'block';
} else {
alert.style.display = 'none';
}
}
async function loadRecurring() {
var r = await fetch('/api/recurring');
var rec = await r.json();
var html = '';
for (var i = 0; i < rec.length; i++) {
html += '<div class="recurring-item">';
html += '<span>' + rec[i].libelle + '</span>';
html += '<span>' + rec[i].montant + '€ (' + rec[i].category + ') - jour ' + rec[i].jour + '</span>';
html += '<a href="/api/recurring/del/' + rec[i].id + '" style="color:var(--danger)">🗑️</a>';
html += '</div>';
}
document.getElementById('recurring-list').innerHTML = html || '<p style="color:var(--text-dim)">Aucune depense recurrente</p>';
}
function renderFilters() {
var cats = config.categories || ['Courses', 'Essence', 'Loisirs', 'Maison', 'Santé', 'Transport', 'Autre'];
var html = '<div class="filter-chip' + (filterCategory === '' ? ' active' : '') + '" onclick="setFilter(\'\')">Tous</div>';
for (var i = 0; i < cats.length; i++) {
html += '<div class="filter-chip' + (filterCategory === cats[i] ? ' active' : '') + '" onclick="setFilter(\'' + cats[i] + '\')">' + cats[i] + '</div>';
}
document.getElementById('filter-bar').innerHTML = html;
}
function setFilter(cat) {
filterCategory = cat;
renderFilters();
render();
}
function initAutoCategory() {
document.getElementById('libelle-input').addEventListener('input', function() {
var lib = this.value.toLowerCase();
var catSelect = document.getElementById('category-select');
if (lib.includes('carrefour') || lib.includes('courses') || lib.includes('lidl') || lib.includes('auchan') || lib.includes('boutique')) catSelect.value = 'Courses';
else if (lib.includes('essence') || lib.includes('carburant') || lib.includes('gazole') || lib.includes('total') || lib.includes('station')) catSelect.value = 'Transport';
else if (lib.includes('loto') || lib.includes('euromillion') || lib.includes('turf') || lib.includes('bar') || lib.includes('cafe') || lib.includes('cinéma')) catSelect.value = 'Loisirs';
else if (lib.includes('controle') || lib.includes('ct ') || lib.includes('médicament') || lib.includes('pharmacie') || lib.includes('dentiste')) catSelect.value = 'Santé';
else if (lib.includes('lumiere') || lib.includes('lampe') || lib.includes('brico') || lib.includes('maison') || lib.includes('ikea')) catSelect.value = 'Maison';
});
} }
function checkPage() { function checkPage() {
@@ -144,80 +277,100 @@
} }
function render() { function render() {
// Prenoms
document.getElementById('prenom-select').innerHTML = '<option value="">Choisir...</option>' + config.prenoms.map(function(p) { return '<option value="' + p + '">' + p + '</option>'; }).join(''); document.getElementById('prenom-select').innerHTML = '<option value="">Choisir...</option>' + config.prenoms.map(function(p) { return '<option value="' + p + '">' + p + '</option>'; }).join('');
document.getElementById('prenom-tags').innerHTML = config.prenoms.map(function(p, i) { return '<span class="prenom-tag">' + p + '<a href="/api/prenom/del/' + i + '" style="color:#fff;text-decoration:none;margin-left:5px">×</a></span>'; }).join(''); document.getElementById('prenom-tags').innerHTML = config.prenoms.map(function(p, i) { return '<span class="prenom-tag">' + p + '<a href="/api/prenom/del/' + i + '" style="color:#fff;text-decoration:none;margin-left:5px">×</a></span>'; }).join('');
document.getElementById('format-input').value = config.format || '{prenom} - {date} - {libelle} - {montant}€'; // Categories
var fmt = config.format || '{prenom} - {date} - {libelle} - {montant}€'; var cats = config.categories || ['Courses', 'Essence', 'Loisirs', 'Maison', 'Santé', 'Transport', 'Autre'];
fmt = fmt.replace('{prenom}', 'Papa').replace('{date}', '27/02/2026').replace('{libelle}', 'Courses').replace('{montant}', '45.50'); document.getElementById('category-select').innerHTML = cats.map(function(c) { return '<option value="' + c + '">' + c + '</option>'; }).join('');
document.getElementById('format-preview').textContent = fmt; document.getElementById('category-tags').innerHTML = cats.map(function(c, i) { return '<span class="prenom-tag">' + c + '<a href="/api/category/del/' + i + '" style="color:#fff;text-decoration:none;margin-left:5px">×</a></span>'; }).join('');
document.getElementById('recurring-cat').innerHTML = cats.map(function(c) { return '<option value="' + c + '">' + c + '</option>'; }).join('');
// Edit dropdowns
document.getElementById('edit-prenom').innerHTML = config.prenoms.map(function(p) { return '<option value="' + p + '">' + p + '</option>'; }).join('');
document.getElementById('edit-category').innerHTML = cats.map(function(c) { return '<option value="' + c + '">' + c + '</option>'; }).join('');
// Format
document.getElementById('format-input').value = config.format || '{prenom} - {date} - {libelle} - {montant}€';
// Budget
document.getElementById('budget-input').value = budget.budget || '';
// Trello
if (config.trello) { if (config.trello) {
document.getElementById('trello-api_key').value = config.trello.api_key || ''; document.getElementById('trello-api_key').value = config.trello.api_key || '';
document.getElementById('trello-token').value = config.trello.token || ''; document.getElementById('trello-token').value = config.trello.token || '';
document.getElementById('trello-list_id').value = config.trello.list_id || ''; document.getElementById('trello-list_id').value = config.trello.list_id || '';
} }
document.getElementById('count').textContent = depenses.length; // Expenses (with filter)
var total = depenses.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0); var filtered = filterCategory ? depenses.filter(function(d) { return d.category === filterCategory; }) : depenses;
document.getElementById('count').textContent = filtered.length;
var total = filtered.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
document.getElementById('total').textContent = total.toFixed(2) + '€'; document.getElementById('total').textContent = total.toFixed(2) + '€';
var html = ''; var html = '';
for (var i = 0; i < depenses.length; i++) { for (var i = 0; i < filtered.length; i++) {
var d = depenses[i]; var d = filtered[i];
var isSent = d.status === 'Envoyé ✅'; var isSent = d.status === 'Envoyé ✅';
var statusClass = isSent ? 'status-sent' : 'status-pending'; var statusClass = isSent ? 'sent' : 'pending';
var sendBtn = isSent ? '' : '<button class="btn btn-sm btn-send" onclick="sendOne(' + d.id + ')">🟦</button>'; var sendBtn = isSent ? '' : '<button class="btn btn-sm" style="background:#0088cc;padding:6px 10px" onclick="sendOne(' + d.id + ')">🟦</button>';
html += '<div class="expense-item">'; html += '<div class="expense-item">';
html += '<div style="display:flex;align-items:center;gap:10px">'; html += '<div style="display:flex;flex-direction:column;gap:5px">';
html += '<span class="expense-prenom">' + d.prenom + '</span>'; html += '<span class="expense-prenom">' + d.prenom + '</span>';
html += '<span>' + formatDate(d.date) + ' - ' + d.libelle + '</span>'; html += '<span class="expense-cat">' + (d.category || 'Autre') + '</span>';
html += '</div>'; html += '</div>';
html += '<div style="flex:1;padding:0 10px">' + formatDate(d.date) + ' - ' + d.libelle + '</div>';
html += '<div style="display:flex;align-items:center;gap:10px">'; html += '<div style="display:flex;align-items:center;gap:10px">';
html += '<span class="expense-status ' + statusClass + '">' + (d.status || 'En attente') + '</span>'; html += '<span class="status ' + statusClass + '">' + (d.status || 'En attente') + '</span>';
html += '<span class="expense-montant">' + parseFloat(d.montant).toFixed(2) + '€</span>'; html += '<span class="expense-montant">' + parseFloat(d.montant).toFixed(2) + '€</span>';
html += sendBtn; html += sendBtn;
html += '<button class="btn btn-sm" style="background:#ffc800;padding:6px 10px" onclick="editDepense(' + d.id + ')">✏️</button>';
html += '<a href="/api/del/' + d.id + '" style="color:var(--danger);text-decoration:none;font-size:1.2em">🗑️</a>'; html += '<a href="/api/del/' + d.id + '" style="color:var(--danger);text-decoration:none;font-size:1.2em">🗑️</a>';
html += '</div></div>'; html += '</div></div>';
} }
document.getElementById('expenses').innerHTML = html; document.getElementById('expenses').innerHTML = html;
var today = new Date(); var today = new Date();
var dd = String(today.getDate()).padStart(2, '0'); document.getElementById('depense-date').value = String(today.getDate()).padStart(2, '0') + '/' + String(today.getMonth() + 1).padStart(2, '0') + '/' + today.getFullYear();
var mm = String(today.getMonth() + 1).padStart(2, '0');
var yyyy = today.getFullYear();
document.getElementById('depense-date').value = dd + '/' + mm + '/' + yyyy;
} }
function exportCSV() { function editDepense(id) {
window.location.href = '/api/export'; var d = depenses.find(function(x) { return x.id === id; });
if (!d) return;
document.getElementById('edit-id').value = id;
document.getElementById('edit-prenom').value = d.prenom;
document.getElementById('edit-category').value = d.category || 'Autre';
document.getElementById('edit-date').value = formatDate(d.date);
document.getElementById('edit-libelle').value = d.libelle;
document.getElementById('edit-montant').value = d.montant;
document.getElementById('edit-modal').style.display = 'block';
} }
function importCSV(input) { function closeModal() {
var file = input.files[0]; document.getElementById('edit-modal').style.display = 'none';
if (!file) return;
var fd = new FormData();
fd.append('file', file);
fetch('/api/import', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) {
alert('Importé: ' + d.imported + ' dépenses!');
load();
} else {
alert('Erreur: ' + d.error);
} }
function resetForm() {
document.getElementById('form-add').reset();
var today = new Date();
document.getElementById('depense-date').value = String(today.getDate()).padStart(2, '0') + '/' + String(today.getMonth() + 1).padStart(2, '0') + '/' + today.getFullYear();
}
function loadDepenses() {
fetch('/api/depenses').then(function(r) { return r.json(); }).then(function(d) {
depenses = d;
render();
checkBudget();
}); });
input.value = '';
} }
async function sendOne(id) { async function sendOne(id) {
if (!config.trello || !config.trello.api_key) { alert('Configure Trello!'); showPage('config'); return; } if (!config.trello || !config.trello.api_key) { alert('Configure Trello!'); showPage('config'); return; }
var r = await fetch('/api/trello/send_one/' + id, { method: 'POST' }); var r = await fetch('/api/trello/send_one/' + id, { method: 'POST' });
if (r.ok) { alert('Envoyé sur Trello!'); load(); } if (r.ok) { loadDepenses(); }
else { var e = await r.json(); alert('Erreur: ' + e.error); } else { var e = await r.json(); alert('Erreur: ' + e.error); }
} }
@@ -226,35 +379,59 @@
var fd = new FormData(e.target); var fd = new FormData(e.target);
fetch('/api/add', { method: 'POST', body: fd }).then(function() { fetch('/api/add', { method: 'POST', body: fd }).then(function() {
e.target.reset(); e.target.reset();
load(); loadDepenses();
});
};
document.getElementById('form-edit').onsubmit = function(e) {
e.preventDefault();
var id = document.getElementById('edit-id').value;
var fd = new FormData();
fd.append('prenom', document.getElementById('edit-prenom').value);
fd.append('category', document.getElementById('edit-category').value);
fd.append('date', document.getElementById('edit-date').value);
fd.append('libelle', document.getElementById('edit-libelle').value);
fd.append('montant', document.getElementById('edit-montant').value);
fetch('/api/update/' + id, { method: 'POST', body: fd }).then(function() {
closeModal();
loadDepenses();
}); });
}; };
document.getElementById('form-prenom').onsubmit = function(e) { document.getElementById('form-prenom').onsubmit = function(e) {
e.preventDefault(); e.preventDefault();
var fd = new FormData(e.target); var fd = new FormData(e.target);
fetch('/api/prenom/add', { method: 'POST', body: fd }).then(function() { fetch('/api/prenom/add', { method: 'POST', body: fd }).then(function() { load(); });
e.target.reset(); };
load();
}); document.getElementById('form-category').onsubmit = function(e) {
e.preventDefault();
var fd = new FormData(e.target);
fetch('/api/category/add', { method: 'POST', body: fd }).then(function() { load(); });
}; };
document.getElementById('form-format').onsubmit = function(e) { document.getElementById('form-format').onsubmit = function(e) {
e.preventDefault(); e.preventDefault();
var fd = new FormData(e.target); var fd = new FormData(e.target);
fetch('/api/format', { method: 'POST', body: fd }).then(function() { fetch('/api/format', { method: 'POST', body: fd }).then(function() { load(); });
alert('Format sauvegardé!');
load();
});
}; };
document.getElementById('form-trello').onsubmit = function(e) { document.getElementById('form-trello').onsubmit = function(e) {
e.preventDefault(); e.preventDefault();
var fd = new FormData(e.target); var fd = new FormData(e.target);
fetch('/api/trello/save', { method: 'POST', body: fd }).then(function() { fetch('/api/trello/save', { method: 'POST', body: fd }).then(function() { load(); });
alert('Config Trello sauvegardée!'); };
load();
}); document.getElementById('form-budget').onsubmit = function(e) {
e.preventDefault();
var fd = new FormData(e.target);
fetch('/api/budget', { method: 'POST', body: fd }).then(function() { load(); });
};
document.getElementById('form-recurring').onsubmit = function(e) {
e.preventDefault();
var fd = new FormData(e.target);
fetch('/api/recurring/add', { method: 'POST', body: fd }).then(function() { load(); });
}; };
async function generate() { async function generate() {
@@ -272,15 +449,56 @@
if (r.ok) sent++; if (r.ok) sent++;
} }
} }
alert('Envoyé(s): ' + sent); alert('Envoye(s): ' + sent);
load(); loadDepenses();
} }
async function clearAll() { function exportCSV() { window.location.href = '/api/export'; }
if (confirm('Tout effacer?')) {
await fetch('/api/clear', { method: 'POST' }); function exportPDF() {
load(); var { jsPDF } = window.jspdf;
var doc = new jsPDF();
doc.setFontSize(18);
doc.text("Depenses Trello", 105, 20, { align: 'center' });
doc.setFontSize(10);
var y = 35;
doc.text("Date", 20, y);
doc.text("Personne", 50, y);
doc.text("Categorie", 80, y);
doc.text("Libelle", 110, y);
doc.text("Montant", 170, y);
y += 5;
doc.line(20, y, 190, y);
y += 5;
for (var i = 0; i < depenses.length; i++) {
var d = depenses[i];
if (y > 270) { doc.addPage(); y = 20; }
doc.text(formatDate(d.date), 20, y);
doc.text(d.prenom || '', 50, y);
doc.text(d.category || 'Autre', 80, y);
doc.text((d.libelle || '').substring(0, 25), 110, y);
doc.text(d.montant + ' eur', 170, y);
y += 8;
} }
var total = depenses.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0);
y += 5;
doc.line(20, y, 190, y);
y += 8;
doc.setFontSize(12);
doc.text("Total: " + total.toFixed(2) + " eur", 170, y);
doc.save('depenses_' + new Date().toISOString().split('T')[0] + '.pdf');
}
function importCSV(input) {
var file = input.files[0];
if (!file) return;
var fd = new FormData();
fd.append('file', file);
fetch('/api/import', { method: 'POST', body: fd }).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) { loadDepenses(); alert('Importe: ' + d.imported); }
else { alert('Erreur: ' + d.error); }
});
input.value = '';
} }
load(); load();