v1.4 - Catégories auto + Budget + Récurrent + PDF + Tests
This commit is contained in:
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
88
app.py
88
app.py
@@ -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)
|
||||||
BIN
depenses.db
BIN
depenses.db
Binary file not shown.
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user