diff --git a/app.py b/app.py index a4ab68c..79224b0 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 -from flask import Flask, request, jsonify, redirect +from flask import Flask, request, jsonify, redirect, send_file import json import os import requests +import csv +import io app = Flask(__name__) CONFIG_FILE = '/home/h3r7/depenses_trello/config.json' @@ -73,6 +75,73 @@ def clear_depenses(): save_config(config) return jsonify({'success': True}) +# Export CSV +@app.route('/api/export') +def export_csv(): + config = load_config() + depenses = config.get('depenses', []) + + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(['prenom', 'date', 'libelle', 'montant', 'status']) + + for d in depenses: + writer.writerow([ + d.get('prenom', ''), + d.get('date', ''), + d.get('libelle', ''), + d.get('montant', 0), + d.get('status', '') + ]) + + output.seek(0) + return send_file( + io.BytesIO(output.getvalue().encode('utf-8')), + mimetype='text/csv', + as_attachment=True, + download_name='depenses_2026.csv' + ) + +# Import CSV +@app.route('/api/import', methods=['POST']) +def import_csv(): + if 'file' not in request.files: + return jsonify({'error': 'Aucun fichier'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'Fichier vide'}), 400 + + try: + content = file.read().decode('utf-8') + reader = csv.reader(content.splitlines()) + next(reader) # Skip header + + config = load_config() + if 'depenses' not in config: + config['depenses'] = [] + + max_id = max([d.get('id', 0) for d in config.get('depenses', [])], default=0) + + imported = 0 + for row in reader: + if len(row) >= 4: + max_id += 1 + config['depenses'].append({ + 'id': max_id, + 'prenom': row[0].strip(), + 'date': row[1].strip(), + 'libelle': row[2].strip(), + 'montant': float(row[3]) if row[3] else 0, + 'status': row[4].strip() if len(row) > 4 else 'En attente' + }) + imported += 1 + + save_config(config) + return jsonify({'success': True, 'imported': imported}) + except Exception as e: + return jsonify({'error': str(e)}), 400 + # Generate text for ONE expense @app.route('/api/generate_one/', methods=['GET']) def generate_one(id): @@ -104,7 +173,6 @@ def send_one(id): if not t.get('api_key') or not t.get('token') or not t.get('list_id'): return jsonify({'error': 'Config Trello manquante'}), 400 - # Find the expense d = None for exp in config.get('depenses', []): if exp.get('id') == id: @@ -114,11 +182,9 @@ def send_one(id): if not d: return jsonify({'error': 'Dépense non trouvée'}), 404 - # Skip if already sent if d.get('status') == 'Envoyé ✅': return jsonify({'error': 'Déjà envoyé'}), 400 - # Generate text template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') line = template ddate = d.get('date', '') or '' @@ -129,7 +195,6 @@ def send_one(id): line = line.replace('{libelle}', d.get('libelle', '')) line = line.replace('{montant}', str(d.get('montant', 0))) - # Send to Trello url = 'https://api.trello.com/1/cards' params = { 'key': t.get('api_key'), @@ -141,7 +206,6 @@ def send_one(id): try: r = requests.post(url, params=params) if r.status_code == 200: - # Mark as sent for exp in config.get('depenses', []): if exp.get('id') == id: exp['status'] = 'Envoyé ✅' @@ -159,11 +223,9 @@ def send_trello(): if not t.get('api_key') or not t.get('token') or not t.get('list_id'): return jsonify({'error': 'Config Trello manquante'}), 400 - # Send only pending expenses sent_count = 0 for d in config.get('depenses', []): if d.get('status') == 'En attente': - # Generate text template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') line = template ddate = d.get('date', '') or '' @@ -174,7 +236,6 @@ def send_trello(): line = line.replace('{libelle}', d.get('libelle', '')) line = line.replace('{montant}', str(d.get('montant', 0))) - # Send to Trello url = 'https://api.trello.com/1/cards' params = { 'key': t.get('api_key'), diff --git a/config.json b/config.json index c8e2402..a22a890 100644 --- a/config.json +++ b/config.json @@ -1,9 +1,4 @@ { - "prenoms": [ - "HERY", - "BINOU" - ], - "format": "{prenom} - {date} - {libelle} - {montant}\u20ac", "depenses": [ { "id": 1, @@ -25,7 +20,7 @@ "id": 3, "prenom": "HERY", "date": "2026-02-17", - "libelle": "ALIEXPRESS BOITIER AMPOUL REGLABLE", + "libelle": "ALIEXPRESS BOITIER AMPOULE REGLABLE", "montant": 22.43, "status": "Envoy\u00e9 \u2705" }, @@ -33,7 +28,7 @@ "id": 4, "prenom": "HERY", "date": "2026-02-24", - "libelle": "LIDL GAUFFRIER", + "IDL GAUFFlibelle": "LRIER", "montant": 24.98, "status": "Envoy\u00e9 \u2705" }, @@ -49,7 +44,7 @@ "id": 6, "prenom": "HERY", "date": "2026-02-02", - "libelle": "CB LCL GAZOLE TOTAL LOOS ", + "libelle": "CB LCL GAZOLE TOTAL LOOS", "montant": 12.54, "status": "Envoy\u00e9 \u2705" }, @@ -81,7 +76,7 @@ "id": 10, "prenom": "HERY", "date": "2026-02-17", - "libelle": "CT CONTRE VISITE", + "libelle": "CT CONTRE VISITE", "montant": 25.0, "status": "Envoy\u00e9 \u2705" }, @@ -89,7 +84,7 @@ "id": 11, "prenom": "HERY", "date": "2026-02-16", - "libelle": "CONTROLE TECHNIQUE", + "libelle": "CONTROLE TECHNIQUE", "montant": 54.0, "status": "Envoy\u00e9 \u2705" }, @@ -105,7 +100,7 @@ "id": 13, "prenom": "HERY", "date": "2026-02-14", - "libelle": "LUMIERE TOILLETTES ", + "libelle": "LUMIERE TOILETTES", "montant": 14.78, "status": "Envoy\u00e9 \u2705" }, @@ -113,14 +108,14 @@ "id": 14, "prenom": "HERY", "date": "2026-02-09", - "libelle": "COMPTOIRE AFRIQ WAZEMME", + "libelle": "COMPTOIRE AFRIQ WAZEMME", "montant": 9.4, "status": "Envoy\u00e9 \u2705" }, { "id": 15, "prenom": "HERY", - "date": "2026-02-7", + "date": "2026-02-07", "libelle": "BOUCHERIE LILLE", "montant": 35.02, "status": "Envoy\u00e9 \u2705" @@ -142,6 +137,10 @@ "status": "Envoy\u00e9 \u2705" } ], + "format": "{prenom} - {date} - {libelle} - {montant}\u20ac", + "prenoms": [ + "HERY" + ], "trello": { "api_key": "0e86fa879fe9cc1bd5bff8a4b32bceff", "token": "ATTA55718bc108ac05e2828a9406b9a0bf843dc47e958bd659d215210a7b0726bd6f4C4A53CE", diff --git a/templates/index.html b/templates/index.html index c62721b..ece38ea 100644 --- a/templates/index.html +++ b/templates/index.html @@ -11,7 +11,7 @@ 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: 12 var(--bg-cardpx; background:); 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; } .card { background: var(--bg-card); border-radius: 15px; padding: 15px; margin-bottom: 15px; } h2 { font-size: 1em; margin-bottom: 12px; } @@ -25,6 +25,7 @@ .btn-success { background: linear-gradient(135deg, var(--success), #00cc66); color: var(--bg-dark); } .btn-danger { background: var(--danger); color: #fff; } .btn-send { background: linear-gradient(135deg, #0088cc, #006699); 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-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; } @@ -37,6 +38,9 @@ .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-row { display: flex; gap: 10px; } + .actions-row .btn { margin-top: 0; } + .file-input { display: none; } @@ -64,6 +68,15 @@
Total: 0.00€
+
+ + +
+
+ + + +
@@ -130,24 +143,20 @@ } function render() { - // Prenoms document.getElementById('prenom-select').innerHTML = '' + config.prenoms.map(function(p) { return ''; }).join(''); document.getElementById('prenom-tags').innerHTML = config.prenoms.map(function(p, i) { return '' + p + '×'; }).join(''); - // Format document.getElementById('format-input').value = config.format || '{prenom} - {date} - {libelle} - {montant}€'; var fmt = config.format || '{prenom} - {date} - {libelle} - {montant}€'; fmt = fmt.replace('{prenom}', 'Papa').replace('{date}', '27/02/2026').replace('{libelle}', 'Courses').replace('{montant}', '45.50'); document.getElementById('format-preview').textContent = fmt; - // Trello if (config.trello) { document.getElementById('trello-api_key').value = config.trello.api_key || ''; document.getElementById('trello-token').value = config.trello.token || ''; document.getElementById('trello-list_id').value = config.trello.list_id || ''; } - // Expenses document.getElementById('count').textContent = depenses.length; var total = depenses.reduce(function(s, d) { return s + (parseFloat(d.montant) || 0); }, 0); document.getElementById('total').textContent = total.toFixed(2) + '€'; @@ -180,6 +189,30 @@ document.getElementById('depense-date').value = dd + '/' + mm + '/' + yyyy; } + function exportCSV() { + window.location.href = '/api/export'; + } + + 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) { + alert('Importé: ' + d.imported + ' dépenses!'); + load(); + } else { + alert('Erreur: ' + d.error); + } + }); + input.value = ''; + } + async function sendOne(id) { if (!config.trello || !config.trello.api_key) { alert('Configure Trello!'); showPage('config'); return; } var r = await fetch('/api/trello/send_one/' + id, { method: 'POST' });