Files
depenses_trello/templates/index.html
Paperclip 7f0688d00a feat(HRT-143): Refonte design /depenses/ glassmorphism + modern UI
- Refonte complète index.html (1444 lignes) :
  layout 3 colonnes desktop / mobile responsive
  Header sticky glassmorphism avec nav tabs
  4 KPI cards animées (total, budget restant, nb, max)
  Donut chart répartition catégories en sidebar droite
  Top 5 dépenses avec progress bars
  Formulaire avec labels uppercase + inputs focus glow
  Liste dépenses avec badges colorés par catégorie/statut
  Modales overlay backdrop-blur + animation spring
  Budget bar gradient animée (matrix → rose selon %)

- Refonte complète dashboard.html (731 lignes) :
  Grid 3 colonnes avec cards glassmorphism
  4 KPI cards (total, count, moyenne, catégories)
  Chart principal multi-mode (bar/line/pie)
  Donut répartition + table catégories avec %
  Résumé par personne avec progress bars
  Top 7 dépenses avec progress bars
  Sparkline tendance 6 derniers mois

- Stack : HTML/CSS/JS vanilla + Tailwind CDN + JetBrains Mono
- Palette : --bg-primary #020617, --cyan #00d9ff, --violet #a855f7
- 0 modification app.py, toutes APIs inchangées

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-11 16:01:58 +02:00

