274 lines
12 KiB
JavaScript
274 lines
12 KiB
JavaScript
(function (global) {
|
|
var WIDGET_META = {
|
|
kpis: { title: 'CEO KPI\'s', icon: '📊' },
|
|
executive: { title: 'Alles op een rij', icon: '📋' },
|
|
briefing: { title: 'Herman briefing', icon: '📝' },
|
|
rss: { title: 'RSS live feed', 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: '/marketing' },
|
|
rss_items: { label: 'RSS items', sub: 'totaal in DB', icon: '📡', color: '#0ea5e9', link: '/marketing' },
|
|
rss_bookmarks: { label: 'RSS bookmarks', sub: 'opgeslagen', icon: '★', color: '#ffd700', link: '/marketing' },
|
|
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 = '<label class="viz-global-label">Visualisatie:</label>' +
|
|
global.VizEngine.MODES.map(function (m) {
|
|
var active = prefs.global_viz_mode === m.id ? ' active' : '';
|
|
return '<button type="button" class="viz-mode-btn' + active + '" data-viz="' + m.id + '">' + m.label + '</button>';
|
|
}).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 =
|
|
'<div class="hm-customize-panel">' +
|
|
'<div class="hm-customize-col">' +
|
|
'<h4>Widgets tonen/verbergen</h4>' +
|
|
'<p class="hm-customize-hint">Sleep widgets om te ordenen. Vink uit om te verbergen.</p>' +
|
|
'<div class="hm-customize-list" id="hm-widget-toggles">' +
|
|
catalog.map(function (w) {
|
|
var checked = widgets[w.id] !== false ? ' checked' : '';
|
|
return '<label class="hm-customize-item"><input type="checkbox" data-widget-toggle="' + w.id + '"' + checked + '> ' +
|
|
(w.icon || '') + ' ' + (w.label || w.id) + '</label>';
|
|
}).join('') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="hm-customize-col">' +
|
|
'<h4>KPI\'s selecteren</h4>' +
|
|
'<p class="hm-customize-hint">Kies welke KPI-kaarten bovenaan staan. Sleep om volgorde te wijzigen.</p>' +
|
|
'<div class="hm-kpi-picker" id="hm-kpi-picker">' +
|
|
kpiCatalog.map(function (k) {
|
|
var on = kpis.indexOf(k.id) >= 0;
|
|
return '<label class="hm-kpi-chip' + (on ? ' active' : '') + '" draggable="true" data-kpi-id="' + k.id + '">' +
|
|
'<input type="checkbox" data-kpi-toggle="' + k.id + '"' + (on ? ' checked' : '') + '> ' +
|
|
(k.icon || '') + ' ' + (k.label || k.id) + '</label>';
|
|
}).join('') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
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);
|