(function (global) { var WIDGET_META = { kpis: { title: 'CEO KPI\'s', icon: '๐Ÿ“Š' }, executive: { title: 'Alles op een rij', icon: '๐Ÿ“‹' }, briefing: { title: 'Herman briefing', icon: '๐Ÿ“' }, retail: { title: 'Retail operatie', icon: '๐Ÿช' }, analytics: { title: 'Data & analytics', icon: '๐Ÿ“ˆ' }, approvals: { title: 'Agent goedkeuringen', icon: 'โœ“' }, feed: { title: 'Agent feed', icon: 'โšก' }, }; var KPI_META = { pipeline: { label: 'Pipeline', sub: 'actieve deals', icon: '๐Ÿ’ฐ', color: '#00e5ff', link: '/deals' }, clients_active: { label: 'Actieve klanten', sub: 'CRM ยท zaken mee', icon: '๐Ÿค', color: '#22c55e', link: '/clients' }, clients_total: { label: 'Totaal klanten', sub: 'CRM database', icon: '๐Ÿ‘ฅ', color: '#4ade80', link: '/clients' }, crm_partnerships: { label: 'CRM partnerships', sub: 'actieve filialen', icon: '๐Ÿช', color: '#ff9f43', link: '/retail' }, supermarkets: { label: 'Supermarkten', sub: 'Retail 360 DB', icon: '๐Ÿ›’', color: '#b8ff3c', link: '/retail' }, wholesalers: { label: 'Groothandels', sub: 'Retail 360 DB', icon: '๐Ÿ“ฆ', color: '#a78bfa', link: '/retail' }, trending_food: { label: 'Food trends', sub: 'RSS live', icon: '๐Ÿ“ฐ', color: '#38bdf8', link: '/?tab=rss' }, rss_items: { label: 'RSS items', sub: 'totaal in DB', icon: '๐Ÿ“ก', color: '#0ea5e9', link: '/?tab=rss' }, rss_bookmarks: { label: 'RSS bookmarks', sub: 'opgeslagen', icon: 'โ˜…', color: '#ffd700', link: '/marketing?tab=bookmarks' }, pending_approvals: { label: 'Goedkeuringen', sub: 'wacht op OK', icon: 'โœ“', color: '#ffd700', link: '/' }, deals: { label: 'Deals', sub: 'totaal CRM', icon: '๐Ÿ’ผ', color: '#f472b6', link: '/deals' }, products: { label: 'Producten', sub: 'catalogus', icon: '๐Ÿฅซ', color: '#fb923c', link: '/products' }, suppliers: { label: 'Leveranciers', sub: 'sourcing', icon: '๐Ÿšš', color: '#94a3b8', link: '/suppliers' }, nas_docs: { label: 'NAS documenten', sub: 'geanalyseerd', icon: '๐Ÿ“Š', color: '#818cf8', link: '/documents' }, promo_campaigns: { label: 'Promo campagnes', sub: 'actief', icon: '๐Ÿ“', color: '#c084fc', link: '/marketing?tab=reclame' }, market_best: { label: 'Aandeel top', sub: 'retail markt', icon: '๐Ÿ“ˆ', color: '#34d399', link: '/analytics' }, }; var DEFAULT_LAYOUT = Object.keys(WIDGET_META); var DEFAULT_KPIS = ['pipeline', 'clients_active', 'crm_partnerships', 'supermarkets', 'trending_food', 'pending_approvals']; function loadPrefs() { return fetch('/api/preferences/ui').then(function (r) { return r.json(); }).catch(function () { return { dashboard_layout: DEFAULT_LAYOUT.slice(), dashboard_widgets: defaultWidgets(), dashboard_kpis: DEFAULT_KPIS.slice(), global_viz_mode: 'neo-bars', viz_modes: {}, }; }); } function defaultWidgets() { var m = {}; DEFAULT_LAYOUT.forEach(function (id) { m[id] = true; }); return m; } function savePrefs(patch) { return fetch('/api/preferences/ui', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }).then(function (r) { return r.json(); }); } function saveLayout(order) { return savePrefs({ dashboard_layout: order }); } function saveViz(globalMode, vizModes) { return savePrefs({ global_viz_mode: globalMode, viz_modes: vizModes || {} }); } function saveDashboardConfig(layout, widgets, kpis) { var patch = {}; if (layout) patch.dashboard_layout = layout; if (widgets) patch.dashboard_widgets = widgets; if (kpis) patch.dashboard_kpis = kpis; return savePrefs(patch); } function visibleLayout(prefs) { var layout = prefs.dashboard_layout || DEFAULT_LAYOUT.slice(); var widgets = prefs.dashboard_widgets || defaultWidgets(); return layout.filter(function (id) { return widgets[id] !== false; }); } function applyOrder(container, order) { if (!container) return; var map = {}; Array.from(container.children).forEach(function (el) { if (el.dataset && el.dataset.widget) map[el.dataset.widget] = el; }); order.forEach(function (id) { if (map[id]) container.appendChild(map[id]); }); } function applyVisibility(container, widgets) { if (!container) return; var wmap = widgets || defaultWidgets(); Array.from(container.querySelectorAll('[data-widget]')).forEach(function (el) { var id = el.dataset.widget; if (wmap[id] === false) el.classList.add('hm-widget-hidden'); else el.classList.remove('hm-widget-hidden'); }); } function initDrag(container, onReorder, editMode) { if (!container) return function () {}; var dragEl = null; function setDraggable(on) { Array.from(container.querySelectorAll('[data-widget]')).forEach(function (el) { if (el.classList.contains('hm-widget-hidden')) { el.removeAttribute('draggable'); el.classList.remove('hm-widget-draggable'); return; } el.setAttribute('draggable', on ? 'true' : 'false'); el.classList.toggle('hm-widget-draggable', on); }); } function bind() { Array.from(container.querySelectorAll('[data-widget]')).forEach(function (el) { el.removeEventListener('dragstart', el._hmDragStart); el.removeEventListener('dragend', el._hmDragEnd); el.removeEventListener('dragover', el._hmDragOver); if (el.classList.contains('hm-widget-hidden')) return; el._hmDragStart = function (ev) { dragEl = el; el.classList.add('hm-widget-dragging'); ev.dataTransfer.effectAllowed = 'move'; }; el._hmDragEnd = function () { el.classList.remove('hm-widget-dragging'); dragEl = null; var order = Array.from(container.querySelectorAll('[data-widget]:not(.hm-widget-hidden)')) .map(function (n) { return n.dataset.widget; }); var hidden = Array.from(container.querySelectorAll('[data-widget].hm-widget-hidden')) .map(function (n) { return n.dataset.widget; }); if (onReorder) onReorder(order.concat(hidden)); }; el._hmDragOver = function (ev) { ev.preventDefault(); if (!dragEl || dragEl === el) return; var rect = el.getBoundingClientRect(); var after = ev.clientY > rect.top + rect.height / 2; if (after) el.after(dragEl); else el.before(dragEl); }; el.addEventListener('dragstart', el._hmDragStart); el.addEventListener('dragend', el._hmDragEnd); el.addEventListener('dragover', el._hmDragOver); }); } setDraggable(!!editMode); bind(); return function (on) { setDraggable(on); bind(); }; } function buildVizSelector(container, prefs, onChange) { if (!container || !global.VizEngine) return; container.innerHTML = '' + global.VizEngine.MODES.map(function (m) { var active = prefs.global_viz_mode === m.id ? ' active' : ''; return ''; }).join(''); container.querySelectorAll('.viz-mode-btn').forEach(function (btn) { btn.addEventListener('click', function () { container.querySelectorAll('.viz-mode-btn').forEach(function (b) { b.classList.remove('active'); }); btn.classList.add('active'); if (onChange) onChange(btn.dataset.viz); }); }); } function buildCustomizePanel(container, prefs, onChange) { if (!container) return; var widgets = prefs.dashboard_widgets || defaultWidgets(); var kpis = prefs.dashboard_kpis || DEFAULT_KPIS.slice(); var catalog = prefs.widget_catalog || Object.keys(WIDGET_META).map(function (id) { return { id: id, label: (WIDGET_META[id] || {}).title || id, icon: (WIDGET_META[id] || {}).icon || 'โ–ช' }; }); var kpiCatalog = prefs.kpi_catalog || Object.keys(KPI_META).map(function (id) { var m = KPI_META[id]; return { id: id, label: m.label, icon: m.icon }; }); container.innerHTML = '
' + '
' + '

Widgets tonen/verbergen

' + '

Sleep widgets om te ordenen. Vink uit om te verbergen.

' + '
' + catalog.map(function (w) { var checked = widgets[w.id] !== false ? ' checked' : ''; return ''; }).join('') + '
' + '
' + '
' + '

KPI\'s selecteren

' + '

Kies welke KPI-kaarten bovenaan staan. Sleep om volgorde te wijzigen.

' + '
' + kpiCatalog.map(function (k) { var on = kpis.indexOf(k.id) >= 0; return ''; }).join('') + '
' + '
' + '
'; var picker = container.querySelector('#hm-kpi-picker'); var dragKpi = null; container.querySelectorAll('[data-widget-toggle]').forEach(function (cb) { cb.addEventListener('change', function () { widgets[cb.dataset.widgetToggle] = cb.checked; if (onChange) onChange({ widgets: Object.assign({}, widgets) }); }); }); function syncKpiOrder() { var chips = Array.from(picker.querySelectorAll('.hm-kpi-chip')); kpis = chips.filter(function (c) { return c.querySelector('input').checked; }) .map(function (c) { return c.dataset.kpiId; }); } container.querySelectorAll('[data-kpi-toggle]').forEach(function (cb) { cb.addEventListener('change', function () { var chip = cb.closest('.hm-kpi-chip'); chip.classList.toggle('active', cb.checked); syncKpiOrder(); if (onChange) onChange({ kpis: kpis.slice() }); }); }); picker.querySelectorAll('.hm-kpi-chip').forEach(function (chip) { chip.addEventListener('dragstart', function (ev) { dragKpi = chip; chip.classList.add('hm-kpi-dragging'); ev.dataTransfer.effectAllowed = 'move'; }); chip.addEventListener('dragend', function () { chip.classList.remove('hm-kpi-dragging'); dragKpi = null; syncKpiOrder(); if (onChange) onChange({ kpis: kpis.slice() }); }); chip.addEventListener('dragover', function (ev) { ev.preventDefault(); if (!dragKpi || dragKpi === chip) return; var rect = chip.getBoundingClientRect(); var after = ev.clientX > rect.left + rect.width / 2; if (after) chip.after(dragKpi); else chip.before(dragKpi); }); }); } global.DashboardLayout = { WIDGET_META: WIDGET_META, KPI_META: KPI_META, DEFAULT_KPIS: DEFAULT_KPIS, loadPrefs: loadPrefs, saveLayout: saveLayout, saveViz: saveViz, savePrefs: savePrefs, saveDashboardConfig: saveDashboardConfig, visibleLayout: visibleLayout, applyOrder: applyOrder, applyVisibility: applyVisibility, initDrag: initDrag, buildVizSelector: buildVizSelector, buildCustomizePanel: buildCustomizePanel, }; })(window);