SysOps: manual — 2026-07-30 09:33 UTC

This commit is contained in:
sysops
2026-07-30 09:33:03 +00:00
parent f21d58483b
commit 5dd69a1138
25 changed files with 4148 additions and 734 deletions
+31 -10
View File
@@ -1,24 +1,36 @@
/* Sidebar — clean, uniform nav (loads last, wins over topnav-neo) */
.app-shell {
display: grid;
grid-template-columns: 220px 1fr;
display: block;
min-height: 100vh;
}
.app-sidebar {
position: sticky;
position: fixed;
top: 0;
align-self: start;
height: auto;
max-height: none;
overflow: visible;
overflow-x: hidden;
padding: 0.75rem 0.6rem;
left: 0;
bottom: 0;
width: 220px;
z-index: 200;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0.75rem 0.6rem 0;
background: #080c12;
border-right: 1px solid rgba(148, 163, 184, 0.1);
}
.app-main {
margin-left: 220px;
min-width: 0;
min-height: 100vh;
}
.app-sidebar-brand,
.persona-switch.brand-switch {
flex-shrink: 0;
}
.app-sidebar-brand {
padding: 0.25rem 0.5rem 0.75rem;
margin-bottom: 0.5rem;
@@ -59,7 +71,16 @@
.brand-btn-cucina.active { color: #fdba74; border-color: rgba(251, 146, 60, 0.4); background: rgba(251, 146, 60, 0.1); }
.brand-btn-foodlinkk.active { color: #7dd3fc; border-color: rgba(14, 165, 233, 0.4); background: rgba(14, 165, 233, 0.1); }
.persona-nav { display: contents; }
.persona-nav {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
padding-bottom: 0.75rem;
scrollbar-gutter: stable;
}
.app-sidebar-group {
margin-bottom: 0.65rem;
+135 -8
View File
@@ -1,19 +1,146 @@
/* Wereldexport — KPIs + layout lock */
/* Wereldexport — full-width map workspace */
.wereldexport-workspace.vtabs-layout {
display: flex !important;
flex-direction: row !important;
align-items: flex-start !important;
gap: 1rem !important;
flex-direction: column !important;
align-items: stretch !important;
gap: 0.65rem !important;
width: 100%;
min-height: calc(100dvh - 220px);
}
.wereldexport-workspace > .vtabs-nav {
flex: 0 0 220px !important;
width: 220px !important;
max-width: 220px !important;
flex: 0 0 auto !important;
width: 100% !important;
max-width: none !important;
position: sticky;
top: 0;
z-index: 60;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 0.35rem;
overflow-x: auto;
overflow-y: hidden;
padding: 0.5rem 0.65rem;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.wereldexport-workspace > .vtabs-nav::before { display: none; }
.wereldexport-workspace .vtabs-nav-header { display: none; }
.wereldexport-workspace > .vtabs-content {
flex: 1 1 0 !important;
flex: 1 1 auto !important;
min-width: 0 !important;
min-height: 0 !important;
display: flex;
flex-direction: column;
}
.wereldexport-workspace .vtab-btn.vtab-btn-rich {
flex: 0 0 auto;
min-width: 128px;
max-width: 168px;
grid-template-columns: auto 1fr;
padding: 0.45rem 0.55rem;
}
.wereldexport-workspace .ei-vtab-meta,
.wereldexport-workspace .ei-vtab-key,
.wereldexport-workspace .ei-vtab-sub { display: none; }
.wereldexport-workspace .vtab-btn.active {
box-shadow: inset 0 -2px 0 var(--tab-accent, #0ea5e9);
}
.ei-tab-panel:has(.ei-workspace-body) {
background: rgba(8, 11, 16, 0.95);
flex: 1;
display: flex;
flex-direction: column;
min-height: calc(100dvh - 280px);
border-radius: 12px;
overflow: hidden;
}
.ei-tab-panel:has(.ei-workspace-body-map) .ei-tab-panel-head {
display: none;
}
.ei-workspace-body,
.ei-workspace-body-map {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 0 !important;
}
.ei-workspace-body-map .ei-map-legend {
flex-shrink: 0;
margin: 0;
padding: 0.45rem 0.75rem;
font-size: 0.68rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.08);
}
.ei-workspace-body-map .ei-map-empty {
flex-shrink: 0;
margin: 0;
padding: 0 0.75rem 0.45rem;
}
.ei-workspace-body-map .ei-map-empty-inner {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.75rem;
padding: 0.55rem 0.75rem;
}
.ei-workspace-body-map .ei-map-empty-inner strong {
display: inline;
margin: 0;
font-size: 0.78rem;
}
.ei-workspace-body-map .ei-map-empty-inner p {
flex: 1 1 200px;
margin: 0;
font-size: 0.72rem;
}
.ei-workspace-body-map .ei-map-empty-actions {
margin: 0;
flex-shrink: 0;
}
.ei-workspace-body-map .ei-map-truncated,
.ei-workspace-body-map .ei-map-error {
flex-shrink: 0;
margin: 0 0.75rem 0.35rem;
}
.ei-workspace-body-map > .ei-split.ei-split-side {
flex: 1 1 auto;
min-height: 420px;
}
@media (min-width: 900px) {
.ei-workspace-body-map > .ei-split.ei-split-side {
min-height: calc(100dvh - 320px);
}
}
@media (max-width: 899px) {
.wereldexport-workspace > .vtabs-nav {
flex-wrap: nowrap;
}
.wereldexport-workspace .vtab-btn.vtab-btn-rich {
min-width: 110px;
}
}
.ei-neo-kpi-row {
+676 -32
View File
@@ -13,33 +13,102 @@
.ei-filter-bar {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0.65rem 1rem;
flex-direction: column;
gap: 0.65rem;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: linear-gradient(135deg, rgba(14,165,233,0.08), rgba(15,20,25,0.95));
border: 1px solid rgba(148,163,184,0.15);
border-radius: 12px;
}
.ei-filter-group { display: flex; flex-wrap: wrap; gap: 0.5rem 0.75rem; align-items: flex-end; }
.ei-filter-group-search { flex: 1 1 200px; min-width: 180px; }
.ei-filter-label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: #94a3b8; font-weight: 600; }
.ei-filter-grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 0.55rem 0.65rem;
align-items: end;
}
.ei-filter-cell { min-width: 0; }
.ei-filter-cell-search { grid-column: span 3; }
.ei-filter-cell-check {
grid-column: span 2;
align-self: end;
padding-bottom: 0.45rem;
}
.ei-filter-cell-meta {
grid-column: span 3;
justify-self: end;
}
.ei-filter-label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #94a3b8;
font-weight: 600;
}
.ei-filter-label select,
.ei-filter-label input {
.ei-filter-label input[type="search"],
.ei-filter-label input[type="text"] {
width: 100%;
box-sizing: border-box;
font-size: 0.82rem;
padding: 0.45rem 0.65rem;
border-radius: 8px;
border: 1px solid rgba(148,163,184,0.22);
background: #0a0e12;
color: #e2e8f0;
min-width: 8rem;
}
.ei-filter-search-wrap input { width: 100%; min-width: 0; }
.ei-filter-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
.ei-filter-meta { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.ei-filter-meta {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.5rem;
padding-bottom: 0.35rem;
}
.ei-filter-count { font-size: 0.75rem; color: #64748b; white-space: nowrap; }
.ei-filter-clear { font-size: 0.72rem; opacity: 0.85; }
.ei-filter-map-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.65rem 0.85rem;
padding-top: 0.15rem;
border-top: 1px solid rgba(148,163,184,0.1);
}
.ei-filter-map-row-halal { border-top: none; padding-top: 0; }
.ei-filter-map-label { margin: 0; white-space: nowrap; }
@media (min-width: 1100px) {
.ei-filter-grid {
grid-template-areas: "regio land zoek type contact conf stad crm fav meta meta meta";
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.ei-fa-regio { grid-area: regio; }
.ei-fa-land { grid-area: land; }
.ei-fa-zoek { grid-area: zoek; }
.ei-fa-type { grid-area: type; }
.ei-fa-contact { grid-area: contact; }
.ei-fa-conf { grid-area: conf; }
.ei-fa-stad { grid-area: stad; }
.ei-fa-extra { grid-area: stad; }
.ei-fa-crm { grid-area: crm; }
.ei-fa-fav { grid-area: fav; }
.ei-fa-meta { grid-area: meta; }
.ei-filter-cell-search,
.ei-filter-cell-check,
.ei-filter-cell-meta { grid-column: auto; }
}
@media (max-width: 1099px) {
.ei-filter-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.ei-filter-cell-search { grid-column: span 2; }
.ei-filter-cell-meta { grid-column: 1 / -1; justify-self: end; }
}
@media (max-width: 640px) {
.ei-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ei-filter-cell-search { grid-column: 1 / -1; }
}
.ei-split { align-items: stretch; }
.ei-split > .ei-map-wrap { width: 100%; }
@@ -93,21 +162,106 @@
.ei-split {
display: flex;
flex-direction: column;
gap: 1rem;
gap: 0;
min-height: 0;
}
@media (min-width: 1200px) {
@media (min-width: 900px) {
.ei-split.ei-split-side {
display: grid;
grid-template-columns: minmax(0, 1.65fr) minmax(320px, 1fr);
grid-template-rows: minmax(480px, 62vh);
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 400px);
grid-template-rows: minmax(0, 1fr);
align-items: stretch;
height: 100%;
min-height: 420px;
}
.ei-split.ei-split-side > .ei-map-wrap {
grid-column: 1;
grid-row: 1;
height: 100%;
min-height: 0;
}
.ei-split.ei-split-side > .ei-side-panel {
grid-column: 2;
grid-row: 1;
height: 100%;
min-height: 0;
}
.ei-split.ei-split-side > .ei-map-wrap { grid-column: 1; grid-row: 1; height: 100%; min-height: 480px; }
.ei-split.ei-split-side > .ei-panel { grid-column: 2; grid-row: 1; max-height: 62vh; overflow: auto; }
}
.ei-map-wrap { position: relative; }
.ei-workspace-body {
padding: 0 !important;
}
.ei-workspace-body-map {
padding: 0 1rem 1rem !important;
}
.ei-workspace-body-map .ei-map-legend,
.ei-workspace-body-map .ei-map-empty,
.ei-workspace-body-map .ei-map-error,
.ei-workspace-body-map .ei-map-truncated {
margin-left: 0;
margin-right: 0;
}
.ei-workspace-body > .ei-split {
border-radius: 0 0 12px 12px;
overflow: hidden;
}
.ei-side-panel {
display: flex;
flex-direction: column;
min-height: 0;
background: linear-gradient(180deg, rgba(12, 16, 22, 0.98) 0%, rgba(8, 11, 16, 0.98) 100%);
border-left: 1px solid rgba(148, 163, 184, 0.1);
}
.ei-side-panel-head {
flex-shrink: 0;
padding: 0.85rem 1rem 0.75rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
background: rgba(15, 23, 42, 0.35);
}
.ei-side-panel-head h3 {
margin: 0;
font-size: 0.92rem;
font-weight: 600;
color: #f1f5f9;
}
.ei-side-panel-head p {
margin: 0.25rem 0 0;
font-size: 0.78rem;
color: #64748b;
line-height: 1.35;
}
.ei-side-panel-body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 0;
}
.ei-side-section {
padding: 0.65rem 0.75rem 0.85rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.08);
}
.ei-side-section h4 {
margin: 0 0 0.5rem;
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #64748b;
font-weight: 600;
}
.ei-side-hint {
margin: 0;
padding: 0.65rem 0.85rem;
font-size: 0.78rem;
color: #64748b;
border-bottom: 1px solid rgba(148, 163, 184, 0.08);
background: rgba(14, 165, 233, 0.04);
}
.ei-action-tile[disabled], .ei-action-tile.ei-action-busy { opacity: 0.55; pointer-events: none; }
.ei-map-loading {
position: absolute; inset: 0; z-index: 500;
display: flex; align-items: center; justify-content: center; gap: 0.65rem;
@@ -125,17 +279,37 @@
.ei-map-error { margin: 0 0 0.65rem; padding: 0.65rem 0.85rem; border-radius: 10px; border: 1px solid rgba(251,113,133,0.35); background: rgba(127,29,29,0.25); color: #fecdd3; font-size: 0.82rem; }
.ei-map-wrap {
border-radius: 14px;
overflow: hidden;
border: 1px solid rgba(14,165,233,0.2);
background: #070b10;
}
.ei-workspace-body .ei-map-wrap {
border: none;
border-radius: 0;
}
.ei-workspace-body-map .ei-split-side > .ei-map-wrap {
border: 1px solid rgba(14, 165, 233, 0.18);
border-radius: 12px;
}
.ei-split:not(.ei-split-side) > .ei-map-wrap {
height: min(62vh, 720px);
min-height: 480px;
background: #0a0e14;
border-radius: 12px;
border: 1px solid rgba(14, 165, 233, 0.18);
}
.ei-map-wrap #ei-map {
.ei-map-wrap #ei-map,
.ei-map-wrap [id^="ei-map-"] {
height: 100%;
min-height: 480px;
width: 100%;
min-height: 280px;
}
@media (max-width: 899px) {
.ei-split > .ei-map-wrap {
height: min(48vh, 440px);
min-height: 280px;
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
}
.ei-side-panel { border-left: none; border-top: 1px solid rgba(148, 163, 184, 0.1); }
.ei-side-panel-body { max-height: 50vh; }
}
/* Leaflet popups — rich card */
@@ -204,7 +378,6 @@
box-shadow: 0 0 0 1px rgba(14,165,233,0.2);
}
.ei-filter-group-maptypes { flex: 1 1 100%; max-width: 100%; }
.ei-type-chips {
display: flex;
flex-wrap: wrap;
@@ -228,12 +401,6 @@
background: rgba(34,197,94,0.12);
}
.ei-filter-group-halal {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
.ei-filter-check {
display: inline-flex;
align-items: center;
@@ -354,10 +521,10 @@
.ei-filter-fav { color: #facc15 !important; margin-left: 0.35rem; }
.ei-pipeline-hint { font-size: 0.82rem; color: #94a3b8; margin: 0 0 1rem; }
.ei-modal-root { position: fixed; inset: 0; z-index: 1300; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.ei-modal-root { position: fixed; inset: 0; z-index: 10050; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.ei-modal-card {
position: relative;
z-index: 1302;
z-index: 10051;
width: min(420px, 96vw);
padding: 1.25rem;
border-radius: 14px;
@@ -439,7 +606,7 @@
.ei-drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 1200; }
.ei-drawer-root { position: relative; z-index: 1201; }
.ei-drawer { position: fixed; top: 0; right: 0; width: min(480px, 100vw); height: 100vh; background: #0f1419; border-left: 1px solid rgba(14,165,233,0.2); z-index: 1202; overflow-y: auto; box-shadow: -8px 0 32px rgba(0,0,0,0.45); }
.ei-drawer-rich { width: min(480px, 100vw); padding: 0; display: flex; flex-direction: column; }
.ei-drawer-rich { width: min(640px, 94vw); padding: 0; display: flex; flex-direction: column; }
.ei-drawer-head { padding: 0.75rem 1rem; border-bottom: 1px solid rgba(148,163,184,0.12); }
.ei-drawer-body { padding: 1rem; overflow-y: auto; flex: 1; }
.ei-drawer-hero { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.35rem; }
@@ -464,3 +631,480 @@
.ei-link-btn:hover { text-decoration: underline; }
.ei-td-actions { white-space: nowrap; }
.ei-load-more { display: flex; justify-content: center; padding: 1rem 0 0.5rem; }
/* Entity rows — click to open full profile */
.ei-th-open, .ei-td-open { width: 88px; text-align: right; }
.ei-open-chevron {
color: #38bdf8;
font-size: 0.8125rem;
font-weight: 600;
opacity: 0.75;
transition: opacity 0.15s, transform 0.15s;
}
.ei-table tbody tr:hover .ei-open-chevron { opacity: 1; transform: translateX(2px); }
.ei-table tbody tr { transition: background 0.12s; }
.ei-table tbody tr:hover { background: rgba(14, 165, 233, 0.06) !important; }
.ei-row-active { box-shadow: inset 3px 0 0 #0ea5e9; background: rgba(14, 165, 233, 0.1) !important; }
/* Full profile drawer */
.ei-drawer-rich {
width: min(640px, 94vw) !important;
display: flex;
flex-direction: column;
padding: 0;
}
.ei-drawer-head-rich {
padding: 1.1rem 1.25rem 0.85rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
background: linear-gradient(180deg, rgba(14, 165, 233, 0.08) 0%, transparent 100%);
flex-shrink: 0;
}
.ei-drawer-head-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.75rem; }
.ei-drawer-close {
border: 1px solid rgba(148, 163, 184, 0.2);
background: rgba(15, 23, 42, 0.8);
color: #94a3b8;
border-radius: 8px;
width: 36px;
height: 36px;
cursor: pointer;
font-size: 1rem;
flex-shrink: 0;
}
.ei-drawer-close:hover { color: #fff; border-color: rgba(56, 189, 248, 0.4); }
.ei-drawer-loading {
display: flex; align-items: center; justify-content: center; gap: 0.65rem;
padding: 3rem 1rem; color: #94a3b8;
}
.ei-drawer-toolbar {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 0.55rem;
padding: 0.85rem 1.25rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
background: rgba(8, 12, 18, 0.5);
flex-shrink: 0;
}
.ei-action-tile {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.1rem;
padding: 0.65rem 0.75rem;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.15);
background: rgba(15, 23, 42, 0.7);
color: #e2e8f0;
text-decoration: none;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, transform 0.12s;
min-height: 72px;
}
.ei-action-tile:hover { border-color: rgba(56, 189, 248, 0.35); background: rgba(14, 165, 233, 0.08); transform: translateY(-1px); }
.ei-action-tile:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.ei-action-icon { font-size: 1.1rem; line-height: 1; }
.ei-action-label { font-size: 0.875rem; font-weight: 600; color: #f1f5f9; }
.ei-action-sub { font-size: 0.75rem; color: #64748b; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; max-width: 100%; white-space: nowrap; }
.ei-action-crm { border-color: rgba(74, 222, 128, 0.35); background: rgba(74, 222, 128, 0.08); }
.ei-action-crm:hover { border-color: rgba(74, 222, 128, 0.55); background: rgba(74, 222, 128, 0.12); }
.ei-action-crm-done { border-color: rgba(74, 222, 128, 0.4); background: rgba(74, 222, 128, 0.1); }
.ei-action-edit { border-color: rgba(56, 189, 248, 0.3); }
.ei-action-fav-on { border-color: rgba(251, 191, 36, 0.4); background: rgba(251, 191, 36, 0.08); }
.ei-drawer-body-rich { padding: 1rem 1.25rem 1.5rem; }
.ei-drawer-section {
background: rgba(10, 14, 20, 0.6);
border: 1px solid rgba(148, 163, 184, 0.1);
border-radius: 12px;
padding: 0.85rem 1rem;
margin-bottom: 0.75rem;
}
.ei-drawer-section h4 { font-size: 0.8125rem !important; color: #94a3b8 !important; margin-bottom: 0.65rem !important; }
.ei-drawer-edit {
margin: 0 1.25rem 1rem;
border-radius: 12px;
}
.ei-drawer-title { font-size: 1.35rem !important; margin-top: 0.5rem; }
.ei-drawer-meta { font-size: 0.875rem !important; margin-bottom: 0 !important; }
.ei-drawer-kv { font-size: 0.9375rem !important; }
/* Profile modal — visibility via .is-open (not Alpine x-show) */
#ei-profile-modal { display: none !important; }
#ei-profile-modal.is-open {
display: flex !important;
z-index: 99999 !important;
}
/* Profiel popup modal */
body.ei-profile-open { overflow: hidden; }
.ei-profile-modal-root {
position: fixed;
inset: 0;
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.ei-profile-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.68);
backdrop-filter: blur(4px);
animation: ei-fade-in 0.18s ease-out;
}
.ei-profile-panel {
position: relative;
z-index: 1;
width: min(720px, 96vw);
max-height: min(90vh, 920px);
display: flex;
flex-direction: column;
border-radius: 16px;
border: 1px solid rgba(14, 165, 233, 0.28);
background: linear-gradient(180deg, #121820 0%, #0c1016 100%);
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(56, 189, 248, 0.08);
overflow: hidden;
animation: ei-modal-pop 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes ei-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ei-modal-pop {
from { opacity: 0; transform: translateY(12px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.ei-profile-inner { display: flex; flex-direction: column; min-height: 0; flex: 1; overflow: hidden; }
.ei-drawer-head-rich { flex-shrink: 0; }
.ei-drawer-toolbar { flex-shrink: 0; max-height: 38vh; overflow-y: auto; }
.ei-drawer-body-rich { flex: 1; overflow-y: auto; }
@media (max-width: 640px) {
.ei-profile-panel { width: 100%; max-height: 96vh; border-radius: 14px 14px 0 0; }
.ei-profile-modal-root { padding: 0; align-items: flex-end; }
}
.ei-row-open-btn {
background: none; border: none; padding: 0; margin: 0;
color: #f1f5f9; font: inherit; font-weight: 600; text-align: left;
cursor: pointer; max-width: 100%;
}
.ei-row-open-btn:hover { color: #38bdf8; text-decoration: underline; }
#ei-profile-mount { display: flex; flex-direction: column; min-height: 0; flex: 1; overflow: hidden; max-height: min(90vh, 920px); }
#ei-profile-mount .ei-drawer-body-rich { flex: 1; overflow-y: auto; }
.ei-filter-bar {
position: sticky;
top: 0;
z-index: 200;
backdrop-filter: blur(8px);
background: rgba(8, 12, 18, 0.92);
}
.ei-tab-filters {
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
background: rgba(10, 14, 20, 0.6);
}
.ei-tab-filters-row {
display: flex;
flex-wrap: wrap;
gap: 0.65rem 0.85rem;
align-items: flex-end;
}
.ei-tab-filter-search { flex: 1 1 180px; min-width: 160px; }
.ei-tab-filter-search input { width: 100%; }
.ei-tab-filter-count {
margin-left: auto;
font-size: 0.8125rem;
color: #64748b;
white-space: nowrap;
padding-bottom: 0.35rem;
}
@media (max-width: 768px) {
.ei-tab-filters-row { flex-direction: column; align-items: stretch; }
.ei-tab-filter-count { margin-left: 0; }
}
/* ── Side panel entity list — readable, scrolls inside panel ── */
.ei-side-panel-body {
display: flex;
flex-direction: column;
overflow: hidden;
}
.ei-side-panel .ei-table-wrap {
flex: 1 1 auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.ei-table-compact {
table-layout: fixed;
width: 100%;
min-width: 0;
border-collapse: collapse;
}
.ei-table-compact th,
.ei-table-compact td {
vertical-align: middle;
padding: 0.38rem 0.4rem;
line-height: 1.35;
}
.ei-table-compact thead th {
position: sticky;
top: 0;
z-index: 3;
background: #0f1419;
font-size: 0.65rem;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.12);
color: #64748b;
}
.ei-table-compact tbody tr {
height: auto;
min-height: 2.35rem;
}
.ei-table-compact .ei-th-check,
.ei-table-compact .ei-td-check {
width: 28px;
padding: 0.2rem 0.15rem;
text-align: center;
}
.ei-table-compact .ei-th-fav,
.ei-table-compact .ei-td-fav {
width: 30px;
padding: 0.2rem 0.15rem;
text-align: center;
}
.ei-table-compact .ei-td-check input[type="checkbox"] { margin: 0; }
.ei-table-compact .ei-th-name,
.ei-table-compact .ei-td-name {
width: 46%;
min-width: 0;
overflow: hidden;
}
.ei-name-cell {
display: flex;
align-items: flex-start;
gap: 0.35rem;
min-width: 0;
}
.ei-table-compact .ei-th-type,
.ei-table-compact .ei-td-type {
width: 22%;
min-width: 72px;
text-align: left;
}
.ei-table-compact .ei-th-city,
.ei-table-compact .ei-td-city {
width: 32%;
min-width: 88px;
font-size: 0.72rem;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hide extra columns in side panel — details in profiel popup */
.ei-table-compact .ei-td-contact,
.ei-table-compact .ei-th-contact,
.ei-table-compact .ei-td-country,
.ei-table-compact .ei-th-country,
.ei-table-compact .ei-td-conf,
.ei-table-compact .ei-th-conf,
.ei-table-compact .ei-td-open,
.ei-table-compact .ei-th-open,
.ei-table-compact .ei-col-hide-side {
display: none !important;
}
.ei-table-compact .ei-td-score,
.ei-table-compact .ei-th-score {
width: 44px;
text-align: center;
font-size: 0.72rem;
}
.ei-table-compact .ei-type-pill,
.ei-table-compact .ei-type-pill-sm {
display: inline-block;
white-space: nowrap;
font-size: 0.62rem;
padding: 0.12rem 0.38rem;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.ei-table-compact .ei-row-open-btn {
flex: 1 1 auto;
min-width: 0;
text-align: left;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: normal;
line-height: 1.25;
font-size: 0.78rem;
font-weight: 600;
color: #f1f5f9;
}
.ei-table-compact .ei-crm-badge {
flex-shrink: 0;
margin: 0.1rem 0 0;
font-size: 0.55rem;
}
.ei-table-compact .ei-fav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.35rem;
height: 1.35rem;
font-size: 0.9rem;
}
.ei-table-compact tbody tr { cursor: pointer; }
.ei-table-compact tbody tr:hover .ei-row-open-btn { color: #38bdf8; }
.ei-table-compact .ei-td-pipe-meta,
.ei-table-compact .ei-th-pipe-meta { display: none; }
.ei-table-compact:not(.ei-table-contacts) .ei-td-actions,
.ei-table-compact:not(.ei-table-contacts) th:empty:last-child { display: none; }
.ei-table-compact .ei-td-actions {
width: 68px;
padding: 0.2rem;
vertical-align: middle;
}
.ei-table-compact .ei-td-actions .btn { font-size: 0.6rem; padding: 0.18rem 0.32rem; }
.ei-table-brand .ei-td-name { width: 42%; }
.ei-table-brand .ei-td-country { width: 48px; }
@media (min-width: 1400px) {
.ei-split.ei-split-side {
grid-template-columns: minmax(0, 1.15fr) minmax(440px, 560px);
}
.ei-table-compact.ei-table-pipeline .ei-td-pipe-meta,
.ei-table-compact.ei-table-pipeline .ei-th-pipe-meta {
display: table-cell;
font-size: 0.68rem;
}
}
@media (max-width: 899px) {
.ei-split.ei-split-side {
grid-template-columns: 1fr;
grid-template-rows: auto auto;
}
.ei-side-panel-body { max-height: 50vh; }
}
.ei-popup-web-list { display: flex; flex-direction: column; gap: 0.25rem; align-items: flex-start; }
.ei-popup-web {
display: inline-block;
max-width: 100%;
padding: 0.12rem 0.4rem;
border-radius: 6px;
font-size: 0.72rem;
font-weight: 600;
color: #7dd3fc !important;
background: rgba(14, 165, 233, 0.1);
border: 1px solid rgba(56, 189, 248, 0.22);
text-decoration: none !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ei-popup-web:hover { background: rgba(14, 165, 233, 0.18); color: #bae6fd !important; }
.ei-popup-websites .ei-popup-v { max-width: 200px; }
.ei-popup-card { max-height: 420px; overflow-y: auto; }
.ei-popup-meta { font-size: 0.72rem; color: #64748b; margin: 0.15rem 0 0.45rem; }
.ei-popup-reach { display: flex; flex-direction: column; gap: 0.45rem; margin: 0.35rem 0 0.5rem; }
.ei-popup-section { border-top: 1px solid rgba(148, 163, 184, 0.1); padding-top: 0.35rem; }
.ei-popup-section-title { font-size: 0.62rem; letter-spacing: 0.06em; text-transform: uppercase; color: #64748b; margin-bottom: 0.25rem; font-weight: 700; }
.ei-popup-channel-list, .ei-popup-web-list { display: flex; flex-direction: column; gap: 0.2rem; }
.ei-popup-link {
display: flex; flex-direction: column; gap: 0.05rem;
padding: 0.22rem 0.35rem; border-radius: 6px;
background: rgba(15, 23, 42, 0.55); border: 1px solid rgba(148, 163, 184, 0.12);
text-decoration: none !important; color: #e2e8f0 !important;
}
.ei-popup-link:hover { border-color: rgba(56, 189, 248, 0.35); background: rgba(14, 165, 233, 0.08); }
.ei-popup-link-label { font-size: 0.58rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.04em; }
.ei-popup-link-val { font-size: 0.72rem; color: #7dd3fc; word-break: break-all; line-height: 1.25; }
.ei-popup-plain { padding: 0.15rem 0.35rem; font-size: 0.72rem; color: #cbd5e1; }
.ei-popup-person { padding: 0.35rem 0.4rem; margin-top: 0.2rem; border-radius: 8px; background: rgba(8, 12, 18, 0.55); border: 1px solid rgba(148, 163, 184, 0.1); }
.ei-popup-person-head { font-size: 0.75rem; font-weight: 600; color: #f1f5f9; }
.ei-popup-person-sub { font-size: 0.65rem; color: #64748b; margin: 0.1rem 0 0.25rem; }
.ei-popup-person-links { display: flex; flex-direction: column; gap: 0.15rem; }
.ei-popup-primary { font-size: 0.55rem; color: #4ade80; border: 1px solid rgba(74, 222, 128, 0.35); padding: 0 0.25rem; border-radius: 4px; margin-left: 0.2rem; }
.ei-popup-maps { display: inline-block; font-size: 0.72rem; color: #7dd3fc !important; text-decoration: none !important; }
.ei-popup-empty-contact { font-size: 0.72rem; color: #64748b; padding: 0.35rem 0; font-style: italic; }
.ei-reach-block h5 { margin: 0.5rem 0 0.25rem; font-size: 0.68rem; letter-spacing: 0.05em; text-transform: uppercase; color: #64748b; }
.leaflet-popup-content { margin: 10px 12px !important; }
.leaflet-popup-content-wrapper { border-radius: 12px !important; }
.ei-chip-count { font-size: 0.62rem; opacity: 0.75; margin-left: 0.15rem; }
.ei-type-chip.active .ei-chip-count { opacity: 1; }
.ei-map-filter-hint { color: #fbbf24 !important; }
.ei-sync-monitor {
margin: 0.65rem 0 0.35rem;
border: 1px solid rgba(56, 189, 248, 0.25);
border-radius: 10px;
background: rgba(15, 23, 42, 0.65);
}
.ei-sync-monitor-active { border-color: rgba(34, 197, 94, 0.45); box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.15); }
.ei-sync-monitor-toggle {
width: 100%; display: flex; align-items: center; gap: 0.6rem;
padding: 0.55rem 0.75rem; background: transparent; border: 0; color: #e2e8f0;
cursor: pointer; font-size: 0.82rem; text-align: left;
}
.ei-sync-monitor-badge {
margin-left: auto; font-size: 0.72rem; color: #7dd3fc;
background: rgba(14, 165, 233, 0.15); padding: 0.15rem 0.45rem; border-radius: 999px;
}
.ei-sync-monitor-panel { padding: 0 0.75rem 0.75rem; }
.ei-sync-monitor-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; font-size: 0.8rem; }
.ei-sync-live { color: #4ade80; font-size: 0.72rem; animation: ei-pulse 1.5s ease-in-out infinite; }
@keyframes ei-pulse { 0%,100%{opacity:1} 50%{opacity:.45} }
.ei-sync-job { margin-bottom: 0.65rem; padding: 0.55rem 0.65rem; border-radius: 8px; background: rgba(2, 6, 23, 0.55); }
.ei-sync-job-title { font-size: 0.78rem; font-weight: 600; color: #f8fafc; }
.ei-sync-job-msg { font-size: 0.74rem; color: #94a3b8; margin-top: 0.2rem; }
.ei-sync-job-bar { height: 6px; border-radius: 999px; background: rgba(51, 65, 85, 0.8); margin-top: 0.45rem; overflow: hidden; }
.ei-sync-job-bar-fill { height: 100%; background: linear-gradient(90deg, #0ea5e9, #22c55e); transition: width 0.4s ease; }
.ei-sync-job-meta { font-size: 0.7rem; color: #64748b; margin-top: 0.25rem; }
.ei-sync-coverage { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.45rem; margin: 0.5rem 0; }
.ei-sync-coverage div { background: rgba(2, 6, 23, 0.45); border-radius: 8px; padding: 0.4rem 0.5rem; display: flex; flex-direction: column; gap: 0.15rem; }
.ei-sync-coverage span { font-size: 0.68rem; color: #64748b; }
.ei-sync-coverage strong { font-size: 0.85rem; color: #e2e8f0; }
.ei-sync-events { max-height: 180px; overflow: auto; border-top: 1px solid rgba(51, 65, 85, 0.6); padding-top: 0.45rem; }
.ei-sync-events-title { font-size: 0.72rem; color: #64748b; margin-bottom: 0.35rem; }
.ei-sync-event { display: flex; gap: 0.45rem; font-size: 0.72rem; padding: 0.2rem 0; color: #cbd5e1; }
.ei-sync-event-time { color: #64748b; min-width: 4.2rem; font-variant-numeric: tabular-nums; }
.ei-google-places { margin: 0.55rem 0; padding: 0.55rem 0.65rem; border-radius: 8px; background: rgba(2,6,23,.45); border: 1px solid rgba(100,116,139,.35); }
.ei-google-places-head { display: flex; align-items: center; gap: 0.5rem; font-size: 0.78rem; margin-bottom: 0.35rem; }
.ei-google-status { font-size: 0.68rem; color: #4ade80; margin-left: auto; }
.ei-google-status.ei-google-missing { color: #fbbf24; }
.ei-google-hint { font-size: 0.7rem; color: #64748b; margin: 0 0 0.45rem; }
.ei-google-key-row { display: flex; gap: 0.4rem; margin-bottom: 0.45rem; }
.ei-google-key-row .form-input { flex: 1; min-width: 0; font-size: 0.75rem; padding: 0.35rem 0.5rem; }
+9
View File
@@ -97,6 +97,9 @@ body { overflow-x: hidden; padding-bottom: var(--safe-bottom); }
grid-template-columns: 1fr !important;
display: block !important;
}
.app-main {
margin-left: 0 !important;
}
.app-sidebar {
position: fixed !important;
top: 0; left: 0; bottom: 0;
@@ -113,6 +116,12 @@ body { overflow-x: hidden; padding-bottom: var(--safe-bottom); }
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
.persona-nav {
flex: none;
min-height: auto;
overflow: visible;
padding-bottom: 0;
}
body.nav-open .app-sidebar { transform: translateX(0); }
body.nav-open { overflow: hidden; }
.app-topbar {
+118
View File
@@ -892,3 +892,121 @@ body:has(.topbar) .main { margin-left: 0; }
border-radius: 8px; color: #94a3b8; font-size: 0.85rem;
}
.novnc-fallback a { color: var(--accent-cyan); }
/* ── AI / LLM settings ── */
.llm-hero-card {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.25rem;
padding: 1.1rem 1.25rem;
border-radius: 12px;
border: 1px solid rgba(56, 189, 248, 0.28);
background: linear-gradient(135deg, rgba(14, 165, 233, 0.1), rgba(8, 12, 18, 0.95));
}
.llm-hero-body { flex: 1 1 280px; min-width: 0; }
.llm-hero-badge {
display: inline-block;
margin-bottom: 0.45rem;
padding: 0.15rem 0.5rem;
border-radius: 999px;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #7dd3fc;
background: rgba(14, 165, 233, 0.15);
border: 1px solid rgba(56, 189, 248, 0.25);
}
.llm-hero-card h3 {
margin: 0 0 0.4rem;
font-size: 1.05rem;
font-weight: 600;
color: #f1f5f9;
}
.llm-hero-lead {
margin: 0;
font-size: 0.875rem;
line-height: 1.45;
color: #94a3b8;
max-width: 42rem;
}
.llm-hero-lead code {
font-size: 0.8rem;
padding: 0.1rem 0.35rem;
border-radius: 4px;
background: rgba(15, 23, 42, 0.8);
color: #bae6fd;
}
.llm-hero-steps {
margin: 0.65rem 0 0;
padding-left: 1.15rem;
font-size: 0.8125rem;
line-height: 1.55;
color: #94a3b8;
}
.llm-hero-steps a { color: #38bdf8; }
.llm-section-title {
margin: 0 0 0.25rem;
font-size: 0.92rem;
font-weight: 600;
color: #e2e8f0;
}
.llm-section-sub {
margin: 0 0 0.75rem;
font-size: 0.8125rem;
color: #64748b;
}
.llm-preset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.65rem;
margin-bottom: 1.5rem;
}
.llm-preset-card {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.35rem;
width: 100%;
min-height: 7.5rem;
padding: 0.85rem 0.95rem;
text-align: left;
cursor: pointer;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(15, 23, 42, 0.55);
color: inherit;
font: inherit;
transition: border-color 0.15s, background 0.15s, transform 0.12s;
}
.llm-preset-card:hover {
border-color: rgba(56, 189, 248, 0.35);
background: rgba(14, 165, 233, 0.08);
transform: translateY(-1px);
}
.llm-preset-name {
font-size: 0.875rem;
font-weight: 600;
color: #f1f5f9;
line-height: 1.25;
}
.llm-preset-hint {
font-size: 0.75rem;
line-height: 1.4;
color: #64748b;
flex: 1;
}
.llm-preset-models {
font-size: 0.68rem;
font-family: ui-monospace, monospace;
color: #475569;
word-break: break-all;
line-height: 1.35;
}
.llm-provider-list { margin-top: 0.25rem; }
+302 -91
View File
@@ -1,13 +1,7 @@
/**
* Export Intel map — Leaflet + clustering + rich popups
* Export Intel map — Leaflet + clustering (multi-instance per tab)
*/
window.ExportIntelMap = (function () {
var map = null;
var clusterLayer = null;
var onEntityClick = null;
var halalMode = false;
var markersById = {};
var HALAL_COLORS = {
hot: '#22c55e',
warm: '#eab308',
@@ -27,6 +21,9 @@ window.ExportIntelMap = (function () {
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, ' ') };
}
@@ -52,19 +49,128 @@ window.ExportIntelMap = (function () {
);
}
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 '<a class="ei-popup-link ' + (cls || '') + '" href="' + escapeHtml(href) + '" target="_blank" rel="noopener" title="' + val + '">' +
(label ? '<span class="ei-popup-link-label">' + label + '</span>' : '') +
'<span class="ei-popup-link-val">' + val + '</span></a>';
}
return '<div class="ei-popup-plain">' + (label ? '<span class="ei-popup-link-label">' + label + '</span>' : '') +
'<span class="ei-popup-link-val">' + val + '</span></div>';
}).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('<a class="ei-popup-link" href="mailto:' + escapeHtml(c.email) + '">📧 ' + escapeHtml(c.email) + '</a>');
if (c.email_alt) bits.push('<a class="ei-popup-link" href="mailto:' + escapeHtml(c.email_alt) + '">📧 ' + escapeHtml(c.email_alt) + ' (alt)</a>');
if (c.phone) bits.push('<a class="ei-popup-link" href="tel:' + escapeHtml(c.phone) + '">📞 ' + escapeHtml(c.phone) + '</a>');
if (c.mobile) bits.push('<a class="ei-popup-link" href="tel:' + escapeHtml(c.mobile) + '">📱 ' + escapeHtml(c.mobile) + '</a>');
if (c.whatsapp) bits.push('<a class="ei-popup-link" href="https://wa.me/' + escapeHtml(String(c.whatsapp).replace(/\D/g, '')) + '" target="_blank" rel="noopener">💬 ' + escapeHtml(c.whatsapp) + '</a>');
if (c.website) bits.push('<a class="ei-popup-link" href="' + escapeHtml(c.website) + '" target="_blank" rel="noopener">🌐 Website</a>');
if (c.linkedin_url) bits.push('<a class="ei-popup-link" href="' + escapeHtml(c.linkedin_url) + '" target="_blank" rel="noopener">in LinkedIn</a>');
return '<div class="ei-popup-person">' +
'<div class="ei-popup-person-head">' + head + (c.is_primary ? ' <span class="ei-popup-primary">primair</span>' : '') + '</div>' +
(sub ? '<div class="ei-popup-person-sub">' + sub + '</div>' : '') +
(bits.length ? '<div class="ei-popup-person-links">' + bits.join('') + '</div>' : '') +
'</div>';
}).join('');
return '<div class="ei-popup-section"><div class="ei-popup-section-title">Contactpersonen</div>' + cards + '</div>';
}
function reachBlock(p) {
var r = p.reach || {};
if (!r.has_any && !p.email && !p.phone && !(p.websites && p.websites.length)) {
return '<div class="ei-popup-empty-contact">Nog geen contact — sync contacten of vul handmatig aan.</div>';
}
var html = '<div class="ei-popup-reach">';
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 += '<div class="ei-popup-section"><div class="ei-popup-section-title">E-mail</div><div class="ei-popup-channel-list">' + channelList(emails) + '</div></div>';
}
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 += '<div class="ei-popup-section"><div class="ei-popup-section-title">Telefoon</div><div class="ei-popup-channel-list">' + channelList(phones) + '</div></div>';
}
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 += '<div class="ei-popup-section"><div class="ei-popup-section-title">Adres</div><div class="ei-popup-channel-list">' + channelList(r.addresses) + '</div></div>';
}
if (r.social && r.social.length) {
html += '<div class="ei-popup-section"><div class="ei-popup-section-title">Social</div><div class="ei-popup-channel-list">' + channelList(r.social) + '</div></div>';
}
if (r.maps_url) {
html += '<div class="ei-popup-section"><a class="ei-popup-maps" href="' + escapeHtml(r.maps_url) + '" target="_blank" rel="noopener">📍 Open in Google Maps</a></div>';
}
html += contactPeopleBlock(r.contact_people);
html += '</div>';
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 '<a class="ei-popup-web" href="' + escapeHtml(href) + '" target="_blank" rel="noopener" title="' + disp + '">' + label + '</a>';
}).filter(Boolean).join('');
if (!links) return '';
return (
'<div class="ei-popup-row ei-popup-websites">' +
'<span class="ei-popup-k">Websites</span>' +
'<span class="ei-popup-v ei-popup-web-list">' + links + '</span>' +
'</div>'
);
}
function halalColor(p) {
var tier = p.halal_tier || 'low';
return HALAL_COLORS[tier] || HALAL_COLORS.low;
}
function popupHtml(p) {
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 web = p.website;
if (web && !/^https?:\/\//i.test(web)) web = 'https://' + web;
var halalRow = '';
if (p.halal_score != null) {
halalRow = row('Halal kans', p.halal_score + '/100' + (halalMode ? ' · ' + (p.halal_tier || '') : ''));
halalRow = row('Halal kans', p.halal_score + '/100' + (this.halalMode ? ' · ' + (p.halal_tier || '') : ''));
}
var contactBits = [];
if (p.email) contactBits.push('📧 ' + escapeHtml(p.email));
@@ -75,7 +181,7 @@ window.ExportIntelMap = (function () {
return (
'<div class="ei-popup-card" data-entity-id="' + escapeHtml(id) + '">' +
'<div class="ei-popup-top">' +
'<span class="ei-popup-badge" style="--badge-color:' + (halalMode ? halalColor(p) : c.color) + '">' + c.icon + ' ' + escapeHtml(c.label) + '</span>' +
'<span class="ei-popup-badge" style="--badge-color:' + (this.halalMode ? halalColor(p) : c.color) + '">' + c.icon + ' ' + escapeHtml(c.label) + '</span>' +
'<span class="ei-popup-flag">' + escapeHtml(p.country_iso2 || '') + '</span>' +
'</div>' +
'<h3 class="ei-popup-name">' + escapeHtml(p.name) + '</h3>' +
@@ -86,7 +192,7 @@ window.ExportIntelMap = (function () {
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) +
row('Website', p.website, web) +
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) +
@@ -97,12 +203,12 @@ window.ExportIntelMap = (function () {
'</div>' +
'</div>'
);
}
};
function pinIcon(type, p) {
MapInstance.prototype.pinIcon = function (type, p) {
var c = cfg(type);
var color = halalMode && p && p.halal_score != null ? halalColor(p) : c.color;
var ring = halalMode && p && p.halal_tier === 'hot' ? ' ei-pin-hot' : '';
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:
@@ -114,22 +220,58 @@ window.ExportIntelMap = (function () {
iconAnchor: [15, 15],
popupAnchor: [0, -16],
});
}
};
function openEntityId(rawId) {
MapInstance.prototype.openEntityId = function (rawId) {
var id = Number(rawId);
if (!Number.isFinite(id)) return;
if (onEntityClick) {
onEntityClick(id);
if (this.onEntityClick) {
this.onEntityClick(id);
return;
}
try {
document.dispatchEvent(new CustomEvent('export-intel:open-entity', { detail: { id: id } }));
} catch (e) { /* empty */ }
}
};
function wirePopup(container) {
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);
@@ -143,7 +285,7 @@ window.ExportIntelMap = (function () {
e.preventDefault();
e.stopPropagation();
}
openEntityId(id);
self.openEntityId(id);
};
if (window.L && L.DomEvent) {
L.DomEvent.on(btn, 'click', onBtn);
@@ -156,38 +298,39 @@ window.ExportIntelMap = (function () {
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) openEntityId(card.getAttribute('data-entity-id'));
if (card) self.openEntityId(card.getAttribute('data-entity-id'));
});
}
};
function invalidateMapSize() {
if (!map) return;
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);
setTimeout(function () { if (map) map.invalidateSize(true); }, 800);
}
};
function init(containerId) {
var el = document.getElementById(containerId);
MapInstance.prototype.init = function () {
var el = document.getElementById(this.containerId);
if (!el || !window.L) return null;
if (map && map.getContainer() === el) {
invalidateMapSize();
return map;
if (this.map && this.map.getContainer() === el) {
this.invalidateSize();
return this.map;
}
if (map) {
map.remove();
map = null;
clusterLayer = null;
if (this.map) {
this.map.remove();
this.map = null;
this.clusterLayer = null;
}
map = L.map(containerId, { zoomControl: true, attributionControl: true }).setView([20, 10], 2);
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: '&copy; OSM &copy; CARTO',
maxZoom: 19,
}).addTo(map);
}).addTo(this.map);
if (window.L.markerClusterGroup) {
clusterLayer = L.markerClusterGroup({
this.clusterLayer = L.markerClusterGroup({
maxClusterRadius: 42,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
@@ -202,100 +345,168 @@ window.ExportIntelMap = (function () {
});
},
});
map.addLayer(clusterLayer);
this.map.addLayer(this.clusterLayer);
} else {
clusterLayer = L.layerGroup();
map.addLayer(clusterLayer);
this.clusterLayer = L.layerGroup();
this.map.addLayer(this.clusterLayer);
}
map.on('popupopen', function (e) {
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);
}
wirePopup(root.querySelector('.leaflet-popup-content') || 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);
});
invalidateMapSize();
return map;
}
this.invalidateSize();
return this.map;
};
function setFeatures(geojson, opts) {
MapInstance.prototype.setFeatures = function (geojson, opts) {
opts = opts || {};
if (!map || !clusterLayer) return;
clusterLayer.clearLayers();
markersById = {};
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: pinIcon(p.entity_type, p) });
var marker = L.marker([coords[1], coords[0]], { icon: self.pinIcon(p.entity_type, p) });
marker._eiId = p.id;
if (p.id != null) markersById[p.id] = marker;
marker.bindPopup(popupHtml(p), { maxWidth: 320, minWidth: 260, className: 'ei-leaflet-popup' });
marker.on('dblclick', function () {
if (onEntityClick && p.id) onEntityClick(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 (clusterLayer.addLayers) clusterLayer.addLayers(markers);
else markers.forEach(function (m) { clusterLayer.addLayer(m); });
if (this.clusterLayer.addLayers) this.clusterLayer.addLayers(markers);
else markers.forEach(function (m) { self.clusterLayer.addLayer(m); });
invalidateMapSize();
this.invalidateSize();
if (opts.autoFit === false) {
map.setView([20, 10], 2);
this.map.setView([20, 10], 2);
} else if (features.length === 1) {
var c = features[0].geometry.coordinates;
map.setView([c[1], c[0]], 10);
} else if (opts.autoFit && markers.length > 1 && clusterLayer.getBounds) {
this.map.setView([c[1], c[0]], 10);
} else if (opts.autoFit && markers.length > 1 && this.clusterLayer.getBounds) {
try {
map.fitBounds(clusterLayer.getBounds(), { padding: [40, 40], maxZoom: 12 });
this.map.fitBounds(this.clusterLayer.getBounds(), { padding: [40, 40], maxZoom: 12 });
} catch (e) { /* empty */ }
}
}
};
function setHalalMode(on) {
halalMode = !!on;
}
MapInstance.prototype.setHalalMode = function (on) {
this.halalMode = !!on;
};
function setOnEntityClick(fn) {
onEntityClick = fn;
}
MapInstance.prototype.setOnEntityClick = function (fn) {
this.onEntityClick = typeof fn === 'function' ? fn : null;
};
function flyTo(lat, lon, zoom) {
if (map && lat && lon) map.setView([lat, lon], zoom || 10);
}
MapInstance.prototype.flyTo = function (lat, lon, zoom) {
if (this.map && lat && lon) this.map.setView([lat, lon], zoom || 10);
};
function highlightEntity(id) {
var marker = markersById[id];
if (!marker || !map) return;
MapInstance.prototype.highlightEntity = function (id) {
var marker = this.markersById[id];
if (!marker || !this.map) return;
var latlng = marker.getLatLng();
map.setView(latlng, Math.max(map.getZoom(), 14));
this.map.setView(latlng, Math.max(this.map.getZoom(), 14));
marker.openPopup();
}
};
function closePopup() {
if (map) map.closePopup();
}
MapInstance.prototype.closePopup = function () {
if (this.map) this.map.closePopup();
};
function destroy() {
if (map) {
map.remove();
map = null;
clusterLayer = null;
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', function () { invalidateMapSize(); });
window.addEventListener('resize', invalidateAll);
}
return { init, setFeatures, setOnEntityClick, setHalalMode, flyTo, closePopup, highlightEntity, destroy, TYPE_CFG: TYPE_CFG, cfg: cfg };
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,
};
})();
+591
View File
@@ -0,0 +1,591 @@
/**
* Export Intel — profiel popup (imperatief, geen Alpine in modal).
*/
(function (global) {
'use strict';
function normalizeWebUrl(u) {
if (!u) return '';
u = String(u).trim();
if (!u) return '';
if (!/^https?:\/\//i.test(u)) u = 'https://' + u.replace(/^\/+/, '');
return u;
}
function contactGapHint(e) {
var r = e.reach || {};
var hasDirect = !!(e.phone || e.email || e.website || (r.phones && r.phones.length) || (r.emails && r.emails.length) || (r.websites && r.websites.length));
if (hasDirect) return '';
var src = e.source || 'osm';
return '<p class="ei-drawer-empty ei-drawer-hint">Geen telefoon of e-mail in bron (' + esc(src) + '). Gratis opties: <strong>Bewerken</strong> (handmatig), of <strong>Sync contacten</strong> (scant websites) / <strong>Sync distributeurs</strong> (haalt OSM-tags opnieuw).</p>';
}
function reachSectionHtml(e) {
var r = e.reach || {};
var html = '';
function links(title, items, render) {
if (!items || !items.length) return;
html += '<div class="ei-reach-block"><h5>' + esc(title) + '</h5>';
for (var i = 0; i < items.length; i++) html += render(items[i]);
html += '</div>';
}
links('E-mail', r.emails, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'E-mail') + '</span><a href="' + esc(it.href || ('mailto:' + it.value)) + '">' + esc(it.value) + '</a></div>';
});
links('Telefoon', r.phones, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'Tel') + '</span><a href="' + esc(it.href || ('tel:' + it.value)) + '">' + esc(it.value) + '</a></div>';
});
if (!r.emails || !r.emails.length) {
if (e.email) html += '<div class="ei-drawer-kv"><span>E-mail</span><a href="mailto:' + esc(e.email) + '">' + esc(e.email) + '</a></div>';
}
if (!r.phones || !r.phones.length) {
if (e.phone) html += '<div class="ei-drawer-kv"><span>Telefoon</span><a href="tel:' + esc(e.phone) + '">' + esc(e.phone) + '</a></div>';
}
html += websitesHtml(e);
links('Adres', r.addresses, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'Adres') + '</span><span>' + esc(it.value) + '</span></div>';
});
links('Social', r.social, function (it) {
return '<div class="ei-drawer-kv"><span>' + esc(it.label || 'Link') + '</span><a href="' + esc(it.href) + '" target="_blank" rel="noopener">' + esc(it.value) + '</a></div>';
});
if (r.maps_url) {
html += '<div class="ei-drawer-kv"><span>Maps</span><a href="' + esc(r.maps_url) + '" target="_blank" rel="noopener">Google Maps →</a></div>';
}
return html;
}
function websitesHtml(e) {
var sites = e.websites;
if ((!sites || !sites.length) && e.website) {
sites = [{ label: 'Organisatie', url: e.website, display: e.website }];
}
if (!sites || !sites.length) return '';
var rows = '';
for (var i = 0; i < sites.length; i++) {
var s = sites[i];
var href = normalizeWebUrl(s.url || s);
if (!href) continue;
rows += '<div class="ei-drawer-kv"><span>' + esc(s.label || 'Website') + '</span>' +
'<a href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(s.display || s.url || href) + '</a></div>';
}
return rows;
}
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function typeLabel(t) {
return (t || '').replace(/_/g, ' ');
}
function getApp() {
return global.__exportIntelApp || null;
}
function territoryName(iso2) {
var app = getApp();
if (!iso2) return '';
if (app && app.territories) {
for (var i = 0; i < app.territories.length; i++) {
if (app.territories[i].country_iso2 === iso2) return app.territories[i].name_nl;
}
}
return iso2;
}
function formatCoords(lat, lon) {
if (lat == null || lon == null) return '';
var a = Number(lat);
var b = Number(lon);
if (!Number.isFinite(a) || !Number.isFinite(b)) return '';
return a.toFixed(5) + ', ' + b.toFixed(5);
}
function mountEl() {
return document.getElementById('ei-profile-mount');
}
function activeMapId(app) {
if (!app) return 'ei-map';
if (app.tab === 'map') return 'ei-map';
if (app.tabMapId && typeof app.tabMapId === 'function') {
return app.tabMapId() || 'ei-map';
}
return 'ei-map-' + (app.tab || 'map');
}
var Profile = {
_busy: false,
showShell: function (on) {
var el = document.getElementById('ei-profile-modal');
if (!el) return;
if (on) {
el.classList.add('is-open');
el.setAttribute('aria-hidden', 'false');
document.body.classList.add('ei-profile-open');
} else {
el.classList.remove('is-open');
el.setAttribute('aria-hidden', 'true');
document.body.classList.remove('ei-profile-open');
}
},
clearMount: function () {
var m = mountEl();
if (m) m.innerHTML = '';
},
renderLoading: function () {
var m = mountEl();
if (!m) return;
m.innerHTML =
'<div class="ei-drawer-loading" style="padding:3rem 1.5rem;text-align:center">' +
'<span class="ei-map-spinner"></span>' +
'<p style="margin-top:1rem;color:#94a3b8">Profiel laden…</p></div>';
},
renderError: function (msg) {
var m = mountEl();
if (!m) return;
m.innerHTML =
'<div class="ei-drawer-loading" style="padding:2rem 1.5rem">' +
'<p style="color:#fecdd3;margin-bottom:1rem">' + esc(msg || 'Laden mislukt') + '</p>' +
'<button type="button" class="btn btn-sm" data-ei-action="close">Sluiten</button></div>';
},
patchEntityInApp: function (app, entity) {
if (!app || !entity || !entity.id) return;
var id = entity.id;
if (Array.isArray(app.entities)) {
for (var i = 0; i < app.entities.length; i++) {
if (app.entities[i].id === id) {
Object.assign(app.entities[i], entity);
}
}
}
if (Array.isArray(app.crmPipeline)) {
for (var j = 0; j < app.crmPipeline.length; j++) {
if (app.crmPipeline[j].id === id) {
Object.assign(app.crmPipeline[j], entity);
}
}
}
if (Array.isArray(app.halalTopEntities)) {
for (var k = 0; k < app.halalTopEntities.length; k++) {
if (app.halalTopEntities[k].id === id) {
Object.assign(app.halalTopEntities[k], entity);
}
}
}
},
renderFull: function (e, app) {
var m = mountEl();
if (!m || !e) return;
app = app || getApp();
var contacts = e.contacts || [];
var contactRows = '';
for (var i = 0; i < contacts.length; i++) {
var c = contacts[i];
contactRows +=
'<div class="ei-contact-row">' +
'<strong>' + esc(c.name || c.role || 'Contact') + '</strong>' +
(c.role ? '<span class="ei-contact-role">' + esc(c.role) + '</span>' : '') +
(c.email ? '<div>📧 <a href="mailto:' + esc(c.email) + '">' + esc(c.email) + '</a></div>' : '') +
(c.phone ? '<div>📞 <a href="tel:' + esc(c.phone) + '">' + esc(c.phone) + '</a></div>' : '') +
(c.website_contact_url ? '<div>🌐 <a href="' + esc(normalizeWebUrl(c.website_contact_url)) + '" target="_blank" rel="noopener">' + esc(c.website_contact_url) + '</a></div>' : '') +
(c.linkedin_url ? '<div>in <a href="' + esc(normalizeWebUrl(c.linkedin_url)) + '" target="_blank" rel="noopener">LinkedIn</a></div>' : '') +
'</div>';
}
var halalTags = '';
var labels = e.halal_reason_labels || [];
for (var j = 0; j < labels.length; j++) {
halalTags += '<span class="ei-halal-tag">' + esc(labels[j]) + '</span>';
}
var crmBtn = e.client_id
? '<a class="ei-action-tile ei-action-crm-done" href="/clients"><span class="ei-action-icon">✓</span><span class="ei-action-label">Open CRM</span><span class="ei-action-sub">' +
esc(e.crm_client_name || ('Klant #' + e.client_id)) + '</span></a>'
: '<button type="button" class="ei-action-tile ei-action-crm" data-ei-action="crm" data-ei-id="' + e.id + '"><span class="ei-action-icon">➕</span><span class="ei-action-label">Naar CRM</span><span class="ei-action-sub">Klant + deal</span></button>';
var mapsBtn = e.maps_url
? '<a class="ei-action-tile" href="' + esc(e.maps_url) + '" target="_blank" rel="noopener"><span class="ei-action-icon">📍</span><span class="ei-action-label">Maps</span><span class="ei-action-sub">Locatie</span></a>'
: '';
var dealBtn = e.deal_id
? '<a class="ei-action-tile" href="/deals"><span class="ei-action-icon">💼</span><span class="ei-action-label">Deal</span><span class="ei-action-sub">Pipeline</span></a>'
: '';
var favOn = !!e.is_favorite;
m.innerHTML =
'<header class="ei-drawer-head-rich">' +
'<div class="ei-drawer-head-top">' +
'<div class="ei-drawer-hero">' +
'<span class="ei-drawer-badge ei-type-' + esc(e.entity_type) + '">' + esc(typeLabel(e.entity_type)) + '</span>' +
'<span class="ei-drawer-flag">' + esc(e.country_iso2) + '</span>' +
(e.client_id ? '<span class="ei-crm-badge">In CRM</span>' : '') +
(favOn ? '<span class="ei-crm-badge" style="color:#facc15;border-color:rgba(251,191,36,.4)">★ Favoriet</span>' : '') +
(e.halal_score != null ? '<span class="ei-halal-pill ei-halal-hot">' + Math.round(Number(e.halal_score)) + '/100</span>' : '') +
'</div>' +
'<button type="button" class="ei-drawer-close" data-ei-action="close" title="Sluiten">✕</button>' +
'</div>' +
'<h2 class="ei-drawer-title" id="ei-profile-title">' + esc(e.name) + '</h2>' +
'<p class="ei-drawer-meta">' + esc(territoryName(e.country_iso2)) +
' · Confidence <strong>' + (e.confidence || 0) + '%</strong>' +
' · Pipeline <strong>' + esc(e.pipeline_stage || '—') + '</strong>' +
' · Bron <strong>' + esc(e.source || '—') + '</strong></p>' +
'</header>' +
'<div class="ei-drawer-toolbar">' +
crmBtn +
'<button type="button" class="ei-action-tile ei-action-edit" data-ei-action="edit"><span class="ei-action-icon">✎</span><span class="ei-action-label">Bewerken</span><span class="ei-action-sub">Velden</span></button>' +
'<button type="button" class="ei-action-tile' + (favOn ? ' ei-action-fav-on' : '') + '" data-ei-action="favorite" data-ei-id="' + e.id + '" data-ei-fav="' + (favOn ? '1' : '0') + '"><span class="ei-action-icon">' + (favOn ? '★' : '☆') + '</span><span class="ei-action-label">' + (favOn ? 'Favoriet' : 'Favoriet maken') + '</span></button>' +
mapsBtn +
'<button type="button" class="ei-action-tile" data-ei-action="sync-contacts"><span class="ei-action-icon">↻</span><span class="ei-action-label">Contacten</span></button>' +
dealBtn +
'<a class="ei-action-tile" href="/herman?context=export_catalog&entity=' + e.id + '"><span class="ei-action-icon">📋</span><span class="ei-action-label">Herman</span></a>' +
'</div>' +
'<div class="ei-drawer-body ei-drawer-body-rich">' +
(halalTags ? '<div class="ei-halal-tags">' + halalTags + '</div>' : '') +
((e.city || e.address_line || (e.lat && e.lon))
? '<section class="ei-drawer-section"><h4>Locatie</h4>' +
(e.address_line ? '<p>' + esc(e.address_line) + '</p>' : '') +
(e.city ? '<p>' + esc(e.city) + (e.postal_code ? ', ' + esc(e.postal_code) : '') + '</p>' : '') +
((e.lat && e.lon)
? '<p class="ei-drawer-coords">' + esc(formatCoords(e.lat, e.lon)) +
(e.maps_url ? ' <a href="' + esc(e.maps_url) + '" target="_blank" rel="noopener">Maps →</a>' : '') + '</p>'
: '') +
'</section>'
: '') +
'<section class="ei-drawer-section"><h4>Contact & web</h4>' +
reachSectionHtml(e) + contactGapHint(e) +
((!e.reach || !e.reach.has_any) && !e.email && !e.phone && !(e.websites && e.websites.length) && !e.website && !contacts.length ? '<p class="ei-drawer-empty">Geen contact — sync contacten om te verrijken.</p>' : '') +
'</section>' +
(e.client_id
? '<section class="ei-drawer-section"><h4>CRM</h4>' +
'<div class="ei-drawer-kv"><span>Klant</span><a href="/clients">' + esc(e.crm_client_name || ('#' + e.client_id)) + '</a></div>' +
(e.crm_deal_title ? '<div class="ei-drawer-kv"><span>Deal</span><span>' + esc(e.crm_deal_title) + '</span></div>' : '') +
'</section>'
: '') +
'<section class="ei-drawer-section"><h4>Contactpersonen (' + contacts.length + ')</h4>' +
(contactRows || '<p class="ei-drawer-empty">Geen contactpersonen.</p>') +
'</section></div>';
if (app) {
app.selected = e;
app.drawerOpen = true;
app.drawerLoading = false;
app.listFocusDetail = e;
app.listFocusId = e.id;
}
},
fetchEntity: function (id) {
return fetch('/api/export-intel/entities/' + id).then(function (r) {
return r.json().then(function (d) {
if (!r.ok) throw new Error(d.error || d.detail || ('HTTP ' + r.status));
if (d && d.error) throw new Error(d.error);
return d;
});
});
},
highlightOnMap: function (entity, app) {
if (!entity || !entity.lat || !entity.lon || !global.ExportIntelMap) return;
try {
var mapId = activeMapId(app);
global.ExportIntelMap.flyTo(entity.lat, entity.lon, 14, mapId);
global.ExportIntelMap.highlightEntity(entity.id, mapId);
} catch (e) { /* ignore */ }
},
open: function (id) {
id = Number(id);
if (!Number.isFinite(id) || id <= 0) return;
if (this._busy) return;
this._busy = true;
this.showShell(true);
this.renderLoading();
var self = this;
var app = getApp();
this.fetchEntity(id)
.then(function (entity) {
self.renderFull(entity, app);
self.highlightOnMap(entity, app);
})
.catch(function (err) {
self.renderError(err.message || 'Profiel laden mislukt');
})
.finally(function () {
self._busy = false;
});
},
close: function () {
this._busy = false;
this.showShell(false);
this.clearMount();
var app = getApp();
if (app) {
app.drawerOpen = false;
app.drawerLoading = false;
app.selected = null;
app.editMode = false;
}
},
setFavorite: function (id, currentlyFavorite) {
var self = this;
var app = getApp();
id = Number(id);
if (!Number.isFinite(id)) return Promise.reject(new Error('Ongeldig id'));
return fetch('/api/export-intel/entities/favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ entity_ids: [id], favorite: !currentlyFavorite }),
})
.then(function (r) {
return r.json().then(function (d) {
if (!r.ok) throw new Error((d && (d.error || d.detail)) || 'Favoriet mislukt');
return d;
});
})
.then(function () {
return self.fetchEntity(id);
})
.then(function (entity) {
if (app) {
app.selected = entity;
app.listFocusDetail = entity;
app.listFocusId = entity.id;
app.syncMsg = entity.is_favorite ? '★ Favoriet opgeslagen' : 'Favoriet verwijderd';
if (app.stats && typeof app.stats.favorites === 'number') {
app.stats.favorites += entity.is_favorite ? 1 : -1;
if (app.stats.favorites < 0) app.stats.favorites = 0;
}
self.patchEntityInApp(app, entity);
}
self.renderFull(entity, app);
return entity;
});
},
doCrm: function (id) {
var self = this;
var app = getApp();
id = Number(id);
if (!app || !app.pushEntityToCrm) return;
app.pushEntityToCrm(id).then(function () {
return self.fetchEntity(id);
}).then(function (entity) {
if (app) {
app.selected = entity;
app.listFocusDetail = entity;
self.patchEntityInApp(app, entity);
}
self.renderFull(entity, app);
}).catch(function (err) {
if (app) app.syncMsg = 'CRM mislukt: ' + (err.message || err);
});
},
doEdit: function () {
var app = getApp();
if (!app || !app.selected) return;
app.startEdit();
this.renderEdit(app);
},
renderEdit: function (app) {
var e = app.selected;
var f = app.editForm || {};
var m = mountEl();
if (!m || !e) return;
m.innerHTML =
'<header class="ei-drawer-head-rich"><div class="ei-drawer-head-top">' +
'<h2 class="ei-drawer-title">Bewerken · ' + esc(e.name) + '</h2>' +
'<button type="button" class="ei-drawer-close" data-ei-action="close">✕</button></div></header>' +
'<div class="ei-drawer-body ei-drawer-body-rich"><div class="ei-drawer-edit">' +
'<div class="ei-edit-grid">' +
'<label>Naam<input type="text" id="ei-edit-name" value="' + esc(f.name || '') + '"></label>' +
'<label>Stad<input type="text" id="ei-edit-city" value="' + esc(f.city || '') + '"></label>' +
'<label>E-mail<input type="email" id="ei-edit-email" value="' + esc(f.email || '') + '"></label>' +
'<label>Telefoon<input type="text" id="ei-edit-phone" value="' + esc(f.phone || '') + '"></label>' +
'<label>Website<input type="url" id="ei-edit-website" value="' + esc(f.website || '') + '"></label>' +
'</div><div class="ei-edit-actions">' +
'<button type="button" class="btn btn-sm btn-pulse-green" data-ei-action="save-edit">Opslaan</button>' +
'<button type="button" class="btn btn-sm" data-ei-action="cancel-edit">Annuleren</button>' +
'</div></div></div>';
},
saveEdit: function () {
var app = getApp();
if (!app || !app.selected) return;
var self = this;
app.editForm.name = (document.getElementById('ei-edit-name') || {}).value || '';
app.editForm.city = (document.getElementById('ei-edit-city') || {}).value || '';
app.editForm.email = (document.getElementById('ei-edit-email') || {}).value || '';
app.editForm.phone = (document.getElementById('ei-edit-phone') || {}).value || '';
app.editForm.website = (document.getElementById('ei-edit-website') || {}).value || '';
var id = app.selected.id;
Promise.resolve(app.saveEdit()).then(function () {
return self.fetchEntity(id);
}).then(function (entity) {
if (app) {
app.selected = entity;
app.listFocusDetail = entity;
self.patchEntityInApp(app, entity);
}
self.renderFull(entity, app);
});
},
cancelEdit: function () {
var app = getApp();
if (!app) return;
app.cancelEdit();
if (app.selected) this.renderFull(app.selected, app);
},
doFavorite: function (id, isFav) {
var self = this;
this.setFavorite(id, isFav).catch(function (err) {
self.renderError(err.message || 'Favoriet mislukt');
});
},
doSyncContacts: function () {
var app = getApp();
if (app && app.syncKind) app.syncKind('contacts');
},
onModalClick: function (ev) {
var t = ev.target;
if (!t || !t.closest) return;
if (!t.closest('#ei-profile-modal')) return;
if (t.closest('.ei-profile-backdrop')) {
ev.preventDefault();
this.close();
return;
}
var actionEl = t.closest('[data-ei-action]');
if (!actionEl) return;
var action = actionEl.getAttribute('data-ei-action');
if (!action) return;
ev.preventDefault();
ev.stopPropagation();
var id = Number(actionEl.getAttribute('data-ei-id') || (getApp() && getApp().selected && getApp().selected.id));
if (action === 'close') this.close();
else if (action === 'crm') this.doCrm(id);
else if (action === 'edit') this.doEdit();
else if (action === 'favorite') {
var isFav = actionEl.getAttribute('data-ei-fav') === '1';
this.doFavorite(id, isFav);
}
else if (action === 'sync-contacts') this.doSyncContacts();
else if (action === 'save-edit') this.saveEdit();
else if (action === 'cancel-edit') this.cancelEdit();
},
onRowClick: function (ev) {
var t = ev.target;
if (!t || !t.closest) return;
if (t.closest('#ei-profile-modal')) return;
if (t.closest('.ei-td-check, input[type=checkbox], .ei-fav-btn, .ei-td-actions')) return;
if (t.closest('a[href]') && !t.closest('.ei-row-open-btn, .ei-link-btn')) return;
var row = t.closest('[data-entity-id]');
if (!row) return;
var id = row.getAttribute('data-entity-id');
if (!id) return;
ev.preventDefault();
ev.stopPropagation();
this.open(id);
},
bindApp: function (app) {
if (!app) return;
global.__exportIntelApp = app;
var self = this;
app.openEntity = function (id) { self.open(id); };
app.closeDrawer = function () { self.close(); };
},
wireModal: function () {
var modal = document.getElementById('ei-profile-modal');
if (!modal || modal._eiWired) return;
modal._eiWired = true;
var self = this;
modal.addEventListener('click', function (ev) { self.onModalClick(ev); }, true);
},
boot: function () {
var self = this;
this.wireModal();
document.addEventListener('click', function (ev) { self.onRowClick(ev); }, true);
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape') {
var modal = document.getElementById('ei-profile-modal');
if (modal && modal.classList.contains('is-open')) self.close();
}
});
var params = new URLSearchParams(global.location.search);
var openId = params.get('open');
function tryBind() {
var page = document.querySelector('.ei-page');
if (page && page._x_dataStack && page._x_dataStack[0]) {
self.bindApp(page._x_dataStack[0]);
if (openId) self.open(openId);
return true;
}
if (global.__exportIntelApp) {
self.bindApp(global.__exportIntelApp);
if (openId) self.open(openId);
return true;
}
return false;
}
if (!tryBind()) {
var n = 0;
var timer = setInterval(function () {
n++;
if (tryBind() || n > 120) clearInterval(timer);
}, 100);
}
document.addEventListener('alpine:initialized', function () {
tryBind();
});
},
};
global.ExportIntelProfile = Profile;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { Profile.boot(); });
} else {
Profile.boot();
}
})(window);
+435 -52
View File
@@ -5,6 +5,12 @@ function exportIntelApp() {
tabIndicatorH: 40,
busy: false,
syncMsg: '',
syncMonitor: null,
syncMonitorOpen: true,
syncMonitorPoll: null,
googleMapsKey: '',
googleMapsKeySet: false,
googleKeySaving: false,
stats: {},
regions: [],
territories: [],
@@ -27,6 +33,7 @@ function exportIntelApp() {
mapHalalMin: 55,
mapError: '',
mapLoading: false,
tabMapLoading: false,
mapTruncated: false,
halalTopMarkets: [],
halalTopEntities: [],
@@ -37,6 +44,11 @@ function exportIntelApp() {
filterHasEmail: '',
filterFavoritesOnly: false,
filterCrmStatus: '',
filterConfidenceMin: '',
filterCity: '',
filterTenderStatus: '',
filterGovCategory: '',
_cityTimer: null,
selectedIds: [],
crmPipeline: [],
crmModalOpen: false,
@@ -58,12 +70,65 @@ function exportIntelApp() {
{ 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: 'Distributeur', icon: '📦' },
{ v: 'distributor', l: 'B2B / distri', icon: '📦' },
{ v: 'wholesaler', l: 'Groothandel', icon: '🏪' },
{ v: 'importer', l: 'Importeur', icon: '🚢' },
{ v: 'logistics', l: 'Logistiek', icon: '🚚' },
{ v: 'restaurant', l: 'Restaurant', icon: '🍽️' },
{ v: 'restaurant', l: 'Horeca', icon: '🍽️' },
{ v: 'doner_shoarma', l: 'Döner', icon: '🥙' },
{ v: 'butcher', l: 'Slager', icon: '🥩' },
{ v: 'contract_caterer', l: 'Cateraar', icon: '🏢' },
@@ -75,11 +140,13 @@ function exportIntelApp() {
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;
@@ -94,6 +161,7 @@ function exportIntelApp() {
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);
@@ -141,6 +209,10 @@ function exportIntelApp() {
this.filterHasEmail = '';
this.filterFavoritesOnly = false;
this.filterCrmStatus = '';
this.filterConfidenceMin = '';
this.filterCity = '';
this.filterTenderStatus = '';
this.filterGovCategory = '';
this.selectedIds = [];
this.applyMapFilters();
},
@@ -153,6 +225,14 @@ function exportIntelApp() {
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;
},
@@ -170,14 +250,6 @@ function exportIntelApp() {
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();
@@ -187,11 +259,31 @@ function exportIntelApp() {
},
showEmailFilter() {
return this.tab === 'contacts';
return ['map', 'distributors', 'customers', 'caterers', 'contacts'].indexOf(this.tab) >= 0;
},
showCrmFilter() {
return this.tab === 'map' || this.tab === 'contacts';
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() {
@@ -210,6 +302,75 @@ function exportIntelApp() {
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 [
@@ -232,6 +393,15 @@ function exportIntelApp() {
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' },
@@ -253,6 +423,91 @@ function exportIntelApp() {
];
},
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';
@@ -260,20 +515,18 @@ function exportIntelApp() {
if (this.tab === 'map') {
return;
} 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());
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 = '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());
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 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());
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());
@@ -297,6 +550,9 @@ function exportIntelApp() {
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') {
@@ -313,6 +569,9 @@ function exportIntelApp() {
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() {
@@ -355,16 +614,25 @@ function exportIntelApp() {
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); });
window.ExportIntelMap.setHalalMode(this.mapHalalMode);
var autoFit = !!(this.country || this.region);
window.ExportIntelMap.setFeatures(bundle.entities || { features: [] }, { autoFit: autoFit });
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);
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) {
@@ -378,6 +646,9 @@ function exportIntelApp() {
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));
},
@@ -440,34 +711,20 @@ function exportIntelApp() {
this.listFocusDetail = null;
},
async openEntity(id) {
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;
if (window.ExportIntelProfile && typeof window.ExportIntelProfile.open === 'function') {
window.ExportIntelProfile.open(id);
return;
}
this.drawerLoading = false;
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;
@@ -528,7 +785,7 @@ function exportIntelApp() {
async syncKind(kind) {
if (kind === 'world') {
if (!confirm('Wereld-sync: Europa, Midden-Oosten, Afrika en Amerika. OSM + contacten — kan lang duren. Doorgaan?')) return;
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';
@@ -542,7 +799,12 @@ function exportIntelApp() {
region_code: kind === 'region' ? this.region : (this.region || null),
max_priority: 2,
};
if (kind === 'world') body = { 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',
@@ -604,7 +866,128 @@ function exportIntelApp() {
},
typeLabel(t) {
return (t || '').replace(/_/g, ' ');
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) {
@@ -644,7 +1027,7 @@ function exportIntelApp() {
}
return;
}
var list = this.mapHalalMode && this.halalTopEntities.length ? this.halalTopEntities : this.entities;
var list = this.visibleEntities();
if (!list.length) return;
var allSelected = list.every((e) => this.isSelected(e.id));
if (allSelected) {