/** * 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 ( '
' + '' + escapeHtml(label) + '' + '' + inner + '' + '
' ); } 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 '' + (label ? '' + label + '' : '') + '' + val + ''; } return '
' + (label ? '' + label + '' : '') + '' + val + '
'; }).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('πŸ“§ ' + escapeHtml(c.email) + ''); if (c.email_alt) bits.push('πŸ“§ ' + escapeHtml(c.email_alt) + ' (alt)'); if (c.phone) bits.push('πŸ“ž ' + escapeHtml(c.phone) + ''); if (c.mobile) bits.push('πŸ“± ' + escapeHtml(c.mobile) + ''); if (c.whatsapp) bits.push('πŸ’¬ ' + escapeHtml(c.whatsapp) + ''); if (c.website) bits.push('🌐 Website'); if (c.linkedin_url) bits.push('in LinkedIn'); return '
' + '
' + head + (c.is_primary ? ' primair' : '') + '
' + (sub ? '
' + sub + '
' : '') + (bits.length ? '' : '') + '
'; }).join(''); return '
Contactpersonen
' + cards + '
'; } function reachBlock(p) { var r = p.reach || {}; if (!r.has_any && !p.email && !p.phone && !(p.websites && p.websites.length)) { return '
Nog geen contact β€” sync contacten of vul handmatig aan.
'; } var html = '
'; if ((r.emails && r.emails.length) || p.email) { var emails = r.emails && r.emails.length ? r.emails : (p.email ? [{ label: 'E-mail', value: p.email, href: 'mailto:' + p.email }] : []); html += '
E-mail
' + channelList(emails) + '
'; } if ((r.phones && r.phones.length) || p.phone) { var phones = r.phones && r.phones.length ? r.phones : (p.phone ? [{ label: 'Telefoon', value: p.phone, href: 'tel:' + p.phone }] : []); html += '
Telefoon
' + channelList(phones) + '
'; } var webHtml = websitesBlock(p); if (webHtml) html += webHtml.replace('ei-popup-row ei-popup-websites', 'ei-popup-section ei-popup-websites').replace('ei-popup-k">Websites', 'ei-popup-section-title">Websites').replace('ei-popup-v ei-popup-web-list', 'ei-popup-web-list'); if (r.addresses && r.addresses.length) { html += '
Adres
' + channelList(r.addresses) + '
'; } if (r.social && r.social.length) { html += '
Social
' + channelList(r.social) + '
'; } if (r.maps_url) { html += '
πŸ“ Open in Google Maps
'; } html += contactPeopleBlock(r.contact_people); 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 '' + label + ''; }).filter(Boolean).join(''); if (!links) return ''; return ( '
' + 'Websites' + '' + links + '' + '
' ); } 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 ? '
Contact' + contactBits.join('
') + '
' : ''; return ( '
' + '
' + '' + c.icon + ' ' + escapeHtml(c.label) + '' + '' + escapeHtml(p.country_iso2 || '') + '' + '
' + '

' + escapeHtml(p.name) + '

' + '
' + halalRow + contactRow + row('Stad', p.city) + row('Adres', p.address_line) + row('E-mail', p.email, p.email ? 'mailto:' + p.email : null) + row('Telefoon', p.phone, p.phone ? 'tel:' + p.phone : null) + websitesBlock(p) + row('Volume', p.volume_band) + row('Contacten', p.contact_count > 0 ? p.contact_count + ' in registry' : null) + row('Confidence', p.confidence != null ? p.confidence + '%' : null) + row('Pipeline', p.pipeline_stage) + '
' + '
' + '' + '
' + '
' ); }; 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, }; })();