/* ============================================================ BAN YARO — Ausgaben-Tracker Tabs: Übersicht | Einträge | Statistik ============================================================ */ window.Page_expenses = (() => { let _container = null; let _appState = null; let _tab = 'uebersicht'; // Cache let _summary = null; let _entries = []; // Monats-Statistik-Daten (pro Monat und Kategorie) let _statsData = null; const TABS = [ { id: 'uebersicht', label: 'Übersicht', icon: 'house-line' }, { id: 'eintraege', label: 'Einträge', icon: 'list-bullets' }, { id: 'statistik', label: 'Statistik', icon: 'chart-bar' }, ]; const KATEGORIEN = [ { id: 'tierarzt', label: 'Tierarzt', icon: 'syringe', color: '#ef4444' }, { id: 'futter', label: 'Futter', icon: 'bowl-food', color: '#f59e0b' }, { id: 'zubehoer', label: 'Zubehör', icon: 'shopping-bag', color: '#8b5cf6' }, { id: 'versicherung',label: 'Versicherung',icon: 'shield', color: '#3b82f6' }, { id: 'sitter', label: 'Sitter', icon: 'paw-print', color: '#10b981' }, { id: 'sonstiges', label: 'Sonstiges', icon: 'receipt', color: '#6b7280' }, ]; function _kat(id) { return KATEGORIEN.find(k => k.id === id) || { id, label: id, icon: 'receipt', color: '#6b7280' }; } // ---------------------------------------------------------- // LIFECYCLE // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _summary = null; _entries = []; _statsData = null; _render(); } async function refresh() { _summary = null; _entries = []; _statsData = null; await _renderTab(); } // ---------------------------------------------------------- // SHELL // ---------------------------------------------------------- function _render() { _container.innerHTML = `
${TABS.map(t => ` `).join('')}
`; _container.querySelectorAll('#exp-tabs .by-tab').forEach(btn => { btn.addEventListener('click', () => { _tab = btn.dataset.tab; _container.querySelectorAll('#exp-tabs .by-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === _tab) ); _renderTab(); }); }); _container.querySelector('#exp-fab') ?.addEventListener('click', () => _showForm(null)); _renderTab(); } // ---------------------------------------------------------- // TAB ROUTER // ---------------------------------------------------------- async function _renderTab() { const el = _container.querySelector('#exp-content'); if (!el) return; el.innerHTML = `
${UI.skeleton(4)}
`; try { switch (_tab) { case 'uebersicht': await _renderUebersicht(el); break; case 'eintraege': await _renderEintraege(el); break; case 'statistik': await _renderStatistik(el); break; } } catch (e) { el.innerHTML = `
Fehler beim Laden: ${e.message || 'Unbekannter Fehler'}
`; } } // ---------------------------------------------------------- // TAB: ÜBERSICHT // ---------------------------------------------------------- async function _renderUebersicht(el) { if (!_summary) { _summary = await API.get('/expenses/summary'); } const s = _summary; const kacheln = KATEGORIEN.map(k => { const betrag = s.monat[k.id] || 0; return `
${UI.icon(k.icon)}
${k.label}
${_fmt(betrag)}
`; }).join(''); const letzteMonat = await _getLetzteMonateData(); const vergleich = letzteMonat.length > 1 ? _vergleichHtml(letzteMonat) : ''; el.innerHTML = `
Dieser Monat
${_fmt(s.gesamt_monat)}
${UI.icon('calendar')} Dieses Jahr: ${_fmt(s.gesamt_jahr)}
${kacheln}
${vergleich}
`; } async function _getLetzteMonateData() { // Letzten 6 Monate aus den Einträgen berechnen if (!_entries.length) { _entries = await API.get('/expenses?limit=500'); } const monatMap = {}; _entries.forEach(e => { const m = e.datum.substring(0, 7); // YYYY-MM monatMap[m] = (monatMap[m] || 0) + e.betrag; }); return Object.entries(monatMap) .sort((a, b) => b[0].localeCompare(a[0])) .slice(0, 6) .reverse(); } function _vergleichHtml(data) { if (!data.length) return ''; const max = Math.max(...data.map(d => d[1]), 1); const balken = data.map(([monat, summe]) => { const pct = Math.round((summe / max) * 100); const [y, m] = monat.split('-'); const label = new Date(parseInt(y), parseInt(m) - 1, 1) .toLocaleString('de-DE', { month: 'short' }); return `
${label}
${_fmtShort(summe)}
`; }).join(''); return `
${UI.icon('chart-bar')} Verlauf (6 Monate)
${balken}
`; } // ---------------------------------------------------------- // TAB: EINTRÄGE // ---------------------------------------------------------- async function _renderEintraege(el) { if (!_entries.length) { _entries = await API.get('/expenses?limit=500'); } if (!_entries.length) { el.innerHTML = UI.emptyState({ icon: UI.icon('receipt'), title: 'Noch keine Ausgaben', text: 'Tippe auf + um deine erste Ausgabe einzutragen.', }); return; } // Nach Monat gruppieren const groups = {}; _entries.forEach(e => { const m = e.datum.substring(0, 7); if (!groups[m]) groups[m] = []; groups[m].push(e); }); const html = Object.entries(groups) .sort((a, b) => b[0].localeCompare(a[0])) .map(([monat, items]) => { const [y, m] = monat.split('-'); const titel = new Date(parseInt(y), parseInt(m) - 1, 1) .toLocaleString('de-DE', { month: 'long', year: 'numeric' }); const summe = items.reduce((s, e) => s + e.betrag, 0); const rows = items.map(e => { const k = _kat(e.kategorie); const datum = new Date(e.datum + 'T00:00:00') .toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const dogBadge = e.dog_name ? `${UI.icon('paw-print')} ${_esc(e.dog_name)}` : ''; const notiz = e.notiz ? `${_esc(e.notiz)}` : ''; return `
${UI.icon(k.icon)}
${k.label} ${dogBadge} ${datum}
${notiz}
${_fmt(e.betrag)}
`; }).join(''); return `
${titel} ${_fmt(summe)}
${rows}
`; }).join(''); el.innerHTML = `
${html}
`; el.querySelectorAll('.exp-entry').forEach(row => { row.addEventListener('click', () => { const id = parseInt(row.dataset.id); const entry = _entries.find(e => e.id === id); if (entry) _showForm(entry); }); }); } // ---------------------------------------------------------- // TAB: STATISTIK // ---------------------------------------------------------- async function _renderStatistik(el) { if (!_summary) { _summary = await API.get('/expenses/summary'); } if (!_entries.length) { _entries = await API.get('/expenses?limit=500'); } const s = _summary; const gesamtJahr = s.gesamt_jahr || 1; // Jahres-Aufteilung nach Kategorien const katBalken = KATEGORIEN .filter(k => (s.jahr[k.id] || 0) > 0) .sort((a, b) => (s.jahr[b.id] || 0) - (s.jahr[a.id] || 0)) .map(k => { const val = s.jahr[k.id] || 0; const pct = Math.round((val / gesamtJahr) * 100); return `
${UI.icon(k.icon)} ${k.label}
${pct}%
${_fmt(val)}
`; }).join(''); // Monats-Balken (aktuelles Jahr, Monat für Monat) const heute = new Date(); const jahrStr = heute.getFullYear().toString(); const monatMap = {}; _entries .filter(e => e.datum.startsWith(jahrStr)) .forEach(e => { const m = parseInt(e.datum.split('-')[1]); monatMap[m] = (monatMap[m] || 0) + e.betrag; }); const maxMonat = Math.max(...Object.values(monatMap), 1); const MONATE = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']; const monatsBalken = MONATE.map((label, i) => { const val = monatMap[i + 1] || 0; const pct = Math.round((val / maxMonat) * 100); const isAktiv = (i + 1) === (heute.getMonth() + 1); return `
${label}
`; }).join(''); el.innerHTML = `
Gesamt dieses Jahr
${_fmt(s.gesamt_jahr)}
${UI.icon('chart-bar')} Monatsübersicht ${jahrStr}
${monatsBalken}
${UI.icon('chart-pie')} Aufteilung nach Kategorie
${katBalken || `
Noch keine Ausgaben dieses Jahr.
`}
`; } // ---------------------------------------------------------- // FORMULAR — Neu / Bearbeiten // ---------------------------------------------------------- function _showForm(entry) { const isEdit = !!entry; const today = new Date().toISOString().split('T')[0]; const formId = 'exp-form'; const dogOptions = (_appState.dogs || []).map(d => `` ).join(''); const katOptions = KATEGORIEN.map(k => `` ).join(''); const body = `
${dogOptions ? `
` : ''}
`; const footer = isEdit ? ` ` : ` `; const modal = UI.modal.open({ title: isEdit ? 'Ausgabe bearbeiten' : 'Neue Ausgabe', body, footer, }); if (isEdit) { modal.querySelector('#exp-delete-btn')?.addEventListener('click', async () => { if (!window.confirm('Diesen Eintrag wirklich löschen?')) return; try { await API.del(`/expenses/${entry.id}`); UI.modal.close(); UI.toast.success('Ausgabe gelöscht.'); _invalidateCache(); await _renderTab(); } catch (e) { UI.toast.error(e.message || 'Fehler beim Löschen.'); } }); } modal.querySelector(`#${formId}`)?.addEventListener('submit', async (ev) => { ev.preventDefault(); const fd = UI.formData(ev.target); const body = { kategorie: fd.kategorie, betrag: parseFloat(fd.betrag), datum: fd.datum, notiz: fd.notiz || null, dog_id: fd.dog_id ? parseInt(fd.dog_id) : null, }; try { if (isEdit) { await API.patch(`/expenses/${entry.id}`, body); UI.toast.success('Ausgabe aktualisiert.'); } else { await API.post('/expenses', body); UI.toast.success('Ausgabe gespeichert.'); } UI.modal.close(); _invalidateCache(); await _renderTab(); } catch (e) { UI.toast.error(e.message || 'Fehler beim Speichern.'); } }); } // ---------------------------------------------------------- // Hilfsfunktionen // ---------------------------------------------------------- function _invalidateCache() { _summary = null; _entries = []; _statsData = null; } function _fmt(val) { return (val || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }); } function _fmtShort(val) { if (!val) return '0 €'; if (val >= 1000) return (val / 1000).toFixed(1).replace('.', ',') + ' k€'; return Math.round(val) + ' €'; } function _esc(s) { if (!s) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } return { init, refresh }; })();