Files
foodlinkk-command-center/cockpit/templates/base.html
T

318 lines
20 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=5" />
<meta name="theme-color" content="#0a0e14" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Foodlinkk" />
<title>{% block title %}{{ page_title }} · Cucina{% endblock %}</title>
<link rel="manifest" href="/static/manifest.json?v=2" />
<link rel="icon" href="/static/icons/app-icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/static/icons/app-icon.svg" />
<link rel="stylesheet" href="/static/css/palantir-theme.css?v=2" />
<link rel="stylesheet" href="/static/css/pulse-theme.css?v=2" />
<link rel="stylesheet" href="/static/css/topnav-neo.css?v=6" />
<link rel="stylesheet" href="/static/css/vertical-tabs.css?v=8" />
<link rel="stylesheet" href="/static/css/mobile.css?v=5" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=18" />
<script defer src="/static/js/alpine.min.js?v=2"></script>
<script defer src="/static/js/htmx.min.js?v=2"></script>
{% block extra_head %}{% endblock %}
<link rel="stylesheet" href="/static/css/app-sidebar-neo.css?v=6" />
<link rel="stylesheet" href="/static/css/herman-assistant.css?v=1" />
</head>
<body x-data="appShell()" x-init="initApp()" :class="{ 'nav-open': navOpen, 'drawer-open': drawerOpen, 'brand-cucina': brand === 'cucina', 'brand-foodlinkk': brand === 'foodlinkk' }">
<div class="app-shell">
<aside class="app-sidebar" @click="if($event.target.closest('a')) navOpen=false">
<div class="app-sidebar-brand">
<p class="brand-title">Cucina · Foodlinkk</p>
<p class="brand-name" x-text="brand === 'foodlinkk' ? 'Foodlinkk B2B' : 'Cucina B2C'">Cucina B2C</p>
</div>
<div class="persona-switch brand-switch" role="tablist" aria-label="Brand">
<button type="button" role="tab" class="persona-btn brand-btn-cucina"
:class="{ active: brand === 'cucina' }" @click="setBrand('cucina')"
:aria-selected="brand === 'cucina'">Cucina · B2C</button>
<button type="button" role="tab" class="persona-btn brand-btn-foodlinkk"
:class="{ active: brand === 'foodlinkk' }" @click="setBrand('foodlinkk')"
:aria-selected="brand === 'foodlinkk'">Foodlinkk · B2B</button>
</div>
<div class="persona-nav">
<div class="app-sidebar-group" x-show="brand === 'foodlinkk'">
<div class="app-sidebar-label">Foodlinkk B2B</div>
<a href="/export-intel" class="nav-pill nav-pill-wereldexport {% if request.url.path.startswith('/export-intel') %}active{% endif %}"><span class="sidebar-nav-icon">🌍</span><span class="sidebar-nav-label">Wereldexport</span></a>
<a href="/clients" class="nav-pill nav-pill-clients {% if request.url.path.startswith('/clients') %}active{% endif %}"><span class="sidebar-nav-icon">👥</span><span class="sidebar-nav-label" data-i18n="nav.clients">Klanten</span></a>
<a href="/deals" class="nav-pill nav-pill-deals {% if request.url.path.startswith('/deals') %}active{% endif %}"><span class="sidebar-nav-icon">🤝</span><span class="sidebar-nav-label" data-i18n="nav.deals">Deals</span></a>
<a href="/products" class="nav-pill nav-pill-products {% if request.url.path.startswith('/products') %}active{% endif %}"><span class="sidebar-nav-icon">📦</span><span class="sidebar-nav-label" data-i18n="nav.products">Producten</span></a>
<a href="/suppliers" class="nav-pill nav-pill-suppliers {% if request.url.path.startswith('/suppliers') %}active{% endif %}"><span class="sidebar-nav-icon">🏭</span><span class="sidebar-nav-label" data-i18n="nav.suppliers">Leveranciers</span></a>
<a href="/herman" class="nav-pill nav-pill-chat {% if request.url.path.startswith('/herman') %}active{% endif %}"><span class="sidebar-nav-icon">💬</span><span class="sidebar-nav-label" data-i18n="nav.chat">Herman</span></a>
<a href="/reports" class="nav-pill nav-pill-reports {% if request.url.path.startswith('/reports') %}active{% endif %}"><span class="sidebar-nav-icon">📊</span><span class="sidebar-nav-label" data-i18n="nav.reports">Rapporten</span></a>
</div>
<div class="app-sidebar-group" x-show="brand === 'cucina'">
<div class="app-sidebar-label">Cucina B2C</div>
<a href="/" class="nav-pill nav-pill-herman {% if request.url.path == '/' %}active{% endif %}"><span class="sidebar-nav-icon">🏠</span><span class="sidebar-nav-label" data-i18n="nav.ceo_home">Overzicht</span></a>
<a href="/revenue-cockpit" class="nav-pill nav-pill-deals {% if request.url.path.startswith('/revenue-cockpit') %}active{% endif %}"><span class="sidebar-nav-icon">💰</span><span class="sidebar-nav-label" data-i18n="nav.revenue_cockpit">Revenue Cockpit</span></a>
<a href="/retail" class="nav-pill nav-pill-retail {% if request.url.path.startswith('/retail') %}active{% endif %}"><span class="sidebar-nav-icon">🛒</span><span class="sidebar-nav-label" data-i18n="nav.retail">Retail NL</span></a>
<a href="/marketing" class="nav-pill nav-pill-marketing {% if request.url.path.startswith('/marketing') %}active{% endif %}"><span class="sidebar-nav-icon">📣</span><span class="sidebar-nav-label" data-i18n="nav.marketing">Marketing</span></a>
<a href="/packaging" class="nav-pill nav-pill-studio {% if request.url.path.startswith('/packaging') %}active{% endif %}"><span class="sidebar-nav-icon">📦</span><span class="sidebar-nav-label" data-i18n="nav.packaging">Packaging</span></a>
<a href="/beurs" class="nav-pill nav-pill-beurs {% if request.url.path.startswith('/beurs') %}active{% endif %}"><span class="sidebar-nav-icon">🎪</span><span class="sidebar-nav-label" data-i18n="nav.beurs">Beurs Live</span></a>
<a href="/documents" class="nav-pill nav-pill-documents {% if request.url.path.startswith('/documents') %}active{% endif %}"><span class="sidebar-nav-icon">📄</span><span class="sidebar-nav-label" data-i18n="nav.documents">Documenten</span></a>
<a href="/projects" class="nav-pill nav-pill-documents {% if request.url.path.startswith('/projects') %}active{% endif %}"><span class="sidebar-nav-icon">📁</span><span class="sidebar-nav-label" data-i18n="nav.projects">Projecten</span></a>
<a href="/hermes" class="nav-pill nav-pill-telegram {% if request.url.path.startswith('/hermes') %}active{% endif %}"><span class="sidebar-nav-icon">✈️</span><span class="sidebar-nav-label" data-i18n="nav.telegram">Telegram</span></a>
<a href="/analytics" class="nav-pill nav-pill-analytics {% if request.url.path.startswith('/analytics') %}active{% endif %}"><span class="sidebar-nav-icon">📈</span><span class="sidebar-nav-label" data-i18n="nav.analytics">Analytics</span></a>
</div>
<div class="app-sidebar-group">
<div class="app-sidebar-label" data-i18n="nav.platform">Platform</div>
<a href="/ops" class="nav-pill nav-pill-ops {% if request.url.path.startswith('/ops') %}active{% endif %}"><span class="sidebar-nav-icon">⚙️</span><span class="sidebar-nav-label" data-i18n="nav.ops">IT Ops</span></a>
<a href="/agents" class="nav-pill nav-pill-agents {% if request.url.path.startswith('/agents') %}active{% endif %}"><span class="sidebar-nav-icon">🤖</span><span class="sidebar-nav-label" data-i18n="nav.agents">Agents</span></a>
<a href="/browser" class="nav-pill nav-pill-browser {% if request.url.path.startswith('/browser') or request.url.path.startswith('/monitor') %}active{% endif %}"><span class="sidebar-nav-icon">🌐</span><span class="sidebar-nav-label" data-i18n="nav.browser">Browser</span></a>
<a href="/studio" class="nav-pill nav-pill-studio {% if request.url.path.startswith('/studio') %}active{% endif %}"><span class="sidebar-nav-icon">🎨</span><span class="sidebar-nav-label" data-i18n="nav.studio">Studio</span></a>
<a href="/voice" class="nav-pill nav-pill-voice {% if request.url.path.startswith('/voice') %}active{% endif %}"><span class="sidebar-nav-icon">🎙️</span><span class="sidebar-nav-label" data-i18n="nav.voice">Voice</span></a>
<a href="/settings" class="nav-pill nav-pill-settings {% if request.url.path.startswith('/settings') %}active{% endif %}"><span class="sidebar-nav-icon">🔧</span><span class="sidebar-nav-label" data-i18n="nav.settings">Instellingen</span></a>
</div>
</div>
</aside>
<div class="mobile-nav-overlay" x-show="navOpen" @click="navOpen=false" x-cloak></div>
<div class="app-main">
<header class="app-topbar">
<button type="button" class="mobile-menu-btn" @click="navOpen = !navOpen" aria-label="Menu openen"></button>
<span class="page-tip">
<span class="persona-badge" :class="brand === 'foodlinkk' ? 'brand-badge-foodlinkk' : 'brand-badge-cucina'" x-text="brand === 'foodlinkk' ? 'B2B' : 'B2C'"></span>
Cucina · Foodlinkk
</span>
2026-06-09 10:41:13 +00:00
<select class="form-input" style="width:auto;min-width:110px;padding:0.2rem 0.5rem;font-size:0.72rem;margin:0 0.5rem"
x-model="locale" @change="setLocale(locale)" :title="$t ? $t('common.language') : 'Taal'">
<option value="nl">NL</option>
<option value="en">EN</option>
</select>
<div class="app-clock"><strong x-text="clockDate"></strong> · <span x-text="clockTime"></span></div>
</header>
<main class="main-topnav">{% block content %}{% endblock %}</main>
</div>
</div>
<div class="pwa-install-bar" x-show="showInstall" x-cloak>
<p>📱 Installeer Foodlinkk als app op je telefoon</p>
<button type="button" class="btn btn-sm btn-pulse-green" @click="installPwa()">Installeren</button>
<button type="button" class="btn btn-sm" @click="showInstall=false">Later</button>
</div>
<!-- Herman browser assistant (elke pagina) -->
<div x-data="hermanBrowserWidget()" x-init="initWidget()" x-show="!hidden" x-cloak>
<button type="button" class="ha-fab" :class="{ 'is-open': open }" @click="toggleOpen()" title="Praat met Herman" aria-label="Herman assistant">💬</button>
<div class="ha-panel" x-show="open" x-transition @click.outside="open = false">
<header class="ha-head">
<h2>Herman <span style="font-size:0.65rem;color:#a78bfa">browser</span></h2>
<div class="ha-head-actions">
<a href="/voice">Voice live</a>
<button type="button" @click="open = false" aria-label="Sluiten">×</button>
</div>
</header>
<div class="ha-confirm" x-show="pendingAction">
<span x-text="pendingAction ? (pendingAction.type === 'webbuilder_build' ? ('Agy website: ' + (pendingAction.project || '')) : ('Zoeken: ' + (pendingAction.entity_label || '') + ' in ' + (pendingAction.location_label || ''))) : ''"></span>
<div class="ha-confirm-actions">
<button type="button" class="btn btn-sm btn-pulse" @click="confirmPending()" :disabled="busy">Ja</button>
<button type="button" class="btn btn-sm" @click="cancelPending()" :disabled="busy">Nee</button>
</div>
</div>
<div class="ha-log" x-ref="chatLog">
<template x-for="(t, i) in turns" :key="i">
<div class="ha-bubble" :class="t.role === 'user' ? 'ha-bubble-user' : 'ha-bubble-agent'">
<div class="ha-bubble-meta" x-text="t.at + (t.role === 'user' ? ' · jij' : ' · Herman')"></div>
<p x-text="t.text"></p>
</div>
</template>
<p x-show="!turns.length" class="page-tip" style="font-size:0.75rem;margin:0">Typ of spreek — bv. “zoek distributeurs in Dubai”</p>
</div>
<div class="ha-preview" :class="{ 'is-interim': liveInterim }" x-show="livePreview || recording" x-text="livePreview || 'Luisteren…'"></div>
<p class="ha-status" x-show="statusDetail" x-text="statusDetail"></p>
<div class="ha-input-row">
<button type="button" class="ha-mic" :class="micClass()"
@mousedown.prevent="onMicDown()" @mouseup.prevent="onMicUp()"
@touchstart.prevent="onMicDown()" @touchend.prevent="onMicUp()"
@click.prevent="onMicClick()" title="Microfoon">🎙️</button>
<input class="form-input" type="text" placeholder="Vraag aan Herman…" x-model="manualText"
@keydown.enter.prevent="sendText()" :disabled="busy" />
<button type="button" class="btn btn-sm btn-pulse" @click="sendText()" :disabled="busy || !manualText.trim()"></button>
</div>
</div>
<div class="ei-modal-root ha-results-modal" x-show="resultsOpen" @click.self="closeResultsModal()">
<div class="ei-modal-card ha-results-card" @click.stop>
<h3 x-text="resultsTitle"></h3>
<p><strong x-text="resultsTotal"></strong> resultaten</p>
<div class="voice-results-table-wrap" style="max-height:40vh;overflow:auto">
<table class="voice-results-table" style="width:100%;font-size:0.78rem">
<thead><tr><th>Naam</th><th>Type</th><th>Stad</th></tr></thead>
<tbody>
<template x-for="(e, i) in resultsEntities" :key="i">
<tr>
<td><a :href="'/export-intel?open=' + e.id" x-text="e.name"></a></td>
<td x-text="entityTypeLabel(e.entity_type)"></td>
<td x-text="e.city || '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="ei-modal-actions">
<a class="btn btn-sm" :href="resultsOpenUrl">Wereldexport</a>
<button type="button" class="btn btn-sm" @click="closeResultsModal()">Sluiten</button>
</div>
</div>
</div>
<div class="ei-modal-root ha-results-modal" x-show="webbuilderOpen" @click.self="closeWebbuilderModal()">
<div class="ei-modal-card ha-results-card" @click.stop>
<h3 x-text="webbuilderTitle"></h3>
<p><strong x-text="webbuilderProject"></strong> · Agy / Antigravity op Hermes</p>
<p class="page-tip" style="font-size:0.78rem">Preview na afloop: <a :href="webbuilderPreview" target="_blank" rel="noopener" x-text="webbuilderPreview"></a></p>
<p class="page-tip" x-show="webbuilderNas" style="font-size:0.72rem" x-text="'NAS: ' + webbuilderNas"></p>
<div class="ei-modal-actions">
<a class="btn btn-sm btn-pulse" :href="webbuilderAgentsUrl">Agents terminal</a>
<a class="btn btn-sm" :href="webbuilderPreview" target="_blank" rel="noopener">Preview</a>
<button type="button" class="btn btn-sm" @click="closeWebbuilderModal()">Sluiten</button>
</div>
</div>
</div>
</div>
<script defer src="/static/js/herman-assistant.js?v=2"></script>
<script src="/static/js/i18n.js?v=5"></script>
2026-06-09 10:41:13 +00:00
<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=3"></script>
<script>
window.FoodlinkkBrand = (function () {
var FOODLINKK_PREFIXES = ['/export-intel', '/foodlinkk', '/clients', '/deals', '/products', '/suppliers'];
var CUCINA_PREFIXES = ['/retail', '/marketing', '/packaging', '/revenue-cockpit', '/beurs'];
function isFoodlinkkPath(path) {
return FOODLINKK_PREFIXES.some(function (p) { return path === p || path.indexOf(p + '/') === 0; });
}
function isCucinaPath(path) {
return path === '/' || CUCINA_PREFIXES.some(function (p) { return path === p || path.indexOf(p + '/') === 0; });
}
function detect(path) {
var stored = localStorage.getItem('foodlinkk-brand');
if (stored === 'cucina' || stored === 'foodlinkk') return stored;
if (isFoodlinkkPath(path)) return 'foodlinkk';
return 'cucina';
}
return { isFoodlinkkPath: isFoodlinkkPath, isCucinaPath: isCucinaPath, detect: detect };
})();
/* legacy persona helper → brand */
window.FoodlinkkPersona = window.FoodlinkkBrand;
function appShell() {
return {
2026-06-09 10:41:13 +00:00
locale: 'nl',
brand: 'cucina',
drawerOpen: false,
navOpen: false,
showInstall: false,
_deferredPrompt: null,
clockDate: '—',
clockTime: '—',
_offset: 0,
2026-06-09 10:41:13 +00:00
async initApp() {
this.brand = window.FoodlinkkBrand.detect(location.pathname);
if (!localStorage.getItem('flk-cache-v3')) {
if ('serviceWorker' in navigator) {
const regs = await navigator.serviceWorker.getRegistrations();
await Promise.all(regs.map((r) => r.unregister()));
}
if (window.caches) {
const keys = await caches.keys();
await Promise.all(keys.map((k) => caches.delete(k)));
}
localStorage.setItem('flk-cache-v3', '1');
}
2026-06-09 10:41:13 +00:00
if (window.I18n) await window.I18n.init();
this.locale = window.I18n ? window.I18n.locale() : 'nl';
this.initClock();
this.initPwa();
},
setBrand(b) {
if (b === this.brand) return;
this.brand = b;
localStorage.setItem('foodlinkk-brand', b);
var path = location.pathname;
if (b === 'foodlinkk' && window.FoodlinkkBrand.isCucinaPath(path) && !window.FoodlinkkBrand.isFoodlinkkPath(path)) {
location.href = '/export-intel';
return;
}
if (b === 'cucina' && window.FoodlinkkBrand.isFoodlinkkPath(path)) {
location.href = '/';
return;
}
},
2026-06-09 10:41:13 +00:00
async setLocale(loc) {
if (window.I18n) await window.I18n.setLocale(loc);
this.locale = loc;
},
async initClock() {
try {
const r = await fetch('/api/server-time').then(x => x.json());
this._offset = new Date(r.utc).getTime() - Date.now();
} catch (e) { this._offset = 0; }
const tick = () => {
const d = new Date(Date.now() + this._offset);
this.clockDate = d.toLocaleDateString('nl-NL', { timeZone: 'Europe/Amsterdam', weekday: 'short', day: 'numeric', month: 'short' });
this.clockTime = d.toLocaleTimeString('nl-NL', { timeZone: 'Europe/Amsterdam', hour: '2-digit', minute: '2-digit', second: '2-digit' });
};
tick();
setInterval(tick, 1000);
},
initPwa() {
if (window.matchMedia('(display-mode: standalone)').matches) return;
if (/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
this.showInstall = !localStorage.getItem('pwa-dismiss');
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
this._deferredPrompt = e;
this.showInstall = true;
});
},
async installPwa() {
if (this._deferredPrompt) {
this._deferredPrompt.prompt();
await this._deferredPrompt.userChoice;
this._deferredPrompt = null;
this.showInstall = false;
return;
}
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
alert('Tik op Deel ↗ en kies «Zet op beginscherm» om Foodlinkk als app te installeren.');
} else {
alert('Tik op ⋮ menu → «Toevoegen aan startscherm» of «Installeren».');
}
this.showInstall = false;
localStorage.setItem('pwa-dismiss', '1');
}
};
}
</script>
<script>
window.addEventListener('load', function () {
setTimeout(function () {
if (window.Alpine) return;
document.querySelectorAll('[x-cloak]').forEach(function (el) { el.removeAttribute('x-cloak'); });
document.body.classList.add('alpine-missing');
console.error('Alpine.js niet geladen — interactieve UI beperkt. Hard refresh (Ctrl+Shift+R).');
}, 2500);
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>