function exportIntelApp() { return { tab: 'map', tabIndicatorTop: 0, tabIndicatorH: 40, busy: false, syncMsg: '', syncMonitor: null, syncMonitorOpen: true, syncMonitorPoll: null, googleMapsKey: '', googleMapsKeySet: false, googleKeySaving: false, stats: {}, regions: [], territories: [], country: '', region: '', entities: [], contacts: [], contactsHasMore: false, contactsLoadingMore: false, tenders: [], catererPresence: [], catererBrands: [], govSources: [], selected: null, drawerOpen: false, q: '', entityTypeFilter: '', mapTypesSelected: [], mapHalalMode: false, mapHalalMin: 55, mapError: '', mapLoading: false, tabMapLoading: false, mapTruncated: false, halalTopMarkets: [], halalTopEntities: [], listFocusId: null, listFocusDetail: null, listFocusLoading: false, drawerLoading: false, filterHasEmail: '', filterFavoritesOnly: false, filterCrmStatus: '', filterConfidenceMin: '', filterCity: '', filterTenderStatus: '', filterGovCategory: '', _cityTimer: null, selectedIds: [], crmPipeline: [], crmModalOpen: false, crmCreateDeals: true, crmPushBusy: false, editMode: false, editForm: {}, resultCount: 0, _searchTimer: null, tabs: [ { id: 'map', icon: '🗺️', label: 'Kaart', color: '#0ea5e9', key: '1' }, { id: 'distributors', icon: '📦', label: 'Distributeurs', color: '#fbbf24', key: '2' }, { id: 'customers', icon: '🍽️', label: 'Eindklanten', color: '#34d399', key: '3' }, { id: 'caterers', icon: '🏢', label: 'Cateraars', color: '#a78bfa', key: '4' }, { id: 'contacts', icon: '📇', label: 'Contacten', color: '#38bdf8', key: '5' }, { id: 'tenders', icon: '📋', label: 'Tenders', color: '#c084fc', key: '6' }, { id: 'gov', icon: '🏛️', label: 'Overheid', color: '#94a3b8', key: '7' }, { id: 'pipeline', icon: '🔗', label: 'Pipeline', color: '#fb923c', key: '8' }, ], TYPE_GROUPS: { b2b: ['distributor', 'wholesaler', 'importer', 'logistics', 'cold_storage', 'port_agent'], horeca: ['restaurant', 'doner_shoarma', 'butcher', 'foodservice'], caterer: ['contract_caterer'], }, TYPE_CHIP_EXPAND: { distributor: 'b2b', importer: 'b2b', logistics: 'b2b', restaurant: 'horeca', doner_shoarma: 'horeca', butcher: 'horeca', foodservice: 'horeca', }, expandEntityTypes(typeOrList) { var self = this; var raw = Array.isArray(typeOrList) ? typeOrList : [typeOrList]; var out = []; raw.forEach(function (v) { if (!v) return; var key = String(v).trim(); var group = self.TYPE_CHIP_EXPAND[key]; if (group && self.TYPE_GROUPS[group]) { out = out.concat(self.TYPE_GROUPS[group]); } else { out.push(key); } }); return out.filter(function (v, i, a) { return a.indexOf(v) === i; }); }, mapTypesParam() { if (this.entityTypeFilter) { var expanded = this.expandEntityTypes(this.entityTypeFilter); if (expanded.length === 1) return { entity_type: expanded[0] }; if (expanded.length) return { entity_types: expanded.join(',') }; } if (this.mapTypesSelected.length) { var all = this.expandEntityTypes(this.mapTypesSelected); if (all.length === 1) return { entity_type: all[0] }; if (all.length) return { entity_types: all.join(',') }; } return {}; }, shouldAutoFitMap(featCount) { return !!(this.country || this.region || this.q || this.entityTypeFilter || this.mapTypesSelected.length || this.mapHalalMode || this.filterFavoritesOnly || this.filterCrmStatus || (featCount > 0 && featCount <= 800)); }, mapTypeChips: [ { v: 'distributor', l: 'B2B / distri', icon: '📦' }, { v: 'wholesaler', l: 'Groothandel', icon: '🏪' }, { v: 'importer', l: 'Importeur', icon: '🚢' }, { v: 'logistics', l: 'Logistiek', icon: '🚚' }, { v: 'restaurant', l: 'Horeca', icon: '🍽️' }, { v: 'doner_shoarma', l: 'Döner', icon: '🥙' }, { v: 'butcher', l: 'Slager', icon: '🥩' }, { v: 'contract_caterer', l: 'Cateraar', icon: '🏢' }, { v: 'foodservice', l: 'Foodservice', icon: '🍴' }, ], async init() { var params = new URLSearchParams(location.search); var t = params.get('tab'); if (t) this.tab = t; var self = this; window.__exportIntelApp = self; document.addEventListener('export-intel:open-entity', function (ev) { if (ev.detail && ev.detail.id) self.openEntity(ev.detail.id); }); await this.loadMeta(); await this.refresh(); this.startSyncMonitor(true); this.$nextTick(() => this.updateTabIndicator()); window.addEventListener('keydown', (e) => { if (e.target.matches('input, textarea, select')) return; var n = parseInt(e.key, 10); if (n >= 1 && n <= 8) { var btn = this.$refs.vtabsNav?.querySelector('[data-tab="' + this.tabs[n - 1].id + '"]'); this.setTab(this.tabs[n - 1].id, btn); } }); }, async loadMeta() { try { this.regions = await fetch('/api/export-intel/regions').then((r) => r.json()); await this.loadGoogleSettings(); this.territories = await fetch('/api/export-intel/territories').then((r) => r.json()); } catch (e) { console.error(e); } }, async refresh() { this.busy = true; try { var cp = this.country ? '?country=' + encodeURIComponent(this.country) : ''; var rp = this.region ? (cp ? '&' : '?') + 'region=' + encodeURIComponent(this.region) : ''; this.stats = await fetch('/api/export-intel/stats' + cp + rp).then((r) => r.json()); await this.loadTabData(); if (this.tab === 'map') this.loadMap(); } catch (e) { console.error(e); } this.busy = false; }, filterQs() { var parts = []; if (this.country) parts.push('country=' + encodeURIComponent(this.country)); if (this.region) parts.push('region=' + encodeURIComponent(this.region)); if (this.q && this.q.trim()) parts.push('q=' + encodeURIComponent(this.q.trim())); if (this.entityTypeFilter) parts.push('entity_type=' + encodeURIComponent(this.entityTypeFilter)); if (this.filterHasEmail === 'yes') parts.push('has_email=true'); if (this.filterHasEmail === 'no') parts.push('has_email=false'); if (this.filterFavoritesOnly) parts.push('favorite_only=true'); if (this.filterCrmStatus === 'linked') parts.push('crm_linked=true'); if (this.filterCrmStatus === 'not_linked') parts.push('crm_linked=false'); return parts.length ? '&' + parts.join('&') : ''; }, onSearchInput() { clearTimeout(this._searchTimer); this._searchTimer = setTimeout(() => this.applyMapFilters(), 350); }, clearFilters() { this.q = ''; this.entityTypeFilter = ''; this.mapTypesSelected = []; this.mapHalalMode = false; this.filterHasEmail = ''; this.filterFavoritesOnly = false; this.filterCrmStatus = ''; this.filterConfidenceMin = ''; this.filterCity = ''; this.filterTenderStatus = ''; this.filterGovCategory = ''; this.selectedIds = []; this.applyMapFilters(); }, showEntityTypeFilter() { return ['distributors', 'customers', 'caterers', 'contacts'].indexOf(this.tab) >= 0; }, showMapTypeFilter() { return this.tab === 'map'; }, mapTypeCount(v) { var geo = (this.stats && this.stats.entity_types_geo) || {}; var expanded = this.expandEntityTypes(v); var n = 0; expanded.forEach(function (t) { n += Number(geo[t] || 0); }); return n; }, isMapTypeActive(v) { return this.mapTypesSelected.indexOf(v) >= 0; }, toggleMapType(v) { var i = this.mapTypesSelected.indexOf(v); if (i >= 0) this.mapTypesSelected.splice(i, 1); else this.mapTypesSelected.push(v); this.entityTypeFilter = ''; this.applyMapFilters(); }, clearMapTypes() { this.mapTypesSelected = []; this.applyMapFilters(); }, applyMapFilters() { if (this.tab !== 'map') { this.loadTabData(); return; } this.loadTabData().then(() => this.loadMap()); }, showEmailFilter() { return ['map', 'distributors', 'customers', 'caterers', 'contacts'].indexOf(this.tab) >= 0; }, showCrmFilter() { return ['map', 'distributors', 'customers', 'caterers', 'contacts', 'pipeline'].indexOf(this.tab) >= 0; }, showCollectionFilters() { return this.showCrmFilter() || this.showFavoritesFilter(); }, showFavoritesFilter() { return ['map', 'distributors', 'customers', 'caterers', 'contacts'].indexOf(this.tab) >= 0; }, showConfidenceFilter() { return this.tab === 'distributors'; }, showCityFilter() { return ['distributors', 'customers', 'caterers', 'contacts'].indexOf(this.tab) >= 0; }, showContextFilters() { return this.showEntityTypeFilter() || this.showEmailFilter() || this.showConfidenceFilter() || this.showCityFilter() || this.tab === 'tenders' || this.tab === 'gov'; }, showSelectionBar() { return ['map', 'contacts', 'distributors', 'customers', 'caterers'].indexOf(this.tab) >= 0; }, isWorking() { return this.busy || this.crmPushBusy || this.mapLoading || this.drawerLoading; }, progressLabel() { if (this.busy && this.syncMsg) return this.syncMsg; if (this.crmPushBusy) return 'CRM import bezig…'; if (this.mapLoading) return 'Kaart laden…'; if (this.drawerLoading) return 'Profiel laden…'; return 'Bezig…'; }, displayEntities() { var list = this.entities || []; if (this.filterHasEmail === "yes") { list = list.filter(function (e) { return e.email || e.primary_email || e.phone || e.primary_phone || e.has_contact; }); } else if (this.filterHasEmail === "no") { list = list.filter(function (e) { return !e.email && !e.primary_email && !e.phone && !e.primary_phone && !e.has_contact; }); } if (this.filterConfidenceMin) { var min = Number(this.filterConfidenceMin); list = list.filter(function (e) { return (e.confidence || 0) >= min; }); } if (this.filterCity && this.filterCity.trim()) { var c = this.filterCity.trim().toLowerCase(); list = list.filter(function (e) { return (e.city || "").toLowerCase().includes(c); }); } return list; }, displayCount() { if (this.tab === "contacts") return (this.contacts || []).length; return this.displayEntities().length; }, displayPipeline() { var list = this.crmPipeline || []; if (this.q && this.q.trim()) { var ql = this.q.trim().toLowerCase(); list = list.filter(function (p) { return (p.name || "").toLowerCase().includes(ql); }); } return list; }, hasActiveFilters() { return !!(this.q || this.entityTypeFilter || this.mapTypesSelected.length || this.mapHalalMode || this.filterHasEmail || this.filterFavoritesOnly || this.filterCrmStatus || this.filterConfidenceMin || this.filterCity || this.filterTenderStatus || this.filterGovCategory); }, applyEntityFilters() { if (this.tab === "map") this.applyMapFilters(); else { this.loadTabData(); } }, onCityFilterInput() { clearTimeout(this._cityTimer); }, onPipelineFilter() { clearTimeout(this._searchTimer); }, applyGovFilters() { this.loadTabData(); }, visibleEntities() { if (this.tab === "map" && this.mapHalalMode && this.halalTopEntities.length) { return this.halalTopEntities; } return this.displayEntities(); }, entityTypeOptions() { if (this.tab === 'distributors') { return [ { v: '', l: 'Alle types' }, { v: 'distributor', l: 'Distributeur' }, { v: 'wholesaler', l: 'Groothandel' }, { v: 'importer', l: 'Importeur' }, { v: 'logistics', l: 'Logistiek' }, ]; } if (this.tab === 'customers') { return [ { v: '', l: 'Alle types' }, { v: 'restaurant', l: 'Restaurant' }, { v: 'doner_shoarma', l: 'Döner / shoarma' }, { v: 'butcher', l: 'Slager' }, { v: 'foodservice', l: 'Foodservice' }, ]; } if (this.tab === 'caterers') { return [{ v: '', l: 'Alle types' }, { v: 'contract_caterer', l: 'Contract cateraar' }]; } if (this.tab === 'pipeline') { return [ { v: '', l: 'Alle types' }, { v: 'distributor', l: 'Distributeur' }, { v: 'wholesaler', l: 'Groothandel' }, { v: 'contract_caterer', l: 'Cateraar' }, { v: 'restaurant', l: 'Restaurant' }, ]; } if (this.tab === 'contacts') { return [ { v: '', l: 'Alle types' }, { v: 'distributor', l: 'Distributeur' }, { v: 'wholesaler', l: 'Groothandel' }, { v: 'importer', l: 'Importeur' }, { v: 'logistics', l: 'Logistiek' }, { v: 'contract_caterer', l: 'Cateraar' }, { v: 'restaurant', l: 'Restaurant' }, { v: 'foodservice', l: 'Foodservice' }, ]; } return [ { v: '', l: 'Alle types' }, { v: 'distributor', l: 'Distributeur' }, { v: 'wholesaler', l: 'Groothandel' }, { v: 'contract_caterer', l: 'Cateraar' }, { v: 'restaurant', l: 'Restaurant' }, ]; }, tabMapId() { var tabs = ['distributors', 'customers', 'caterers', 'contacts', 'pipeline']; if (tabs.indexOf(this.tab) < 0) return null; return 'ei-map-' + this.tab; }, entitiesToGeojson(list) { var features = []; (list || []).forEach(function (e) { if (e.lat == null || e.lon == null) return; features.push({ type: 'Feature', geometry: { type: 'Point', coordinates: [Number(e.lon), Number(e.lat)] }, properties: { id: e.id, name: e.name, entity_type: e.entity_type, country_iso2: e.country_iso2, city: e.city, address_line: e.address_line, email: e.email || e.primary_email, phone: e.phone || e.primary_phone, website: e.website, confidence: e.confidence, halal_score: e.halal_score, halal_tier: e.halal_tier, contact_count: e.contact_count, pipeline_stage: e.pipeline_stage, volume_band: e.volume_band, }, }); }); return { type: 'FeatureCollection', features: features }; }, async fetchTabMapBundle() { var url = '/api/export-intel/map/bundle'; var qs = []; if (this.country) qs.push('country=' + encodeURIComponent(this.country)); if (this.region) qs.push('region=' + encodeURIComponent(this.region)); if (this.q && this.q.trim()) qs.push('q=' + encodeURIComponent(this.q.trim())); if (this.filterFavoritesOnly) qs.push('favorite_only=true'); if (this.filterCrmStatus === 'linked') qs.push('crm_linked=true'); if (this.filterCrmStatus === 'not_linked') qs.push('crm_linked=false'); var tabTypes = null; if (this.tab === 'distributors') tabTypes = this.TYPE_GROUPS.b2b; else if (this.tab === 'customers') tabTypes = this.TYPE_GROUPS.horeca; else if (this.tab === 'caterers') tabTypes = this.TYPE_GROUPS.caterer; if (this.entityTypeFilter) { var ef = this.expandEntityTypes(this.entityTypeFilter); tabTypes = ef; } if (tabTypes) { if (tabTypes.length === 1) qs.push('entity_type=' + encodeURIComponent(tabTypes[0])); else qs.push('entity_types=' + encodeURIComponent(tabTypes.join(','))); } if (this.tab === 'pipeline') qs.push('crm_linked=true'); if (qs.length) url += '?' + qs.join('&'); var bundle = await fetch(url).then(function (r) { return r.json(); }); this._lastMapBundleMeta = bundle.meta || {}; return bundle.entities || { type: 'FeatureCollection', features: [] }; }, async loadTabMap() { var cid = this.tabMapId(); if (!cid || !document.getElementById(cid)) return; if (!window.L || !window.ExportIntelMap) return; this.tabMapLoading = true; try { var geo = await this.fetchTabMapBundle(); var featN = (geo.features || []).length; var self = this; await this.$nextTick(); window.ExportIntelMap.init(cid); window.ExportIntelMap.setOnEntityClick(function (id) { self.openEntity(id); }, cid); window.ExportIntelMap.setHalalMode(false, cid); window.ExportIntelMap.setFeatures(geo, { autoFit: this.shouldAutoFitMap(featN) }, cid); if (window.ExportIntelMap.invalidateAll) window.ExportIntelMap.invalidateAll(); } catch (e) { console.error(e); } this.tabMapLoading = false; }, async loadTabData(opts) { opts = opts || {}; var base = '/api/export-intel'; var fq = this.filterQs(); if (this.tab === 'map') { return; } else if (this.tab === 'distributors') { var types = this.expandEntityTypes(this.entityTypeFilter || 'b2b').join(','); var r = await fetch(base + '/entities?entity_types=' + encodeURIComponent(types) + '&limit=500' + fq.replace(/&?entity_type=[^&]*/g, '')).then((x) => x.json()); this.entities = r.items || []; this.resultCount = r.total != null ? r.total : this.entities.length; } else if (this.tab === 'customers') { var types2 = this.expandEntityTypes(this.entityTypeFilter || 'horeca').join(','); var r2 = await fetch(base + '/entities?entity_types=' + encodeURIComponent(types2) + '&limit=500' + fq.replace(/&?entity_type=[^&]*/g, '')).then((x) => x.json()); this.entities = r2.items || []; this.resultCount = r2.total != null ? r2.total : this.entities.length; } else if (this.tab === 'caterers') { var types3 = this.expandEntityTypes(this.entityTypeFilter || 'contract_caterer').join(','); var r3 = await fetch(base + '/entities?entity_types=' + encodeURIComponent(types3) + '&limit=500' + fq.replace(/&?entity_type=[^&]*/g, '')).then((x) => x.json()); this.entities = r3.items || []; this.resultCount = r3.total != null ? r3.total : this.entities.length; this.catererPresence = await fetch(base + '/caterers/presence' + (this.country ? '?country=' + this.country : '')).then((x) => x.json()); this.catererBrands = await fetch(base + '/caterers/brands').then((x) => x.json()); } else if (this.tab === 'contacts') { if (!opts.append) { this.contacts = []; this.contactsHasMore = false; } var offset = opts.append ? (this.contacts || []).length : 0; var r4 = await fetch(base + '/contacts?limit=200&offset=' + offset + fq).then((x) => x.json()); var items = r4.items || []; if (opts.append) this.contacts = (this.contacts || []).concat(items); else this.contacts = items; this.resultCount = r4.total != null ? r4.total : this.contacts.length; this.contactsHasMore = this.contacts.length < this.resultCount; } else if (this.tab === 'tenders') { var tenders = await fetch(base + '/tenders' + (this.country ? '?country=' + this.country : '')).then((x) => x.json()); var items = tenders.items || []; if (this.q && this.q.trim()) { var ql = this.q.trim().toLowerCase(); items = items.filter((t) => (t.title || '').toLowerCase().includes(ql)); } if (this.filterTenderStatus) { items = items.filter((t) => (t.status || '').toLowerCase() === this.filterTenderStatus); } this.tenders = items; this.resultCount = items.length; } else if (this.tab === 'gov') { var gov = await fetch(base + '/gov-sources' + (this.country ? '?country=' + this.country : '')).then((x) => x.json()); if (this.q && this.q.trim()) { var ql2 = this.q.trim().toLowerCase(); gov = gov.filter((g) => (g.name || '').toLowerCase().includes(ql2) || (g.category || '').toLowerCase().includes(ql2)); } this.govSources = gov; this.resultCount = gov.length; } else if (this.tab === 'pipeline') { var pipeQs = this.filterQs(); var pipe = await fetch(base + '/crm/pipeline?limit=200' + pipeQs).then((x) => x.json()); this.crmPipeline = pipe.items || []; this.resultCount = this.crmPipeline.length; } if (['distributors','customers','caterers','contacts','pipeline'].indexOf(this.tab) >= 0) { this.$nextTick(() => this.loadTabMap()); } }, async loadMap() { this.clearListFocus(); this.mapLoading = true; this.mapError = ''; var url = '/api/export-intel/map/bundle'; var qs = []; if (this.country) qs.push('country=' + encodeURIComponent(this.country)); if (this.region) qs.push('region=' + encodeURIComponent(this.region)); var mt = this.mapTypesParam(); if (mt.entity_type) qs.push('entity_type=' + encodeURIComponent(mt.entity_type)); else if (mt.entity_types) qs.push('entity_types=' + encodeURIComponent(mt.entity_types)); if (this.q && this.q.trim()) qs.push('q=' + encodeURIComponent(this.q.trim())); if (this.mapHalalMode) qs.push('halal_min=' + encodeURIComponent(this.mapHalalMin)); if (this.filterFavoritesOnly) qs.push('favorite_only=true'); if (this.filterCrmStatus === 'linked') qs.push('crm_linked=true'); if (this.filterCrmStatus === 'not_linked') qs.push('crm_linked=false'); if (qs.length) url += '?' + qs.join('&'); var bundle; try { bundle = await fetch(url).then((r) => r.json()); } catch (e) { this.mapLoading = false; this.mapError = 'Kaart laden mislukt. Probeer opnieuw.'; return; } this.halalTopMarkets = (bundle.meta && bundle.meta.top_markets) || []; this.halalTopEntities = (bundle.meta && bundle.meta.top_halal_entities) || []; this.entities = (bundle.meta && bundle.meta.sidebar_entities) || this.halalTopEntities || []; this.resultCount = (bundle.meta && bundle.meta.entity_count) || this.entities.length; this.mapTruncated = !!(bundle.meta && bundle.meta.truncated); if (!window.L || !window.ExportIntelMap) { this.mapError = 'Kaart kon niet laden. Vernieuw de pagina (Ctrl+F5).'; this.mapLoading = false; return; } this.mapError = ''; if (window.ExportIntelMap) { if (!document.getElementById('ei-map')) { this.mapLoading = false; return; } var self = this; window.ExportIntelMap.init('ei-map'); window.ExportIntelMap.setOnEntityClick(function (id) { self.openEntity(id); }, 'ei-map'); window.ExportIntelMap.setHalalMode(this.mapHalalMode, 'ei-map'); var feats = (bundle.entities && bundle.entities.features) || []; var autoFit = this.shouldAutoFitMap(feats.length); window.ExportIntelMap.setFeatures(bundle.entities || { features: [] }, { autoFit: autoFit }, 'ei-map'); if (this.country) { var t = this.territories.find((x) => x.country_iso2 === this.country); if (t && t.lat && t.lon) window.ExportIntelMap.flyTo(t.lat, t.lon, t.map_zoom || 6, 'ei-map'); } } this.mapLoading = false; var self2 = this; this.$nextTick(function () { requestAnimationFrame(function () { if (window.ExportIntelMap && window.ExportIntelMap.invalidateAll) { window.ExportIntelMap.invalidateAll(); } }); }); }, setTab(id, el) { this.tab = id; this.entityTypeFilter = ''; this.mapTypesSelected = []; this.mapHalalMode = false; this.filterHasEmail = ''; var u = new URL(location.href); u.searchParams.set('tab', id); history.replaceState(null, '', u.pathname + u.search); this.loadTabData(); if (id === 'map') this.$nextTick(() => this.loadMap()); else if (['distributors','customers','caterers','contacts','pipeline'].indexOf(id) >= 0) { this.$nextTick(() => this.loadTabMap()); } this.$nextTick(() => this.updateTabIndicator(el)); }, updateTabIndicator(el) { var nav = this.$refs.vtabsNav; if (!nav) return; var btn = el || nav.querySelector('.vtab-btn.active'); if (!btn) return; var navRect = nav.getBoundingClientRect(); var btnRect = btn.getBoundingClientRect(); this.tabIndicatorTop = btnRect.top - navRect.top + nav.scrollTop; this.tabIndicatorH = btnRect.height; }, async onCountryChange() { await this.refresh(); }, contactPreview(e) { if (!e) return '—'; if (e.email || e.primary_email) return e.email || e.primary_email; if (e.phone || e.primary_phone) return e.phone || e.primary_phone; if (e.website) return e.website.replace(/^https?:\/\//, '').slice(0, 28); if (e.has_contact) return 'Contact in registry'; return 'Geen contact'; }, async fetchEntityDetail(id) { var r = await fetch('/api/export-intel/entities/' + id); if (!r.ok) { var err = await r.json().catch(function () { return {}; }); throw new Error((err && err.error) || ('HTTP ' + r.status)); } var data = await r.json(); if (data && data.error) throw new Error(data.error); return data; }, async selectListEntity(id) { if (!id) return; this.listFocusId = id; this.listFocusLoading = true; this.listFocusDetail = null; try { var detail = await this.fetchEntityDetail(id); this.listFocusDetail = detail; if (window.ExportIntelMap && detail.lat && detail.lon) { window.ExportIntelMap.flyTo(detail.lat, detail.lon, 14); window.ExportIntelMap.highlightEntity(detail.id); } } catch (e) { this.syncMsg = 'Profiel laden mislukt: ' + e.message; this.listFocusId = null; } this.listFocusLoading = false; }, clearListFocus() { this.listFocusId = null; this.listFocusDetail = null; }, openEntity(id) { if (!id) return; if (window.ExportIntelProfile && typeof window.ExportIntelProfile.open === 'function') { window.ExportIntelProfile.open(id); return; } this.syncMsg = 'Profiel-module niet geladen — ververs de pagina'; }, closeDrawer() { if (window.ExportIntelProfile && typeof window.ExportIntelProfile.close === 'function') { window.ExportIntelProfile.close(); return; } this.drawerOpen = false; this.selected = null; this.drawerLoading = false; this.editMode = false; this.editForm = {}; }, startEdit() { if (!this.selected) return; var s = this.selected; this.editForm = { name: s.name || '', entity_type: s.entity_type || '', country_iso2: s.country_iso2 || '', city: s.city || '', address_line: s.address_line || '', postal_code: s.postal_code || '', phone: s.phone || '', email: s.email || '', website: s.website || '', volume_band: s.volume_band || '', pipeline_stage: s.pipeline_stage || 'identified', confidence: s.confidence != null ? s.confidence : 50, halal_cert_notes: s.halal_cert_notes || '', cold_chain: !!s.cold_chain, }; this.editMode = true; }, cancelEdit() { this.editMode = false; this.editForm = {}; }, async saveEdit() { if (!this.selected || !this.selected.id) return; this.busy = true; this.syncMsg = 'Opslaan…'; try { var r = await fetch('/api/export-intel/entities/' + this.selected.id, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.editForm), }); if (!r.ok) { var err = await r.json().catch(function () { return {}; }); throw new Error(err.detail || err.error || 'Opslaan mislukt'); } this.selected = await r.json(); this.editMode = false; this.syncMsg = 'Opgeslagen'; await this.refresh(); } catch (e) { this.syncMsg = 'Opslaan mislukt: ' + e.message; } this.busy = false; }, async syncKind(kind) { if (kind === 'world') { if (!confirm('Wereld-sync: alle regio\'s (Europa, MENA, Afrika, Amerika, Asia, Oceanië). OSM + horeca + contacten — kan uren duren. Doorgaan?')) return; } if (kind === 'region' && !this.region) { this.syncMsg = 'Selecteer eerst een regio in de filterbalk'; return; } this.busy = true; this.syncMsg = kind === 'world' ? 'Wereld-sync gestart…' : 'Sync gestart…'; try { var body = { country_iso2: this.country || null, region_code: kind === 'region' ? this.region : (this.region || null), max_priority: 2, }; if (kind === 'world') body = { max_priority: 3 }; if (kind === 'places' && !this.country && !this.region) { this.syncMsg = 'Selecteer een land of regio voor Google Places'; this.busy = false; return; } var endpoint = kind === 'region' ? 'region' : kind; var r = await fetch('/api/export-intel/sync/' + endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then((x) => x.json()); if (r.error) this.syncMsg = 'Fout: ' + r.error; else if (kind === 'world') this.syncMsg = 'Wereld-sync voltooid — kaart wordt ververst'; else if (kind === 'all' || kind === 'region') this.syncMsg = 'Regio-sync voltooid'; else this.syncMsg = 'Sync voltooid'; await this.refresh(); } catch (e) { this.syncMsg = 'Sync mislukt: ' + e.message; } this.busy = false; }, tabCount(id) { if (id === 'distributors') return this.stats.distributors || 0; if (id === 'caterers') return this.stats.caterers || 0; if (id === 'contacts') return this.stats.contacts || 0; if (id === 'tenders') return this.stats.tenders_open || 0; if (id === 'pipeline') return this.stats.crm_linked || 0; if (id === 'map') return this.stats.entities || 0; return 0; }, tabSub(id) { var n = this.tabCount(id); if (id === 'map') return n + ' op kaart'; if (id === 'gov') return 'Bronnen per land'; if (id === 'pipeline') return 'CRM koppeling'; if (n > 0) return n + ' records'; return ''; }, contactsExportUrl() { var u = '/api/export-intel/contacts/export.csv?'; var parts = []; if (this.country) parts.push('country=' + encodeURIComponent(this.country)); if (this.region) parts.push('region=' + encodeURIComponent(this.region)); if (this.entityTypeFilter) parts.push('entity_type=' + encodeURIComponent(this.entityTypeFilter)); if (this.filterHasEmail === 'yes') parts.push('has_email=true'); if (this.filterHasEmail === 'no') parts.push('has_email=false'); if (this.filterCrmStatus === 'linked') parts.push('crm_linked=true'); if (this.filterCrmStatus === 'not_linked') parts.push('crm_linked=false'); if (this.q && this.q.trim()) parts.push('q=' + encodeURIComponent(this.q.trim())); return u + parts.join('&'); }, async loadMoreContacts() { if (!this.contactsHasMore || this.contactsLoadingMore) return; this.contactsLoadingMore = true; try { await this.loadTabData({ append: true }); } finally { this.contactsLoadingMore = false; } }, typeLabel(t) { var labels = { distributor: 'Distrib.', wholesaler: 'Grooth.', contract_caterer: 'Cateraar', end_customer: 'Eindkl.', hotel: 'Hotel', restaurant: 'Rest.', retailer: 'Retail', importer: 'Import', }; return labels[t] || (t || '').replace(/_/g, ' '); }, filteredTerritories() { var list = (this.territories || []).filter(function (t) { return !this.region || t.region_code === this.region; }, this); var seen = {}; var out = []; list.forEach(function (t) { var iso = t.country_iso2; if (!iso || seen[iso]) return; seen[iso] = true; out.push(t); }); out.sort(function (a, b) { return (a.name_nl || a.country_iso2 || '').localeCompare(b.name_nl || b.country_iso2 || '', 'nl'); }); return out; }, countryEntityCount(iso2) { if (!iso2) return 0; var cc = (this.stats && this.stats.country_entity_counts) || {}; return Number(cc[iso2] || 0); }, countryOptionLabel(t) { if (!t) return ''; var n = this.countryEntityCount(t.country_iso2); return n > 0 ? (t.name_nl + ' (' + n + ')') : t.name_nl; }, async loadGoogleSettings() { try { var s = await fetch('/api/export-intel/settings').then(function (r) { return r.json(); }); this.googleMapsKeySet = !!(s && s.google_maps_api_key_set); this.googleMapsKey = ''; } catch (e) { console.error(e); } }, async saveGoogleMapsKey() { if (!this.googleMapsKey || !this.googleMapsKey.trim()) return; this.googleKeySaving = true; try { await fetch('/api/export-intel/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ google_maps_api_key: this.googleMapsKey.trim() }), }); this.googleMapsKey = ''; this.googleMapsKeySet = true; this.syncMsg = 'Google Places API-key opgeslagen'; } catch (e) { this.syncMsg = 'API-key opslaan mislukt'; } this.googleKeySaving = false; }, startSyncMonitor(silent) { var self = this; if (this.syncMonitorPoll) return; this.pollSyncStatus(); this.syncMonitorPoll = setInterval(function () { self.pollSyncStatus(); }, 3000); }, stopSyncMonitor() { if (this.syncMonitorPoll) { clearInterval(this.syncMonitorPoll); this.syncMonitorPoll = null; } }, async pollSyncStatus() { try { var s = await fetch('/api/export-intel/sync/status').then(function (r) { return r.json(); }); this.syncMonitor = s; if (s.any_running && s.active_jobs && s.active_jobs.length) { var j = s.active_jobs[0]; this.syncMsg = (j.message || j.label || 'Sync bezig') + (j.percent ? ' · ' + j.percent + '%' : ''); } } catch (e) { console.error(e); } }, toggleSyncMonitor() { this.syncMonitorOpen = !this.syncMonitorOpen; }, syncMonitorPercent() { var jobs = (this.syncMonitor && this.syncMonitor.active_jobs) || []; if (!jobs.length) return 0; return jobs[0].percent || 0; }, formatSyncTime(ts) { if (!ts) return ''; var d = new Date(ts * 1000); return d.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); }, syncEventLabel(ev) { if (!ev) return ''; var parts = [ev.event || '']; if (ev.message) parts.push(ev.message); else if (ev.country) parts.push(ev.country); if (ev.step) parts.push('(' + ev.step + ')'); return parts.filter(Boolean).join(' · '); }, territoryName(iso2) { if (!iso2) return ''; var t = this.territories.find(function (x) { return x.country_iso2 === iso2; }); return t ? t.name_nl : iso2; }, isSelected(id) { return this.selectedIds.indexOf(id) >= 0; }, toggleSelect(id, ev) { if (ev) ev.stopPropagation(); var i = this.selectedIds.indexOf(id); if (i >= 0) this.selectedIds.splice(i, 1); else this.selectedIds.push(id); }, toggleSelectAllVisible() { if (this.tab === 'contacts') { var entityIds = []; (this.contacts || []).forEach(function (c) { if (c.entity_id && entityIds.indexOf(c.entity_id) < 0) entityIds.push(c.entity_id); }); if (!entityIds.length) return; var allSelected = entityIds.every((id) => this.isSelected(id)); if (allSelected) { entityIds.forEach((id) => { var i = this.selectedIds.indexOf(id); if (i >= 0) this.selectedIds.splice(i, 1); }); } else { entityIds.forEach((id) => { if (!this.isSelected(id)) this.selectedIds.push(id); }); } return; } var list = this.visibleEntities(); if (!list.length) return; var allSelected = list.every((e) => this.isSelected(e.id)); if (allSelected) { list.forEach((e) => { var i = this.selectedIds.indexOf(e.id); if (i >= 0) this.selectedIds.splice(i, 1); }); } else { list.forEach((e) => { if (!this.isSelected(e.id)) this.selectedIds.push(e.id); }); } }, clearSelection() { this.selectedIds = []; }, async setFavorites(ids, favorite) { if (!ids.length) return; this.busy = true; try { await fetch('/api/export-intel/entities/favorites', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity_ids: ids, favorite: favorite }), }); this.syncMsg = favorite ? ids.length + ' favoriet' + (ids.length > 1 ? 'en' : '') + ' opgeslagen' : 'Favoriet verwijderd'; await this.refresh(); } catch (e) { this.syncMsg = 'Favoriet mislukt: ' + e.message; } this.busy = false; }, async toggleFavorite(id, current, ev) { if (ev) ev.stopPropagation(); await this.setFavorites([id], !current); }, async favoriteSelection() { await this.setFavorites(this.selectedIds.slice(), true); }, async unfavoriteSelection() { await this.setFavorites(this.selectedIds.slice(), false); }, openCrmModal() { if (!this.selectedIds.length) { this.syncMsg = 'Selecteer eerst één of meer rijen (checkbox)'; return; } this.crmModalOpen = true; }, async pushToCrm(ids) { var entityIds = (ids && ids.length) ? ids : this.selectedIds.slice(); if (!entityIds.length) return; this.crmPushBusy = true; this.syncMsg = 'CRM import gestart…'; try { var r = await fetch('/api/export-intel/crm/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity_ids: entityIds, create_deals: this.crmCreateDeals, }), }).then((x) => x.json()); if (r.error) throw new Error(r.error); var created = r.created || 0; var linked = r.linked_existing || 0; this.syncMsg = 'CRM: ' + created + ' nieuw, ' + linked + ' bestaand gekoppeld'; this.crmModalOpen = false; this.selectedIds = []; await this.refresh(); if (this.listFocusDetail && entityIds.indexOf(this.listFocusDetail.id) >= 0) { this.listFocusDetail = await this.fetchEntityDetail(this.listFocusDetail.id); } if (this.tab === 'pipeline') await this.loadTabData(); } catch (e) { this.syncMsg = 'CRM import mislukt: ' + e.message; } this.crmPushBusy = false; }, async pushEntityToCrm(id) { this.crmCreateDeals = true; await this.pushToCrm([id]); }, onFilterCollectionChange() { if (this.tab === 'contacts') { this.loadTabData(); return; } this.applyMapFilters(); }, }; }