window.AnalyticsCharts = (function () { var charts = {}; var colors = { gold: 'rgba(252, 211, 77, 0.85)', cyan: 'rgba(56, 189, 248, 0.85)', green: 'rgba(74, 222, 128, 0.85)', red: 'rgba(251, 113, 133, 0.85)', purple: 'rgba(168, 85, 247, 0.85)', orange: 'rgba(255, 159, 67, 0.85)', gray: 'rgba(159, 176, 196, 0.85)', grid: 'rgba(159, 176, 196, 0.12)', text: '#c8d4e0', }; var palette = [colors.gold, colors.cyan, colors.green, colors.purple, colors.orange, colors.red, colors.gray]; function destroyAll() { Object.keys(charts).forEach(function (k) { if (charts[k]) { charts[k].destroy(); charts[k] = null; } }); } function barChart(id, labels, values, label, horizontal) { var canvas = document.getElementById(id); if (!canvas || typeof Chart === 'undefined') return; if (charts[id]) charts[id].destroy(); charts[id] = new Chart(canvas, { type: 'bar', data: { labels: labels, datasets: [{ label: label, data: values, backgroundColor: palette.slice(0, labels.length), borderRadius: 6 }], }, options: { indexAxis: horizontal ? 'y' : 'x', responsive: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: colors.text, maxRotation: 45 }, grid: { color: colors.grid } }, y: { ticks: { color: colors.text }, grid: { color: colors.grid } }, }, }, }); } function doughnut(id, labels, values) { var canvas = document.getElementById(id); if (!canvas || typeof Chart === 'undefined') return; if (charts[id]) charts[id].destroy(); charts[id] = new Chart(canvas, { type: 'doughnut', data: { labels: labels, datasets: [{ data: values, backgroundColor: palette, borderWidth: 0 }] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { color: colors.text, boxWidth: 12 } } } }, }); } function lineChart(id, labels, values) { var canvas = document.getElementById(id); if (!canvas || typeof Chart === 'undefined') return; if (charts[id]) charts[id].destroy(); charts[id] = new Chart(canvas, { type: 'line', data: { labels: labels, datasets: [{ data: values, borderColor: colors.cyan, backgroundColor: 'rgba(56,189,248,0.15)', fill: true, tension: 0.3 }], }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: colors.text }, grid: { color: colors.grid } }, y: { ticks: { color: colors.text }, grid: { color: colors.grid } }, }, }, }); } function renderKpis(container, kpis) { if (!container || !kpis) return; var items = [ ['Klanten totaal', kpis.clients_total], ['Actieve klanten', kpis.clients_active], ['Pipeline €', '€' + Math.round(kpis.pipeline_eur || 0).toLocaleString('nl-NL')], ['Supermarkten', kpis.supermarkets], ['Groothandels', kpis.wholesalers], ['CRM actief', kpis.crm_partnerships], ['RSS items', kpis.rss_items], ['Bookmarks', kpis.rss_bookmarks], ['Promo's', kpis.promo_campaigns], ['Agent events', kpis.agent_events], ['Goedkeuringen', kpis.pending_approvals], ['Contacten SM', kpis.contacts_supermarket], ['NAS docs', kpis.nas_docs], ]; container.innerHTML = items.map(function (it) { return '
' + it[0] + '' + it[1] + '
'; }).join(''); } function renderTables(data) { var dealsEl = document.getElementById('analytics-deals-table'); var eventsEl = document.getElementById('analytics-events-table'); if (dealsEl) { dealsEl.innerHTML = (data.recent_deals || []).map(function (d) { return '' + (d.title || '—') + '' + (d.stage || '') + '€' + Math.round(Number(d.value || 0)).toLocaleString('nl-NL') + ''; }).join('') || 'Geen deals'; } if (eventsEl) { eventsEl.innerHTML = (data.recent_events || []).map(function (e) { var t = (e.created_at || '').substring(0, 16).replace('T', ' '); return '' + (e.agent_name || '') + '' + (e.title || e.event_type || '') + '' + t + ''; }).join('') || 'Geen events'; } } function render(data) { if (!data) return; renderKpis(document.getElementById('analytics-kpis'), data.kpis); barChart('chart-deals', (data.deals_by_stage || []).map(function (r) { return r.stage; }), (data.deals_by_stage || []).map(function (r) { return Number(r.total || r.cnt || 0); }), 'EUR'); barChart('chart-clients', (data.clients_by_stage || []).map(function (r) { return r.stage; }), (data.clients_by_stage || []).map(function (r) { return Number(r.cnt || 0); }), 'Klanten'); barChart('chart-chains', (data.supermarkets_by_chain || []).map(function (r) { return r.chain; }), (data.supermarkets_by_chain || []).map(function (r) { return Number(r.cnt || 0); }), 'Filialen'); doughnut('chart-provinces', (data.supermarkets_by_province || []).map(function (r) { return r.province; }), (data.supermarkets_by_province || []).map(function (r) { return Number(r.cnt || 0); })); doughnut('chart-partnerships', (data.partnership_breakdown || []).map(function (r) { return r.status; }), (data.partnership_breakdown || []).map(function (r) { return Number(r.cnt || 0); })); barChart('chart-agents', (data.events_by_agent || []).map(function (r) { return r.agent_name; }), (data.events_by_agent || []).map(function (r) { return Number(r.cnt || 0); }), 'Events', true); lineChart('chart-timeline', (data.events_timeline || []).map(function (r) { return String(r.day || '').substring(5); }), (data.events_timeline || []).map(function (r) { return Number(r.cnt || 0); })); doughnut('chart-rss', (data.rss_by_category || []).map(function (r) { return r.category || 'other'; }), (data.rss_by_category || []).map(function (r) { return Number(r.cnt || 0); })); barChart('chart-wholesale', (data.wholesalers_by_province || []).map(function (r) { return r.province; }), (data.wholesalers_by_province || []).map(function (r) { return Number(r.cnt || 0); }), 'GH'); doughnut('chart-sentiment', (data.sentiment_distribution || []).map(function (r) { return r.sentiment_label || 'neutral'; }), (data.sentiment_distribution || []).map(function (r) { return Number(r.cnt || 0); })); barChart('chart-words', (data.top_words || []).slice(0, 10).map(function (r) { return r.lemma; }), (data.top_words || []).slice(0, 10).map(function (r) { return Number(r.total || 0); }), 'Count', true); barChart('chart-opportunities', (data.top_opportunities || []).map(function (r) { return (r.chain || '') + ' ' + (r.city || ''); }), (data.top_opportunities || []).map(function (r) { return Math.round(Number(r.halal_opportunity_score || 0)); }), 'Score', true); barChart('chart-promo', (data.promo_by_chain || []).map(function (r) { return r.chain || '?'; }), (data.promo_by_chain || []).map(function (r) { return Number(r.cnt || 0); }), 'Promo'); doughnut('chart-milestones', (data.milestones_by_status || []).map(function (r) { return r.status; }), (data.milestones_by_status || []).map(function (r) { return Number(r.cnt || 0); })); renderTables(data); } return { render: render, destroyAll: destroyAll }; })(); function analyticsHub() { return { loading: false, updatedAt: '—', liveLabel: 'Live', f: { chain: '', province: '', stage: '', agent: '', days: 90 }, meta: {}, _pollStop: null, init() { var initial = window.ANALYTICS_INITIAL || {}; this.meta = initial.filter_meta || {}; AnalyticsCharts.render(initial); this.updatedAt = (initial.generated_at || '').substring(0, 19).replace('T', ' '); this._pollStop = CockpitLive.startPolling(function () { return this.refresh(false); }.bind(this), 30000); }, params() { var p = new URLSearchParams(); Object.entries(this.f).forEach(function (e) { if (e[1] !== null && e[1] !== '' && e[1] !== undefined) p.set(e[0], e[1]); }); return p.toString(); }, async refresh(toast) { this.loading = true; try { var data = await fetch('/analytics/api/data?' + this.params()).then(function (r) { return r.json(); }); this.meta = data.filter_meta || this.meta; AnalyticsCharts.render(data); this.updatedAt = (data.generated_at || '').substring(0, 19).replace('T', ' '); if (toast !== false) Cockpit.toast('Analytics bijgewerkt', 'success'); } catch (e) { if (toast !== false) Cockpit.toast(e.message, 'error'); } finally { this.loading = false; } }, resetFilters() { this.f = { chain: '', province: '', stage: '', agent: '', days: 90 }; this.refresh(); }, }; }