window.BriefingCharts = (function () { var charts = {}; var typewriterTimer = null; var typewriterAbort = null; var lastDigestSig = ''; 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; el.textContent = text || ''; 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) { // Compact HTML bars — geen vaste canvas-hoogte / geen Chart.js if (!canvas) return; if (charts.pipeline) { try { charts.pipeline.destroy(); } catch (e) {} charts.pipeline = null; } var host = canvas.parentElement; if (!host) return; canvas.style.display = 'none'; canvas.setAttribute('hidden', ''); var panel = host.querySelector('.hm-pipeline-bars'); if (!panel) { panel = document.createElement('div'); panel.className = 'hm-pipeline-bars'; host.appendChild(panel); } var rev = (stats.revenue && stats.revenue.stats) || {}; var crmRows = stats.pipeline_by_stage || stats.deal_stages || []; var crmTotal = Math.round(stats.pipeline_eur || 0); var rows = []; // Prefer revenue top deals when CRM pipeline is empty/useless var projects = (stats.revenue && (stats.revenue.projects || stats.revenue.top_projects)) || []; if (!projects.length && stats.revenue && stats.revenue.by_brand) { var bb = stats.revenue.by_brand; projects = [].concat(bb.cucina || [], bb.foodlinkk || [], bb.shared || [], bb.other || []); } if ((!crmRows || !crmRows.length || crmTotal === 0) && projects.length) { rows = projects .filter(function (p) { return Number(p.margin_month || 0) > 0; }) .sort(function (a, b) { return Number(b.margin_month || 0) - Number(a.margin_month || 0); }) .slice(0, 6) .map(function (p) { return { name: p.name || 'Deal', count: 1, value: Number(p.margin_month || 0) }; }); var sub = document.getElementById('ceo-pipeline-sub'); if (sub) sub.textContent = 'top marge/mnd · Revenue'; } else if (crmRows && crmRows.length) { rows = crmRows.map(function (r) { return { name: r.stage || r.name || 'Stage', count: r.count || r.deals || 0, value: Number(r.value || r.eur || r.pipeline_eur || 0), }; }); var sub2 = document.getElementById('ceo-pipeline-sub'); if (sub2) sub2.textContent = 'CRM stages'; } if (!rows.length) { var dealCount = stats.deals || rev.active_deals || 0; panel.innerHTML = '
' + dealCount + ' dealsgeen stage-waarde
' + '

Revenue Cockpit → · CRM deals →

'; return; } var maxV = Math.max.apply(null, rows.map(function (r) { return r.value || 0 }).concat([1])); var totalEur = rows.reduce(function (s, r) { return s + (r.value || 0); }, 0); var totalCnt = rows.reduce(function (s, r) { return s + (r.count || 0); }, 0); panel.innerHTML = '
' + '€' + Math.round(totalEur).toLocaleString('nl-NL') + '' + '' + (totalCnt || rows.length) + ' regels' + '
' + '
' + rows.map(function (r) { var pct = Math.max(4, Math.round(((r.value || 0) / maxV) * 100)); return '
' + '
' + String(r.name).replace(/' + '€' + Math.round(r.value || 0).toLocaleString('nl-NL') + '
' + '
' + '
'; }).join('') + '
' + '

Revenue Cockpit → · CRM deals →

'; } 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 rssTeaserHtml(stats) { var n = stats.rss_items || (stats.rss_live || []).length || 0; return '

' + n + ' RSS artikelen beschikbaar. ' + 'Open RSS tab →

'; } function renderFoodHighlights(container, stats) { if (!container) return; container.innerHTML = rssTeaserHtml(stats); } 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 kpiValue(id, stats) { var summary = stats.market_summary || {}; var best = summary.best_performer || {}; var trendCount = (stats.trending_food || stats.food_market_highlights || []).length; switch (id) { case 'pipeline': return '€' + Math.round(stats.pipeline_eur || 0).toLocaleString('nl-NL'); case 'margin_month': { var rev = (stats.revenue && stats.revenue.stats) || {}; return '€' + Math.round(rev.total_margin_month || 0).toLocaleString('nl-NL'); } case 'margin_year': { var revY = (stats.revenue && stats.revenue.stats) || {}; return '€' + Math.round(revY.total_margin_year || 0).toLocaleString('nl-NL'); } case 'open_steps': { var revO = (stats.revenue && stats.revenue.stats) || {}; return String(revO.open_objectives || 0); } case 'revenue_deals': { var revD = (stats.revenue && stats.revenue.stats) || {}; return String(revD.active_deals || revD.active_projects || 0); } case 'clients_active': return (stats.clients_active || 0) + ' / ' + (stats.clients_total || stats.clients || 0); case 'clients_total': return String(stats.clients_total || stats.clients || 0); case 'crm_partnerships': return String(stats.crm_partnerships || 0); case 'supermarkets': return (stats.supermarkets || 0).toLocaleString('nl-NL'); case 'wholesalers': return String(stats.wholesalers || 0); case 'trending_food': return String(trendCount || 0); case 'rss_items': return String(stats.rss_items || (stats.rss_live || []).length || 0); case 'rss_bookmarks': return String((stats.rss_bookmarks || []).length || stats.rss_bookmarks_count || 0); case 'pending_approvals': return String(stats.pending_approvals || 0); case 'deals': return String(stats.deals || 0); case 'products': return String(stats.products || 0); case 'suppliers': return String(stats.suppliers || 0); case 'nas_docs': return String(stats.nas_docs || 0); case 'promo_campaigns': return String(stats.promo_campaigns || 0); case 'market_best': return best.symbol ? (best.symbol + ' ' + Number(best.change_pct || 0).toFixed(1) + '%') : '—'; default: return '—'; } } function kpiPct(id, stats) { var summary = stats.market_summary || {}; var trendCount = (stats.trending_food || stats.food_market_highlights || []).length; switch (id) { case 'pipeline': return '72%'; case 'margin_month': return '90%'; case 'margin_year': return '85%'; case 'open_steps': { var ro = (stats.revenue && stats.revenue.stats) || {}; return Math.min(95, Math.round((ro.open_objectives || 0) / 50 * 100)) + '%'; } case 'revenue_deals': return '80%'; case 'clients_active': return Math.min(95, Math.round(((stats.clients_active || 0) / Math.max(stats.clients_total || 1, 1)) * 100)) + '%'; case 'crm_partnerships': return '45%'; case 'supermarkets': return '88%'; case 'trending_food': return Math.min(95, trendCount * 10) + '%'; case 'rss_items': return Math.min(95, ((stats.rss_items || 0) / 10)) + '%'; case 'pending_approvals': return '30%'; case 'market_best': return Math.min(95, Math.abs(Number(summary.avg_change_pct || 0)) * 10) + '%'; default: return '50%'; } } function renderKpis(container, stats, selectedKpis) { if (!container) return; var meta = (window.DashboardLayout && window.DashboardLayout.KPI_META) || {}; var order = selectedKpis || window._dashboardKpis || (window.DashboardLayout && window.DashboardLayout.CEO_KPIS) || (window.DashboardLayout && window.DashboardLayout.DEFAULT_KPIS) || ['margin_month', 'margin_year', 'open_steps', 'revenue_deals', 'crm_partnerships', 'supermarkets']; if (window.DashboardLayout && window.DashboardLayout.ceoKpis) { order = window.DashboardLayout.ceoKpis({ dashboard_kpis: order }); } order = order.slice(0, 6); var items = order.map(function (id) { var m = meta[id] || { label: id, sub: '', icon: '▪', color: '#94a3b8' }; return { id: id, label: m.label, sub: m.sub, icon: m.icon, color: m.color || '#94a3b8', pct: kpiPct(id, stats), val: kpiValue(id, stats), link: m.link, }; }); if (!items.length) { container.innerHTML = '

Geen KPI\'s

'; return; } 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 renderRssFeed(container, stats) { if (!container) return; var items = stats.rss_live || stats.trending_food || stats.food_market_highlights || []; var count = stats.rss_items || items.length; var head = '
' + count + ' items in database' + ' · ' + ' · Marketing Hub →
'; if (!items.length) { container.innerHTML = head + '

Geen RSS items — klik RSS ophalen of ga naar Marketing Hub.

'; var btn = container.querySelector('#btn-rss-refresh-dash'); if (btn) btn.addEventListener('click', function () { if (window._refreshRssDash) window._refreshRssDash(); }); return; } container.innerHTML = head + '
' + items.slice(0, 20).map(function (r) { var when = (r.published_at || '').substring(0, 16).replace('T', ' '); var cat = (r.category || 'feed').toUpperCase(); return '' + '' + cat + '' + '' + (r.title || '') + '' + '' + (r.feed_name || 'RSS') + (when ? ' · ' + when : '') + ''; }).join('') + '
'; var refreshBtn = container.querySelector('#btn-rss-refresh-dash'); if (refreshBtn) refreshBtn.addEventListener('click', function () { if (window._refreshRssDash) window._refreshRssDash(); }); } function renderRetail(container, stats) { if (!container) return; container.innerHTML = rssTeaserHtml(stats); } 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 rev = (stats.revenue && stats.revenue.stats) || {}; var by = (stats.revenue && stats.revenue.by_brand) || {}; var cu = by.cucina || []; var fl = by.foodlinkk || []; var clients = stats.recent_clients || []; var opp = stats.top_opportunities || []; var deals = stats.deals || 0; var pipeline = Math.round(stats.pipeline_eur || 0); var marginM = Math.round(rev.total_margin_month || 0); var openObj = rev.open_objectives || 0; // Narrative context var narrative = []; if (marginM > 0) narrative.push('Cucina + Foodlinkk draaien €' + marginM.toLocaleString('nl-NL') + '/mnd marge over ' + (rev.active_projects || 0) + ' projecten.'); if (openObj) narrative.push(openObj + ' open stappen vragen opvolging.'); if ((stats.crm_partnerships || 0) > 0) narrative.push(stats.crm_partnerships + ' retail-partnerships · ' + (stats.supermarkets || 0).toLocaleString('nl-NL') + ' filialen bereikbaar.'); if (pipeline === 0 && deals > 0) narrative.push(deals + ' CRM-deals staan klaar — pipeline-waarde nog €0 (nog geen getekende budgetten).'); if (!narrative.length) narrative.push('Live business data laden…'); var topDeals = cu.filter(function (p) { return p.margin_month; }).slice(0, 3); var html = ''; html += '

' + escHtml(narrative.join(' ')) + '

'; html += '
' + [ { icon: '💶', label: 'Marge / mnd', val: '€' + marginM.toLocaleString('nl-NL'), sub: (rev.active_projects || 0) + ' projecten', link: '/revenue-cockpit' }, { icon: '💰', label: 'CRM pipeline', val: '€' + pipeline.toLocaleString('nl-NL'), sub: deals + ' deals', link: '/deals' }, { icon: '🤝', label: 'Klanten', val: String(stats.clients_total || stats.clients || 0), sub: (stats.clients_active || 0) + ' actief', link: '/clients' }, { icon: '🏪', label: 'Partnerships', val: String(stats.crm_partnerships || 0), sub: (stats.supermarkets || 0).toLocaleString('nl-NL') + ' filialen', link: '/retail' }, ].map(function (it) { return '' + '' + it.icon + '' + '
' + it.label + '
' + it.val + '
' + '
' + it.sub + '
'; }).join('') + '
'; if (topDeals.length) { html += '

Top Cucina deals

'; topDeals.forEach(function (d) { html += '' + escHtml(d.name) + '' + '€' + Math.round(d.margin_month || 0).toLocaleString('nl-NL') + '/m' + (d.open_objectives ? '' + d.open_objectives + ' stappen' : '') + ''; }); html += '
'; } if (clients.length) { html += '

Recente klanten

'; clients.slice(0, 4).forEach(function (c) { html += '' + escHtml(c.name || '') + '' + '' + escHtml(c.stage || '') + ''; }); html += '
'; } if (opp.length) { html += '

Halal-kansen

'; } html += ''; container.innerHTML = html; } function renderText(root, content, createdAt) { if (!root) return; var meta = document.getElementById('briefing-meta'); if (meta && createdAt) meta.textContent = 'Laatst bijgewerkt: ' + String(createdAt).substring(0, 19).replace('T', ' '); var sumEl = document.getElementById('briefing-summary-text'); if (!sumEl || !content) return; sumEl.dataset.aiMemo = '1'; // Markdown-achtige tekst direct tonen (geen typewriter) var html = escHtml(content) .replace(/^### (.+)$/gm, '
$1
') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/^\- (.+)$/gm, '
  • $1
  • ') .replace(/(
  • .*<\/li>\n?)+/g, function (m) { return ''; }) .replace(/\n\n/g, '

    ') .replace(/\n/g, '
    '); sumEl.innerHTML = '

    ' + html + '

    '; } function escHtml(s) { return String(s || '').replace(/&/g, '&').replace(//g, '>'); } function digestSignature(digest) { if (!digest) return ''; return JSON.stringify({ sections: digest.sections || [], actions: digest.actions || [], long_term: digest.long_term || [], }); } function fillDigestInstant(container, digest) { var sections = (digest && digest.sections) || []; if (!sections.length) { container.innerHTML = '

    Live samenvatting laden…

    '; return; } container.innerHTML = sections.map(function (sec) { var lines = (sec.lines || []).filter(Boolean); var lis = lines.length ? lines.map(function (l) { return '
  • ' + escHtml(l) + '
  • '; }).join('') : '
  • Geen data
  • '; return '
    ' + '
    ' + escHtml((sec.icon || '') + ' ' + (sec.title || '')) + '
    ' + '
    '; }).join(''); } function typewriterLiveDigest(container, digest, opts) { // Typewriter uit — altijd direct tonen fillDigestInstant(container, digest); } function typewriterListItems(listEl, items, opts) { fillListInstant(listEl, items || []); } function fillListInstant(listEl, items) { if (!listEl) return; var rows = (items && items.length) ? items : ['Geen urgente acties — check agents & retail']; listEl.innerHTML = rows.map(function (t) { return '
  • ' + escHtml(t) + '
  • '; }).join(''); } function fillSimpleList(el, items, emptyMsg) { if (!el) return; var rows = (items || []).filter(Boolean); if (!rows.length) { el.innerHTML = '
  • ' + escHtml(emptyMsg || 'Geen items') + '
  • '; return; } el.innerHTML = rows.map(function (x) { if (typeof x === 'string') return '
  • → ' + escHtml(x) + '
  • '; var title = escHtml(x.title || x); var link = x.link ? ('' + title + '') : title; var src = x.source ? ('' + escHtml(x.source) + '') : ''; return '
  • ' + link + src + '
  • '; }).join(''); } function fillBrandCard(el, lines) { if (!el) return; var list = el.querySelector('.hm-ceo-brand-list') || el; var rows = (lines || []).filter(Boolean); list.innerHTML = rows.length ? rows.map(function (l) { return '
  • ' + escHtml(l) + '
  • '; }).join('') : '
  • Geen data
  • '; } function renderCeoDigest(digest) { if (!digest) return; var b2b = digest.b2b, b2c = digest.b2c, focus = digest.focus, trends = digest.trends, news = digest.news; if (!b2b || !b2c) { (digest.sections || []).forEach(function (s) { if (s.brand === 'foodlinkk') b2b = s.lines; if (s.brand === 'cucina') b2c = s.lines; if (s.brand === 'platform' || (s.title || '').indexOf('Focus') >= 0) focus = s.lines; }); } fillBrandCard(document.getElementById('ceo-b2b'), b2b); fillBrandCard(document.getElementById('ceo-b2c'), b2c); fillSimpleList(document.getElementById('ceo-focus-list'), focus || digest.actions, 'Geen urgente acties'); fillSimpleList(document.getElementById('ceo-trends-list'), trends, 'Geen trends'); fillSimpleList(document.getElementById('ceo-news-list'), news, 'Geen nieuws — open RSS tab'); } function renderLiveSummary(digest, pollRefresh) { if (!digest) return; var meta = document.getElementById('briefing-meta'); lastDigestSig = digestSignature(digest); if (meta) { var ts = String(digest.updated_at || '').substring(0, 19).replace('T', ' '); meta.textContent = 'Live · ververst ' + ts + ' UTC'; } if (document.querySelector('.hm-ceo-pro')) { renderCeoDigest(digest); return; } var sumEl = document.getElementById('briefing-summary-text'); if (sumEl) fillDigestInstant(sumEl, digest); } function renderLive(root, stats, vizMode, kpiSelection, liveDigest, opts) { if (!root || !stats) return; opts = opts || {}; var mode = vizMode || window._dashboardVizMode || 'neo-bars'; var kpis = kpiSelection || window._dashboardKpis; if (liveDigest) renderLiveSummary(liveDigest, !!opts.pollRefresh); else if (stats.live_digest) renderLiveSummary(stats.live_digest, !!opts.pollRefresh); renderKpis(document.getElementById('briefing-kpis'), stats, kpis); renderExecutiveSummary(document.getElementById('briefing-executive-summary'), stats); // activity log removed from CEO overview renderPipeline(document.getElementById('chart-pipeline'), stats); // CEO overview: geen sentiment/words/agents-charts (lege hoogte + troep) if (!document.querySelector('.hm-ceo-clean')) { 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, renderLiveSummary: renderLiveSummary, renderExecutiveSummary: renderExecutiveSummary, renderRssFeed: renderRssFeed, renderKpis: renderKpis, destroyAll: destroyAll, }; })();