- 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>
1445 lines
54 KiB
HTML
1445 lines
54 KiB
HTML
<!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>
|