window.BriefingCharts = (function () { var charts = {}; var typewriterTimer = null; function destroyAll() { Object.keys(charts).forEach(function (k) { if (charts[k]) { charts[k].destroy(); charts[k] = null; } }); } function parseContent(content) { var parts = (content || '').split(/\n---\n/); var ai = parts[0] || ''; var summary = '', actions = [], longTerm = []; var sm = ai.match(/##\s*Samenvatting\s*\n([\s\S]*?)(?=##\s*Actiepunten|##\s*Lange termijn|$)/i); if (sm) summary = sm[1].trim().replace(/\*\*/g, ''); var am = ai.match(/##\s*Actiepunten[^\n]*\n([\s\S]*?)(?=##\s*Lange termijn|$)/i); if (am) actions = am[1].split('\n').map(function (l) { return l.replace(/^[-*]\s*/, '').trim(); }).filter(Boolean); var lm = ai.match(/##\s*Lange termijn[^\n]*\n([\s\S]*)/i); if (lm) longTerm = lm[1].split('\n').map(function (l) { return l.replace(/^[-*]\s*/, '').trim(); }).filter(Boolean); if (!summary && ai.trim()) summary = ai.trim().slice(0, 800).replace(/\*\*/g, ''); return { summary: summary, actions: actions, longTerm: longTerm }; } function typewriter(el, text, speed) { if (!el) return; if (typewriterTimer) clearInterval(typewriterTimer); el.textContent = ''; if (!text) { el.textContent = 'Klik «Genereer dagrapport» voor je persoonlijke Herman briefing.'; return; } var i = 0; typewriterTimer = setInterval(function () { if (i < text.length) { el.textContent += text.charAt(i); i++; } else clearInterval(typewriterTimer); }, speed || 8); } function countUp(el, end, prefix, suffix) { if (!el) return; prefix = prefix || ''; suffix = suffix || ''; var start = 0, dur = 600, t0 = performance.now(); function step(t) { var p = Math.min(1, (t - t0) / dur); var v = Math.round(start + (end - start) * p); el.textContent = prefix + v.toLocaleString('nl-NL') + suffix; if (p < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } function chartColors() { return { gold: 'rgba(252, 211, 77, 0.9)', cyan: 'rgba(56, 189, 248, 0.9)', green: 'rgba(74, 222, 128, 0.9)', red: 'rgba(251, 113, 133, 0.9)', gray: 'rgba(159, 176, 196, 0.85)', grid: 'rgba(159, 176, 196, 0.15)', text: '#c8d4e0', }; } function renderPipeline(canvas, stats) { if (!canvas || typeof Chart === 'undefined') return; var rows = stats.deals_by_stage || []; var c = chartColors(); if (charts.pipeline) charts.pipeline.destroy(); charts.pipeline = new Chart(canvas, { type: 'bar', data: { labels: rows.map(function (r) { return r.stage || '?'; }), datasets: [{ label: 'EUR', data: rows.map(function (r) { return Number(r.total) || 0; }), backgroundColor: c.gold, borderRadius: 6 }] }, options: { responsive: true, plugins: { legend: { display: false }, title: { display: true, text: 'Pipeline per stage', color: c.text } }, scales: { y: { ticks: { color: c.text, callback: function (v) { return '€' + v.toLocaleString('nl-NL'); } }, grid: { color: c.grid } }, x: { ticks: { color: c.text }, grid: { display: false } } } }, }); } function renderSentiment(canvas, stats) { if (!canvas || typeof Chart === 'undefined') return; var files = stats.nas_files || [], counts = { positive: 0, neutral: 0, negative: 0 }; files.forEach(function (f) { var s = (f.sentiment_label || 'neutral').toLowerCase(); if (counts[s] !== undefined) counts[s]++; }); if (!files.length) counts.neutral = 1; var c = chartColors(); if (charts.sentiment) charts.sentiment.destroy(); charts.sentiment = new Chart(canvas, { type: 'doughnut', data: { labels: ['Positief', 'Neutraal', 'Negatief'], datasets: [{ data: [counts.positive, counts.neutral, counts.negative], backgroundColor: [c.green, c.gray, c.red], borderWidth: 0 }] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { color: c.text } }, title: { display: true, text: 'NAS sentiment', color: c.text } } }, }); } function renderWords(canvas, stats) { if (!canvas || typeof Chart === 'undefined') return; var rows = (stats.top_words || []).slice(0, 8), c = chartColors(); if (charts.words) charts.words.destroy(); charts.words = new Chart(canvas, { type: 'bar', data: { labels: rows.map(function (r) { return r.lemma; }), datasets: [{ data: rows.map(function (r) { return Number(r.total) || 0; }), backgroundColor: c.cyan, borderRadius: 6 }] }, options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false }, title: { display: true, text: 'Top woorden NAS', color: c.text } }, scales: { x: { ticks: { color: c.text }, grid: { color: c.grid } }, y: { ticks: { color: c.text }, grid: { display: false } } } }, }); } function renderAgents(canvas, stats) { if (!canvas || typeof Chart === 'undefined') return; var map = {}; (stats.recent_events || []).forEach(function (e) { var a = e.agent_name || 'other'; map[a] = (map[a] || 0) + 1; }); var labels = Object.keys(map), c = chartColors(); if (charts.agents) charts.agents.destroy(); charts.agents = new Chart(canvas, { type: 'polarArea', data: { labels: labels, datasets: [{ data: labels.map(function (k) { return map[k]; }), backgroundColor: [c.gold, c.cyan, c.green, c.red, c.gray] }] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { color: c.text } }, title: { display: true, text: 'Agent activiteit', color: c.text } }, scales: { r: { ticks: { display: false }, grid: { color: c.grid } } } }, }); } function eqBars() { return '
'; } function sparklineSvg(values, trend) { if (!values || !values.length) return ''; var min = Math.min.apply(null, values), max = Math.max.apply(null, values); var range = max - min || 1; var pts = values.map(function (v, i) { var x = (i / (values.length - 1 || 1)) * 100; var y = 100 - ((v - min) / range) * 80 - 10; return x.toFixed(1) + ',' + y.toFixed(1); }).join(' '); var color = trend === 'down' ? '#fb7185' : '#4ade80'; return ''; } function stockEqBars(trend) { var cls = trend === 'down' ? ' hm-eq-down' : ''; return '
'; } function renderStocksMini(container, stats) { if (!container) return; var stocks = (stats.market_stocks || []).slice(0, 4); var summary = stats.market_summary || {}; if (!stocks.length) { container.innerHTML = 'Beurs openen →'; return; } container.innerHTML = stocks.map(function (s) { var pct = Number(s.change_pct || 0); return '' + '' + (s.symbol || s.name) + ' ' + (pct >= 0 ? '+' : '') + pct.toFixed(2) + '%'; }).join('') + 'Gem. ' + (summary.avg_change_pct || 0) + '% · alle koersen →'; } function renderStocks(container, stats) { if (!container) return; var stocks = stats.market_stocks || []; var summary = stats.market_summary || {}; var meta = document.getElementById('market-updated-at'); if (meta) { var avg = summary.avg_change_pct; meta.textContent = stocks.length ? ('Gem. ' + (avg >= 0 ? '+' : '') + Number(avg || 0).toFixed(2) + '% · ' + (summary.quote_count || stocks.length) + ' quotes') : 'Beurs data laden…'; } if (!stocks.length) { container.innerHTML = '

Beursdata tijdelijk niet beschikbaar — probeer Live data opnieuw.

'; return; } container.innerHTML = stocks.map(function (s) { var pct = Number(s.change_pct || 0); var up = pct >= 0; var price = s.price != null ? Number(s.price).toFixed(2) : '—'; var cur = s.currency || 'EUR'; return '
' + '
' + (s.symbol || '') + '' + (s.name || '') + '
' + '' + (up ? '▲' : '▼') + ' ' + Math.abs(pct).toFixed(2) + '%
' + '
' + price + ' ' + cur + '
' + '
' + (s.chain || s.market || '') + '
' + sparklineSvg(s.sparkline || [], s.trend) + stockEqBars(s.trend) + '
'; }).join(''); } function renderFoodHighlights(container, stats) { if (!container) return; var items = stats.food_market_highlights || stats.rss_highlights || []; if (!items.length) { container.innerHTML = '

Geen highlights — RSS ophalen

'; return; } container.innerHTML = items.map(function (r) { var cat = (r.category || 'markt').toUpperCase(); return '
' + cat + '' + '' + (r.title || '') + '' + '' + (r.feed_name || '') + '
'; }).join(''); } function renderRegulations(container, stats) { if (!container) return; var items = stats.regulation_highlights || []; if (!items.length) { container.innerHTML = '

Regelgeving feeds — klik RSS refresh in Retail 360

'; return; } container.innerHTML = items.map(function (r) { var cat = r.category === 'cbs' ? 'CBS' : 'REG'; return '
' + cat + '' + '' + (r.title || '') + '' + '' + (r.feed_name || '') + '
'; }).join(''); } function renderTrends(canvas, stats) { if (!canvas || typeof Chart === 'undefined') return; var rows = stats.market_trends || []; var c = chartColors(); if (charts.trends) charts.trends.destroy(); if (!rows.length) return; charts.trends = new Chart(canvas, { type: 'bar', data: { labels: rows.map(function (r) { return (r.trend_name || '?').slice(0, 18); }), datasets: [{ label: 'Kans %', data: rows.map(function (r) { return Math.round(Number(r.opportunity_score || 0) * 100); }), backgroundColor: [c.green, c.cyan, c.gold, c.purple || '#a855f7'], borderRadius: 6, }], }, options: { responsive: true, plugins: { legend: { display: false }, title: { display: true, text: 'Markt trend scores', color: c.text } }, scales: { y: { max: 100, ticks: { color: c.text, callback: function (v) { return v + '%'; } }, grid: { color: c.grid } }, x: { ticks: { color: c.text, maxRotation: 45 }, grid: { display: false } }, }, }, }); } function renderKpis(container, stats) { if (!container) return; var summary = stats.market_summary || {}; var avgPct = Number(summary.avg_change_pct || 0); var best = summary.best_performer || {}; var items = [ { label: 'Pipeline', sub: 'actieve deals', icon: '💰', color: '#00e5ff', pct: '72%', val: '€' + Math.round(stats.pipeline_eur || 0).toLocaleString('nl-NL') }, { label: 'Actieve klanten', sub: 'CRM · zaken mee', icon: '🤝', color: '#22c55e', pct: Math.min(95, Math.round(((stats.clients_active || 0) / Math.max(stats.clients_total || 1, 1)) * 100)) + '%', val: (stats.clients_active || 0) + ' / ' + (stats.clients_total || 0), link: '/clients' }, { label: 'CRM partnerships', sub: 'actieve filialen', icon: '🏪', color: '#ff9f43', pct: '45%', val: stats.crm_partnerships || 0, link: '/retail' }, { label: 'Supermarkten', sub: 'Retail 360 DB', icon: '🏪', color: '#b8ff3c', pct: '88%', val: (stats.supermarkets || 0).toLocaleString('nl-NL') }, { label: 'Food trends', sub: 'RSS live', icon: '📰', color: '#38bdf8', pct: Math.min(95, ((stats.trending_food || stats.food_market_highlights || []).length * 10)) + '%', val: (stats.trending_food || stats.food_market_highlights || []).length || 0, link: '/marketing' }, { label: 'Goedkeuringen', sub: 'wacht op OK', icon: '✓', color: '#ffd700', pct: '30%', val: stats.pending_approvals || 0 }, ]; container.className = 'hm-neo-kpi-row'; container.innerHTML = items.map(function (it) { var inner = '
' + '
' + it.icon + '
' + '
' + it.label + '
' + it.sub + '
' + '
' + it.val + '
' + eqBars() + '
'; return it.link ? '' + inner + '' : inner; }).join(''); } function renderRetail(container, stats) { if (!container) return; var items = (stats.trending_food || stats.food_market_highlights || []).slice(0, 6); if (!items.length) { container.innerHTML = '

Geen trending — RSS ophalen in Marketing Hub

'; return; } container.innerHTML = items.map(function (r) { var when = (r.published_at || '').substring(0, 10); return '' + '' + (r.title || '') + '
' + (r.feed_name || 'Retail') + (when ? ' · ' + when : '') + '
' + '' + ((r.category || 'food').toUpperCase()) + '
'; }).join(''); } function renderMilestones(container, stats) { if (!container) return; var ms = stats.milestones_pending || []; if (!ms.length) { container.innerHTML = '

Nog geen milestones — voeg toe via Retail 360 → Sales tab

'; return; } container.innerHTML = ms.map(function (m) { return '
' + (m.title || '') + '
' + (m.chain || '') + ' ' + (m.store_name || '') + '
'; }).join(''); } function renderExecutiveSummary(container, stats) { if (!container) return; var items = [ { icon: '🤝', label: 'Actieve klanten', val: (stats.clients_active || 0) + ' van ' + (stats.clients_total || stats.clients || 0), link: '/clients' }, { icon: '💰', label: 'Pipeline', val: '€' + Math.round(stats.pipeline_eur || 0).toLocaleString('nl-NL'), link: '/deals' }, { icon: '🏪', label: 'CRM partnerships', val: stats.crm_partnerships || 0, link: '/retail' }, { icon: '🛒', label: 'Supermarkten DB', val: (stats.supermarkets || 0).toLocaleString('nl-NL'), link: '/retail' }, { icon: '📦', label: 'Groothandels', val: stats.wholesalers || 0, link: '/retail' }, { icon: '✓', label: 'Goedkeuringen open', val: stats.pending_approvals || 0, link: '/' }, { icon: '📁', label: 'Actieve promo\'s', val: stats.promo_campaigns || '—', link: '/marketing?tab=reclame' }, { icon: '📊', label: 'NAS documenten', val: stats.nas_docs || 0, link: '/documents' }, ]; var trending = (stats.trending_food || stats.food_market_highlights || []).slice(0, 5); var ms = (stats.milestones_pending || []).slice(0, 3); var html = '
' + items.map(function (it) { return '' + it.icon + '' + '
' + it.label + '
' + it.val + '
'; }).join('') + '
'; if (trending.length) { html += '

Trending food retail

'; } if (ms.length) { html += '

Open milestones

'; } var pending = stats.pending_approval_requests || []; if (pending.length) { html += '

⏳ Open goedkeuringen

'; } html += ''; container.innerHTML = html; } function renderText(root, content, createdAt) { if (!root) return; var parsed = parseContent(content); var meta = document.getElementById('briefing-meta'); if (meta && createdAt) meta.textContent = 'Laatst bijgewerkt: ' + String(createdAt).substring(0, 19).replace('T', ' '); typewriter(document.getElementById('briefing-summary-text'), parsed.summary, 8); var actEl = document.getElementById('briefing-actions-list'); if (actEl) actEl.innerHTML = parsed.actions.length ? parsed.actions.map(function (a) { return '
  • ' + a + '
  • '; }).join('') : '
  • Genereer dagrapport voor actiepunten
  • '; var longEl = document.getElementById('briefing-longterm-list'); if (longEl) longEl.innerHTML = parsed.longTerm.length ? parsed.longTerm.map(function (a) { return '
  • ' + a + '
  • '; }).join('') : '
  • Halal kant-en-klaar partnerships schalen
  • '; } function renderLive(root, stats, vizMode) { if (!root || !stats) return; var mode = vizMode || window._dashboardVizMode || 'neo-bars'; renderKpis(document.getElementById('briefing-kpis'), stats); renderFoodHighlights(document.getElementById('briefing-food-highlights'), stats); renderExecutiveSummary(document.getElementById('briefing-executive-summary'), stats); renderRetail(document.getElementById('briefing-retail'), stats); renderMilestones(document.getElementById('briefing-milestones'), stats); var actEl = document.getElementById('briefing-activity-log'); if (actEl) { var log = stats.activity_log || []; actEl.innerHTML = log.length ? log.slice(0, 12).map(function (e) { return '
  • ' + e + '
  • '; }).join('') : '
  • Nog geen agent-acties vandaag.
  • '; } renderPipeline(document.getElementById('chart-pipeline'), stats); renderWords(document.getElementById('chart-words'), stats); if (window.VizEngine) { VizEngine.renderSentiment(mode, document.getElementById('chart-sentiment'), document.getElementById('viz-sentiment-alt'), stats); VizEngine.renderAgents(mode, document.getElementById('chart-agents'), document.getElementById('viz-agents-alt'), stats); } else { renderSentiment(document.getElementById('chart-sentiment'), stats); renderAgents(document.getElementById('chart-agents'), stats); } } function render(root, stats, content, createdAt, vizMode) { renderLive(root, stats, vizMode); renderText(root, content, createdAt); } return { render: render, renderLive: renderLive, renderText: renderText, renderExecutiveSummary: renderExecutiveSummary, destroyAll: destroyAll }; })();