1445 lines
54 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💸 Dépenses — H3R7</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;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>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'bg-primary': '#020617',
'bg-card': 'rgba(15,23,42,0.8)',
'cyan': '#00d9ff',
'violet': '#a855f7',
'matrix': '#00ff88',
'rose': '#f43f5e',
'amber': '#f59e0b',
},
fontFamily: {
mono: ['JetBrains Mono', 'monospace'],
sans: ['Inter', 'sans-serif'],
}
}
}
}
</script>
<style>
:root {
--bg-primary: #020617;
--bg-card: rgba(15, 23, 42, 0.8);
--bg-input: rgba(30, 41, 59, 0.6);
--cyan: #00d9ff;
--violet: #a855f7;
--matrix: #00ff88;
--rose: #f43f5e;
--amber: #f59e0b;
--text: #f1f5f9;
--text-dim: #64748b;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg-primary);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* Grid background */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(0, 217, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 217, 255, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
/* Ambient glow */
body::after {
content: '';
position: fixed;
top: -200px;
left: 50%;
transform: translateX(-50%);
width: 800px;
height: 400px;
background: radial-gradient(ellipse, rgba(0,217,255,0.06) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
/* Glassmorphism card */
.glass-card {
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(0, 217, 255, 0.12);
border-radius: 16px;
transition: border-color 0.3s, box-shadow 0.3s;
}
.glass-card:hover {
border-color: rgba(0, 217, 255, 0.25);
box-shadow: 0 0 30px rgba(0, 217, 255, 0.06);
}
/* Header */
header {
position: sticky;
top: 0;
z-index: 50;
background: rgba(2, 6, 23, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(0, 217, 255, 0.15);
padding: 12px 20px;
}
.logo-text {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 1.1rem;
background: linear-gradient(135deg, var(--cyan), var(--violet));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Navigation */
.nav-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-dim);
text-decoration: none;
transition: all 0.2s;
border: 1px solid transparent;
}
.nav-tab:hover {
color: var(--text);
background: rgba(0, 217, 255, 0.08);
border-color: rgba(0, 217, 255, 0.15);
}
.nav-tab.active {
color: var(--cyan);
background: rgba(0, 217, 255, 0.12);
border-color: rgba(0, 217, 255, 0.3);
}
/* KPI Cards */
.kpi-card {
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(16px);
border: 1px solid rgba(0, 217, 255, 0.1);
border-radius: 14px;
padding: 16px 20px;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.kpi-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--kpi-color, var(--cyan)), transparent);
}
.kpi-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 217, 255, 0.1);
border-color: rgba(0, 217, 255, 0.25);
}
.kpi-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.5rem;
font-weight: 700;
color: var(--text);
}
/* Form inputs */
.input-field {
width: 100%;
padding: 11px 14px;
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(100, 116, 139, 0.3);
border-radius: 10px;
color: var(--text);
font-size: 0.9rem;
font-family: 'Inter', sans-serif;
transition: all 0.2s;
outline: none;
}
.input-field:focus {
border-color: rgba(0, 217, 255, 0.5);
background: rgba(30, 41, 59, 0.8);
box-shadow: 0 0 0 3px rgba(0, 217, 255, 0.1);
}
.input-field option {
background: #1e293b;
color: var(--text);
}
.form-label {
display: block;
font-size: 0.78rem;
font-weight: 500;
color: var(--text-dim);
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Buttons */
.btn-primary {
background: linear-gradient(135deg, var(--cyan), #0099cc);
color: #020617;
border: none;
border-radius: 10px;
padding: 11px 20px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(0, 217, 255, 0.3);
}
.btn-secondary {
background: rgba(168, 85, 247, 0.15);
color: var(--violet);
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 10px;
padding: 11px 20px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.btn-secondary:hover {
background: rgba(168, 85, 247, 0.25);
}
.btn-danger {
background: rgba(244, 63, 94, 0.15);
color: var(--rose);
border: 1px solid rgba(244, 63, 94, 0.3);
border-radius: 10px;
padding: 11px 20px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.btn-danger:hover { background: rgba(244, 63, 94, 0.25); }
.btn-icon {
padding: 7px 12px;
border-radius: 8px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
background: transparent;
color: var(--text-dim);
}
.btn-icon:hover {
background: rgba(255,255,255,0.08);
color: var(--text);
}
.btn-icon.cyan { color: var(--cyan); border-color: rgba(0,217,255,0.2); }
.btn-icon.cyan:hover { background: rgba(0,217,255,0.1); }
.btn-icon.rose { color: var(--rose); }
.btn-icon.rose:hover { background: rgba(244,63,94,0.1); }
.btn-icon.amber { color: var(--amber); }
.btn-icon.amber:hover { background: rgba(245,158,11,0.1); }
.btn-icon.matrix { color: var(--matrix); border-color: rgba(0,255,136,0.2); }
.btn-icon.matrix:hover { background: rgba(0,255,136,0.1); }
/* Expense items */
.expense-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: rgba(30, 41, 59, 0.4);
border: 1px solid rgba(100, 116, 139, 0.15);
border-radius: 10px;
margin-bottom: 8px;
transition: all 0.2s;
animation: slideIn 0.3s ease-out;
}
.expense-item:hover {
border-color: rgba(0, 217, 255, 0.2);
background: rgba(30, 41, 59, 0.6);
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
padding: 3px 9px;
border-radius: 20px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.03em;
}
.badge-prenom {
background: linear-gradient(135deg, rgba(0,217,255,0.15), rgba(168,85,247,0.15));
color: #a5b4fc;
border: 1px solid rgba(168,85,247,0.2);
}
.badge-cat {
background: rgba(0,217,255,0.08);
color: var(--cyan);
border: 1px solid rgba(0,217,255,0.15);
}
.badge-sent {
background: rgba(0,255,136,0.1);
color: var(--matrix);
border: 1px solid rgba(0,255,136,0.2);
}
.badge-pending {
background: rgba(245,158,11,0.1);
color: var(--amber);
border: 1px solid rgba(245,158,11,0.2);
}
/* Filter chips */
.filter-chip {
padding: 6px 14px;
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(100, 116, 139, 0.2);
border-radius: 20px;
font-size: 0.8rem;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s;
color: var(--text-dim);
}
.filter-chip:hover {
border-color: rgba(0, 217, 255, 0.3);
color: var(--text);
}
.filter-chip.active {
background: rgba(0, 217, 255, 0.12);
border-color: rgba(0, 217, 255, 0.4);
color: var(--cyan);
}
/* Budget progress */
.budget-bar {
height: 6px;
background: rgba(100, 116, 139, 0.2);
border-radius: 3px;
overflow: hidden;
}
.budget-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.8s ease;
animation: progressFill 1s ease-out;
}
@keyframes progressFill {
from { width: 0 !important; }
}
/* Section titles */
.section-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-dim);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title::after {
content: '';
flex: 1;
height: 1px;
background: rgba(100, 116, 139, 0.2);
}
/* Modal */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.open { display: flex; }
.modal-box {
background: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(168, 85, 247, 0.25);
border-radius: 18px;
padding: 24px;
width: 90%;
max-width: 420px;
animation: modalIn 0.25s ease-out;
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.95) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
/* Budget alert */
.budget-alert {
background: rgba(244, 63, 94, 0.1);
border: 1px solid rgba(244, 63, 94, 0.3);
border-radius: 10px;
padding: 10px 14px;
color: var(--rose);
font-size: 0.85rem;
display: none;
}
/* Recurring item */
.recurring-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: rgba(30, 41, 59, 0.4);
border: 1px solid rgba(100, 116, 139, 0.15);
border-radius: 8px;
margin-bottom: 7px;
font-size: 0.85rem;
}
/* Home button */
.home-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
background: rgba(0, 217, 255, 0.1);
border: 1px solid rgba(0, 217, 255, 0.25);
border-radius: 8px;
color: var(--cyan);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s;
}
.home-btn:hover {
background: rgba(0, 217, 255, 0.18);
box-shadow: 0 2px 12px rgba(0,217,255,0.2);
}
/* Scrollbar */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(100,116,139,0.3); border-radius: 3px; }
/* Main layout */
.main-layout {
display: grid;
grid-template-columns: 280px 1fr 260px;
gap: 16px;
padding: 16px;
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
}
@media (max-width: 1100px) {
.main-layout { grid-template-columns: 260px 1fr; }
.stats-panel { display: none; }
}
@media (max-width: 768px) {
.main-layout { grid-template-columns: 1fr; }
.sidebar-left { display: none; }
.mobile-fab { display: flex !important; }
}
.mobile-fab {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
z-index: 40;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, var(--cyan), var(--violet));
color: #020617;
font-size: 1.5rem;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 20px rgba(0,217,255,0.4);
border: none;
transition: transform 0.2s;
}
.mobile-fab:hover { transform: scale(1.1); }
/* Section visibility */
.page-section { display: none; }
.page-section.active { display: block; }
/* Trello toggle */
.token-field-container { position: relative; }
.toggle-token-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--text-dim);
cursor: pointer;
font-size: 0.85rem;
padding: 4px;
}
/* Divider */
.divider {
height: 1px;
background: rgba(100, 116, 139, 0.15);
margin: 14px 0;
}
/* Total display */
.total-display {
background: rgba(0, 217, 255, 0.05);
border: 1px solid rgba(0, 217, 255, 0.15);
border-radius: 10px;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Tag items */
.tag-item {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
background: rgba(30,41,59,0.6);
border: 1px solid rgba(100,116,139,0.2);
border-radius: 20px;
font-size: 0.8rem;
margin: 3px;
}
.tag-item a { color: var(--text-dim); text-decoration: none; transition: color 0.2s; }
.tag-item a:hover { color: var(--rose); }
</style>
</head>
<body>
<!-- HEADER -->
<header>
<div style="display:flex;align-items:center;justify-content:space-between;max-width:1400px;margin:0 auto">
<div style="display:flex;align-items:center;gap:16px">
<a href="https://portal-kolifee.duckdns.org/" class="home-btn">
<span>🏠</span><span>Portail</span>
</a>
<div class="logo-text">H3R7 / Dépenses</div>
</div>
<nav style="display:flex;gap:6px">
<a href="/depenses/?page=saisie" class="nav-tab" id="nav-saisie">✏️ Saisie</a>
<a href="/depenses/dashboard" class="nav-tab">📊 Dashboard</a>
<a href="/depenses/?page=config" class="nav-tab" id="nav-config">⚙️ Config</a>
</nav>
<div id="header-budget-info" style="font-size:0.78rem;color:var(--text-dim);font-family:'JetBrains Mono',monospace;text-align:right">
<div id="header-month" style="color:var(--cyan)"></div>
<div id="header-total-small">0.00€</div>
</div>
</div>
</header>
<!-- KPI BAR -->
<div style="padding:12px 16px;position:relative;z-index:1">
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;max-width:1400px;margin:0 auto" id="kpi-bar">
<div class="kpi-card" style="--kpi-color:var(--rose)">
<div class="form-label">Total dépensé</div>
<div class="kpi-value" id="kpi-total">0.00€</div>
<div style="font-size:0.75rem;color:var(--text-dim);margin-top:4px">ce mois</div>
</div>
<div class="kpi-card" style="--kpi-color:var(--matrix)">
<div class="form-label">Budget restant</div>
<div class="kpi-value" id="kpi-budget-left" style="color:var(--matrix)"></div>
<div class="budget-bar" style="margin-top:8px">
<div class="budget-bar-fill" id="kpi-budget-bar" style="width:0%;background:linear-gradient(90deg,var(--matrix),var(--cyan))"></div>
</div>
</div>
<div class="kpi-card" style="--kpi-color:var(--cyan)">
<div class="form-label">Nb dépenses</div>
<div class="kpi-value" id="kpi-count">0</div>
<div style="font-size:0.75rem;color:var(--text-dim);margin-top:4px" id="kpi-count-sub">ce mois</div>
</div>
<div class="kpi-card" style="--kpi-color:var(--violet)">
<div class="form-label">Plus grosse</div>
<div class="kpi-value" id="kpi-max" style="color:var(--violet)"></div>
<div style="font-size:0.75rem;color:var(--text-dim);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" id="kpi-max-label"></div>
</div>
</div>
</div>
<!-- MAIN LAYOUT -->
<div class="main-layout">
<!-- SIDEBAR LEFT — FORM -->
<div class="sidebar-left">
<div class="glass-card" style="padding:18px">
<div class="section-title"> Nouvelle dépense</div>
<div id="budget-alert" class="budget-alert" style="margin-bottom:12px">⚠️ Budget dépassé !</div>
<form id="form-add">
<div style="margin-bottom:12px">
<label class="form-label">👤 Prénom</label>
<select name="prenom" id="prenom-select" class="input-field" required></select>
</div>
<div style="margin-bottom:12px">
<label class="form-label">📂 Catégorie</label>
<select name="category" id="category-select" class="input-field"></select>
</div>
<div style="margin-bottom:12px">
<label class="form-label">📅 Date</label>
<input type="text" name="date" id="depense-date" placeholder="JJ/MM/AAAA" class="input-field" required>
</div>
<div style="margin-bottom:12px">
<label class="form-label">📝 Libellé</label>
<input type="text" name="libelle" id="libelle-input" placeholder="Courses, Essence..." class="input-field" required>
</div>
<div style="margin-bottom:14px">
<label class="form-label">💰 Montant (€)</label>
<input type="number" name="montant" placeholder="0.00" step="0.01" class="input-field" required>
</div>
<button type="submit" class="btn-primary">💾 Enregistrer</button>
<button type="button" class="btn-danger" onclick="resetForm()" style="margin-top:8px">🧹 Effacer</button>
</form>
<div class="divider"></div>
<div id="mini-stats" style="font-size:0.78rem;color:var(--text-dim)">
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
<span>Dépenses filtrées</span>
<span id="mini-count" style="color:var(--cyan);font-family:'JetBrains Mono',monospace">0</span>
</div>
<div style="display:flex;justify-content:space-between">
<span>Sous-total</span>
<span id="mini-total" style="color:var(--rose);font-family:'JetBrains Mono',monospace">0.00€</span>
</div>
</div>
</div>
</div>
<!-- MAIN PANEL -->
<div>
<!-- SAISIE PAGE -->
<div id="saisie" class="page-section">
<!-- Filter bar -->
<div class="glass-card" style="padding:12px 16px;margin-bottom:14px">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<div style="font-size:0.75rem;color:var(--text-dim);font-weight:600;text-transform:uppercase;letter-spacing:0.08em;white-space:nowrap">Filtre</div>
<div style="display:flex;gap:8px;overflow-x:auto;padding-bottom:2px;flex:1" id="filter-bar"></div>
<div style="flex-shrink:0">
<button class="btn-icon cyan" onclick="loadDepenses()" title="Actualiser">↻ Refresh</button>
</div>
</div>
</div>
<!-- Expense list -->
<div class="glass-card" style="padding:16px">
<div id="expenses"></div>
<div class="total-display" style="margin-top:12px">
<span style="font-size:0.85rem;color:var(--text-dim)"><span id="count">0</span> dépenses</span>
<span style="font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--rose);font-size:1.1rem">
Total : <span id="total">0.00€</span>
</span>
</div>
<!-- Action buttons -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px">
<button class="btn-secondary" onclick="generate()">👁️ Aperçu texte</button>
<button class="btn-primary" onclick="sendAll()">🟦 Envoyer Trello</button>
</div>
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">
<button class="btn-icon cyan" onclick="exportCSV()" style="flex:1">📥 Export CSV</button>
<button class="btn-icon cyan" onclick="exportPDF()" style="flex:1">📥 Export PDF</button>
<button class="btn-icon" onclick="document.getElementById('file-import').click()" style="flex:1">📤 Import CSV</button>
<input type="file" id="file-import" style="display:none" accept=".csv" onchange="importCSV(this)">
</div>
</div>
</div>
<!-- CONFIG PAGE -->
<div id="config" class="page-section">
<div class="glass-card" style="padding:18px;margin-bottom:14px">
<div class="section-title">💰 Budget Mensuel</div>
<form id="form-budget">
<div style="display:flex;gap:10px;align-items:flex-end">
<div style="flex:1">
<label class="form-label">Montant budget (€)</label>
<input type="number" name="budget" id="budget-input" placeholder="ex: 2000.00" step="0.01" class="input-field">
</div>
<button type="submit" class="btn-primary" style="width:auto;padding:11px 20px">💾 Sauvegarder</button>
</div>
</form>
</div>
<div class="glass-card" style="padding:18px;margin-bottom:14px">
<div class="section-title">🔄 Dépenses Récurrentes</div>
<div id="recurring-list"></div>
<form id="form-recurring" style="margin-top:10px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">
<div>
<label class="form-label">Libellé</label>
<input type="text" name="libelle" placeholder="Loyer, EDF..." class="input-field" required>
</div>
<div>
<label class="form-label">Montant (€)</label>
<input type="number" name="montant" placeholder="0.00" step="0.01" class="input-field" required>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px">
<div>
<label class="form-label">Catégorie</label>
<select name="category" id="recurring-cat" class="input-field"></select>
</div>
<div>
<label class="form-label">Jour du mois</label>
<input type="number" name="jour" value="1" min="1" max="31" class="input-field">
</div>
</div>
<button type="submit" class="btn-primary"> Ajouter récurrente</button>
</form>
</div>
<div class="glass-card" style="padding:18px;margin-bottom:14px">
<div class="section-title">👥 Prénoms</div>
<div id="prenom-tags" style="margin-bottom:12px"></div>
<form id="form-prenom">
<div style="display:flex;gap:10px">
<input type="text" name="prenom" placeholder="Nouveau prénom..." class="input-field" style="flex:1">
<button type="submit" class="btn-primary" style="width:auto;padding:11px 16px"></button>
</div>
</form>
</div>
<div class="glass-card" style="padding:18px;margin-bottom:14px">
<div class="section-title">📂 Catégories</div>
<div id="category-tags" style="margin-bottom:12px"></div>
<form id="form-category">
<div style="display:flex;gap:10px">
<input type="text" name="category" placeholder="Nouvelle catégorie..." class="input-field" style="flex:1">
<button type="submit" class="btn-primary" style="width:auto;padding:11px 16px"></button>
</div>
</form>
</div>
<div class="glass-card" style="padding:18px;margin-bottom:14px">
<div class="section-title">📄 Format du texte Trello</div>
<form id="form-format">
<div style="margin-bottom:10px">
<label class="form-label">Template ({prenom}, {date}, {libelle}, {montant})</label>
<input type="text" name="format" id="format-input" class="input-field">
</div>
<button type="submit" class="btn-primary">💾 Sauvegarder format</button>
</form>
</div>
<div class="glass-card" style="padding:18px">
<div class="section-title">🔗 Configuration Trello</div>
<form id="form-trello" onsubmit="return true" autocomplete="off">
<!-- Honey pot fields -->
<input type="text" name="fake_username" autocomplete="username" style="display:none" tabindex="-1" aria-hidden="true">
<input type="password" name="fake_password" autocomplete="current-password" style="display:none" tabindex="-1" aria-hidden="true">
<div style="margin-bottom:12px">
<label class="form-label">🔑 API Key</label>
<input type="text" name="trello-apikey-field" id="trello-api_key" autocomplete="new-password" spellcheck="false" data-lpignore="true" data-form-type="other" class="input-field">
</div>
<div style="margin-bottom:12px">
<label class="form-label">🔐 Token</label>
<div class="token-field-container">
<input type="text" name="trello-tok-field" id="trello-token" autocomplete="new-password" spellcheck="false" data-lpignore="true" data-form-type="other" data-hidden="false" class="input-field" style="padding-right:80px;font-family:'JetBrains Mono',monospace">
<button type="button" id="toggle-token-visibility" class="toggle-token-btn" onclick="toggleTokenVisibility()">🙈 Masquer</button>
</div>
<small id="token-expiry-info" style="display:block;margin-top:5px;font-size:0.78rem"></small>
<small id="token-debug-info" style="display:block;margin-top:2px;color:var(--text-dim);font-size:0.72rem;font-family:'JetBrains Mono',monospace"></small>
</div>
<div style="margin-bottom:12px">
<label class="form-label">🆔 Board ID (optionnel)</label>
<input type="text" name="trello-boardid-field" id="trello-board_id" autocomplete="new-password" spellcheck="false" data-lpignore="true" placeholder="laisser vide = lister tous les boards" class="input-field">
</div>
<div style="margin-bottom:12px">
<label class="form-label">📁 List ID</label>
<div style="display:flex;gap:8px">
<input type="text" name="trello-listid-field" id="trello-list_id" class="input-field" autocomplete="new-password" spellcheck="false" data-lpignore="true" style="flex:1">
<button type="button" class="btn-secondary" onclick="fetchTrelloLists()" style="width:auto;padding:11px 14px;white-space:nowrap">📋 Listes</button>
</div>
</div>
<div id="trello-lists-container" style="display:none;margin-bottom:12px">
<label class="form-label">📋 Listes disponibles</label>
<select id="trello-list-select" onchange="selectTrelloList(this.value)" class="input-field">
<option value="">-- Sélectionner une liste --</option>
</select>
</div>
<div id="trello-save-result" style="margin-bottom:10px;font-size:0.85rem"></div>
<button type="submit" class="btn-primary">💾 Sauvegarder config Trello</button>
</form>
</div>
</div>
</div>
<!-- RIGHT STATS PANEL -->
<div class="stats-panel">
<div class="glass-card" style="padding:16px;margin-bottom:14px">
<div class="section-title">📊 Répartition</div>
<div style="height:180px;position:relative">
<canvas id="donut-chart"></canvas>
</div>
</div>
<div class="glass-card" style="padding:16px">
<div class="section-title">🏆 Top dépenses</div>
<div id="top-expenses-list" style="font-size:0.82rem"></div>
</div>
</div>
</div>
<!-- Mobile FAB -->
<button class="mobile-fab" onclick="toggleMobileSidebar()"></button>
<!-- EDIT MODAL -->
<div id="edit-modal" class="modal-overlay">
<div class="modal-box">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:18px">
<span style="font-weight:600;color:var(--cyan)">✏️ Modifier la dépense</span>
<button onclick="closeModal()" style="background:transparent;border:none;color:var(--text-dim);cursor:pointer;font-size:1.2rem">×</button>
</div>
<form id="form-edit">
<input type="hidden" id="edit-id">
<div style="margin-bottom:12px">
<label class="form-label">👤 Prénom</label>
<select name="prenom" id="edit-prenom" class="input-field"></select>
</div>
<div style="margin-bottom:12px">
<label class="form-label">📂 Catégorie</label>
<select name="category" id="edit-category" class="input-field"></select>
</div>
<div style="margin-bottom:12px">
<label class="form-label">📅 Date</label>
<input type="text" name="date" id="edit-date" placeholder="JJ/MM/AAAA" class="input-field">
</div>
<div style="margin-bottom:12px">
<label class="form-label">📝 Libellé</label>
<input type="text" name="libelle" id="edit-libelle" class="input-field">
</div>
<div style="margin-bottom:16px">
<label class="form-label">💰 Montant (€)</label>
<input type="number" name="montant" id="edit-montant" step="0.01" class="input-field">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<button type="submit" class="btn-primary">💾 Sauvegarder</button>
<button type="button" class="btn-danger" onclick="closeModal()">✕ Annuler</button>
</div>
</form>
</div>
</div>
<script>
var config = { prenoms: [], categories: [], format: '', trello: {} };
var depenses = [];
var budget = 0;
var filterCategory = '';
var donutChart = null;
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; }
function api(path) {
var base = window.location.pathname.includes('/depenses') ? '/depenses' : '';
return base + '/' + path;
}
async function load() {
try {
var r = await fetch(api('api/config'));
config = await r.json();
} catch(e) { console.error('[load] config error:', e); }
try {
r = await fetch(api('api/depenses'));
depenses = await r.json();
} catch(e) { console.error('[load] depenses error:', e); }
try {
r = await fetch(api('api/budget'));
budget = await r.json();
} catch(e) { console.error('[load] budget error:', e); }
render();
checkPage();
initAutoCategory();
renderFilters();
loadRecurring();
checkBudget();
updateKPIs();
updateDonut();
updateTopExpenses();
}
function updateKPIs() {
var now = new Date();
var currentMonth = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0');
var monthExpenses = depenses.filter(function(d) { return d.date && d.date.startsWith(currentMonth); });
var monthTotal = monthExpenses.reduce(function(s,d) { return s+(parseFloat(d.montant)||0); }, 0);
document.getElementById('kpi-total').textContent = monthTotal.toFixed(2) + '€';
document.getElementById('kpi-count').textContent = monthExpenses.length;
document.getElementById('header-month').textContent = now.toLocaleString('fr-FR', {month:'long', year:'numeric'});
document.getElementById('header-total-small').textContent = monthTotal.toFixed(2) + '€';
var budgetVal = budget.budget || 0;
if (budgetVal > 0) {
var left = budgetVal - monthTotal;
var pct = Math.min(100, (monthTotal / budgetVal) * 100);
document.getElementById('kpi-budget-left').textContent = (left >= 0 ? left.toFixed(2) : ''+(Math.abs(left).toFixed(2))) + '€';
document.getElementById('kpi-budget-left').style.color = left < 0 ? 'var(--rose)' : 'var(--matrix)';
var bar = document.getElementById('kpi-budget-bar');
bar.style.width = pct + '%';
bar.style.background = pct > 90 ? 'var(--rose)' : pct > 70 ? 'var(--amber)' : 'linear-gradient(90deg,var(--matrix),var(--cyan))';
} else {
document.getElementById('kpi-budget-left').textContent = '—';
}
// Max expense
if (monthExpenses.length > 0) {
var maxE = monthExpenses.reduce(function(a,b) { return (parseFloat(b.montant)||0) > (parseFloat(a.montant)||0) ? b : a; });
document.getElementById('kpi-max').textContent = parseFloat(maxE.montant).toFixed(2) + '€';
document.getElementById('kpi-max-label').textContent = maxE.libelle || '—';
}
}
function updateDonut() {
var canvas = document.getElementById('donut-chart');
if (!canvas) return;
var now = new Date();
var currentMonth = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0');
var monthExpenses = depenses.filter(function(d) { return d.date && d.date.startsWith(currentMonth); });
var cats = {};
monthExpenses.forEach(function(d) {
var c = d.category || 'Autre';
cats[c] = (cats[c]||0) + (parseFloat(d.montant)||0);
});
var labels = Object.keys(cats);
var values = labels.map(function(l) { return cats[l]; });
var colors = ['#00d9ff','#a855f7','#00ff88','#f43f5e','#f59e0b','#38bdf8','#fb7185','#34d399'];
if (donutChart) donutChart.destroy();
Chart.defaults.color = '#64748b';
donutChart = new Chart(canvas, {
type: 'doughnut',
data: { labels: labels, datasets: [{ data: values, backgroundColor: colors.slice(0, labels.length), borderWidth: 0 }] },
options: {
responsive: true, maintainAspectRatio: false, cutout: '72%',
plugins: {
legend: { position: 'bottom', labels: { color: '#94a3b8', font: { family: 'JetBrains Mono', size: 10 }, boxWidth: 10, padding: 8 } }
}
}
});
}
function updateTopExpenses() {
var now = new Date();
var currentMonth = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0');
var monthExpenses = depenses.filter(function(d) { return d.date && d.date.startsWith(currentMonth); });
monthExpenses.sort(function(a,b) { return (parseFloat(b.montant)||0) - (parseFloat(a.montant)||0); });
var top5 = monthExpenses.slice(0, 5);
var maxVal = top5.length > 0 ? parseFloat(top5[0].montant)||1 : 1;
var html = '';
for (var i=0; i<top5.length; i++) {
var d = top5[i];
var pct = ((parseFloat(d.montant)||0) / maxVal * 100).toFixed(0);
html += '<div style="margin-bottom:10px">';
html += '<div style="display:flex;justify-content:space-between;margin-bottom:4px"><span style="color:var(--text);font-size:0.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:140px">' + (d.libelle||'—') + '</span>';
html += '<span style="font-family:\'JetBrains Mono\',monospace;color:var(--rose);font-size:0.78rem;white-space:nowrap">' + parseFloat(d.montant).toFixed(2) + '€</span></div>';
html += '<div style="height:4px;background:rgba(100,116,139,0.2);border-radius:2px"><div style="height:4px;width:'+pct+'%;background:linear-gradient(90deg,var(--violet),var(--cyan));border-radius:2px"></div></div>';
html += '</div>';
}
if (!html) html = '<div style="color:var(--text-dim);font-size:0.8rem">Aucune dépense ce mois</div>';
var el = document.getElementById('top-expenses-list');
if (el) el.innerHTML = html;
}
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 alertEl = document.getElementById('budget-alert');
if (budget.budget > 0 && monthTotal > budget.budget) {
alertEl.textContent = "⚠️ Budget dépassé ! " + monthTotal.toFixed(2) + "€ / " + budget.budget + "€";
alertEl.style.display = 'block';
} else {
alertEl.style.display = 'none';
}
}
async function loadRecurring() {
var r = await fetch(api('api/recurring'));
var rec = await r.json();
var html = '';
for (var i=0; i<rec.length; i++) {
html += '<div class="recurring-item">';
html += '<span style="color:var(--text)">' + rec[i].libelle + '</span>';
html += '<span style="color:var(--text-dim);font-size:0.78rem">' + rec[i].montant + '€ · ' + rec[i].category + ' · j.' + rec[i].jour + '</span>';
html += '<a href="api/recurring/del/' + rec[i].id + '" class="btn-icon rose">🗑️</a>';
html += '</div>';
}
document.getElementById('recurring-list').innerHTML = html || '<p style="color:var(--text-dim);font-size:0.85rem">Aucune dépense récurrente</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() {
var params = new URLSearchParams(window.location.search);
var page = params.get('page') || 'saisie';
showPage(page);
}
function showPage(page) {
document.getElementById('saisie').className = 'page-section' + (page === 'saisie' ? ' active' : '');
document.getElementById('config').className = 'page-section' + (page === 'config' ? ' active' : '');
document.getElementById('nav-saisie').className = 'nav-tab' + (page === 'saisie' ? ' active' : '');
document.getElementById('nav-config').className = 'nav-tab' + (page === 'config' ? ' active' : '');
}
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-tags').innerHTML = config.prenoms.map(function(p, i) {
return '<span class="tag-item">'+p+'<a href="api/prenom/del/'+i+'">×</a></span>';
}).join('');
// Categories
var cats = config.categories || ['Courses', 'Essence', 'Loisirs', 'Maison', 'Santé', 'Transport', 'Autre'];
document.getElementById('category-select').innerHTML = cats.map(function(c) { return '<option value="'+c+'">'+c+'</option>'; }).join('');
document.getElementById('category-tags').innerHTML = cats.map(function(c, i) {
return '<span class="tag-item">'+c+'<a href="api/category/del/'+i+'">×</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 & budget
document.getElementById('format-input').value = config.format || '{prenom} - {date} - {libelle} - {montant}€';
document.getElementById('budget-input').value = budget.budget || '';
// Trello
if (config.trello) {
var forceSet = function(id, val) {
var el = document.getElementById(id);
if (!el) return;
el.value = val || '';
el.dataset.loadedValue = val || '';
};
forceSet('trello-api_key', config.trello.api_key);
forceSet('trello-token', config.trello.token);
forceSet('trello-list_id', config.trello.list_id);
forceSet('trello-board_id', config.trello.board_id);
var dbg = document.getElementById('token-debug-info');
if (dbg) {
var inp = document.getElementById('trello-token');
var len = (inp.value||'').length;
var preview = inp.value ? inp.value.substring(0,8)+'...' : '(vide)';
dbg.textContent = 'Debug: token = ' + preview + ' (' + len + ' chars)';
}
setTimeout(function() {
var t = document.getElementById('trello-token');
if (t && !t.value && t.dataset.loadedValue) { t.value = t.dataset.loadedValue; }
var k = document.getElementById('trello-api_key');
if (k && !k.value && k.dataset.loadedValue) { k.value = k.dataset.loadedValue; }
var l = document.getElementById('trello-list_id');
if (l && !l.value && l.dataset.loadedValue) { l.value = l.dataset.loadedValue; }
}, 500);
var expInfo = document.getElementById('token-expiry-info');
if (expInfo && config.trello.token_expires_at) {
var dExp = new Date(config.trello.token_expires_at);
var diffDays = Math.floor((dExp - new Date()) / 86400000);
if (diffDays < 0) expInfo.innerHTML = '<span style="color:var(--rose)">❌ Token expiré depuis '+Math.abs(diffDays)+' jour(s)</span>';
else if (diffDays < 7) expInfo.innerHTML = '<span style="color:var(--amber)">⚠️ Expire dans '+diffDays+' jour(s)</span>';
else expInfo.innerHTML = '<span style="color:var(--matrix)">✅ Valide jusqu\'au '+dExp.toLocaleDateString()+'</span>';
}
applyTokenMask();
}
// Expenses (filtered)
var filtered = filterCategory ? depenses.filter(function(d) { return d.category === filterCategory; }) : depenses;
document.getElementById('count').textContent = filtered.length;
document.getElementById('mini-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('mini-total').textContent = total.toFixed(2) + '€';
var html = '';
for (var i=0; i<filtered.length; i++) {
var d = filtered[i];
var isSent = d.status === 'Envoyé ✅';
var statusBadge = isSent
? '<span class="badge badge-sent">✅ Envoyé</span>'
: '<span class="badge badge-pending">⏳ En attente</span>';
var sendBtn = isSent ? '' : '<button class="btn-icon cyan" onclick="sendOne('+d.id+')" title="Envoyer Trello">🟦</button>';
html += '<div class="expense-item">';
html += '<div style="display:flex;flex-direction:column;gap:4px;min-width:80px">';
html += '<span class="badge badge-prenom">'+d.prenom+'</span>';
html += '<span class="badge badge-cat">'+(d.category||'Autre')+'</span>';
html += '</div>';
html += '<div style="flex:1;padding:0 10px;overflow:hidden">';
html += '<div style="color:var(--text);font-size:0.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+d.libelle+'</div>';
html += '<div style="color:var(--text-dim);font-size:0.75rem;margin-top:2px">'+formatDate(d.date)+'</div>';
html += '</div>';
html += '<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">';
html += statusBadge;
html += '<span style="font-family:\'JetBrains Mono\',monospace;font-weight:700;color:var(--rose);white-space:nowrap">'+parseFloat(d.montant).toFixed(2)+'€</span>';
html += sendBtn;
html += '<button class="btn-icon amber" onclick="editDepense('+d.id+')" title="Modifier">✏️</button>';
html += '<a href="api/del/'+d.id+'" class="btn-icon rose" title="Supprimer">🗑️</a>';
html += '</div></div>';
}
if (!html) html = '<div style="text-align:center;padding:40px;color:var(--text-dim)"><div style="font-size:2rem;margin-bottom:8px">📭</div><div>Aucune dépense</div></div>';
document.getElementById('expenses').innerHTML = html;
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 editDepense(id) {
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').classList.add('open');
}
function closeModal() {
document.getElementById('edit-modal').classList.remove('open');
}
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('api/depenses')).then(function(r) { return r.json(); }).then(function(d) {
depenses = d;
render();
checkBudget();
updateKPIs();
updateDonut();
updateTopExpenses();
});
}
async function sendOne(id) {
if (!config.trello || !config.trello.api_key) { alert('Configure Trello d\'abord !'); showPage('config'); return; }
var r = await fetch(api('api/trello/send_one/' + id), { method: 'POST' });
if (r.ok) { loadDepenses(); }
else { var e = await r.json(); alert('Erreur: ' + e.error); }
}
document.getElementById('form-add').onsubmit = function(e) {
e.preventDefault();
var fd = new FormData(e.target);
var base = window.location.pathname.includes('/depenses') ? '/depenses' : '';
fetch(base + '/api/add', { method: 'POST', body: fd, credentials: 'include' })
.then(function(r) {
if (r.redirected) { window.location.href = r.url; }
else { e.target.reset(); loadDepenses(); var today=new Date(); document.getElementById('depense-date').value = String(today.getDate()).padStart(2,'0')+'/'+String(today.getMonth()+1).padStart(2,'0')+'/'+today.getFullYear(); }
})
.catch(function(err) { console.error('[add] error:', err); });
};
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('api/update/' + id), { method: 'POST', body: fd }).then(function() {
closeModal(); loadDepenses();
});
};
document.getElementById('form-prenom').onsubmit = function(e) {
e.preventDefault();
fetch(api('api/prenom/add'), { method: 'POST', body: new FormData(e.target) }).then(function() { load(); });
};
document.getElementById('form-category').onsubmit = function(e) {
e.preventDefault();
fetch(api('api/category/add'), { method: 'POST', body: new FormData(e.target) }).then(function() { load(); });
};
document.getElementById('form-format').onsubmit = function(e) {
e.preventDefault();
fetch(api('api/format'), { method: 'POST', body: new FormData(e.target) }).then(function() { load(); });
};
document.getElementById('form-trello').onsubmit = function(e) {
e.preventDefault();
var apiKeyInp = document.getElementById('trello-api_key');
var tokenInp = document.getElementById('trello-token');
var listInp = document.getElementById('trello-list_id');
var boardInp = document.getElementById('trello-board_id');
var apiKey = (apiKeyInp.value || '').trim();
var listId = (listInp.value || '').trim();
var boardId = boardInp ? (boardInp.value || '').trim() : '';
var realToken;
if (tokenInp.dataset.hidden === 'true') {
realToken = window._trelloRealToken || '';
} else {
realToken = tokenInp.value;
}
realToken = (realToken || '').trim();
var fd = new FormData();
fd.set('api_key', apiKey);
fd.set('token', realToken);
fd.set('list_id', listId);
fd.set('board_id', boardId);
var resultEl = document.getElementById('trello-save-result');
if (resultEl) resultEl.innerHTML = '<span style="color:var(--text-dim)">⏳ Validation du token...</span>';
fetch(api('api/trello/save'), { method: 'POST', body: fd, redirect: 'follow' })
.then(function(r) { return r.json().then(function(j) { return { ok: r.ok, status: r.status, body: j }; }); })
.then(function(res) {
if (res.ok) {
if (resultEl) resultEl.innerHTML = '<span style="color:var(--matrix)">✅ Token valide et sauvegardé</span>';
load();
} else {
var msg = (res.body && res.body.error) ? res.body.error : ('HTTP ' + res.status);
if (resultEl) resultEl.innerHTML = '<span style="color:var(--rose)">❌ ' + msg + '</span>';
}
}).catch(function(err) {
if (resultEl) resultEl.innerHTML = '<span style="color:var(--rose)">❌ Erreur réseau: ' + err + '</span>';
});
};
window._trelloRealToken = '';
function applyTokenMask() {
var inp = document.getElementById('trello-token');
if (!inp) return;
window._trelloRealToken = inp.value;
inp.dataset.hidden = 'false';
var btn = document.getElementById('toggle-token-visibility');
if (btn) btn.textContent = '🙈 Masquer';
if (!inp._listenerAttached) {
inp.addEventListener('input', function() {
if (inp.dataset.hidden === 'true') {
inp.dataset.hidden = 'false';
var btn2 = document.getElementById('toggle-token-visibility');
if (btn2) btn2.textContent = '🙈 Masquer';
}
window._trelloRealToken = inp.value;
});
inp._listenerAttached = true;
}
}
function toggleTokenVisibility() {
var inp = document.getElementById('trello-token');
var btn = document.getElementById('toggle-token-visibility');
if (!inp) return;
if (inp.dataset.hidden === 'true') {
inp.value = window._trelloRealToken || '';
inp.dataset.hidden = 'false';
if (btn) btn.textContent = '🙈 Masquer';
} else {
window._trelloRealToken = inp.value;
if (inp.value) inp.value = '•'.repeat(Math.min(inp.value.length, 40));
inp.dataset.hidden = 'true';
if (btn) btn.textContent = '👁️ Afficher';
}
}
document.getElementById('form-budget').onsubmit = function(e) {
e.preventDefault();
fetch(api('api/budget'), { method: 'POST', body: new FormData(e.target) }).then(function() { load(); });
};
document.getElementById('form-recurring').onsubmit = function(e) {
e.preventDefault();
fetch(api('api/recurring/add'), { method: 'POST', body: new FormData(e.target) }).then(function() { load(); });
};
async function fetchTrelloLists() {
var apiKey = document.getElementById('trello-api_key').value;
var token = document.getElementById('trello-token').value;
if (!apiKey || !token) { alert('Entrez d\'abord API Key et Token'); return; }
var r = await fetch(api('api/trello/lists'));
var d = await r.json();
if (d.error) { alert('Erreur: ' + d.error); return; }
var select = document.getElementById('trello-list-select');
select.innerHTML = '<option value="">-- Sélectionner --</option>';
d.lists.forEach(function(lst) {
var opt = document.createElement('option');
opt.value = lst.id;
opt.textContent = lst.name;
select.appendChild(opt);
});
document.getElementById('trello-lists-container').style.display = 'block';
}
function selectTrelloList(listId) {
if (listId) document.getElementById('trello-list_id').value = listId;
}
async function generate() {
var r = await fetch(api('api/generate'), { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({depenses: depenses}) });
var d = await r.json();
alert(d.text);
}
async function sendAll() {
if (!config.trello || !config.trello.api_key) { alert('Configure Trello!'); showPage('config'); return; }
var sent = 0;
for (var i=0; i<depenses.length; i++) {
if (depenses[i].status !== 'Envoyé ✅') {
var r = await fetch(api('api/trello/send_one/' + depenses[i].id), { method: 'POST' });
if (r.ok) sent++;
}
}
alert('Envoyé(s): ' + sent);
loadDepenses();
}
function exportCSV() { window.location.href = 'api/export'; }
function exportPDF() {
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('api/import'), { method: 'POST', body: fd }).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) { loadDepenses(); alert('Importé: ' + d.imported); }
else { alert('Erreur: ' + d.error); }
});
input.value = '';
}
function toggleMobileSidebar() {
var sidebar = document.querySelector('.sidebar-left');
if (sidebar) {
var isVisible = sidebar.style.display !== 'none' && sidebar.style.display !== '';
sidebar.style.display = isVisible ? 'none' : 'block';
}
}
// Close modal on overlay click
document.getElementById('edit-modal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
load();
</script>
</body>
</html>