diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 0acff7a..1930060 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -6947,3 +6947,438 @@ svg.empty-state-icon { color: var(--c-text); line-height: 1.5; } + +/* ============================================================ + Ausgaben-Tracker (expenses.js) + ============================================================ */ + +/* FAB */ +.exp-fab { + position: fixed; + bottom: calc(var(--nav-height, 64px) + var(--space-4)); + right: var(--space-4); + z-index: 100; + width: 52px; + height: 52px; + border-radius: 50%; + background: var(--c-primary); + color: #fff; + border: none; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 14px rgba(0,0,0,.25); + cursor: pointer; + font-size: 1.35rem; + transition: transform .15s, box-shadow .15s; +} +.exp-fab:active { + transform: scale(.93); + box-shadow: 0 2px 8px rgba(0,0,0,.2); +} + +/* Lade-/Fehler-Zustände */ +.exp-loading { padding: var(--space-4); } +.exp-error { + padding: var(--space-4); + color: var(--c-danger); + font-size: var(--text-sm); + text-align: center; +} +.exp-empty-hint { + color: var(--c-text-secondary); + font-size: var(--text-sm); + padding: var(--space-3) 0; + text-align: center; +} + +/* ---- Hero-Card (Übersicht & Statistik oben) ---- */ +.exp-hero-card { + background: linear-gradient(135deg, var(--c-primary) 0%, color-mix(in srgb, var(--c-primary) 75%, #000) 100%); + color: #fff; + border-radius: var(--radius-xl, 16px); + padding: var(--space-5) var(--space-4); + margin: var(--space-3) var(--space-3) var(--space-4); + text-align: center; + box-shadow: 0 6px 20px rgba(0,0,0,.15); +} +.exp-hero-card--sm { + padding: var(--space-4) var(--space-4); +} +.exp-hero-label { + font-size: var(--text-sm); + font-weight: var(--weight-medium); + opacity: .85; + margin-bottom: var(--space-1); + text-transform: uppercase; + letter-spacing: .04em; +} +.exp-hero-betrag { + font-size: clamp(1.9rem, 7vw, 2.8rem); + font-weight: var(--weight-bold); + line-height: 1.1; + letter-spacing: -.02em; +} +.exp-hero-meta { + margin-top: var(--space-2); + font-size: var(--text-sm); + opacity: .85; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: var(--space-2); +} + +/* Trend-Badge */ +.exp-trend { + display: inline-flex; + align-items: center; + gap: 2px; + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + padding: 2px 8px; + border-radius: 999px; +} +.exp-trend--up { background: rgba(239,68,68,.25); } +.exp-trend--down { background: rgba(16,185,129,.25); } + +/* ---- Kachel-Grid (Übersicht) ---- */ +.exp-kachel-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--space-2); + padding: 0 var(--space-3) var(--space-3); +} +.exp-kachel { + background: var(--c-surface); + border: 1px solid var(--c-border); + border-radius: var(--radius-lg); + padding: var(--space-3) var(--space-2); + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-1); +} +.exp-kachel-icon { + width: 44px; + height: 44px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.3rem; + margin-bottom: var(--space-1); +} +.exp-kachel-betrag { + font-size: var(--text-sm); + font-weight: var(--weight-bold); + line-height: 1.1; +} +.exp-kachel-label { + font-size: var(--text-xs); + color: var(--c-text-secondary); + line-height: 1.2; +} + +/* ---- Sektion-Block (Verlauf etc.) ---- */ +.exp-section { + margin: 0 var(--space-3) var(--space-4); + background: var(--c-surface); + border: 1px solid var(--c-border); + border-radius: var(--radius-lg); + padding: var(--space-4); +} +.exp-section-title { + font-size: var(--text-sm); + font-weight: var(--weight-semibold); + color: var(--c-text-secondary); + margin-bottom: var(--space-3); + display: flex; + align-items: center; + gap: var(--space-1); + text-transform: uppercase; + letter-spacing: .04em; +} + +/* ---- Balkendiagramm (Verlauf) ---- */ +.exp-bar-chart { + display: flex; + align-items: flex-end; + gap: var(--space-1); + height: 80px; +} +.exp-bar-chart--12 { + height: 90px; + gap: 4px; +} +.exp-bar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; +} +.exp-bar-item--aktiv .exp-bar-label { + color: var(--c-primary); + font-weight: var(--weight-semibold); +} +.exp-bar-track { + width: 100%; + height: 60px; + background: var(--c-surface-2, #f3f4f6); + border-radius: var(--radius-sm) var(--radius-sm) 0 0; + display: flex; + flex-direction: column; + justify-content: flex-end; + overflow: hidden; +} +.exp-bar-track--stack { + height: 70px; +} +.exp-bar-fill { + width: 100%; + background: var(--c-primary); + border-radius: var(--radius-sm) var(--radius-sm) 0 0; + transition: height .4s ease; +} +.exp-bar-fill--aktiv { background: var(--c-primary); } +.exp-stack-seg { + width: 100%; + min-height: 2px; + transition: height .4s ease; +} +.exp-bar-label { + font-size: var(--text-xs); + color: var(--c-text-muted, #9ca3af); + white-space: nowrap; +} +.exp-bar-val { + font-size: var(--text-xs); + color: var(--c-text-secondary); +} + +/* ---- Einträge-Liste ---- */ +.exp-list { + padding: 0 var(--space-3); +} +.exp-month-group { + margin-bottom: var(--space-3); +} +.exp-month-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-2) var(--space-3); + background: var(--c-surface-2, #f3f4f6); + border-radius: var(--radius-md); + margin-bottom: var(--space-2); +} +.exp-month-title { + font-size: var(--text-sm); + font-weight: var(--weight-semibold); + color: var(--c-text-secondary); + text-transform: uppercase; + letter-spacing: .04em; +} +.exp-month-summe { + font-size: var(--text-sm); + font-weight: var(--weight-bold); + color: var(--c-primary); +} + +/* Einzelner Eintrag */ +.exp-entry { + display: flex; + align-items: center; + gap: var(--space-3); + background: var(--c-surface); + border: 1px solid var(--c-border); + border-radius: var(--radius-md); + padding: var(--space-3); + margin-bottom: var(--space-2); + cursor: pointer; + transition: background .15s; +} +.exp-entry:active { background: var(--c-surface-2, #f3f4f6); } + +/* Icon-Badge mit Kategorie-Farbe */ +.exp-entry-icon-badge { + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--kat-color) 15%, transparent); + color: var(--kat-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; +} + +.exp-entry-body { + flex: 1; + min-width: 0; +} +.exp-entry-head { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-1); + margin-bottom: 2px; +} +.exp-entry-datum { + font-size: var(--text-xs); + color: var(--c-text-muted, #9ca3af); + flex-shrink: 0; +} +.exp-entry-kat { + font-size: var(--text-sm); + font-weight: var(--weight-medium); + color: var(--c-text); +} +.exp-entry-notiz { + display: block; + font-size: var(--text-xs); + color: var(--c-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.exp-dog-badge { + display: inline-flex; + align-items: center; + gap: 2px; + font-size: var(--text-xs); + color: var(--c-text-secondary); + background: var(--c-surface-2, #f3f4f6); + border-radius: 999px; + padding: 1px 6px; +} + +/* Rechte Spalte: Betrag + Löschen-Icon */ +.exp-entry-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: var(--space-1); + flex-shrink: 0; +} +.exp-entry-betrag { + font-size: var(--text-base); + font-weight: var(--weight-bold); + color: var(--c-text); + white-space: nowrap; +} +.exp-entry-del { + background: transparent; + border: none; + color: var(--c-text-muted, #9ca3af); + cursor: pointer; + padding: 2px 4px; + border-radius: var(--radius-sm); + font-size: 1rem; + line-height: 1; + transition: color .15s; +} +.exp-entry-del:hover { color: var(--c-danger); } + +/* ---- Statistik: Kategorie-Balken-Reihen ---- */ +.exp-stat-rows { + display: flex; + flex-direction: column; + gap: var(--space-2); +} +.exp-stat-row { + display: grid; + grid-template-columns: 120px 1fr 36px 80px; + align-items: center; + gap: var(--space-2); +} +.exp-stat-label { + display: flex; + align-items: center; + gap: var(--space-1); + font-size: var(--text-sm); + color: var(--c-text); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.exp-stat-icon { flex-shrink: 0; } +.exp-stat-bar-wrap { + height: 8px; + background: var(--c-surface-2, #f3f4f6); + border-radius: 999px; + overflow: hidden; +} +.exp-stat-bar { + height: 8px; + border-radius: 999px; + transition: width .5s ease; +} +.exp-stat-pct { + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + color: var(--c-text-secondary); + text-align: right; +} +.exp-stat-val { + font-size: var(--text-sm); + font-weight: var(--weight-semibold); + color: var(--c-text); + text-align: right; + white-space: nowrap; +} + +/* ---- Donut-Diagramm (CSS conic-gradient) ---- */ +.exp-donut-wrap { + display: flex; + align-items: center; + gap: var(--space-5); + flex-wrap: wrap; +} +.exp-donut { + position: relative; + width: 120px; + height: 120px; + border-radius: 50%; + flex-shrink: 0; +} +.exp-donut-hole { + position: absolute; + inset: 28px; + background: var(--c-surface); + border-radius: 50%; +} +.exp-donut-legend { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--space-2); + min-width: 130px; +} +.exp-donut-legend-item { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: var(--text-sm); +} +.exp-donut-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} +.exp-donut-legend-label { + flex: 1; + color: var(--c-text); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.exp-donut-legend-pct { + font-weight: var(--weight-semibold); + color: var(--c-text-secondary); +} diff --git a/backend/static/js/pages/expenses.js b/backend/static/js/pages/expenses.js index c37d19e..e7c2b61 100644 --- a/backend/static/js/pages/expenses.js +++ b/backend/static/js/pages/expenses.js @@ -12,7 +12,6 @@ window.Page_expenses = (() => { // Cache let _summary = null; let _entries = []; - // Monats-Statistik-Daten (pro Monat und Kategorie) let _statsData = null; const TABS = [ @@ -114,6 +113,10 @@ window.Page_expenses = (() => { } const s = _summary; + // Vormonatsvergleich berechnen + const letzteMonat = await _getLetzteMonateData(); + const trendHtml = _trendHtml(letzteMonat); + const kacheln = KATEGORIEN.map(k => { const betrag = s.monat[k.id] || 0; return ` @@ -121,38 +124,35 @@ window.Page_expenses = (() => {
+