2026-06-09 00:41:27 +00:00
window . BriefingCharts = ( function () {
var charts = {};
var typewriterTimer = null ;
2026-07-19 18:25:01 +00:00
var typewriterAbort = null ;
var lastDigestSig = '' ;
2026-06-09 00:41:27 +00:00
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 '<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>' ;
}
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 '<svg class="hm-spark" viewBox="0 0 100 100" preserveAspectRatio="none"><polyline fill="none" stroke="' + color + '" stroke-width="3" points="' + pts + '"/></svg>' ;
}
function stockEqBars ( trend ) {
var cls = trend === 'down' ? ' hm-eq-down' : '' ;
return '<div class="hm-eq' + cls + '"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>' ;
}
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 = '<a href="/beurs" class="btn btn-sm">Beurs openen →</a>' ;
return ;
}
container . innerHTML = stocks . map ( function ( s ) {
var pct = Number ( s . change_pct || 0 );
return '<a href="/beurs" class="beurs-mini-chip ' + ( pct >= 0 ? 'up' : 'down' ) + '">' +
'<strong>' + ( s . symbol || s . name ) + '</strong> ' +
( pct >= 0 ? '+' : '' ) + pct . toFixed ( 2 ) + '%</a>' ;
}). join ( '' ) + '<small style="display:block;margin-top:0.35rem;color:#64748b">Gem. ' +
( summary . avg_change_pct || 0 ) + '% · <a href="/beurs">alle koersen →</a></small>' ;
}
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 = '<p class="empty-state">Beursdata tijdelijk niet beschikbaar — probeer Live data opnieuw.</p>' ;
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 '<div class="hm-stock-card hm-stock-' + ( s . trend || ( up ? 'up' : 'down' )) + '">' +
'<div class="hm-stock-head"><div><strong>' + ( s . symbol || '' ) + '</strong><small>' + ( s . name || '' ) + '</small></div>' +
'<span class="hm-stock-pct ' + ( up ? 'up' : 'down' ) + '">' + ( up ? '▲' : '▼' ) + ' ' + Math . abs ( pct ). toFixed ( 2 ) + '%</span></div>' +
'<div class="hm-stock-price">' + price + ' <small>' + cur + '</small></div>' +
'<div class="hm-stock-chain">' + ( s . chain || s . market || '' ) + '</div>' +
sparklineSvg ( s . sparkline || [], s . trend ) + stockEqBars ( s . trend ) + '</div>' ;
}). join ( '' );
}
2026-06-23 10:04:23 +00:00
function rssTeaserHtml ( stats ) {
var n = stats . rss_items || ( stats . rss_live || []). length || 0 ;
return '<p class="hm-rss-teaser">' + n + ' RSS artikelen beschikbaar. ' +
'<a href="/?tab=rss">Open RSS tab →</a></p>' ;
}
2026-06-09 00:41:27 +00:00
function renderFoodHighlights ( container , stats ) {
if ( ! container ) return ;
2026-06-23 10:04:23 +00:00
container . innerHTML = rssTeaserHtml ( stats );
2026-06-09 00:41:27 +00:00
}
function renderRegulations ( container , stats ) {
if ( ! container ) return ;
var items = stats . regulation_highlights || [];
if ( ! items . length ) {
container . innerHTML = '<p class="empty-state">Regelgeving feeds — klik RSS refresh in Retail 360</p>' ;
return ;
}
container . innerHTML = items . map ( function ( r ) {
var cat = r . category === 'cbs' ? 'CBS' : 'REG' ;
return '<div class="hm-highlight-item"><span class="hm-cat-badge hm-cat-' + ( r . category || 'reg' ) + '">' + cat + '</span>' +
'<a href="' + ( r . link || '#' ) + '" target="_blank" rel="noopener"><strong>' + ( r . title || '' ) + '</strong></a>' +
'<small>' + ( r . feed_name || '' ) + '</small></div>' ;
}). 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 } },
},
},
});
}
2026-06-09 10:59:45 +00:00
function kpiValue ( id , stats ) {
2026-06-09 00:41:27 +00:00
var summary = stats . market_summary || {};
var best = summary . best_performer || {};
2026-06-09 10:59:45 +00:00
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 '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 '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 . DEFAULT_KPIS ) ||
[ 'pipeline' , 'clients_active' , 'crm_partnerships' , 'supermarkets' , 'trending_food' , 'pending_approvals' ];
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' ,
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>' ;
return ;
}
2026-06-09 00:41:27 +00:00
container . className = 'hm-neo-kpi-row' ;
container . innerHTML = items . map ( function ( it ) {
var inner = '<div class="hm-neo-kpi"><div class="hm-neo-kpi-top">' +
'<div class="hm-neo-ring" style="--ring-color:' + it . color + ';--ring-pct:' + it . pct + '"><div class="hm-neo-ring-inner">' + it . icon + '</div></div>' +
'<div><div class="hm-neo-kpi-label">' + it . label + '</div><div class="hm-neo-kpi-sub">' + it . sub + '</div></div></div>' +
'<div class="hm-neo-kpi-value">' + it . val + '</div>' + eqBars () + '</div>' ;
return it . link ? '<a href="' + it . link + '" class="hm-neo-kpi-link">' + inner + '</a>' : inner ;
}). join ( '' );
}
2026-06-09 10:59:45 +00:00
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 = '<div class="hm-rss-head"><span class="hm-live-dot"></span> ' + count + ' items in database' +
' · <button type="button" class="btn btn-sm" id="btn-rss-refresh-dash">↻ RSS ophalen</button>' +
' · <a href="/marketing">Marketing Hub →</a></div>' ;
if ( ! items . length ) {
container . innerHTML = head + '<p class="empty-state">Geen RSS items — klik <strong>RSS ophalen</strong> of ga naar <a href="/marketing">Marketing Hub</a>.</p>' ;
var btn = container . querySelector ( '#btn-rss-refresh-dash' );
if ( btn ) btn . addEventListener ( 'click' , function () {
if ( window . _refreshRssDash ) window . _refreshRssDash ();
});
return ;
}
container . innerHTML = head + '<div class="hm-rss-list">' + items . slice ( 0 , 20 ). map ( function ( r ) {
var when = ( r . published_at || '' ). substring ( 0 , 16 ). replace ( 'T' , ' ' );
var cat = ( r . category || 'feed' ). toUpperCase ();
return '<a href="' + ( r . link || '#' ) + '" target="_blank" rel="noopener" class="hm-rss-item">' +
'<span class="hm-rss-cat">' + cat + '</span>' +
'<span class="hm-rss-body"><strong>' + ( r . title || '' ) + '</strong>' +
'<small>' + ( r . feed_name || 'RSS' ) + ( when ? ' · ' + when : '' ) + '</small></span></a>' ;
}). join ( '' ) + '</div>' ;
var refreshBtn = container . querySelector ( '#btn-rss-refresh-dash' );
if ( refreshBtn ) refreshBtn . addEventListener ( 'click' , function () {
if ( window . _refreshRssDash ) window . _refreshRssDash ();
});
}
2026-06-09 00:41:27 +00:00
function renderRetail ( container , stats ) {
if ( ! container ) return ;
2026-06-23 10:04:23 +00:00
container . innerHTML = rssTeaserHtml ( stats );
2026-06-09 00:41:27 +00:00
}
function renderMilestones ( container , stats ) {
if ( ! container ) return ;
var ms = stats . milestones_pending || [];
if ( ! ms . length ) {
container . innerHTML = '<p class="empty-state">Nog geen milestones — voeg toe via <a href="/retail">Retail 360 → Sales tab</a></p>' ;
return ;
}
container . innerHTML = ms . map ( function ( m ) {
return '<div class="milestone-item"><span class="milestone-dot"></span><div><strong>' + ( m . title || '' ) + '</strong><br><small>' + ( m . chain || '' ) + ' ' + ( m . store_name || '' ) + '</small></div></div>' ;
}). 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' },
];
2026-06-23 10:04:23 +00:00
var trending = ( stats . trending_food || stats . food_market_highlights || []). length ;
var rssN = stats . rss_items || ( stats . rss_live || []). length || trending ;
2026-06-09 00:41:27 +00:00
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>' ;
2026-06-23 10:04:23 +00:00
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>' ;
2026-06-09 00:41:27 +00:00
}
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>' ;
});
html += '</ul>' ;
}
2026-06-09 10:41:13 +00:00
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>' ;
});
html += '</ul>' ;
}
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>' ;
2026-06-09 00:41:27 +00:00
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' , ' ' );
2026-07-19 18:25:01 +00:00
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 );
}
function escHtml ( s ) {
return String ( s || '' ). replace ( /&/g , '&' ). 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 = '<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 ( l ) { return '<li>' + escHtml ( l ) + '</li>' ; }). join ( '' )
: '<li class="muted">Geen data</li>' ;
return '<div class="live-digest-section brand-' + escHtml ( sec . brand || 'shared' ) + '">' +
'<h5>' + escHtml (( sec . icon || '' ) + ' ' + ( sec . title || '' )) + '</h5>' +
'<ul class="live-digest-list">' + lis + '</ul></div>' ;
}). join ( '' );
}
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 );
}
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 ; };
}
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' );
2026-06-09 00:41:27 +00:00
var actEl = document . getElementById ( 'briefing-actions-list' );
var longEl = document . getElementById ( 'briefing-longterm-list' );
2026-07-19 18:25:01 +00:00
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 || []);
return ;
}
var isUpdate = !! lastDigestSig && ! pollRefresh ;
var twOpts = { charMs : isUpdate ? 8 : 12 , linePause : isUpdate ? 70 : 100 , sectionPause : isUpdate ? 120 : 180 };
if ( sumEl ) {
if ( pollRefresh ) fillDigestInstant ( sumEl , digest );
else typewriterLiveDigest ( sumEl , digest , twOpts );
}
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 });
}
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 );
}
2026-06-09 00:41:27 +00:00
}
2026-07-19 18:25:01 +00:00
function renderLive ( root , stats , vizMode , kpiSelection , liveDigest , opts ) {
2026-06-09 00:41:27 +00:00
if ( ! root || ! stats ) return ;
2026-07-19 18:25:01 +00:00
opts = opts || {};
2026-06-09 10:41:13 +00:00
var mode = vizMode || window . _dashboardVizMode || 'neo-bars' ;
2026-06-09 10:59:45 +00:00
var kpis = kpiSelection || window . _dashboardKpis ;
2026-07-19 18:25:01 +00:00
if ( liveDigest ) renderLiveSummary ( liveDigest , !! opts . pollRefresh );
else if ( stats . live_digest ) renderLiveSummary ( stats . live_digest , !! opts . pollRefresh );
2026-06-09 10:59:45 +00:00
renderKpis ( document . getElementById ( 'briefing-kpis' ), stats , kpis );
2026-06-09 00:41:27 +00:00
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 );
2026-06-09 10:41:13 +00:00
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>' ;
}
2026-06-09 00:41:27 +00:00
renderPipeline ( document . getElementById ( 'chart-pipeline' ), stats );
renderWords ( document . getElementById ( 'chart-words' ), stats );
2026-06-09 10:41:13 +00:00
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 );
}
2026-06-09 00:41:27 +00:00
}
2026-06-09 10:41:13 +00:00
function render ( root , stats , content , createdAt , vizMode ) {
renderLive ( root , stats , vizMode );
2026-06-09 00:41:27 +00:00
renderText ( root , content , createdAt );
}
2026-06-09 10:59:45 +00:00
return {
2026-07-19 18:25:01 +00:00
render : render , renderLive : renderLive , renderText : renderText , renderLiveSummary : renderLiveSummary ,
2026-06-09 10:59:45 +00:00
renderExecutiveSummary : renderExecutiveSummary , renderRssFeed : renderRssFeed ,
renderKpis : renderKpis , destroyAll : destroyAll ,
};
2026-06-09 00:41:27 +00:00
})();