diff --git a/app.py b/app.py index 86e9c6e..f835085 100644 --- a/app.py +++ b/app.py @@ -1,21 +1,43 @@ #!/usr/bin/env python3 from flask import Flask, request, jsonify, redirect, send_file -import json +import sqlite3 import os import requests import csv import io app = Flask(__name__) -CONFIG_FILE = '/home/h3r7/depenses_trello/config.json' +DB_FILE = '/home/h3r7/depenses_trello/depenses.db' -def load_config(): - with open(CONFIG_FILE, 'r') as f: - return json.load(f) +def init_db(): + conn = sqlite3.connect(DB_FILE) + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS depenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prenom TEXT, + date TEXT, + libelle TEXT, + montant REAL, + status TEXT DEFAULT 'En attente', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )''') + c.execute('''CREATE TABLE IF NOT EXISTS config ( + key TEXT PRIMARY KEY, + value TEXT + )''') + c.execute('''CREATE TABLE IF NOT EXISTS prenoms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE + )''') + conn.commit() + conn.close() -def save_config(data): - with open(CONFIG_FILE, 'w') as f: - json.dump(data, f, indent=2) +def get_db(): + conn = sqlite3.connect(DB_FILE) + conn.row_factory = sqlite3.Row + return conn + +init_db() def parse_date(d): if not d: return "" @@ -38,66 +60,90 @@ def dashboard(): # API Config @app.route('/api/config') def get_config(): - return jsonify(load_config()) + conn = get_db() + c = conn.cursor() + + # Get prenoms + c.execute('SELECT name FROM prenoms') + prenoms = [row[0] for row in c.fetchall()] + + # Get format + c.execute("SELECT value FROM config WHERE key='format'") + format_row = c.fetchone() + format_val = format_row[0] if format_row else '{prenom} - {date} - {libelle} - {montant}€' + + # Get trello config + c.execute("SELECT value FROM config WHERE key='trello'") + trello_row = c.fetchone() + trello = {} + if trello_row: + import json + trello = json.loads(trello_row[0]) + + conn.close() + + return jsonify({ + 'format': format_val, + 'prenoms': prenoms, + 'trello': trello + }) # Add depense @app.route('/api/add', methods=['POST']) def add_depense(): - config = load_config() - data = { - 'id': len(config.get('depenses', [])) + 1, - 'prenom': request.form.get('prenom'), - 'date': parse_date(request.form.get('date', '')), - 'libelle': request.form.get('libelle'), - 'montant': float(request.form.get('montant', 0)), - 'status': 'En attente' - } - if 'depenses' not in config: - config['depenses'] = [] - config['depenses'].append(data) - save_config(config) + conn = get_db() + c = conn.cursor() + c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status) VALUES (?, ?, ?, ?, ?)', + (request.form.get('prenom'), parse_date(request.form.get('date', '')), + request.form.get('libelle'), float(request.form.get('montant', 0)), 'En attente')) + conn.commit() + conn.close() return redirect('/?page=saisie') # Get depenses @app.route('/api/depenses') def get_depenses(): - config = load_config() - return jsonify(config.get('depenses', [])) + conn = get_db() + c = conn.cursor() + c.execute('SELECT id, prenom, date, libelle, montant, status FROM depenses ORDER BY date DESC') + rows = c.fetchall() + conn.close() + return jsonify([dict(row) for row in rows]) # Delete depense @app.route('/api/del/') def delete_depense(id): - config = load_config() - config['depenses'] = [d for d in config.get('depenses', []) if d.get('id') != id] - save_config(config) + conn = get_db() + c = conn.cursor() + c.execute('DELETE FROM depenses WHERE id=?', (id,)) + conn.commit() + conn.close() return redirect('/?page=saisie') # Clear all @app.route('/api/clear', methods=['POST']) def clear_depenses(): - config = load_config() - config['depenses'] = [] - save_config(config) + conn = get_db() + c = conn.cursor() + c.execute('DELETE FROM depenses') + conn.commit() + conn.close() return jsonify({'success': True}) # Export CSV @app.route('/api/export') def export_csv(): - config = load_config() - depenses = config.get('depenses', []) + conn = get_db() + c = conn.cursor() + c.execute('SELECT prenom, date, libelle, montant, status FROM depenses ORDER BY date DESC') + rows = c.fetchall() + conn.close() 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', '') - ]) + for row in rows: + writer.writerow(row) output.seek(0) return send_file( @@ -120,86 +166,68 @@ def import_csv(): try: content = file.read().decode('utf-8') reader = csv.reader(content.splitlines()) - next(reader) # Skip header + next(reader) - 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) + conn = get_db() + c = conn.cursor() 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' - }) + c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status) VALUES (?, ?, ?, ?, ?)', + (row[0].strip(), row[1].strip(), row[2].strip(), + float(row[3]) if row[3] else 0, row[4].strip() if len(row) > 4 else 'En attente')) imported += 1 - save_config(config) + conn.commit() + conn.close() 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): - config = load_config() - d = None - for exp in config.get('depenses', []): - if exp.get('id') == id: - d = exp - break - if not d: - return jsonify({'text': ''}) - - template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') - line = template - ddate = d.get('date', '') or '' - if ddate: - ddate = '/'.join(ddate.split('-')[::-1]) - line = line.replace('{prenom}', d.get('prenom', '')) - line = line.replace('{date}', ddate) - line = line.replace('{libelle}', d.get('libelle', '')) - line = line.replace('{montant}', str(d.get('montant', 0))) - return jsonify({'text': line}) - -# Send to Trello - ONE card per pending expense +# Send ONE to Trello @app.route('/api/trello/send_one/', methods=['POST']) def send_one(id): - config = load_config() - t = config.get('trello', {}) - if not t.get('api_key') or not t.get('token') or not t.get('list_id'): + conn = get_db() + c = conn.cursor() + c.execute("SELECT value FROM config WHERE key='trello'") + trello_row = c.fetchone() + if not trello_row: + conn.close() return jsonify({'error': 'Config Trello manquante'}), 400 - d = None - for exp in config.get('depenses', []): - if exp.get('id') == id: - d = exp - break + import json + t = json.loads(trello_row[0]) + if not t.get('api_key') or not t.get('token') or not t.get('list_id'): + conn.close() + return jsonify({'error': 'Config Trello incomplète'}), 400 + c.execute('SELECT * FROM depenses WHERE id=?', (id,)) + d = c.fetchone() if not d: + conn.close() return jsonify({'error': 'Dépense non trouvée'}), 404 - if d.get('status') == 'Envoyé ✅': + if d['status'] == 'Envoyé ✅': + conn.close() return jsonify({'error': 'Déjà envoyé'}), 400 - template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') - line = template - ddate = d.get('date', '') or '' + # Generate text + c.execute("SELECT value FROM config WHERE key='format'") + format_row = c.fetchone() + template = format_row[0] if format_row else '{prenom} - {date} - {libelle} - {montant}€' + + ddate = d['date'] or '' if ddate: ddate = '/'.join(ddate.split('-')[::-1]) - line = line.replace('{prenom}', d.get('prenom', '')) - line = line.replace('{date}', ddate) - line = line.replace('{libelle}', d.get('libelle', '')) - line = line.replace('{montant}', str(d.get('montant', 0))) + line = template + line = line.replace('{prenom}', d['prenom'] or '') + line = line.replace('{date}', ddate) + line = line.replace('{libelle}', d['libelle'] or '') + line = line.replace('{montant}', str(d['montant'])) + + # Send to Trello url = 'https://api.trello.com/1/cards' params = { 'key': t.get('api_key'), @@ -208,64 +236,31 @@ def send_one(id): 'name': line, 'desc': line } + try: r = requests.post(url, params=params) if r.status_code == 200: - for exp in config.get('depenses', []): - if exp.get('id') == id: - exp['status'] = 'Envoyé ✅' - save_config(config) + c.execute('UPDATE depenses SET status=? WHERE id=?', ('Envoyé ✅', id)) + conn.commit() + conn.close() return jsonify({'success': True}) + conn.close() return jsonify({'error': r.text}), r.status_code except Exception as e: + conn.close() return jsonify({'error': str(e)}), 500 -# Legacy - send all (for compatibility) -@app.route('/api/trello/send', methods=['POST']) -def send_trello(): - config = load_config() - t = config.get('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 - - sent_count = 0 - for d in config.get('depenses', []): - if d.get('status') == 'En attente': - template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') - line = template - ddate = d.get('date', '') or '' - if ddate: - ddate = '/'.join(ddate.split('-')[::-1]) - line = line.replace('{prenom}', d.get('prenom', '')) - line = line.replace('{date}', ddate) - line = line.replace('{libelle}', d.get('libelle', '')) - line = line.replace('{montant}', str(d.get('montant', 0))) - - url = 'https://api.trello.com/1/cards' - params = { - 'key': t.get('api_key'), - 'token': t.get('token'), - 'idList': t.get('list_id'), - 'name': line, - 'desc': line - } - try: - r = requests.post(url, params=params) - if r.status_code == 200: - d['status'] = 'Envoyé ✅' - sent_count += 1 - except: - pass - - save_config(config) - return jsonify({'success': True, 'sent': sent_count}) - # Generate text (all) @app.route('/api/generate', methods=['POST']) def generate_text(): - config = load_config() + conn = get_db() + c = conn.cursor() + c.execute("SELECT value FROM config WHERE key='format'") + format_row = c.fetchone() + template = format_row[0] if format_row else '{prenom} - {date} - {libelle} - {montant}€' + conn.close() + data = request.json - template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') lines = [] for d in data.get('depenses', []): line = template @@ -282,42 +277,53 @@ def generate_text(): # Add prenom @app.route('/api/prenom/add', methods=['POST']) def add_prenom(): - config = load_config() + conn = get_db() + c = conn.cursor() prenom = request.form.get('prenom', '').strip() - if prenom and prenom not in config.get('prenoms', []): - if 'prenoms' not in config: - config['prenoms'] = [] - config['prenoms'].append(prenom) - save_config(config) + if prenom: + try: + c.execute('INSERT INTO prenoms (name) VALUES (?)', (prenom,)) + conn.commit() + except: + pass + conn.close() return redirect('/?page=config') # Del prenom @app.route('/api/prenom/del/') def del_prenom(idx): - config = load_config() - if 0 <= idx < len(config.get('prenoms', [])): - config['prenoms'].pop(idx) - save_config(config) + conn = get_db() + c = conn.cursor() + c.execute('DELETE FROM prenoms WHERE id=?', (idx+1,)) + conn.commit() + conn.close() return redirect('/?page=config') # Save format @app.route('/api/format', methods=['POST']) def save_format(): - config = load_config() - config['format'] = request.form.get('format', '{prenom} - {date} - {libelle} - {montant}€') - save_config(config) + conn = get_db() + c = conn.cursor() + format_val = request.form.get('format', '{prenom} - {date} - {libelle} - {montant}€') + c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('format', format_val)) + conn.commit() + conn.close() return redirect('/?page=config') # Save trello config @app.route('/api/trello/save', methods=['POST']) def save_trello(): - config = load_config() - config['trello'] = { + import json + conn = get_db() + c = conn.cursor() + t = json.dumps({ 'api_key': request.form.get('api_key', ''), 'token': request.form.get('token', ''), 'list_id': request.form.get('list_id', '') - } - save_config(config) + }) + c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('trello', t)) + conn.commit() + conn.close() return redirect('/?page=config') if __name__ == '__main__': diff --git a/config.json b/config.json index 8e0df81..f892d7e 100644 --- a/config.json +++ b/config.json @@ -1,151 +1,11 @@ { - "depenses": [ - { - "id": 1, - "prenom": "HERY", - "date": "2026-02-06", - "libelle": "ALIEXPRESS - PISTOLET MASSAGE", - "montant": 13.67, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 2, - "prenom": "HERY", - "date": "2026-02-10", - "libelle": "LUMINAIRE LAMPADAIRE", - "montant": 97.87, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 3, - "prenom": "HERY", - "date": "2026-02-17", - "libelle": "ALIEXPRESS BOITIER AMPOULE REGLABLE", - "montant": 22.43, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 4, - "prenom": "HERY", - "date": "2026-02-24", - "libelle": "LIDL GAUFFRIER", - "montant": 24.98, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 5, - "prenom": "HERY", - "date": "2026-02-11", - "libelle": "LEETCHI FABIENNE", - "montant": 30.0, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 6, - "prenom": "HERY", - "date": "2026-02-02", - "libelle": "CB LCL GAZOLE TOTAL LOOS", - "montant": 12.54, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 7, - "prenom": "HERY", - "date": "2026-02-16", - "libelle": "BAR LA CLOCHE (soleil)", - "montant": 8.0, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 8, - "prenom": "HERY", - "date": "2026-02-23", - "libelle": "COURSES CARREFOUR MOSELLE", - "montant": 44.47, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 9, - "prenom": "HERY", - "date": "2026-02-24", - "libelle": "AMAZON CAFE FOLLIER", - "montant": 16.2, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 10, - "prenom": "HERY", - "date": "2026-02-17", - "libelle": "CT CONTRE VISITE", - "montant": 25.0, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 11, - "prenom": "HERY", - "date": "2026-02-16", - "libelle": "CONTROLE TECHNIQUE", - "montant": 54.0, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 12, - "prenom": "HERY", - "date": "2026-02-16", - "libelle": "LOTO - EUROMILLION", - "montant": 9.0, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 13, - "prenom": "HERY", - "date": "2026-02-14", - "libelle": "LUMIERE TOILETTES", - "montant": 14.78, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 14, - "prenom": "HERY", - "date": "2026-02-09", - "libelle": "COMPTOIRE AFRIQ WAZEMME", - "montant": 9.4, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 15, - "prenom": "HERY", - "date": "2026-02-07", - "libelle": "BOUCHERIE LILLE", - "montant": 35.02, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 16, - "prenom": "HERY", - "date": "2026-02-06", - "libelle": "COURSES CARREFOUR", - "montant": 83.66, - "status": "Envoy\u00e9 \u2705" - }, - { - "id": 17, - "prenom": "HERY", - "date": "2026-02-02", - "libelle": "PARKING", - "montant": 2.0, - "status": "Envoy\u00e9 \u2705" - } - ], - "format": "{prenom} - {date} - {libelle} - {montant}\u20ac", - "prenoms": [ - "HERY", - "Papa", - "Maman" - ], + "prenoms": ["Papa", "Maman", "H3R7"], + "format": "{prenom} - {date} - {libelle} - {montant}€", + "depenses": [], "trello": { - "api_key": "0e86fa879fe9cc1bd5bff8a4b32bceff", - "token": "ATTA55718bc108ac05e2828a9406b9a0bf843dc47e958bd659d215210a7b0726bd6f4C4A53CE", - "list_id": "698499e98171f1383a04dbd6" + "api_key": "", + "token": "", + "board_id": "", + "list_id": "" } -} \ No newline at end of file +} diff --git a/depenses.db b/depenses.db new file mode 100644 index 0000000..646c52c Binary files /dev/null and b/depenses.db differ