Add live CEO briefing, Wereldexport CRM, halal engine, and realtime cockpit.
Ship export intel with contact filters and CRM push, Herman live digest with Telegram briefing, halal recommendation engine, revenue cockpit, and dashboard polling/WebSocket fixes.
This commit is contained in:
@@ -189,7 +189,7 @@
|
||||
<script src="/static/js/cockpit.js?v=2"></script>
|
||||
<script src="/static/js/viz-engine.js?v=3"></script>
|
||||
<script src="/static/js/global-viz.js?v=1"></script>
|
||||
<script src="/static/js/live-pulse.js?v=2"></script>
|
||||
<script src="/static/js/live-pulse.js?v=3"></script>
|
||||
<script>
|
||||
window.FoodlinkkBrand = (function () {
|
||||
var FOODLINKK_PREFIXES = ['/export-intel', '/foodlinkk', '/clients', '/deals', '/products', '/suppliers'];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block extra_head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/hermes.css" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=17" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=21" />
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="herman-shell hm-neo">
|
||||
@@ -93,8 +93,8 @@
|
||||
<h3 class="hm-section-title">Herman briefing</h3>
|
||||
<div class="briefing-summary-grid">
|
||||
<div class="briefing-card summary">
|
||||
<h4>Samenvatting</h4>
|
||||
<p id="briefing-summary-text" class="briefing-typewriter"></p>
|
||||
<h4>Samenvatting · live</h4>
|
||||
<div id="briefing-summary-text" class="live-digest-body"></div>
|
||||
</div>
|
||||
<div class="briefing-card" style="max-height:220px;overflow-y:auto">
|
||||
<h4>📋 Herman activiteitenlog</h4>
|
||||
@@ -252,7 +252,7 @@
|
||||
{% block scripts %}
|
||||
<script src="/static/js/viz-engine.js?v=3"></script>
|
||||
<script src="/static/js/dashboard-layout.js?v=4"></script>
|
||||
<script src="/static/js/briefing-charts.js?v=13"></script>
|
||||
<script src="/static/js/briefing-charts.js?v=19"></script>
|
||||
<script>
|
||||
localStorage.setItem('foodlinkk-persona', 'ceo');
|
||||
const INITIAL_BRIEFING = {{ briefing_payload | tojson }};
|
||||
@@ -264,7 +264,9 @@ function dashboardBriefing() {
|
||||
rssItems: [], rssTotal: 0, rssCategory: '', rssSearch: '', rssLoading: false,
|
||||
rssRegulations: {}, rssBookmarks: [],
|
||||
content: INITIAL_BRIEFING.content || '', createdAt: INITIAL_BRIEFING.created_at || '',
|
||||
liveDigest: INITIAL_BRIEFING.live_digest || null,
|
||||
_pollStop: null, _wsStop: null, _vizMode: 'neo-bars', _stats: null,
|
||||
_liveRefreshing: false, _pollTick: 0,
|
||||
_prefs: null, _setDragMode: null, _saveTimer: null,
|
||||
applyDashboardPrefs(prefs) {
|
||||
this._prefs = prefs;
|
||||
@@ -274,7 +276,7 @@ function dashboardBriefing() {
|
||||
DashboardLayout.applyOrder(dash, order);
|
||||
DashboardLayout.applyVisibility(dash, prefs.dashboard_widgets);
|
||||
if (this._setDragMode) this._setDragMode(this.editLayout);
|
||||
if (this._stats) BriefingCharts.renderLive(dash, this._stats, this._vizMode, window._dashboardKpis);
|
||||
if (this._stats) BriefingCharts.renderLive(dash, this._stats, this._vizMode, window._dashboardKpis, this.liveDigest);
|
||||
},
|
||||
scheduleSave(patch) {
|
||||
if (!this._prefs) return;
|
||||
@@ -375,9 +377,14 @@ function dashboardBriefing() {
|
||||
if (cnt < 3) { sessionStorage.setItem('rss_auto_refreshed', '1'); await this.refreshRss(false); }
|
||||
}
|
||||
await this.loadProjectsMini();
|
||||
if (this.content) BriefingCharts.renderText(dash, this.content, this.createdAt);
|
||||
if (this.liveDigest) {
|
||||
BriefingCharts.renderLiveSummary(this.liveDigest, false);
|
||||
} else if (this.content) {
|
||||
BriefingCharts.renderText(dash, this.content, this.createdAt);
|
||||
}
|
||||
window._refreshBriefingLive = (t) => this.refreshLive(t);
|
||||
this._pollStop = CockpitLive.startPolling(() => { this.refreshLive(false); }, 30000);
|
||||
this._pollStop = CockpitLive.startPolling(() => { this.refreshLive(false); }, 20000);
|
||||
this._wsStop = CockpitLive.connectFeed(() => { this.refreshLive(false); });
|
||||
if (this.hermanTab === 'rss') await this.loadRssTab();
|
||||
},
|
||||
async refreshRss(toast) {
|
||||
@@ -448,12 +455,34 @@ function dashboardBriefing() {
|
||||
'<a href="/retail" class="beurs-mini-chip"><strong>' + (stats.crm_partnerships||0) + '</strong> partnerships</a>' +
|
||||
'<a href="/marketing?tab=reclame" class="beurs-mini-chip"><strong>' + (stats.promo_campaigns||0) + '</strong> promo\'s</a>';
|
||||
},
|
||||
updateLiveMeta(at, digest) {
|
||||
const tsRaw = at || (digest && digest.updated_at) || '';
|
||||
const ts = String(tsRaw).substring(0, 19).replace('T', ' ');
|
||||
const meta = document.getElementById('briefing-meta');
|
||||
if (meta && ts) {
|
||||
const n = digest && digest.activity_count != null ? ' · ' + digest.activity_count + ' activiteiten' : '';
|
||||
meta.textContent = 'Live · ververst ' + ts + ' UTC' + n;
|
||||
}
|
||||
const badge = document.querySelector('.herman-hero .live-badge');
|
||||
if (badge && tsRaw) {
|
||||
try {
|
||||
badge.textContent = 'Live · ' + new Date(tsRaw).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
async refreshLive(toast) {
|
||||
if (this._liveRefreshing) return;
|
||||
this._liveRefreshing = true;
|
||||
const isPoll = toast === false;
|
||||
if (isPoll) this._pollTick = (this._pollTick || 0) + 1;
|
||||
if (toast !== false) Cockpit.toast('Live data ophalen…', 'info');
|
||||
try {
|
||||
const r = await fetch('/api/herman/briefing/stats').then(x => x.json());
|
||||
const light = isPoll && (this._pollTick % 6 !== 0);
|
||||
const url = light ? '/api/herman/briefing/stats?light=1' : '/api/herman/briefing/stats';
|
||||
const r = await fetch(url).then(x => x.json());
|
||||
if (r.stats) {
|
||||
this._stats = r.stats;
|
||||
if (r.live_digest) this.liveDigest = r.live_digest;
|
||||
try {
|
||||
const live = await fetch('/api/retail/rss/live?limit=25').then(x => x.json());
|
||||
if (live.items && live.items.length) {
|
||||
@@ -462,13 +491,14 @@ function dashboardBriefing() {
|
||||
this.rssTotal = this._stats.rss_items;
|
||||
}
|
||||
} catch (e) {}
|
||||
BriefingCharts.renderLive(document.getElementById('briefing-dashboard'), this._stats, this._vizMode, window._dashboardKpis);
|
||||
BriefingCharts.renderLive(document.getElementById('briefing-dashboard'), this._stats, this._vizMode, window._dashboardKpis, this.liveDigest, { pollRefresh: isPoll });
|
||||
this.renderClientsMini(this._stats);
|
||||
this.renderActivityLog(this._stats.activity_log || []);
|
||||
document.getElementById('briefing-meta').textContent = 'Live sync: ' + (r.at || '').substring(0, 19).replace('T', ' ') + ' UTC';
|
||||
this.updateLiveMeta(r.at, this.liveDigest);
|
||||
}
|
||||
if (toast !== false) Cockpit.toast('Live data bijgewerkt', 'success');
|
||||
} catch (e) { if (toast !== false) Cockpit.toast(e.message, 'error'); }
|
||||
finally { this._liveRefreshing = false; }
|
||||
},
|
||||
pushLiveEntry(entry) {
|
||||
this.liveLog.push(entry);
|
||||
@@ -523,6 +553,7 @@ function dashboardBriefing() {
|
||||
this.content = result.content || '';
|
||||
this.createdAt = result.generated_at || new Date().toISOString();
|
||||
this._stats = result.stats;
|
||||
if (result.stats && result.stats.live_digest) this.liveDigest = result.stats.live_digest;
|
||||
BriefingCharts.render(document.getElementById('briefing-dashboard'), result.stats, result.content, this.createdAt, this._vizMode);
|
||||
this.renderClientsMini(result.stats || {});
|
||||
this.renderActivityLog((result.stats || {}).activity_log || []);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
|
||||
<link rel="stylesheet" href="/static/css/export-intel.css?v=11" />
|
||||
<link rel="stylesheet" href="/static/css/export-intel.css?v=12" />
|
||||
<link rel="stylesheet" href="/static/css/export-intel-tabs.css?v=10" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
||||
@@ -31,10 +31,10 @@
|
||||
|
||||
<p class="ei-sync-msg" x-show="syncMsg" x-text="syncMsg"></p>
|
||||
|
||||
<div class="ei-selection-bar" x-show="tab === 'map'" x-cloak>
|
||||
<div class="ei-selection-bar" x-show="showSelectionBar()" x-cloak>
|
||||
<div class="ei-selection-left">
|
||||
<span class="ei-selection-count" x-show="selectedIds.length" x-text="selectedIds.length + ' geselecteerd'"></span>
|
||||
<span class="ei-selection-hint" x-show="!selectedIds.length">Selecteer rijen met ☑ voor bulk-acties</span>
|
||||
<span class="ei-selection-hint" x-show="!selectedIds.length" x-text="tab === 'contacts' ? 'Selecteer contacten met ☑ — organisatie gaat naar CRM' : 'Selecteer rijen met ☑ voor bulk-acties'"></span>
|
||||
</div>
|
||||
<div class="ei-selection-actions">
|
||||
<button type="button" class="btn btn-sm" @click="toggleSelectAllVisible()" title="Alles op lijst">☑ Alles</button>
|
||||
@@ -137,8 +137,8 @@
|
||||
<strong x-text="mapHalalMin"></strong>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ei-filter-group ei-filter-group-collection" x-show="tab === 'map'" x-cloak>
|
||||
<label class="ei-filter-check">
|
||||
<div class="ei-filter-group ei-filter-group-collection" x-show="showCrmFilter()" x-cloak>
|
||||
<label class="ei-filter-check" x-show="tab === 'map'">
|
||||
<input type="checkbox" x-model="filterFavoritesOnly" @change="onFilterCollectionChange()" />
|
||||
<span>★ Alleen favorieten</span>
|
||||
</label>
|
||||
@@ -478,31 +478,67 @@
|
||||
<section x-show="tab==='contacts'" x-cloak>
|
||||
<div class="ei-tab-panel">
|
||||
<div class="ei-tab-panel-head">
|
||||
<div><h2>Contactregistry</h2><p>E-mail · telefoon · outreach-ready</p></div>
|
||||
<a class="btn btn-sm" :href="contactsExportUrl()" target="_blank">CSV export</a>
|
||||
<div><h2>Contactregistry</h2><p>E-mail · telefoon · outreach-ready · direct naar CRM</p></div>
|
||||
<div class="ei-tab-panel-actions">
|
||||
<button type="button" class="btn btn-sm btn-pulse-green" :disabled="!selectedIds.length || crmPushBusy" @click="openCrmModal()">➕ Geselecteerde naar CRM</button>
|
||||
<a class="btn btn-sm" :href="contactsExportUrl()" target="_blank">CSV export</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ei-tab-panel-body">
|
||||
<div class="ei-panel" style="border:none;background:transparent;padding:0">
|
||||
<div class="ei-table-wrap">
|
||||
<table class="ei-table">
|
||||
<thead><tr><th>Organisatie</th><th>Type</th><th>Land</th><th>Rol</th><th>Naam</th><th>E-mail</th><th>Telefoon</th></tr></thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ei-th-check"><input type="checkbox" @change="toggleSelectAllVisible()" title="Alles selecteren" /></th>
|
||||
<th>Organisatie</th>
|
||||
<th>Stad</th>
|
||||
<th>Type</th>
|
||||
<th>Land</th>
|
||||
<th>Rol</th>
|
||||
<th>Naam</th>
|
||||
<th>E-mail</th>
|
||||
<th>Telefoon</th>
|
||||
<th>CRM</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="c in contacts" :key="c.id">
|
||||
<tr>
|
||||
<td x-text="c.entity_name"></td>
|
||||
<td x-text="typeLabel(c.entity_type)"></td>
|
||||
<tr :class="{ 'ei-row-selected': isSelected(c.entity_id), 'ei-row-crm': c.client_id }">
|
||||
<td class="ei-td-check" @click.stop>
|
||||
<input type="checkbox" :checked="isSelected(c.entity_id)" @change="toggleSelect(c.entity_id, $event)" />
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="ei-link-btn" @click="openEntity(c.entity_id)" x-text="c.entity_name"></button>
|
||||
</td>
|
||||
<td x-text="c.entity_city || '—'"></td>
|
||||
<td><span class="ei-type-pill ei-type-pill-sm" :class="'ei-type-'+c.entity_type" x-text="typeLabel(c.entity_type)"></span></td>
|
||||
<td x-text="c.country_iso2"></td>
|
||||
<td x-text="c.role"></td>
|
||||
<td x-text="c.role || '—'"></td>
|
||||
<td x-text="c.name||'—'"></td>
|
||||
<td x-text="c.email||'—'"></td>
|
||||
<td x-text="c.phone||c.mobile||'—'"></td>
|
||||
<td>
|
||||
<span class="ei-crm-badge" x-show="c.client_id" :title="c.crm_client_name || 'In CRM'">CRM</span>
|
||||
<span class="muted" x-show="!c.client_id">—</span>
|
||||
</td>
|
||||
<td class="ei-td-actions" @click.stop>
|
||||
<button type="button" class="btn btn-sm btn-pulse-green" x-show="!c.client_id" @click="pushEntityToCrm(c.entity_id)" :disabled="crmPushBusy">➕ CRM</button>
|
||||
<a class="btn btn-sm" x-show="c.client_id" :href="'/clients'" title="Bekijk in CRM">Open</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="ei-empty" x-show="!contacts.length">
|
||||
<p>Contacten worden aangevuld in fase B (Places, websites, tenders).</p>
|
||||
<div class="ei-load-more" x-show="contactsHasMore">
|
||||
<button type="button" class="btn btn-sm" @click="loadMoreContacts()" :disabled="contactsLoadingMore || busy">
|
||||
<span x-text="contactsLoadingMore ? 'Laden…' : ('Meer laden (' + contacts.length + ' / ' + resultCount + ')')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ei-empty" x-show="!contacts.length && !busy">
|
||||
<p>Geen contacten voor deze filters — pas land, type of zoekterm aan.</p>
|
||||
<button class="btn btn-sm btn-pulse-green" @click="syncKind('contacts')">Sync contacten</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -760,5 +796,5 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/export-intel.js?v=10"></script>
|
||||
<script src="/static/js/export-intel.js?v=11"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -258,5 +258,5 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/revenue-cockpit.js?v=5"></script>
|
||||
<script src="/static/js/revenue-cockpit.js?v=6"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user