/**
* Export Intel map β Leaflet + clustering (multi-instance per tab)
*/
window.ExportIntelMap = (function () {
var HALAL_COLORS = {
hot: '#22c55e',
warm: '#eab308',
mild: '#f97316',
low: '#64748b',
};
var TYPE_CFG = {
distributor: { color: '#f59e0b', icon: 'π¦', label: 'Distributeur' },
wholesaler: { color: '#eab308', icon: 'πͺ', label: 'Groothandel' },
importer: { color: '#f97316', icon: 'π’', label: 'Importeur' },
logistics: { color: '#38bdf8', icon: 'π', label: 'Logistiek' },
contract_caterer:{ color: '#a78bfa', icon: 'π’', label: 'Cateraar' },
restaurant: { color: '#34d399', icon: 'π½οΈ', label: 'Restaurant' },
doner_shoarma: { color: '#4ade80', icon: 'π₯', label: 'DΓΆner / shoarma' },
butcher: { color: '#f87171', icon: 'π₯©', label: 'Slager' },
foodservice: { color: '#2dd4bf', icon: 'π΄', label: 'Foodservice' },
};
var instances = {};
var DEFAULT_ID = 'ei-map';
function cfg(type) {
return TYPE_CFG[type] || { color: '#0ea5e9', icon: 'π', label: String(type || 'entity').replace(/_/g, ' ') };
}
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function row(label, val, link) {
if (!val) return '';
var inner = link
? '' + escapeHtml(val) + ''
: escapeHtml(val);
return (
'
'
);
}
function normalizeUrl(u) {
if (!u) return null;
u = String(u).trim();
if (!u) return null;
if (/^mailto:/i.test(u) || /^tel:/i.test(u)) return u;
if (!/^https?:\/\//i.test(u)) u = 'https://' + u.replace(/^\/+/, '');
return u;
}
function channelList(items, cls) {
if (!items || !items.length) return '';
return items.map(function (it) {
var val = escapeHtml(it.value || it.display || '');
var label = escapeHtml(it.label || '');
var href = it.href || it.url;
if (href) {
return '';
}
return '';
}).join('');
}
function contactPeopleBlock(people) {
if (!people || !people.length) return '';
var cards = people.map(function (c) {
var head = escapeHtml(c.name || c.role || 'Contact');
var sub = escapeHtml([c.role, c.title, c.department].filter(Boolean).join(' Β· '));
var bits = [];
if (c.email) bits.push('');
if (c.email_alt) bits.push('');
if (c.phone) bits.push('');
if (c.mobile) bits.push('');
if (c.whatsapp) bits.push('');
if (c.website) bits.push('');
if (c.linkedin_url) bits.push('');
return '';
}).join('');
return '';
}
function reachBlock(p) {
var r = p.reach || {};
if (!r.has_any && !p.email && !p.phone && !(p.websites && p.websites.length)) {
return '';
}
var html = '';
return html;
}
function websitesBlock(p) {
var sites = p.websites;
if ((!sites || !sites.length) && p.website) {
var w = normalizeUrl(p.website);
if (w) sites = [{ label: 'Organisatie', url: w, display: p.website }];
}
if (!sites || !sites.length) return '';
var links = sites.map(function (s) {
var href = normalizeUrl(s.url || s);
if (!href) return '';
var label = escapeHtml(s.label || 'Website');
var disp = escapeHtml(s.display || s.url || '');
return '';
}).filter(Boolean).join('');
if (!links) return '';
return (
''
);
}
function halalColor(p) {
var tier = p.halal_tier || 'low';
return HALAL_COLORS[tier] || HALAL_COLORS.low;
}
function MapInstance(containerId) {
this.containerId = containerId;
this.map = null;
this.clusterLayer = null;
this.onEntityClick = null;
this.halalMode = false;
this.markersById = {};
}
MapInstance.prototype.popupHtml = function (p) {
var c = cfg(p.entity_type);
var id = p.id;
var halalRow = '';
if (p.halal_score != null) {
halalRow = row('Halal kans', p.halal_score + '/100' + (this.halalMode ? ' Β· ' + (p.halal_tier || '') : ''));
}
var contactBits = [];
if (p.email) contactBits.push('π§ ' + escapeHtml(p.email));
if (p.phone) contactBits.push('π ' + escapeHtml(p.phone));
var contactRow = contactBits.length
? ''
: '';
return (
''
);
};
MapInstance.prototype.pinIcon = function (type, p) {
var c = cfg(type);
var color = this.halalMode && p && p.halal_score != null ? halalColor(p) : c.color;
var ring = this.halalMode && p && p.halal_tier === 'hot' ? ' ei-pin-hot' : '';
return L.divIcon({
className: 'ei-pin-wrap',
html:
'' +
'' +
'' + c.icon + '' +
'
',
iconSize: [30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -16],
});
};
MapInstance.prototype.openEntityId = function (rawId) {
var id = Number(rawId);
if (!Number.isFinite(id)) return;
if (this.onEntityClick) {
this.onEntityClick(id);
return;
}
try {
document.dispatchEvent(new CustomEvent('export-intel:open-entity', { detail: { id: id } }));
} catch (e) { /* empty */ }
};
MapInstance.prototype.enrichPopup = function (entityId, popup) {
if (!entityId || !popup) return;
var self = this;
fetch('/api/export-intel/entities/' + entityId)
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (ent) {
if (!ent || !popup || !popup.isOpen || !popup.isOpen()) return;
var merged = {
id: ent.id,
name: ent.name,
entity_type: ent.entity_type,
country_iso2: ent.country_iso2,
city: ent.city,
address_line: ent.address_line,
postal_code: ent.postal_code,
email: ent.email,
phone: ent.phone,
website: ent.website,
websites: ent.websites || [],
reach: ent.reach || null,
volume_band: ent.volume_band,
confidence: ent.confidence,
pipeline_stage: ent.pipeline_stage,
contact_count: ent.contact_count,
halal_score: ent.halal_score,
halal_tier: ent.halal_tier,
};
popup.setContent(self.popupHtml(merged));
var el = popup.getElement();
if (el) self.wirePopup(el.querySelector('.leaflet-popup-content') || el);
})
.catch(function () { /* keep cached popup */ });
};
MapInstance.prototype.wirePopup = function (container) {
if (!container) return;
var self = this;
var popupRoot = container.closest('.leaflet-popup') || container;
if (window.L && L.DomEvent) {
L.DomEvent.disableClickPropagation(popupRoot);
L.DomEvent.disableScrollPropagation(popupRoot);
}
var btn = container.querySelector('.ei-popup-btn');
var id = btn && btn.getAttribute('data-entity-id');
if (btn && id) {
var onBtn = function (e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
self.openEntityId(id);
};
if (window.L && L.DomEvent) {
L.DomEvent.on(btn, 'click', onBtn);
L.DomEvent.on(btn, 'mousedown', L.DomEvent.stopPropagation);
} else {
btn.addEventListener('click', onBtn);
btn.addEventListener('mousedown', function (e) { e.stopPropagation(); });
}
}
container.addEventListener('click', function (e) {
if (e.target.closest('.ei-popup-btn') || e.target.closest('a')) return;
var card = e.target.closest('[data-entity-id]');
if (card) self.openEntityId(card.getAttribute('data-entity-id'));
});
};
MapInstance.prototype.invalidateSize = function () {
if (!this.map) return;
var map = this.map;
map.invalidateSize(true);
setTimeout(function () { if (map) map.invalidateSize(true); }, 120);
setTimeout(function () { if (map) map.invalidateSize(true); }, 400);
};
MapInstance.prototype.init = function () {
var el = document.getElementById(this.containerId);
if (!el || !window.L) return null;
if (this.map && this.map.getContainer() === el) {
this.invalidateSize();
return this.map;
}
if (this.map) {
this.map.remove();
this.map = null;
this.clusterLayer = null;
}
var self = this;
this.map = L.map(this.containerId, { zoomControl: true, attributionControl: true }).setView([20, 10], 2);
L.tileLayer('/api/map/tiles/{z}/{x}/{y}.png', {
attribution: '© OSM © CARTO',
maxZoom: 19,
}).addTo(this.map);
if (window.L.markerClusterGroup) {
this.clusterLayer = L.markerClusterGroup({
maxClusterRadius: 42,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
zoomToBoundsOnClick: true,
iconCreateFunction: function (cluster) {
var n = cluster.getChildCount();
var size = n < 10 ? 'sm' : n < 50 ? 'md' : 'lg';
return L.divIcon({
html: '' + n + '
',
className: 'ei-cluster-wrap',
iconSize: L.point(44, 44),
});
},
});
this.map.addLayer(this.clusterLayer);
} else {
this.clusterLayer = L.layerGroup();
this.map.addLayer(this.clusterLayer);
}
this.map.on('popupopen', function (e) {
var root = e.popup && e.popup.getElement();
if (!root) return;
if (window.L && L.DomEvent) {
L.DomEvent.disableClickPropagation(root);
L.DomEvent.disableScrollPropagation(root);
}
self.wirePopup(root.querySelector('.leaflet-popup-content') || root);
var card = root.querySelector('[data-entity-id]');
var eid = card && card.getAttribute('data-entity-id');
if (eid) self.enrichPopup(Number(eid), e.popup);
});
this.invalidateSize();
return this.map;
};
MapInstance.prototype.setFeatures = function (geojson, opts) {
opts = opts || {};
if (!this.map || !this.clusterLayer) return;
this.clusterLayer.clearLayers();
this.markersById = {};
var features = (geojson && geojson.features) || [];
var markers = [];
var self = this;
features.forEach(function (f) {
var coords = f.geometry && f.geometry.coordinates;
if (!coords) return;
var p = f.properties || {};
var marker = L.marker([coords[1], coords[0]], { icon: self.pinIcon(p.entity_type, p) });
marker._eiId = p.id;
if (p.id != null) self.markersById[p.id] = marker;
marker.bindPopup(self.popupHtml(p), { maxWidth: 360, minWidth: 280, maxHeight: 420, className: 'ei-leaflet-popup', autoClose: false, closeOnClick: false });
marker.on('click', function (e) {
if (window.L && L.DomEvent) L.DomEvent.stopPropagation(e);
marker.openPopup();
if (self.onEntityClick && p.id != null) self.onEntityClick(p.id);
});
markers.push(marker);
});
if (this.clusterLayer.addLayers) this.clusterLayer.addLayers(markers);
else markers.forEach(function (m) { self.clusterLayer.addLayer(m); });
this.invalidateSize();
if (opts.autoFit === false) {
this.map.setView([20, 10], 2);
} else if (features.length === 1) {
var c = features[0].geometry.coordinates;
this.map.setView([c[1], c[0]], 10);
} else if (opts.autoFit && markers.length > 1 && this.clusterLayer.getBounds) {
try {
this.map.fitBounds(this.clusterLayer.getBounds(), { padding: [40, 40], maxZoom: 12 });
} catch (e) { /* empty */ }
}
};
MapInstance.prototype.setHalalMode = function (on) {
this.halalMode = !!on;
};
MapInstance.prototype.setOnEntityClick = function (fn) {
this.onEntityClick = typeof fn === 'function' ? fn : null;
};
MapInstance.prototype.flyTo = function (lat, lon, zoom) {
if (this.map && lat && lon) this.map.setView([lat, lon], zoom || 10);
};
MapInstance.prototype.highlightEntity = function (id) {
var marker = this.markersById[id];
if (!marker || !this.map) return;
var latlng = marker.getLatLng();
this.map.setView(latlng, Math.max(this.map.getZoom(), 14));
marker.openPopup();
};
MapInstance.prototype.closePopup = function () {
if (this.map) this.map.closePopup();
};
MapInstance.prototype.destroy = function () {
if (this.map) {
this.map.remove();
this.map = null;
this.clusterLayer = null;
}
delete instances[this.containerId];
};
function resolveId(containerId) {
return containerId || DEFAULT_ID;
}
function getOrCreate(containerId) {
var id = resolveId(containerId);
if (!instances[id]) instances[id] = new MapInstance(id);
return instances[id];
}
function init(containerId) {
return getOrCreate(containerId).init();
}
function setFeatures(geojson, opts, containerId) {
getOrCreate(containerId).setFeatures(geojson, opts);
}
function setHalalMode(on, containerId) {
getOrCreate(containerId).setHalalMode(on);
}
function setOnEntityClick(fn, containerId) {
getOrCreate(containerId).setOnEntityClick(fn);
}
function flyTo(lat, lon, zoom, containerId) {
getOrCreate(containerId).flyTo(lat, lon, zoom);
}
function highlightEntity(id, containerId) {
getOrCreate(containerId).highlightEntity(id);
}
function closePopup(containerId) {
getOrCreate(containerId).closePopup();
}
function destroy(containerId) {
if (containerId) getOrCreate(containerId).destroy();
else {
Object.keys(instances).forEach(function (k) { instances[k].destroy(); });
}
}
function invalidateAll() {
Object.keys(instances).forEach(function (k) { instances[k].invalidateSize(); });
}
if (typeof window !== 'undefined') {
window.addEventListener('resize', invalidateAll);
}
return {
init: init,
setFeatures: setFeatures,
setOnEntityClick: setOnEntityClick,
setHalalMode: setHalalMode,
flyTo: flyTo,
closePopup: closePopup,
highlightEntity: highlightEntity,
destroy: destroy,
invalidateAll: invalidateAll,
TYPE_CFG: TYPE_CFG,
cfg: cfg,
};
})();