Fix map layout jump, CRM/edit distributeurs, progress overlay.

Stable map grid, no fitBounds on world view, entity PATCH edit form, CRM buttons on distributors, and loading animation for sync/wait states.
This commit is contained in:
Aissa
2026-07-19 18:47:12 +00:00
parent 6856d09cd1
commit 8d2766efb6
6 changed files with 263 additions and 14 deletions
+7
View File
@@ -86,6 +86,13 @@ async def api_entities(
return await _proxy("GET", "/entities", params=params) return await _proxy("GET", "/entities", params=params)
@router.patch("/api/export-intel/entities/{entity_id}")
async def api_update_entity(entity_id: int, request: Request):
body = await request.json()
return await _proxy("PATCH", f"/entities/{entity_id}", json=body)
@router.get("/api/export-intel/entities/{entity_id}") @router.get("/api/export-intel/entities/{entity_id}")
async def api_entity(entity_id: int): async def api_entity(entity_id: int):
return await _proxy("GET", f"/entities/{entity_id}") return await _proxy("GET", f"/entities/{entity_id}")
+50
View File
@@ -40,6 +40,56 @@
.ei-filter-meta { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; } .ei-filter-meta { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.ei-filter-count { font-size: 0.75rem; color: #64748b; white-space: nowrap; } .ei-filter-count { font-size: 0.75rem; color: #64748b; white-space: nowrap; }
.ei-filter-clear { font-size: 0.72rem; opacity: 0.85; } .ei-filter-clear { font-size: 0.72rem; opacity: 0.85; }
.ei-split { align-items: stretch; }
.ei-split > .ei-map-wrap { grid-column: 1; }
.ei-split > .ei-panel { grid-column: 2; }
.ei-progress-overlay {
position: fixed; inset: 0; z-index: 1200;
display: flex; align-items: center; justify-content: center;
background: rgba(4, 8, 14, 0.55); backdrop-filter: blur(2px);
}
.ei-progress-card {
min-width: 260px; max-width: 90vw; padding: 1.1rem 1.25rem;
border-radius: 14px; border: 1px solid rgba(56, 189, 248, 0.35);
background: rgba(10, 14, 20, 0.95); text-align: center;
box-shadow: 0 12px 40px rgba(0,0,0,0.45);
}
.ei-progress-card p { margin: 0.65rem 0 0.85rem; color: #e2e8f0; font-size: 0.88rem; }
.ei-progress-spinner {
width: 28px; height: 28px; margin: 0 auto; border-radius: 50%;
border: 3px solid rgba(56, 189, 248, 0.2); border-top-color: #38bdf8;
animation: ei-spin 0.75s linear infinite;
}
.ei-progress-bar {
height: 4px; border-radius: 999px; background: rgba(148, 163, 184, 0.2); overflow: hidden;
}
.ei-progress-bar-fill {
height: 100%; width: 35%; border-radius: 999px;
background: linear-gradient(90deg, #0ea5e9, #38bdf8, #0ea5e9);
background-size: 200% 100%; animation: ei-progress-slide 1.2s ease-in-out infinite;
}
@keyframes ei-progress-slide {
0% { transform: translateX(-120%); }
100% { transform: translateX(320%); }
}
.ei-th-actions, .ei-td-actions { white-space: nowrap; width: 1%; }
.ei-td-actions { display: flex; gap: 0.25rem; flex-wrap: wrap; }
.ei-td-actions .btn { padding: 0.2rem 0.45rem; font-size: 0.68rem; }
.ei-drawer-edit { margin: 1rem 0; padding: 0.85rem; border-radius: 12px; border: 1px solid rgba(56,189,248,0.25); background: rgba(8,12,18,0.6); }
.ei-drawer-edit h4 { margin: 0 0 0.65rem; font-size: 0.85rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.06em; }
.ei-edit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.55rem; }
.ei-edit-grid label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.68rem; color: #64748b; text-transform: uppercase; }
.ei-edit-grid input, .ei-edit-grid select, .ei-edit-grid textarea {
padding: 0.45rem 0.55rem; border-radius: 8px; border: 1px solid rgba(148,163,184,0.2);
background: rgba(15,23,42,0.8); color: #e2e8f0; font-size: 0.82rem;
}
.ei-edit-full { grid-column: 1 / -1; }
.ei-edit-actions { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
.ei-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; min-height: 420px; } .ei-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; min-height: 420px; }
@media (max-width: 900px) { .ei-split { grid-template-columns: 1fr; } } @media (max-width: 900px) { .ei-split { grid-template-columns: 1fr; } }
+6 -3
View File
@@ -221,7 +221,8 @@ window.ExportIntelMap = (function () {
return map; return map;
} }
function setFeatures(geojson) { function setFeatures(geojson, opts) {
opts = opts || {};
if (!map || !clusterLayer) return; if (!map || !clusterLayer) return;
clusterLayer.clearLayers(); clusterLayer.clearLayers();
markersById = {}; markersById = {};
@@ -247,10 +248,12 @@ window.ExportIntelMap = (function () {
invalidateMapSize(); invalidateMapSize();
if (features.length === 1) { if (opts.autoFit === false) {
map.setView([20, 10], 2);
} else if (features.length === 1) {
var c = features[0].geometry.coordinates; var c = features[0].geometry.coordinates;
map.setView([c[1], c[0]], 10); map.setView([c[1], c[0]], 10);
} else if (markers.length > 1 && clusterLayer.getBounds) { } else if (opts.autoFit && markers.length > 1 && clusterLayer.getBounds) {
try { try {
map.fitBounds(clusterLayer.getBounds(), { padding: [40, 40], maxZoom: 12 }); map.fitBounds(clusterLayer.getBounds(), { padding: [40, 40], maxZoom: 12 });
} catch (e) { /* empty */ } } catch (e) { /* empty */ }
+70 -2
View File
@@ -42,6 +42,8 @@ function exportIntelApp() {
crmModalOpen: false, crmModalOpen: false,
crmCreateDeals: true, crmCreateDeals: true,
crmPushBusy: false, crmPushBusy: false,
editMode: false,
editForm: {},
resultCount: 0, resultCount: 0,
_searchTimer: null, _searchTimer: null,
@@ -193,7 +195,19 @@ function exportIntelApp() {
}, },
showSelectionBar() { showSelectionBar() {
return this.tab === 'map' || this.tab === 'contacts'; 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…';
}, },
entityTypeOptions() { entityTypeOptions() {
@@ -343,7 +357,8 @@ function exportIntelApp() {
window.ExportIntelMap.init('ei-map'); window.ExportIntelMap.init('ei-map');
window.ExportIntelMap.setOnEntityClick(function (id) { self.openEntity(id); }); window.ExportIntelMap.setOnEntityClick(function (id) { self.openEntity(id); });
window.ExportIntelMap.setHalalMode(this.mapHalalMode); window.ExportIntelMap.setHalalMode(this.mapHalalMode);
window.ExportIntelMap.setFeatures(bundle.entities || { features: [] }); var autoFit = !!(this.country || this.region);
window.ExportIntelMap.setFeatures(bundle.entities || { features: [] }, { autoFit: autoFit });
if (this.country) { if (this.country) {
var t = this.territories.find((x) => x.country_iso2 === 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); if (t && t.lat && t.lon) window.ExportIntelMap.flyTo(t.lat, t.lon, t.map_zoom || 6);
@@ -456,6 +471,59 @@ function exportIntelApp() {
this.drawerOpen = false; this.drawerOpen = false;
this.selected = null; this.selected = null;
this.drawerLoading = false; 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) { async syncKind(kind) {
+85 -8
View File
@@ -4,14 +4,21 @@
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css?v=1" /> <link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css?v=1" />
<link rel="stylesheet" href="/static/vendor/leaflet.markercluster/MarkerCluster.css?v=1" /> <link rel="stylesheet" href="/static/vendor/leaflet.markercluster/MarkerCluster.css?v=1" />
<link rel="stylesheet" href="/static/vendor/leaflet.markercluster/MarkerCluster.Default.css?v=1" /> <link rel="stylesheet" href="/static/vendor/leaflet.markercluster/MarkerCluster.Default.css?v=1" />
<link rel="stylesheet" href="/static/css/export-intel.css?v=14" /> <link rel="stylesheet" href="/static/css/export-intel.css?v=15" />
<link rel="stylesheet" href="/static/css/export-intel-tabs.css?v=10" /> <link rel="stylesheet" href="/static/css/export-intel-tabs.css?v=10" />
<script src="/static/vendor/leaflet/leaflet.js?v=1"></script> <script src="/static/vendor/leaflet/leaflet.js?v=1"></script>
<script src="/static/vendor/leaflet.markercluster/leaflet.markercluster.js?v=1"></script> <script src="/static/vendor/leaflet.markercluster/leaflet.markercluster.js?v=1"></script>
<script src="/static/js/export-intel-map.js?v=10"></script> <script src="/static/js/export-intel-map.js?v=11"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="ei-page" x-data="exportIntelApp()" x-init="init()"> <div class="ei-page" x-data="exportIntelApp()" x-init="init()">
<div class="ei-progress-overlay" x-show="isWorking()" x-cloak>
<div class="ei-progress-card">
<div class="ei-progress-spinner"></div>
<p x-text="progressLabel()"></p>
<div class="ei-progress-bar"><div class="ei-progress-bar-fill"></div></div>
</div>
</div>
<header class="ei-header page-header"> <header class="ei-header page-header">
<div> <div>
<h1>Wereldexport <span class="live-badge">Live</span></h1> <h1>Wereldexport <span class="live-badge">Live</span></h1>
@@ -235,9 +242,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="ei-map-error" x-show="mapError" x-cloak x-text="mapError"></div>
<p class="ei-map-truncated" x-show="mapTruncated && !mapLoading" x-cloak>Wereldoverzicht toont de belangrijkste ~2.500 locaties. Filter op regio of land voor meer detail.</p>
<div class="ei-split"> <div class="ei-split">
<div class="ei-map-error" x-show="mapError" x-cloak x-text="mapError"></div>
<p class="ei-map-truncated" x-show="mapTruncated && !mapLoading" x-cloak>Wereldoverzicht toont de belangrijkste ~2.500 locaties. Filter op regio of land voor meer detail.</p>
<div class="ei-map-wrap"><div class="ei-map-loading" x-show="mapLoading" x-cloak><span class="ei-map-spinner"></span> Kaart laden…</div><div id="ei-map"></div></div> <div class="ei-map-wrap"><div class="ei-map-loading" x-show="mapLoading" x-cloak><span class="ei-map-spinner"></span> Kaart laden…</div><div id="ei-map"></div></div>
<div class="ei-panel"> <div class="ei-panel">
<h3 style="margin:0 0 0.5rem;font-size:0.9rem" x-text="mapHalalMode ? 'Top halal-kansen' : 'Entities op kaart'"></h3> <h3 style="margin:0 0 0.5rem;font-size:0.9rem" x-text="mapHalalMode ? 'Top halal-kansen' : 'Entities op kaart'"></h3>
@@ -292,6 +299,8 @@
</template> </template>
<div class="ei-list-focus-actions"> <div class="ei-list-focus-actions">
<button type="button" class="btn btn-sm btn-pulse" @click="openEntity(listFocusDetail.id)">Volledig profiel →</button> <button type="button" class="btn btn-sm btn-pulse" @click="openEntity(listFocusDetail.id)">Volledig profiel →</button>
<button type="button" class="btn btn-sm btn-pulse-green" x-show="!listFocusDetail.client_id" @click="pushEntityToCrm(listFocusDetail.id)" :disabled="crmPushBusy"> CRM</button>
<a class="btn btn-sm btn-pulse" x-show="listFocusDetail.client_id" href="/clients">Open CRM</a>
<a class="btn btn-sm" x-show="listFocusDetail.maps_url" :href="listFocusDetail.maps_url" target="_blank" rel="noopener">Google Maps</a> <a class="btn btn-sm" x-show="listFocusDetail.maps_url" :href="listFocusDetail.maps_url" target="_blank" rel="noopener">Google Maps</a>
</div> </div>
</div> </div>
@@ -321,6 +330,7 @@
<th>Contact</th> <th>Contact</th>
<th x-show="mapHalalMode">Score</th> <th x-show="mapHalalMode">Score</th>
<th>Land</th> <th>Land</th>
<th class="ei-th-actions">Acties</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -345,6 +355,11 @@
<td class="ei-contact-cell" x-text="contactPreview(e)"></td> <td class="ei-contact-cell" x-text="contactPreview(e)"></td>
<td x-show="mapHalalMode"><span class="ei-halal-pill" :class="'ei-halal-'+(e.halal_score>=75?'hot':e.halal_score>=55?'warm':'mild')" x-text="Math.round(e.halal_score||0)"></span></td> <td x-show="mapHalalMode"><span class="ei-halal-pill" :class="'ei-halal-'+(e.halal_score>=75?'hot':e.halal_score>=55?'warm':'mild')" x-text="Math.round(e.halal_score||0)"></span></td>
<td x-text="e.country_iso2"></td> <td x-text="e.country_iso2"></td>
<td class="ei-td-actions" @click.stop>
<button type="button" class="btn btn-sm btn-pulse-green" x-show="!e.client_id" @click="pushEntityToCrm(e.id)" :disabled="crmPushBusy">CRM</button>
<a class="btn btn-sm" x-show="e.client_id" href="/clients">CRM ✓</a>
<button type="button" class="btn btn-sm" @click="openEntity(e.id)">Edit</button>
</td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
@@ -372,16 +387,30 @@
<div class="ei-panel" style="border:none;background:transparent;padding:0"> <div class="ei-panel" style="border:none;background:transparent;padding:0">
<div class="ei-table-wrap"> <div class="ei-table-wrap">
<table class="ei-table"> <table class="ei-table">
<thead><tr><th>Naam</th><th>Land</th><th>Stad</th><th>E-mail</th><th>Telefoon</th><th>Confidence</th></tr></thead> <thead><tr>
<th class="ei-th-check"><input type="checkbox" @change="toggleSelectAllVisible()" title="Alles" /></th>
<th>Naam</th><th>Land</th><th>Stad</th><th>E-mail</th><th>Telefoon</th><th>Confidence</th><th class="ei-th-actions">Acties</th>
</tr></thead>
<tbody> <tbody>
<template x-for="e in entities" :key="e.id"> <template x-for="e in entities" :key="e.id">
<tr @click="openEntity(e.id)" style="cursor:pointer"> <tr @click="openEntity(e.id)" :class="{ 'ei-row-selected': isSelected(e.id), 'ei-row-crm': e.client_id }" style="cursor:pointer">
<td x-text="e.name"></td> <td class="ei-td-check" @click.stop>
<input type="checkbox" :checked="isSelected(e.id)" @change="toggleSelect(e.id, $event)" />
</td>
<td>
<span x-text="e.name"></span>
<span class="ei-crm-badge" x-show="e.client_id">CRM</span>
</td>
<td x-text="e.country_iso2"></td> <td x-text="e.country_iso2"></td>
<td x-text="e.city||'—'"></td> <td x-text="e.city||'—'"></td>
<td x-text="e.primary_email||e.email||'—'"></td> <td x-text="e.primary_email||e.email||'—'"></td>
<td x-text="e.primary_phone||e.phone||'—'"></td> <td x-text="e.primary_phone||e.phone||'—'"></td>
<td class="ei-confidence" x-text="e.confidence+'%'"></td> <td class="ei-confidence" x-text="e.confidence+'%'"></td>
<td class="ei-td-actions" @click.stop>
<button type="button" class="btn btn-sm btn-pulse-green" x-show="!e.client_id" @click="pushEntityToCrm(e.id)" :disabled="crmPushBusy">CRM</button>
<a class="btn btn-sm" x-show="e.client_id" href="/clients">CRM ✓</a>
<button type="button" class="btn btn-sm" @click="openEntity(e.id)">Edit</button>
</td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
@@ -671,6 +700,7 @@
</div> </div>
<template x-if="selected && !drawerLoading"> <template x-if="selected && !drawerLoading">
<div class="ei-drawer-body"> <div class="ei-drawer-body">
<div x-show="!editMode">
<div class="ei-drawer-hero"> <div class="ei-drawer-hero">
<span class="ei-drawer-badge" :class="'ei-type-'+selected.entity_type" x-text="typeLabel(selected.entity_type)"></span> <span class="ei-drawer-badge" :class="'ei-type-'+selected.entity_type" x-text="typeLabel(selected.entity_type)"></span>
<span class="ei-drawer-flag" x-text="selected.country_iso2"></span> <span class="ei-drawer-flag" x-text="selected.country_iso2"></span>
@@ -760,7 +790,54 @@
<p x-show="!(selected.contacts||[]).length" class="ei-drawer-empty">Geen contactpersonen in registry.</p> <p x-show="!(selected.contacts||[]).length" class="ei-drawer-empty">Geen contactpersonen in registry.</p>
</section> </section>
</div>
<div class="ei-drawer-edit" x-show="editMode" x-cloak>
<h4>Bewerken</h4>
<div class="ei-edit-grid">
<label>Naam<input type="text" x-model="editForm.name" /></label>
<label>Type
<select x-model="editForm.entity_type">
<option value="distributor">Distributeur</option>
<option value="wholesaler">Groothandel</option>
<option value="importer">Importeur</option>
<option value="logistics">Logistiek</option>
<option value="contract_caterer">Cateraar</option>
<option value="restaurant">Restaurant</option>
<option value="doner_shoarma">Döner/shoarma</option>
<option value="butcher">Slager</option>
<option value="foodservice">Foodservice</option>
</select>
</label>
<label>Land (ISO2)<input type="text" maxlength="2" x-model="editForm.country_iso2" /></label>
<label>Stad<input type="text" x-model="editForm.city" /></label>
<label>Adres<input type="text" x-model="editForm.address_line" /></label>
<label>E-mail<input type="email" x-model="editForm.email" /></label>
<label>Telefoon<input type="text" x-model="editForm.phone" /></label>
<label>Website<input type="url" x-model="editForm.website" /></label>
<label>Pipeline
<select x-model="editForm.pipeline_stage">
<option value="identified">Geïdentificeerd</option>
<option value="qualified">Gekwalificeerd</option>
<option value="contacted">Gecontacteerd</option>
<option value="proposal">Voorstel</option>
<option value="won">Gewonnen</option>
<option value="lost">Verloren</option>
</select>
</label>
<label>Confidence %<input type="number" min="0" max="100" x-model.number="editForm.confidence" /></label>
<label>Volume<input type="text" x-model="editForm.volume_band" placeholder="pallet / container" /></label>
<label class="ei-edit-full">Halal notities<textarea rows="2" x-model="editForm.halal_cert_notes"></textarea></label>
<label class="ei-filter-check ei-edit-full"><input type="checkbox" x-model="editForm.cold_chain" /> Cold chain</label>
</div>
<div class="ei-edit-actions">
<button type="button" class="btn btn-sm btn-pulse-green" @click="saveEdit()" :disabled="busy">Opslaan</button>
<button type="button" class="btn btn-sm" @click="cancelEdit()">Annuleren</button>
</div>
</div>
<div class="ei-drawer-actions"> <div class="ei-drawer-actions">
<button type="button" class="btn btn-sm btn-pulse" x-show="!editMode" @click="startEdit()">✎ Bewerken</button>
<button type="button" class="btn btn-sm" :class="selected.is_favorite ? 'btn-pulse' : ''" @click="toggleFavorite(selected.id, selected.is_favorite)"> <button type="button" class="btn btn-sm" :class="selected.is_favorite ? 'btn-pulse' : ''" @click="toggleFavorite(selected.id, selected.is_favorite)">
<span x-text="selected.is_favorite ? '★ Favoriet' : '☆ Favoriet maken'"></span> <span x-text="selected.is_favorite ? '★ Favoriet' : '☆ Favoriet maken'"></span>
</button> </button>
@@ -798,5 +875,5 @@
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/export-intel.js?v=13"></script> <script src="/static/js/export-intel.js?v=14"></script>
{% endblock %} {% endblock %}
+45 -1
View File
@@ -10,7 +10,7 @@ from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel, Field
from app.db import execute, execute_returning, fetch_all, fetch_one from app.db import execute, execute_returning, fetch_all, fetch_one
from app.export_halal_scoring import COUNTRY_HALAL_MARKET, halal_pin_tier, score_entity from app.export_halal_scoring import COUNTRY_HALAL_MARKET, halal_pin_tier, score_entity
@@ -139,6 +139,24 @@ class EntityIdsIn(BaseModel):
entity_ids: list[int] entity_ids: list[int]
class EntityUpdateIn(BaseModel):
name: Optional[str] = None
entity_type: Optional[str] = None
country_iso2: Optional[str] = None
city: Optional[str] = None
address_line: Optional[str] = None
postal_code: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
volume_band: Optional[str] = None
pipeline_stage: Optional[str] = None
confidence: Optional[int] = Field(None, ge=0, le=100)
halal_cert_notes: Optional[str] = None
cold_chain: Optional[bool] = None
class FavoritesIn(BaseModel): class FavoritesIn(BaseModel):
entity_ids: list[int] entity_ids: list[int]
favorite: bool = True favorite: bool = True
@@ -428,6 +446,30 @@ def export_contacts_csv(
) )
@router.patch("/entities/{entity_id}")
def update_entity(entity_id: int, payload: EntityUpdateIn) -> dict[str, Any]:
row = fetch_one("SELECT id FROM export_market_entities WHERE id = %s", (entity_id,))
if not row:
raise HTTPException(status_code=404, detail="Entity not found")
data = payload.model_dump(exclude_unset=True)
if not data:
raise HTTPException(status_code=400, detail="Geen velden om bij te werken")
if "country_iso2" in data and data["country_iso2"]:
data["country_iso2"] = data["country_iso2"].upper()[:2]
sets = [f"{k} = %s" for k in data]
sets.append("updated_at = NOW()")
params = list(data.values()) + [entity_id]
execute(
f"UPDATE export_market_entities SET {', '.join(sets)} WHERE id = %s",
tuple(params),
)
_MAP_CACHE.clear()
return get_entity(entity_id)
@router.post("/entities/{entity_id}/contacts") @router.post("/entities/{entity_id}/contacts")
def add_contact(entity_id: int, payload: ContactCreateIn) -> dict[str, Any]: def add_contact(entity_id: int, payload: ContactCreateIn) -> dict[str, Any]:
ent = fetch_one("SELECT id FROM export_market_entities WHERE id = %s", (entity_id,)) ent = fetch_one("SELECT id FROM export_market_entities WHERE id = %s", (entity_id,))
@@ -691,6 +733,8 @@ def map_bundle(
"email": row.get("email") or row.get("contact_email"), "email": row.get("email") or row.get("contact_email"),
"phone": row.get("phone") or row.get("contact_phone"), "phone": row.get("phone") or row.get("contact_phone"),
"halal_score": h_score, "halal_score": h_score,
"client_id": row.get("client_id"),
"is_favorite": bool(row.get("is_favorite")),
"has_contact": bool( "has_contact": bool(
row.get("email") or row.get("contact_email") row.get("email") or row.get("contact_email")
or row.get("phone") or row.get("contact_phone") or row.get("phone") or row.get("contact_phone")