Improve CEO cockpit clarity and Revenue visuals, with Hermes briefing.
Tighten overview KPIs/pipeline height, add selectable Revenue views and a resizable sidebar, and keep supporting Hermes/Wereldexport updates.
This commit is contained in:
@@ -26,6 +26,8 @@ window.BriefingCharts = (function () {
|
||||
|
||||
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; }
|
||||
@@ -58,16 +60,81 @@ window.BriefingCharts = (function () {
|
||||
}
|
||||
|
||||
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 } } } },
|
||||
});
|
||||
// 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 =
|
||||
'<div class="hm-pipeline-head"><strong>' + dealCount + ' deals</strong><span>geen stage-waarde</span></div>' +
|
||||
'<p class="hm-pipeline-foot"><a href="/revenue-cockpit">Revenue Cockpit →</a> · <a href="/deals">CRM deals →</a></p>';
|
||||
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 =
|
||||
'<div class="hm-pipeline-head">' +
|
||||
'<strong>€' + Math.round(totalEur).toLocaleString('nl-NL') + '</strong>' +
|
||||
'<span>' + (totalCnt || rows.length) + ' regels</span>' +
|
||||
'</div>' +
|
||||
'<div class="hm-pipeline-list">' + rows.map(function (r) {
|
||||
var pct = Math.max(4, Math.round(((r.value || 0) / maxV) * 100));
|
||||
return '<div class="hm-pipeline-row">' +
|
||||
'<div class="hm-pipeline-meta"><span class="hm-pipeline-stage">' + String(r.name).replace(/</g, '') + '</span>' +
|
||||
'<span class="hm-pipeline-vals">€' + Math.round(r.value || 0).toLocaleString('nl-NL') + '</span></div>' +
|
||||
'<div class="hm-pipeline-track"><div class="hm-pipeline-fill" style="width:' + pct + '%"></div></div>' +
|
||||
'</div>';
|
||||
}).join('') + '</div>' +
|
||||
'<p class="hm-pipeline-foot"><a href="/revenue-cockpit">Revenue Cockpit →</a> · <a href="/deals">CRM deals →</a></p>';
|
||||
}
|
||||
|
||||
function renderSentiment(canvas, stats) {
|
||||
@@ -236,6 +303,22 @@ window.BriefingCharts = (function () {
|
||||
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);
|
||||
@@ -260,6 +343,13 @@ window.BriefingCharts = (function () {
|
||||
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%';
|
||||
@@ -276,17 +366,22 @@ window.BriefingCharts = (function () {
|
||||
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) ||
|
||||
['pipeline', 'clients_active', 'crm_partnerships', 'supermarkets', 'trending_food', 'pending_approvals'];
|
||||
['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 {
|
||||
label: m.label, sub: m.sub, icon: m.icon, color: m.color || '#94a3b8',
|
||||
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 = '<p class="empty-state">Geen KPI\'s geselecteerd — klik <strong>Dashboard instellen</strong>.</p>';
|
||||
container.innerHTML = '<p class="empty-state">Geen KPI\'s</p>';
|
||||
return;
|
||||
}
|
||||
container.className = 'hm-neo-kpi-row';
|
||||
@@ -299,6 +394,7 @@ window.BriefingCharts = (function () {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
|
||||
function renderRssFeed(container, stats) {
|
||||
if (!container) return;
|
||||
var items = stats.rss_live || stats.trending_food || stats.food_market_highlights || [];
|
||||
@@ -347,67 +443,94 @@ window.BriefingCharts = (function () {
|
||||
|
||||
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 || []).length;
|
||||
var rssN = stats.rss_items || (stats.rss_live || []).length || trending;
|
||||
var ms = (stats.milestones_pending || []).slice(0, 3);
|
||||
var html = '<div class="hm-exec-grid">' + items.map(function (it) {
|
||||
return '<a href="' + it.link + '" class="hm-exec-item"><span class="hm-exec-icon">' + it.icon + '</span>' +
|
||||
'<div><strong>' + it.label + '</strong><div class="hm-exec-val">' + it.val + '</div></div></a>';
|
||||
}).join('') + '</div>';
|
||||
if (rssN) {
|
||||
html += '<p class="hm-rss-teaser" style="margin-top:1rem">' + rssN + ' RSS feeds — <a href="/?tab=rss">bekijk in RSS tab →</a></p>';
|
||||
}
|
||||
if (ms.length) {
|
||||
html += '<h4 style="margin:1rem 0 0.5rem;font-size:0.8rem;color:#94a3b8">Open milestones</h4><ul class="hm-exec-list">';
|
||||
ms.forEach(function (m) {
|
||||
html += '<li>' + (m.title || '') + ' · ' + (m.chain || '') + ' ' + (m.store_name || '') + '</li>';
|
||||
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 += '<div class="hm-context-banner"><p>' + escHtml(narrative.join(' ')) + '</p></div>';
|
||||
html += '<div class="hm-exec-grid hm-exec-grid-rich">' +
|
||||
[
|
||||
{ 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 '<a href="' + it.link + '" class="hm-exec-item hm-exec-rich">' +
|
||||
'<span class="hm-exec-icon">' + it.icon + '</span>' +
|
||||
'<div><strong>' + it.label + '</strong><div class="hm-exec-val">' + it.val + '</div>' +
|
||||
'<div class="hm-exec-sub">' + it.sub + '</div></div></a>';
|
||||
}).join('') + '</div>';
|
||||
|
||||
if (topDeals.length) {
|
||||
html += '<div class="hm-context-block"><h4>Top Cucina deals</h4><div class="hm-deal-chips">';
|
||||
topDeals.forEach(function (d) {
|
||||
html += '<a class="hm-deal-chip" href="/revenue-cockpit"><strong>' + escHtml(d.name) + '</strong>' +
|
||||
'<span>€' + Math.round(d.margin_month || 0).toLocaleString('nl-NL') + '/m</span>' +
|
||||
(d.open_objectives ? '<em>' + d.open_objectives + ' stappen</em>' : '') + '</a>';
|
||||
});
|
||||
html += '</ul>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
var pending = stats.pending_approval_requests || [];
|
||||
if (pending.length) {
|
||||
html += '<h4 style="margin:1rem 0 0.5rem;font-size:0.8rem;color:#f59e0b">⏳ Open goedkeuringen</h4><ul class="hm-exec-list">';
|
||||
pending.forEach(function (p) {
|
||||
html += '<li><strong>@' + (p.agent_key || '') + '</strong> · ' + (p.title || '') + '</li>';
|
||||
|
||||
if (clients.length) {
|
||||
html += '<div class="hm-context-block"><h4>Recente klanten</h4><div class="hm-deal-chips">';
|
||||
clients.slice(0, 4).forEach(function (c) {
|
||||
html += '<a class="hm-deal-chip hm-deal-chip-client" href="/clients"><strong>' + escHtml(c.name || '') + '</strong>' +
|
||||
'<span>' + escHtml(c.stage || '') + '</span></a>';
|
||||
});
|
||||
html += '</ul>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
html += '<p class="hm-exec-footer" style="margin-top:1rem;font-size:0.8rem;color:#64748b">' +
|
||||
'<a href="http://10.4.7.18:3001/aissa/foodlinkk-command-center" target="_blank" rel="noopener">Gitea</a> · ' +
|
||||
'<a href="/ops">IT Ops</a> · <a href="/packaging">Packaging</a> · ' +
|
||||
'<a href="/marketing?tab=publish">Automatisering</a> · ' +
|
||||
'<a href="/marketing">Marketing Hub</a> · <a href="/analytics">Analytics</a></p>';
|
||||
|
||||
if (opp.length) {
|
||||
html += '<div class="hm-context-block"><h4>Halal-kansen</h4><ul class="hm-context-list">';
|
||||
opp.slice(0, 3).forEach(function (o) {
|
||||
var score = Math.round(o.halal_opportunity_score || o.score || 0);
|
||||
html += '<li><strong>' + escHtml(o.chain || o.name || '') + '</strong> · ' +
|
||||
escHtml(o.city || '') + ' <span class="hm-score">score ' + score + '</span></li>';
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
|
||||
html += '<p class="hm-exec-footer">' +
|
||||
'<a href="/revenue-cockpit">Revenue Cockpit</a> · <a href="/retail">Retail 360</a> · ' +
|
||||
'<a href="/clients">Clients</a> · <a href="/deals">Deals</a></p>';
|
||||
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', ' ');
|
||||
var sumEl = document.getElementById('briefing-summary-text');
|
||||
if (sumEl && parsed.summary) {
|
||||
var lines = parsed.summary.split(/(?<=[.!?])\s+/).filter(Boolean);
|
||||
typewriterLiveDigest(sumEl, {
|
||||
sections: [{ brand: 'platform', title: 'Herman rapport', icon: '📝', lines: lines.length ? lines : [parsed.summary] }],
|
||||
}, { charMs: 10, linePause: 90 });
|
||||
}
|
||||
var actEl = document.getElementById('briefing-actions-list');
|
||||
if (actEl) typewriterListItems(actEl, parsed.actions.length ? parsed.actions : ['Genereer dagrapport voor actiepunten'], { charMs: 8 });
|
||||
var longEl = document.getElementById('briefing-longterm-list');
|
||||
if (longEl) setTimeout(function () {
|
||||
typewriterListItems(longEl, parsed.longTerm.length ? parsed.longTerm : ['Halal kant-en-klaar partnerships schalen'], { charMs: 8 });
|
||||
}, 300);
|
||||
if (!sumEl || !content) return;
|
||||
sumEl.dataset.aiMemo = '1';
|
||||
// Markdown-achtige tekst direct tonen (geen typewriter)
|
||||
var html = escHtml(content)
|
||||
.replace(/^### (.+)$/gm, '<h5>$1</h5>')
|
||||
.replace(/^## (.+)$/gm, '<h4>$1</h4>')
|
||||
.replace(/^# (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/^\- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>\n?)+/g, function (m) { return '<ul class="live-digest-list">' + m + '</ul>'; })
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
sumEl.innerHTML = '<div class="hm-ceo-memo-md"><p>' + html + '</p></div>';
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
@@ -441,152 +564,77 @@ window.BriefingCharts = (function () {
|
||||
}
|
||||
|
||||
function typewriterLiveDigest(container, digest, opts) {
|
||||
if (!container) return;
|
||||
opts = opts || {};
|
||||
var charMs = opts.charMs || 14;
|
||||
var linePause = opts.linePause || 120;
|
||||
var sectionPause = opts.sectionPause || 200;
|
||||
|
||||
if (typewriterAbort) { typewriterAbort(); typewriterAbort = null; }
|
||||
if (typewriterTimer) { clearInterval(typewriterTimer); typewriterTimer = null; }
|
||||
|
||||
var sections = (digest && digest.sections) || [];
|
||||
if (!sections.length) {
|
||||
var summary = (digest && digest.summary) || '';
|
||||
if (summary) {
|
||||
typewriter(container, summary, charMs);
|
||||
} else {
|
||||
container.innerHTML = '<p class="muted">Live samenvatting laden…</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sections.map(function (sec) {
|
||||
var lines = (sec.lines || []).filter(Boolean);
|
||||
var lis = lines.length
|
||||
? lines.map(function () { return '<li class="tw-line"></li>'; }).join('')
|
||||
: '<li class="muted">Geen data</li>';
|
||||
return '<div class="live-digest-section brand-' + escHtml(sec.brand || 'shared') + '">' +
|
||||
'<h5 class="tw-heading">' + escHtml((sec.icon || '') + ' ' + (sec.title || '')) + '</h5>' +
|
||||
'<ul class="live-digest-list">' + lis + '</ul></div>';
|
||||
}).join('');
|
||||
|
||||
var queue = [];
|
||||
container.querySelectorAll('.live-digest-section').forEach(function (secEl, si) {
|
||||
var lines = (sections[si] && sections[si].lines) || [];
|
||||
var lis = secEl.querySelectorAll('li.tw-line');
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
queue.push({ el: lis[i], text: lines[i] || '', section: si });
|
||||
}
|
||||
});
|
||||
|
||||
var cancelled = false;
|
||||
typewriterAbort = function () { cancelled = true; };
|
||||
container.classList.add('is-typing');
|
||||
|
||||
function tick(qi, ci, prevSection) {
|
||||
if (cancelled) return;
|
||||
if (qi >= queue.length) {
|
||||
container.classList.remove('is-typing');
|
||||
container.querySelectorAll('.is-typing').forEach(function (n) { n.classList.remove('is-typing'); });
|
||||
return;
|
||||
}
|
||||
var item = queue[qi];
|
||||
var el = item.el;
|
||||
var text = item.text;
|
||||
if (prevSection !== item.section && ci === 0) {
|
||||
setTimeout(function () { if (!cancelled) tick(qi, ci, item.section); }, sectionPause);
|
||||
return;
|
||||
}
|
||||
el.classList.add('is-typing');
|
||||
container.querySelectorAll('li.is-typing').forEach(function (n) { if (n !== el) n.classList.remove('is-typing'); });
|
||||
if (ci < text.length) {
|
||||
el.textContent = text.substring(0, ci + 1);
|
||||
setTimeout(function () { tick(qi, ci + 1, item.section); }, charMs);
|
||||
} else {
|
||||
el.classList.remove('is-typing');
|
||||
setTimeout(function () { tick(qi + 1, 0, item.section); }, linePause);
|
||||
}
|
||||
}
|
||||
tick(0, 0, -1);
|
||||
// Typewriter uit — altijd direct tonen
|
||||
fillDigestInstant(container, digest);
|
||||
}
|
||||
|
||||
function typewriterListItems(listEl, items, opts) {
|
||||
if (!listEl || !items || !items.length) return;
|
||||
opts = opts || {};
|
||||
var charMs = opts.charMs || 10;
|
||||
listEl.innerHTML = items.map(function () { return '<li class="tw-line"></li>'; }).join('');
|
||||
var lis = listEl.querySelectorAll('li.tw-line');
|
||||
var qi = 0, ci = 0, cancelled = false;
|
||||
function next() {
|
||||
if (cancelled || qi >= items.length) return;
|
||||
var el = lis[qi];
|
||||
var text = items[qi];
|
||||
el.classList.add('is-typing');
|
||||
if (ci < text.length) {
|
||||
el.textContent = text.substring(0, ci + 1);
|
||||
ci++;
|
||||
setTimeout(next, charMs);
|
||||
} else {
|
||||
el.classList.remove('is-typing');
|
||||
qi++; ci = 0;
|
||||
setTimeout(next, 90);
|
||||
}
|
||||
}
|
||||
next();
|
||||
return function () { cancelled = true; };
|
||||
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 '<li>' + escHtml(t) + '</li>'; }).join('');
|
||||
}
|
||||
|
||||
function renderLiveSummary(digest, pollRefresh) {
|
||||
if (!digest) return;
|
||||
var sumEl = document.getElementById('briefing-summary-text');
|
||||
var actEl = document.getElementById('briefing-actions-list');
|
||||
var longEl = document.getElementById('briefing-longterm-list');
|
||||
var meta = document.getElementById('briefing-meta');
|
||||
|
||||
var sig = digestSignature(digest);
|
||||
var unchanged = pollRefresh && sig === lastDigestSig;
|
||||
if (!unchanged) lastDigestSig = sig;
|
||||
|
||||
if (meta) {
|
||||
var ts = String(digest.updated_at || '').substring(0, 19).replace('T', ' ');
|
||||
var n = digest.activity_count != null ? ' · ' + digest.activity_count + ' activiteiten' : '';
|
||||
meta.textContent = 'Live · ververst ' + ts + ' UTC' + n;
|
||||
}
|
||||
|
||||
if (unchanged) {
|
||||
if (actEl) fillListInstant(actEl, digest.actions || []);
|
||||
if (longEl) fillListInstant(longEl, digest.long_term || []);
|
||||
function fillSimpleList(el, items, emptyMsg) {
|
||||
if (!el) return;
|
||||
var rows = (items || []).filter(Boolean);
|
||||
if (!rows.length) {
|
||||
el.innerHTML = '<li class="muted">' + escHtml(emptyMsg || 'Geen items') + '</li>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = rows.map(function (x) {
|
||||
if (typeof x === 'string') return '<li><span class="hm-ceo-bullet">→</span> ' + escHtml(x) + '</li>';
|
||||
var title = escHtml(x.title || x);
|
||||
var link = x.link ? ('<a href="' + escHtml(x.link) + '" target="_blank" rel="noopener">' + title + '</a>') : title;
|
||||
var src = x.source ? ('<small>' + escHtml(x.source) + '</small>') : '';
|
||||
return '<li>' + link + src + '</li>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
var isUpdate = !!lastDigestSig && !pollRefresh;
|
||||
var twOpts = { charMs: isUpdate ? 8 : 12, linePause: isUpdate ? 70 : 100, sectionPause: isUpdate ? 120 : 180 };
|
||||
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 '<li>' + escHtml(l) + '</li>'; }).join('')
|
||||
: '<li class="muted">Geen data</li>';
|
||||
}
|
||||
|
||||
if (sumEl) {
|
||||
if (pollRefresh) fillDigestInstant(sumEl, digest);
|
||||
else typewriterLiveDigest(sumEl, digest, twOpts);
|
||||
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');
|
||||
}
|
||||
|
||||
var actions = digest.actions || [];
|
||||
if (actEl) {
|
||||
if (pollRefresh) fillListInstant(actEl, actions.length ? actions : ['Geen urgente acties — check agents & retail']);
|
||||
else typewriterListItems(actEl, actions.length ? actions : ['Geen urgente acties — check agents & retail'], { charMs: isUpdate ? 6 : 8 });
|
||||
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';
|
||||
}
|
||||
|
||||
var lt = digest.long_term || [];
|
||||
if (longEl) {
|
||||
if (pollRefresh) fillListInstant(longEl, lt.length ? lt : ['Halal kant-en-klaar partnerships schalen']);
|
||||
else setTimeout(function () {
|
||||
typewriterListItems(longEl, lt.length ? lt : ['Halal kant-en-klaar partnerships schalen'], { charMs: isUpdate ? 6 : 8 });
|
||||
}, isUpdate ? 200 : 400);
|
||||
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) {
|
||||
@@ -597,24 +645,19 @@ window.BriefingCharts = (function () {
|
||||
if (liveDigest) renderLiveSummary(liveDigest, !!opts.pollRefresh);
|
||||
else if (stats.live_digest) renderLiveSummary(stats.live_digest, !!opts.pollRefresh);
|
||||
renderKpis(document.getElementById('briefing-kpis'), stats, kpis);
|
||||
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 '<li>' + e + '</li>'; }).join('')
|
||||
: '<li class="muted">Nog geen agent-acties vandaag.</li>';
|
||||
}
|
||||
// activity log removed from CEO overview
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user