Files
foodlinkk-command-center/cockpit/static/js/export-intel-profile.js
T

592 lines
23 KiB
JavaScript
Raw Normal View History

2026-07-30 09:33:03 +00:00
/**
* Export Intel — profiel popup (imperatief, geen Alpine in modal).
*/
(function (global) {
'use strict';
function normalizeWebUrl(u) {
if (!u) return '';
u = String(u).trim();
if (!u) return '';
if (!/^https?:\/\//i.test(u)) u = 'https://' + u.replace(/^\/+/, '');
return u;
}
function contactGapHint(e) {
var r = e.reach || {};
var hasDirect = !!(e.phone || e.email || e.website || (r.phones && r.phones.length) || (r.emails && r.emails.length) || (r.websites && r.websites.length));
if (hasDirect) return '';
var src = e.source || 'osm';
return '<p class="ei-drawer-empty ei-drawer-hint">Geen telefoon of e-mail in bron (' + esc(src) + '). Gratis opties: <strong>Bewerken</strong> (handmatig), of <strong>Sync contacten</strong> (scant websites) / <strong>Sync distributeurs</strong> (haalt OSM-tags opnieuw).</p>';
}
function reachSectionHtml(e) {
var r = e.reach || {};
var html = '';
function links(title, items, render) {
if (!items || !items.length) return;
html += '<div class="ei-reach-block"><h5>' + esc(title) + '</h5>';
for (var i = 0; i < items.length; i++) html += render(items[i]);
html += '</div>';
}
links('E-mail', r.emails, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'E-mail') + '</span><a href="' + esc(it.href || ('mailto:' + it.value)) + '">' + esc(it.value) + '</a></div>';
});
links('Telefoon', r.phones, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'Tel') + '</span><a href="' + esc(it.href || ('tel:' + it.value)) + '">' + esc(it.value) + '</a></div>';
});
if (!r.emails || !r.emails.length) {
if (e.email) html += '<div class="ei-drawer-kv"><span>E-mail</span><a href="mailto:' + esc(e.email) + '">' + esc(e.email) + '</a></div>';
}
if (!r.phones || !r.phones.length) {
if (e.phone) html += '<div class="ei-drawer-kv"><span>Telefoon</span><a href="tel:' + esc(e.phone) + '">' + esc(e.phone) + '</a></div>';
}
html += websitesHtml(e);
links('Adres', r.addresses, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'Adres') + '</span><span>' + esc(it.value) + '</span></div>';
});
links('Social', r.social, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'Link') + '</span><a href="' + esc(it.href) + '" target="_blank" rel="noopener">' + esc(it.value) + '</a></div>';
});
if (r.maps_url) {
html += '<div class="ei-drawer-kv"><span>Maps</span><a href="' + esc(r.maps_url) + '" target="_blank" rel="noopener">Google Maps →</a></div>';
}
return html;
}
function websitesHtml(e) {
var sites = e.websites;
if ((!sites || !sites.length) && e.website) {
sites = [{ label: 'Organisatie', url: e.website, display: e.website }];
}
if (!sites || !sites.length) return '';
var rows = '';
for (var i = 0; i < sites.length; i++) {
var s = sites[i];
var href = normalizeWebUrl(s.url || s);
if (!href) continue;
rows += '<div class="ei-drawer-kv"><span>' + esc(s.label || 'Website') + '</span>' +
'<a href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(s.display || s.url || href) + '</a></div>';
}
return rows;
}
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function typeLabel(t) {
return (t || '').replace(/_/g, ' ');
}
function getApp() {
return global.__exportIntelApp || null;
}
function territoryName(iso2) {
var app = getApp();
if (!iso2) return '';
if (app && app.territories) {
for (var i = 0; i < app.territories.length; i++) {
if (app.territories[i].country_iso2 === iso2) return app.territories[i].name_nl;
}
}
return iso2;
}
function formatCoords(lat, lon) {
if (lat == null || lon == null) return '';
var a = Number(lat);
var b = Number(lon);
if (!Number.isFinite(a) || !Number.isFinite(b)) return '';
return a.toFixed(5) + ', ' + b.toFixed(5);
}
function mountEl() {
return document.getElementById('ei-profile-mount');
}
function activeMapId(app) {
if (!app) return 'ei-map';
if (app.tab === 'map') return 'ei-map';
if (app.tabMapId && typeof app.tabMapId === 'function') {
return app.tabMapId() || 'ei-map';
}
return 'ei-map-' + (app.tab || 'map');
}
var Profile = {
_busy: false,
showShell: function (on) {
var el = document.getElementById('ei-profile-modal');
if (!el) return;
if (on) {
el.classList.add('is-open');
el.setAttribute('aria-hidden', 'false');
document.body.classList.add('ei-profile-open');
} else {
el.classList.remove('is-open');
el.setAttribute('aria-hidden', 'true');
document.body.classList.remove('ei-profile-open');
}
},
clearMount: function () {
var m = mountEl();
if (m) m.innerHTML = '';
},
renderLoading: function () {
var m = mountEl();
if (!m) return;
m.innerHTML =
'<div class="ei-drawer-loading" style="padding:3rem 1.5rem;text-align:center">' +
'<span class="ei-map-spinner"></span>' +
'<p style="margin-top:1rem;color:#94a3b8">Profiel laden…</p></div>';
},
renderError: function (msg) {
var m = mountEl();
if (!m) return;
m.innerHTML =
'<div class="ei-drawer-loading" style="padding:2rem 1.5rem">' +
'<p style="color:#fecdd3;margin-bottom:1rem">' + esc(msg || 'Laden mislukt') + '</p>' +
'<button type="button" class="btn btn-sm" data-ei-action="close">Sluiten</button></div>';
},
patchEntityInApp: function (app, entity) {
if (!app || !entity || !entity.id) return;
var id = entity.id;
if (Array.isArray(app.entities)) {
for (var i = 0; i < app.entities.length; i++) {
if (app.entities[i].id === id) {
Object.assign(app.entities[i], entity);
}
}
}
if (Array.isArray(app.crmPipeline)) {
for (var j = 0; j < app.crmPipeline.length; j++) {
if (app.crmPipeline[j].id === id) {
Object.assign(app.crmPipeline[j], entity);
}
}
}
if (Array.isArray(app.halalTopEntities)) {
for (var k = 0; k < app.halalTopEntities.length; k++) {
if (app.halalTopEntities[k].id === id) {
Object.assign(app.halalTopEntities[k], entity);
}
}
}
},
renderFull: function (e, app) {
var m = mountEl();
if (!m || !e) return;
app = app || getApp();
var contacts = e.contacts || [];
var contactRows = '';
for (var i = 0; i < contacts.length; i++) {
var c = contacts[i];
contactRows +=
'<div class="ei-contact-row">' +
'<strong>' + esc(c.name || c.role || 'Contact') + '</strong>' +
(c.role ? '<span class="ei-contact-role">' + esc(c.role) + '</span>' : '') +
(c.email ? '<div>📧 <a href="mailto:' + esc(c.email) + '">' + esc(c.email) + '</a></div>' : '') +
(c.phone ? '<div>📞 <a href="tel:' + esc(c.phone) + '">' + esc(c.phone) + '</a></div>' : '') +
(c.website_contact_url ? '<div>🌐 <a href="' + esc(normalizeWebUrl(c.website_contact_url)) + '" target="_blank" rel="noopener">' + esc(c.website_contact_url) + '</a></div>' : '') +
(c.linkedin_url ? '<div>in <a href="' + esc(normalizeWebUrl(c.linkedin_url)) + '" target="_blank" rel="noopener">LinkedIn</a></div>' : '') +
'</div>';
}
var halalTags = '';
var labels = e.halal_reason_labels || [];
for (var j = 0; j < labels.length; j++) {
halalTags += '<span class="ei-halal-tag">' + esc(labels[j]) + '</span>';
}
var crmBtn = e.client_id
? '<a class="ei-action-tile ei-action-crm-done" href="/clients"><span class="ei-action-icon">✓</span><span class="ei-action-label">Open CRM</span><span class="ei-action-sub">' +
esc(e.crm_client_name || ('Klant #' + e.client_id)) + '</span></a>'
: '<button type="button" class="ei-action-tile ei-action-crm" data-ei-action="crm" data-ei-id="' + e.id + '"><span class="ei-action-icon">➕</span><span class="ei-action-label">Naar CRM</span><span class="ei-action-sub">Klant + deal</span></button>';
var mapsBtn = e.maps_url
? '<a class="ei-action-tile" href="' + esc(e.maps_url) + '" target="_blank" rel="noopener"><span class="ei-action-icon">📍</span><span class="ei-action-label">Maps</span><span class="ei-action-sub">Locatie</span></a>'
: '';
var dealBtn = e.deal_id
? '<a class="ei-action-tile" href="/deals"><span class="ei-action-icon">💼</span><span class="ei-action-label">Deal</span><span class="ei-action-sub">Pipeline</span></a>'
: '';
var favOn = !!e.is_favorite;
m.innerHTML =
'<header class="ei-drawer-head-rich">' +
'<div class="ei-drawer-head-top">' +
'<div class="ei-drawer-hero">' +
'<span class="ei-drawer-badge ei-type-' + esc(e.entity_type) + '">' + esc(typeLabel(e.entity_type)) + '</span>' +
'<span class="ei-drawer-flag">' + esc(e.country_iso2) + '</span>' +
(e.client_id ? '<span class="ei-crm-badge">In CRM</span>' : '') +
(favOn ? '<span class="ei-crm-badge" style="color:#facc15;border-color:rgba(251,191,36,.4)">★ Favoriet</span>' : '') +
(e.halal_score != null ? '<span class="ei-halal-pill ei-halal-hot">' + Math.round(Number(e.halal_score)) + '/100</span>' : '') +
'</div>' +
'<button type="button" class="ei-drawer-close" data-ei-action="close" title="Sluiten">✕</button>' +
'</div>' +
'<h2 class="ei-drawer-title" id="ei-profile-title">' + esc(e.name) + '</h2>' +
'<p class="ei-drawer-meta">' + esc(territoryName(e.country_iso2)) +
' · Confidence <strong>' + (e.confidence || 0) + '%</strong>' +
' · Pipeline <strong>' + esc(e.pipeline_stage || '—') + '</strong>' +
' · Bron <strong>' + esc(e.source || '—') + '</strong></p>' +
'</header>' +
'<div class="ei-drawer-toolbar">' +
crmBtn +
'<button type="button" class="ei-action-tile ei-action-edit" data-ei-action="edit"><span class="ei-action-icon">✎</span><span class="ei-action-label">Bewerken</span><span class="ei-action-sub">Velden</span></button>' +
'<button type="button" class="ei-action-tile' + (favOn ? ' ei-action-fav-on' : '') + '" data-ei-action="favorite" data-ei-id="' + e.id + '" data-ei-fav="' + (favOn ? '1' : '0') + '"><span class="ei-action-icon">' + (favOn ? '★' : '☆') + '</span><span class="ei-action-label">' + (favOn ? 'Favoriet' : 'Favoriet maken') + '</span></button>' +
mapsBtn +
'<button type="button" class="ei-action-tile" data-ei-action="sync-contacts"><span class="ei-action-icon">↻</span><span class="ei-action-label">Contacten</span></button>' +
dealBtn +
'<a class="ei-action-tile" href="/herman?context=export_catalog&entity=' + e.id + '"><span class="ei-action-icon">📋</span><span class="ei-action-label">Herman</span></a>' +
'</div>' +
'<div class="ei-drawer-body ei-drawer-body-rich">' +
(halalTags ? '<div class="ei-halal-tags">' + halalTags + '</div>' : '') +
((e.city || e.address_line || (e.lat && e.lon))
? '<section class="ei-drawer-section"><h4>Locatie</h4>' +
(e.address_line ? '<p>' + esc(e.address_line) + '</p>' : '') +
(e.city ? '<p>' + esc(e.city) + (e.postal_code ? ', ' + esc(e.postal_code) : '') + '</p>' : '') +
((e.lat && e.lon)
? '<p class="ei-drawer-coords">' + esc(formatCoords(e.lat, e.lon)) +
(e.maps_url ? ' <a href="' + esc(e.maps_url) + '" target="_blank" rel="noopener">Maps →</a>' : '') + '</p>'
: '') +
'</section>'
: '') +
'<section class="ei-drawer-section"><h4>Contact & web</h4>' +
reachSectionHtml(e) + contactGapHint(e) +
((!e.reach || !e.reach.has_any) && !e.email && !e.phone && !(e.websites && e.websites.length) && !e.website && !contacts.length ? '<p class="ei-drawer-empty">Geen contact — sync contacten om te verrijken.</p>' : '') +
'</section>' +
(e.client_id
? '<section class="ei-drawer-section"><h4>CRM</h4>' +
'<div class="ei-drawer-kv"><span>Klant</span><a href="/clients">' + esc(e.crm_client_name || ('#' + e.client_id)) + '</a></div>' +
(e.crm_deal_title ? '<div class="ei-drawer-kv"><span>Deal</span><span>' + esc(e.crm_deal_title) + '</span></div>' : '') +
'</section>'
: '') +
'<section class="ei-drawer-section"><h4>Contactpersonen (' + contacts.length + ')</h4>' +
(contactRows || '<p class="ei-drawer-empty">Geen contactpersonen.</p>') +
'</section></div>';
if (app) {
app.selected = e;
app.drawerOpen = true;
app.drawerLoading = false;
app.listFocusDetail = e;
app.listFocusId = e.id;
}
},
fetchEntity: function (id) {
return fetch('/api/export-intel/entities/' + id).then(function (r) {
return r.json().then(function (d) {
if (!r.ok) throw new Error(d.error || d.detail || ('HTTP ' + r.status));
if (d && d.error) throw new Error(d.error);
return d;
});
});
},
highlightOnMap: function (entity, app) {
if (!entity || !entity.lat || !entity.lon || !global.ExportIntelMap) return;
try {
var mapId = activeMapId(app);
global.ExportIntelMap.flyTo(entity.lat, entity.lon, 14, mapId);
global.ExportIntelMap.highlightEntity(entity.id, mapId);
} catch (e) { /* ignore */ }
},
open: function (id) {
id = Number(id);
if (!Number.isFinite(id) || id <= 0) return;
if (this._busy) return;
this._busy = true;
this.showShell(true);
this.renderLoading();
var self = this;
var app = getApp();
this.fetchEntity(id)
.then(function (entity) {
self.renderFull(entity, app);
self.highlightOnMap(entity, app);
})
.catch(function (err) {
self.renderError(err.message || 'Profiel laden mislukt');
})
.finally(function () {
self._busy = false;
});
},
close: function () {
this._busy = false;
this.showShell(false);
this.clearMount();
var app = getApp();
if (app) {
app.drawerOpen = false;
app.drawerLoading = false;
app.selected = null;
app.editMode = false;
}
},
setFavorite: function (id, currentlyFavorite) {
var self = this;
var app = getApp();
id = Number(id);
if (!Number.isFinite(id)) return Promise.reject(new Error('Ongeldig id'));
return fetch('/api/export-intel/entities/favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ entity_ids: [id], favorite: !currentlyFavorite }),
})
.then(function (r) {
return r.json().then(function (d) {
if (!r.ok) throw new Error((d && (d.error || d.detail)) || 'Favoriet mislukt');
return d;
});
})
.then(function () {
return self.fetchEntity(id);
})
.then(function (entity) {
if (app) {
app.selected = entity;
app.listFocusDetail = entity;
app.listFocusId = entity.id;
app.syncMsg = entity.is_favorite ? '★ Favoriet opgeslagen' : 'Favoriet verwijderd';
if (app.stats && typeof app.stats.favorites === 'number') {
app.stats.favorites += entity.is_favorite ? 1 : -1;
if (app.stats.favorites < 0) app.stats.favorites = 0;
}
self.patchEntityInApp(app, entity);
}
self.renderFull(entity, app);
return entity;
});
},
doCrm: function (id) {
var self = this;
var app = getApp();
id = Number(id);
if (!app || !app.pushEntityToCrm) return;
app.pushEntityToCrm(id).then(function () {
return self.fetchEntity(id);
}).then(function (entity) {
if (app) {
app.selected = entity;
app.listFocusDetail = entity;
self.patchEntityInApp(app, entity);
}
self.renderFull(entity, app);
}).catch(function (err) {
if (app) app.syncMsg = 'CRM mislukt: ' + (err.message || err);
});
},
doEdit: function () {
var app = getApp();
if (!app || !app.selected) return;
app.startEdit();
this.renderEdit(app);
},
renderEdit: function (app) {
var e = app.selected;
var f = app.editForm || {};
var m = mountEl();
if (!m || !e) return;
m.innerHTML =
'<header class="ei-drawer-head-rich"><div class="ei-drawer-head-top">' +
'<h2 class="ei-drawer-title">Bewerken · ' + esc(e.name) + '</h2>' +
'<button type="button" class="ei-drawer-close" data-ei-action="close">✕</button></div></header>' +
'<div class="ei-drawer-body ei-drawer-body-rich"><div class="ei-drawer-edit">' +
'<div class="ei-edit-grid">' +
'<label>Naam<input type="text" id="ei-edit-name" value="' + esc(f.name || '') + '"></label>' +
'<label>Stad<input type="text" id="ei-edit-city" value="' + esc(f.city || '') + '"></label>' +
'<label>E-mail<input type="email" id="ei-edit-email" value="' + esc(f.email || '') + '"></label>' +
'<label>Telefoon<input type="text" id="ei-edit-phone" value="' + esc(f.phone || '') + '"></label>' +
'<label>Website<input type="url" id="ei-edit-website" value="' + esc(f.website || '') + '"></label>' +
'</div><div class="ei-edit-actions">' +
'<button type="button" class="btn btn-sm btn-pulse-green" data-ei-action="save-edit">Opslaan</button>' +
'<button type="button" class="btn btn-sm" data-ei-action="cancel-edit">Annuleren</button>' +
'</div></div></div>';
},
saveEdit: function () {
var app = getApp();
if (!app || !app.selected) return;
var self = this;
app.editForm.name = (document.getElementById('ei-edit-name') || {}).value || '';
app.editForm.city = (document.getElementById('ei-edit-city') || {}).value || '';
app.editForm.email = (document.getElementById('ei-edit-email') || {}).value || '';
app.editForm.phone = (document.getElementById('ei-edit-phone') || {}).value || '';
app.editForm.website = (document.getElementById('ei-edit-website') || {}).value || '';
var id = app.selected.id;
Promise.resolve(app.saveEdit()).then(function () {
return self.fetchEntity(id);
}).then(function (entity) {
if (app) {
app.selected = entity;
app.listFocusDetail = entity;
self.patchEntityInApp(app, entity);
}
self.renderFull(entity, app);
});
},
cancelEdit: function () {
var app = getApp();
if (!app) return;
app.cancelEdit();
if (app.selected) this.renderFull(app.selected, app);
},
doFavorite: function (id, isFav) {
var self = this;
this.setFavorite(id, isFav).catch(function (err) {
self.renderError(err.message || 'Favoriet mislukt');
});
},
doSyncContacts: function () {
var app = getApp();
if (app && app.syncKind) app.syncKind('contacts');
},
onModalClick: function (ev) {
var t = ev.target;
if (!t || !t.closest) return;
if (!t.closest('#ei-profile-modal')) return;
if (t.closest('.ei-profile-backdrop')) {
ev.preventDefault();
this.close();
return;
}
var actionEl = t.closest('[data-ei-action]');
if (!actionEl) return;
var action = actionEl.getAttribute('data-ei-action');
if (!action) return;
ev.preventDefault();
ev.stopPropagation();
var id = Number(actionEl.getAttribute('data-ei-id') || (getApp() && getApp().selected && getApp().selected.id));
if (action === 'close') this.close();
else if (action === 'crm') this.doCrm(id);
else if (action === 'edit') this.doEdit();
else if (action === 'favorite') {
var isFav = actionEl.getAttribute('data-ei-fav') === '1';
this.doFavorite(id, isFav);
}
else if (action === 'sync-contacts') this.doSyncContacts();
else if (action === 'save-edit') this.saveEdit();
else if (action === 'cancel-edit') this.cancelEdit();
},
onRowClick: function (ev) {
var t = ev.target;
if (!t || !t.closest) return;
if (t.closest('#ei-profile-modal')) return;
if (t.closest('.ei-td-check, input[type=checkbox], .ei-fav-btn, .ei-td-actions')) return;
if (t.closest('a[href]') && !t.closest('.ei-row-open-btn, .ei-link-btn')) return;
var row = t.closest('[data-entity-id]');
if (!row) return;
var id = row.getAttribute('data-entity-id');
if (!id) return;
ev.preventDefault();
ev.stopPropagation();
this.open(id);
},
bindApp: function (app) {
if (!app) return;
global.__exportIntelApp = app;
var self = this;
app.openEntity = function (id) { self.open(id); };
app.closeDrawer = function () { self.close(); };
},
wireModal: function () {
var modal = document.getElementById('ei-profile-modal');
if (!modal || modal._eiWired) return;
modal._eiWired = true;
var self = this;
modal.addEventListener('click', function (ev) { self.onModalClick(ev); }, true);
},
boot: function () {
var self = this;
this.wireModal();
document.addEventListener('click', function (ev) { self.onRowClick(ev); }, true);
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape') {
var modal = document.getElementById('ei-profile-modal');
if (modal && modal.classList.contains('is-open')) self.close();
}
});
var params = new URLSearchParams(global.location.search);
var openId = params.get('open');
function tryBind() {
var page = document.querySelector('.ei-page');
if (page && page._x_dataStack && page._x_dataStack[0]) {
self.bindApp(page._x_dataStack[0]);
if (openId) self.open(openId);
return true;
}
if (global.__exportIntelApp) {
self.bindApp(global.__exportIntelApp);
if (openId) self.open(openId);
return true;
}
return false;
}
if (!tryBind()) {
var n = 0;
var timer = setInterval(function () {
n++;
if (tryBind() || n > 120) clearInterval(timer);
}, 100);
}
document.addEventListener('alpine:initialized', function () {
tryBind();
});
},
};
global.ExportIntelProfile = Profile;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { Profile.boot(); });
} else {
Profile.boot();
}
})(window);