/** * Dashboard-KPIs + Activity-Feed Component (Alpine.js) * * Used in: dashboard.html and app.html (default landing page) * * Loads: * GET /api/v1/dashboard/kpis → { open_deals_count, pipeline_value, won_this_month, conversion_rate } * GET /api/v1/dashboard/feed?limit=20 → ActivityFeedItem[] * * Renders: 4 KPI cards + an activity feed list. * * R-5: x-text only. User-controlled strings are not interpolated as HTML. */ import { api } from '/js/api.js'; export function dashboardKpis() { return { loading: true, kpis: null, feed: [], error: null, async load() { this.loading = true; this.error = null; try { // Fetch KPIs and feed in parallel const [kpis, feed] = await Promise.all([ api.get('/dashboard/kpis'), api.get('/dashboard/feed?limit=20'), ]); this.kpis = kpis; this.feed = Array.isArray(feed) ? feed : (feed && feed.items) || []; } catch (err) { this.error = err.message || 'Fehler beim Laden der Dashboard-Daten.'; if (window.Alpine) { Alpine.store('notifications').error(this.error); } } finally { this.loading = false; } }, formatCurrency(value) { if (value === null || value === undefined) return '–'; const num = Number(value); if (Number.isNaN(num)) return '–'; return num.toLocaleString('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0, }); }, formatPercent(value) { if (value === null || value === undefined) return '–'; const num = Number(value); if (Number.isNaN(num)) return '–'; // Backend may return 0-1 or 0-100. Heuristic: >1 → already in %. const pct = num > 1 ? num : num * 100; return pct.toFixed(1) + ' %'; }, formatDate(iso) { if (!iso) return ''; try { return new Date(iso).toLocaleDateString('de-DE'); } catch { return iso; } }, }; } // Register globally for `x-data="dashboardKpis()"` if (typeof window !== 'undefined') { document.addEventListener('alpine:init', () => { if (window.Alpine) { window.Alpine.data('dashboardKpis', dashboardKpis); } }); } export default dashboardKpis;