Files
foodlinkk-command-center/cockpit/static/js/export-intel.js
T
Aissa 4dd1b8265f Add live CEO briefing, Wereldexport CRM, halal engine, and realtime cockpit.
Ship export intel with contact filters and CRM push, Herman live digest with Telegram briefing, halal recommendation engine, revenue cockpit, and dashboard polling/WebSocket fixes.
2026-07-19 18:25:21 +00:00

667 lines
24 KiB
JavaScript

function exportIntelApp() {
return {
tab: 'map',
tabIndicatorTop: 0,
tabIndicatorH: 40,
busy: false,
syncMsg: '',
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,
halalTopMarkets: [],
halalTopEntities: [],
listFocusId: null,
listFocusDetail: null,
listFocusLoading: false,
drawerLoading: false,
filterHasEmail: '',
filterFavoritesOnly: false,
filterCrmStatus: '',
selectedIds: [],
crmPipeline: [],
crmModalOpen: false,
crmCreateDeals: true,
crmPushBusy: false,
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' },
],
mapTypeChips: [
{ v: 'distributor', l: 'Distributeur', icon: '📦' },
{ v: 'wholesaler', l: 'Groothandel', icon: '🏪' },
{ v: 'importer', l: 'Importeur', icon: '🚢' },
{ v: 'logistics', l: 'Logistiek', icon: '🚚' },
{ v: 'restaurant', l: 'Restaurant', 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;
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.$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());
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.selectedIds = [];
this.applyMapFilters();
},
showEntityTypeFilter() {
return ['distributors', 'customers', 'caterers', 'contacts'].indexOf(this.tab) >= 0;
},
showMapTypeFilter() {
return this.tab === 'map';
},
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();
},
mapTypesParam() {
if (this.entityTypeFilter) return { entity_type: this.entityTypeFilter };
if (this.mapTypesSelected.length) {
return { entity_types: this.mapTypesSelected.join(',') };
}
return {};
},
applyMapFilters() {
if (this.tab !== 'map') {
this.loadTabData();
return;
}
this.loadTabData().then(() => this.loadMap());
},
showEmailFilter() {
return this.tab === 'contacts';
},
showCrmFilter() {
return this.tab === 'map' || this.tab === 'contacts';
},
showSelectionBar() {
return this.tab === 'map' || this.tab === 'contacts';
},
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 === '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' },
];
},
async loadTabData(opts) {
opts = opts || {};
var base = '/api/export-intel';
var fq = this.filterQs();
if (this.tab === 'map') {
var mapQs = this.filterQs();
var mt = this.mapTypesParam();
if (mt.entity_type) mapQs += '&entity_type=' + encodeURIComponent(mt.entity_type);
else if (mt.entity_types) mapQs += '&entity_types=' + encodeURIComponent(mt.entity_types);
var rm = await fetch(base + '/entities?limit=200' + mapQs).then((x) => x.json());
this.entities = rm.items || [];
this.resultCount = rm.total != null ? rm.total : this.entities.length;
} else if (this.tab === 'distributors') {
var types = 'distributor,wholesaler,importer,logistics';
var et = this.entityTypeFilter ? '&entity_type=' + encodeURIComponent(this.entityTypeFilter) : '';
var r = await fetch(base + '/entities?entity_types=' + types + '&limit=500' + fq.replace(/&?entity_type=[^&]*/g, '') + et).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 = 'restaurant,doner_shoarma,butcher,foodservice';
var et2 = this.entityTypeFilter ? '&entity_type=' + encodeURIComponent(this.entityTypeFilter) : '';
var r2 = await fetch(base + '/entities?entity_types=' + types2 + '&limit=500' + fq.replace(/&?entity_type=[^&]*/g, '') + et2).then((x) => x.json());
this.entities = r2.items || [];
this.resultCount = r2.total != null ? r2.total : this.entities.length;
} else if (this.tab === 'caterers') {
var et3 = this.entityTypeFilter || 'contract_caterer';
var r3 = await fetch(base + '/entities?entity_type=' + et3 + '&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));
}
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;
}
},
async loadMap() {
this.clearListFocus();
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 = await fetch(url).then((r) => r.json());
this.halalTopMarkets = (bundle.meta && bundle.meta.top_markets) || [];
this.halalTopEntities = (bundle.meta && bundle.meta.top_halal_entities) || [];
if (window.ExportIntelMap) {
if (!document.getElementById('ei-map')) return;
var self = this;
window.ExportIntelMap.init('ei-map');
window.ExportIntelMap.setOnEntityClick(function (id) { self.openEntity(id); });
window.ExportIntelMap.setHalalMode(this.mapHalalMode);
window.ExportIntelMap.setFeatures(bundle.entities || { features: [] });
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);
}
}
},
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());
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;
},
async openEntity(id) {
if (!id) return;
this.drawerLoading = true;
this.drawerOpen = true;
try {
if (this.listFocusDetail && this.listFocusDetail.id === id) {
this.selected = this.listFocusDetail;
} else {
this.selected = await this.fetchEntityDetail(id);
this.listFocusId = id;
this.listFocusDetail = this.selected;
}
if (window.ExportIntelMap && this.selected.lat && this.selected.lon) {
window.ExportIntelMap.flyTo(this.selected.lat, this.selected.lon, 14);
window.ExportIntelMap.highlightEntity(this.selected.id);
}
if (window.ExportIntelMap && window.ExportIntelMap.closePopup) {
window.ExportIntelMap.closePopup();
}
} catch (e) {
this.syncMsg = 'Profiel laden mislukt: ' + e.message;
this.drawerOpen = false;
this.selected = null;
}
this.drawerLoading = false;
},
closeDrawer() {
this.drawerOpen = false;
this.selected = null;
this.drawerLoading = false;
},
async syncKind(kind) {
if (kind === 'world') {
if (!confirm('Wereld-sync: Europa, Midden-Oosten, Afrika en Amerika. OSM + contacten — kan lang 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: 2 };
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) {
return (t || '').replace(/_/g, ' ');
},
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.mapHalalMode && this.halalTopEntities.length ? this.halalTopEntities : this.entities;
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();
},
};
}