v1.0 - Dépenses Trello avec status, envoi par ligne
This commit is contained in:
167
app.py
167
app.py
@@ -15,6 +15,14 @@ def save_config(data):
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def parse_date(d):
|
||||
if not d: return ""
|
||||
if "/" in d:
|
||||
parts = d.split("/")
|
||||
if len(parts) == 3:
|
||||
return f"{parts[2]}-{parts[1]}-{parts[0]}"
|
||||
return d
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
with open('/home/h3r7/depenses_trello/templates/index.html', 'r') as f:
|
||||
@@ -32,15 +40,16 @@ def add_depense():
|
||||
data = {
|
||||
'id': len(config.get('depenses', [])) + 1,
|
||||
'prenom': request.form.get('prenom'),
|
||||
'date': request.form.get('date'),
|
||||
'date': parse_date(request.form.get('date', '')),
|
||||
'libelle': request.form.get('libelle'),
|
||||
'montant': float(request.form.get('montant', 0))
|
||||
'montant': float(request.form.get('montant', 0)),
|
||||
'status': 'En attente'
|
||||
}
|
||||
if 'depenses' not in config:
|
||||
config['depenses'] = []
|
||||
config['depenses'].append(data)
|
||||
save_config(config)
|
||||
return redirect('/')
|
||||
return redirect('/?page=saisie')
|
||||
|
||||
# Get depenses
|
||||
@app.route('/api/depenses')
|
||||
@@ -64,7 +73,128 @@ def clear_depenses():
|
||||
save_config(config)
|
||||
return jsonify({'success': True})
|
||||
|
||||
# Generate text
|
||||
# Generate text for ONE expense
|
||||
@app.route('/api/generate_one/<int:id>', 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
|
||||
@app.route('/api/trello/send_one/<int:id>', 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'):
|
||||
return jsonify({'error': 'Config Trello manquante'}), 400
|
||||
|
||||
# Find the expense
|
||||
d = None
|
||||
for exp in config.get('depenses', []):
|
||||
if exp.get('id') == id:
|
||||
d = exp
|
||||
break
|
||||
|
||||
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 ''
|
||||
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)))
|
||||
|
||||
# Send to Trello
|
||||
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:
|
||||
# Mark as sent
|
||||
for exp in config.get('depenses', []):
|
||||
if exp.get('id') == id:
|
||||
exp['status'] = 'Envoyé ✅'
|
||||
save_config(config)
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'error': r.text}), r.status_code
|
||||
except Exception as e:
|
||||
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
|
||||
|
||||
# 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 ''
|
||||
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)))
|
||||
|
||||
# Send to Trello
|
||||
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()
|
||||
@@ -73,10 +203,10 @@ def generate_text():
|
||||
lines = []
|
||||
for d in data.get('depenses', []):
|
||||
line = template
|
||||
line = line.replace('{prenom}', d.get('prenom', ''))
|
||||
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)))
|
||||
@@ -88,7 +218,7 @@ def generate_text():
|
||||
def add_prenom():
|
||||
config = load_config()
|
||||
prenom = request.form.get('prenom', '').strip()
|
||||
if prenom and prenom not in config.get('prenom', []):
|
||||
if prenom and prenom not in config.get('prenoms', []):
|
||||
if 'prenoms' not in config:
|
||||
config['prenoms'] = []
|
||||
config['prenoms'].append(prenom)
|
||||
@@ -124,30 +254,5 @@ def save_trello():
|
||||
save_config(config)
|
||||
return redirect('/?page=config')
|
||||
|
||||
# Send to Trello
|
||||
@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
|
||||
|
||||
data = request.json
|
||||
url = 'https://api.trello.com/1/cards'
|
||||
params = {
|
||||
'key': t.get('api_key'),
|
||||
'token': t.get('token'),
|
||||
'idList': t.get('list_id'),
|
||||
'name': data.get('title', 'Dépenses'),
|
||||
'desc': data.get('text', '')
|
||||
}
|
||||
try:
|
||||
r = requests.post(url, params=params)
|
||||
if r.status_code == 200:
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'error': r.text}), r.status_code
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8769, debug=False)
|
||||
|
||||
28
config.json
28
config.json
@@ -1,13 +1,29 @@
|
||||
{
|
||||
"prenoms": [
|
||||
"HERY"
|
||||
"H3R7"
|
||||
],
|
||||
"format": "{prenom} - {date} - {libelle} - {montant}\u20ac",
|
||||
"depenses": [],
|
||||
"depenses": [
|
||||
{
|
||||
"id": 1,
|
||||
"prenom": "H3R7",
|
||||
"date": "2026-02-27",
|
||||
"libelle": "COURSES CARREFOUR test ",
|
||||
"montant": 25.36,
|
||||
"status": "Envoy\u00e9 \u2705"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prenom": "H3R7",
|
||||
"date": "2026-02-23",
|
||||
"libelle": "GAZOLE",
|
||||
"montant": 33.55,
|
||||
"status": "Envoy\u00e9 \u2705"
|
||||
}
|
||||
],
|
||||
"trello": {
|
||||
"api_key": "",
|
||||
"token": "",
|
||||
"board_id": "",
|
||||
"list_id": ""
|
||||
"api_key": "0e86fa879fe9cc1bd5bff8a4b32bceff",
|
||||
"token": "ATTA55718bc108ac05e2828a9406b9a0bf843dc47e958bd659d215210a7b0726bd6f4C4A53CE",
|
||||
"list_id": "698499e98171f1383a04dbd6"
|
||||
}
|
||||
}
|
||||
@@ -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: 12px; background: var(--bg-card); border-radius: 10px; text-align: center; color: var(--text-dim); text-decoration: none; }
|
||||
.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.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; }
|
||||
@@ -20,14 +20,19 @@
|
||||
.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; margin-top: 10px; }
|
||||
.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-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; }
|
||||
.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; }
|
||||
.expense-prenom { background: linear-gradient(135deg, var(--primary), var(--secondary)); padding: 4px 10px; border-radius: 15px; font-size: 0.75em; white-space: nowrap; }
|
||||
.expense-montant { font-weight: 700; color: var(--accent); }
|
||||
.expense-status { font-size: 0.75em; padding: 3px 8px; border-radius: 10px; }
|
||||
.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 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; }
|
||||
@@ -47,7 +52,7 @@
|
||||
<h2>➕ Nouvelle dépense</h2>
|
||||
<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>📅 Date</label><input type="date" name="date" id="depense-date" 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>💰 Montant (€)</label><input type="number" name="montant" placeholder="0.00" step="0.01" required></div>
|
||||
<button type="submit" class="btn btn-primary">➕ Ajouter</button>
|
||||
@@ -59,11 +64,8 @@
|
||||
<div id="expenses"></div>
|
||||
<div class="total">Total: <span id="total">0.00€</span></div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="generate()">👁️ Aperçu</button>
|
||||
<button class="btn btn-success" onclick="sendTrello()">🟦 Envoyer Trello</button>
|
||||
<button class="btn btn-danger" onclick="clearAll()">🗑️ Tout effacer</button>
|
||||
</div>
|
||||
<div class="preview" id="preview" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -153,19 +155,36 @@
|
||||
var html = '';
|
||||
for (var i = 0; i < depenses.length; i++) {
|
||||
var d = depenses[i];
|
||||
var isSent = d.status === 'Envoyé ✅';
|
||||
var statusClass = isSent ? 'status-sent' : 'status-pending';
|
||||
var sendBtn = isSent ? '' : '<button class="btn btn-sm btn-send" onclick="sendOne(' + d.id + ')">🟦</button>';
|
||||
|
||||
html += '<div class="expense-item">';
|
||||
html += '<div style="display:flex;align-items:center;gap:10px">';
|
||||
html += '<span class="expense-prenom">' + d.prenom + '</span>';
|
||||
html += '<span>' + formatDate(d.date) + ' - ' + d.libelle + '</span>';
|
||||
html += '</div>';
|
||||
html += '<div style="display:flex;align-items:center;gap:10px">';
|
||||
html += '<span class="expense-status ' + statusClass + '">' + (d.status || 'En attente') + '</span>';
|
||||
html += '<span class="expense-montant">' + parseFloat(d.montant).toFixed(2) + '€</span>';
|
||||
html += sendBtn;
|
||||
html += '<a href="/api/del/' + d.id + '" style="color:var(--danger);text-decoration:none;font-size:1.2em">🗑️</a>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
document.getElementById('expenses').innerHTML = html;
|
||||
|
||||
document.getElementById('depense-date').value = new Date().toISOString().split('T')[0];
|
||||
var today = new Date();
|
||||
var dd = String(today.getDate()).padStart(2, '0');
|
||||
var mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||
var yyyy = today.getFullYear();
|
||||
document.getElementById('depense-date').value = dd + '/' + mm + '/' + yyyy;
|
||||
}
|
||||
|
||||
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' });
|
||||
if (r.ok) { alert('Envoyé sur Trello!'); load(); }
|
||||
else { var e = await r.json(); alert('Erreur: ' + e.error); }
|
||||
}
|
||||
|
||||
document.getElementById('form-add').onsubmit = function(e) {
|
||||
@@ -207,17 +226,20 @@
|
||||
async function generate() {
|
||||
var r = await fetch('/api/generate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({depenses: depenses}) });
|
||||
var d = await r.json();
|
||||
document.getElementById('preview').textContent = d.text;
|
||||
document.getElementById('preview').style.display = 'block';
|
||||
alert(d.text);
|
||||
}
|
||||
|
||||
async function sendTrello() {
|
||||
async function sendAll() {
|
||||
if (!config.trello || !config.trello.api_key) { alert('Configure Trello!'); showPage('config'); return; }
|
||||
await generate();
|
||||
var text = document.getElementById('preview').textContent;
|
||||
var r = await fetch('/api/trello/send', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title: 'Dépenses ' + new Date().toLocaleDateString('fr'), text: text}) });
|
||||
if (r.ok) { alert('Envoyé sur Trello!'); clearAll(); }
|
||||
else { var e = await r.json(); alert('Erreur: ' + e.error); }
|
||||
var sent = 0;
|
||||
for (var i = 0; i < depenses.length; i++) {
|
||||
if (depenses[i].status !== 'Envoyé ✅') {
|
||||
var r = await fetch('/api/trello/send_one/' + depenses[i].id, { method: 'POST' });
|
||||
if (r.ok) sent++;
|
||||
}
|
||||
}
|
||||
alert('Envoyé(s): ' + sent);
|
||||
load();
|
||||
}
|
||||
|
||||
async function clearAll() {
|
||||
|
||||
Reference in New Issue
Block a user