Improve CEO cockpit clarity and Revenue visuals, with Hermes briefing.

Tighten overview KPIs/pipeline height, add selectable Revenue views and a resizable sidebar, and keep supporting Hermes/Wereldexport updates.
This commit is contained in:
Aissa
2026-07-30 11:53:43 +00:00
parent 5dd69a1138
commit 62239f0a4e
14 changed files with 1876 additions and 647 deletions
+35 -3
View File
@@ -20,17 +20,19 @@
<link rel="stylesheet" href="/static/css/topnav-neo.css?v=6" />
<link rel="stylesheet" href="/static/css/vertical-tabs.css?v=9" />
<link rel="stylesheet" href="/static/css/mobile.css?v=5" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=22" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=33" />
<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=10" />
<link rel="stylesheet" href="/static/css/app-sidebar-neo.css?v=11" />
<link rel="stylesheet" href="/static/css/cockpit-typography.css?v=1" />
<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">
<aside class="app-sidebar" :style="'--sidebar-width:' + sidebarWidth + 'px'" @click="if($event.target.closest('a')) navOpen=false">
<div class="app-sidebar-resizer" title="Sleep om breder te maken"
@mousedown.prevent="startSidebarResize($event)"></div>
<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>
@@ -222,6 +224,7 @@
brand: 'cucina',
drawerOpen: false,
navOpen: false,
sidebarWidth: 280,
showInstall: false,
_deferredPrompt: null,
clockDate: '—',
@@ -242,9 +245,38 @@
}
if (window.I18n) await window.I18n.init();
this.locale = window.I18n ? window.I18n.locale() : 'nl';
this.initSidebarWidth();
this.initClock();
this.initPwa();
},
initSidebarWidth() {
var saved = parseInt(localStorage.getItem('flk-sidebar-width') || '280', 10);
if (!isFinite(saved)) saved = 280;
this.sidebarWidth = Math.min(420, Math.max(220, saved));
document.documentElement.style.setProperty('--sidebar-width', this.sidebarWidth + 'px');
},
startSidebarResize(e) {
var self = this;
var startX = e.clientX;
var startW = this.sidebarWidth;
var handle = e.currentTarget;
handle.classList.add('is-dragging');
document.body.classList.add('sidebar-resizing');
function onMove(ev) {
var next = Math.min(420, Math.max(220, startW + (ev.clientX - startX)));
self.sidebarWidth = next;
document.documentElement.style.setProperty('--sidebar-width', next + 'px');
}
function onUp() {
handle.classList.remove('is-dragging');
document.body.classList.remove('sidebar-resizing');
localStorage.setItem('flk-sidebar-width', String(self.sidebarWidth));
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
},
setBrand(b) {
if (b === this.brand) return;
this.brand = b;
+71 -104
View File
@@ -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=23" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=33" />
{% endblock %}
{% block content %}
<div class="herman-shell hm-neo">
@@ -12,7 +12,7 @@
<div>
<p class="hm-hero-eyebrow">Foodlinkk Command Center</p>
<h1>CEO Dashboard <span class="live-badge hm-live-badge">Live</span></h1>
<p class="hero-sub">Cucina B2C · retail · pipeline · Herman briefing</p>
<p class="hero-sub">Foodlinkk B2B · Cucina B2C</p>
</div>
<div class="hm-hero-status" id="hero-live-status">
<span class="hm-live-dot"></span>
@@ -20,14 +20,9 @@
</div>
</div>
<div class="hm-hero-links beurs-mini-row">
<a href="/clients" class="beurs-mini-chip">👥 Klanten</a>
<a href="/deals" class="beurs-mini-chip">💼 Deals</a>
<a href="/packaging" class="beurs-mini-chip">📐 Packaging</a>
<a href="/marketing?tab=publish" class="beurs-mini-chip">🚀 Automatisering</a>
<a href="/marketing?tab=reclame" class="beurs-mini-chip">📁 Reclame folders</a>
<a href="/projects" class="beurs-mini-chip">📁 Projecten</a>
<a href="/retail" class="beurs-mini-chip">🛒 Retail</a>
<a href="/analytics" class="beurs-mini-chip">📊 Analytics</a>
<a href="/clients" class="beurs-mini-chip">Klanten</a>
<a href="/deals" class="beurs-mini-chip">Deals</a>
<a href="/revenue-cockpit" class="beurs-mini-chip">Revenue</a>
</div>
</div>
@@ -49,12 +44,9 @@
<div class="briefing-meta" id="briefing-meta"></div>
</div>
<div class="herman-briefing-actions">
<button type="button" class="btn btn-sm" :class="editLayout ? 'btn-pulse' : ''" @click="toggleEditLayout()" x-text="editLayout ? '✓ Klaar' : '⚙ Dashboard instellen'"></button>
<button type="button" class="btn btn-pulse" @click="refreshLive()" :disabled="loading">↻ Live data</button>
<button type="button" class="btn btn-pulse-green" @click="generate()" :disabled="loading">✨ Genereer dagrapport</button>
<a href="/clients" class="btn btn-pulse-green">CRM Clients</a>
<a href="/marketing?tab=reclame" class="btn btn-pulse-purple">Reclame folders</a>
<a href="/analytics" class="btn">Analytics</a>
<button type="button" class="btn btn-pulse-green" @click="generate()" :disabled="loading">Genereer briefing</button>
<button type="button" class="btn btn-pulse" @click="refreshLive()" :disabled="loading">Live data</button>
<button type="button" class="btn btn-sm" :class="editLayout ? 'btn-pulse' : ''" @click="toggleEditLayout()" x-text="editLayout ? 'Klaar' : 'Indeling'"></button>
</div>
</header>
@@ -76,105 +68,79 @@
</ol>
</div>
<div id="viz-toolbar" class="viz-toolbar"></div>
<div id="viz-toolbar" class="viz-toolbar" x-show="editLayout" x-cloak></div>
<div id="dashboard-customize" class="hm-customize-wrap" x-show="editLayout" x-cloak></div>
<div id="briefing-dashboard" class="hm-dash-layout" :class="editLayout ? 'hm-dash-editing' : ''" x-show="hermanTab === 'overview'">
<div id="briefing-dashboard" class="hm-dash-layout hm-ceo-clean hm-ceo-pro" :class="editLayout ? 'hm-dash-editing' : ''" x-show="hermanTab === 'overview'">
<section class="hm-dash-section hm-dash-full" data-widget="kpis">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · CEO KPI's</div>
<h3 class="hm-section-title"><span class="hm-live-dot"></span> CEO KPI's <small class="hm-widget-hint" x-show="editLayout">— kies KPI's hieronder</small></h3>
<section class="hm-dash-section hm-dash-full hm-ceo-kpis" data-widget="kpis">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · KPI's</div>
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Kerncijfers</h3>
<div id="briefing-kpis" class="hm-neo-kpi-row"></div>
</section>
<section class="hm-dash-section hm-dash-full" data-widget="executive">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Executive</div>
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Alles op een rij</h3>
<div class="hm-chart-panel">
<div class="hm-chart-head"><h4>Executive samenvatting</h4><small>live database</small></div>
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Overzicht</div>
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Overzicht</h3>
<div class="hm-chart-panel hm-overview-panel">
<div class="hm-chart-head"><h4>Waar staan we</h4><small>live context</small></div>
<div id="briefing-executive-summary"></div>
</div>
<div id="briefing-clients-mini" class="beurs-mini-row" style="margin-top:0.75rem"></div>
</section>
<section class="hm-dash-section hm-dash-full" data-widget="briefing">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Briefing</div>
<h3 class="hm-section-title">Herman briefing</h3>
<div class="briefing-summary-grid">
<div class="briefing-card summary">
<h4>Samenvatting · live</h4>
<div id="briefing-summary-text" class="live-digest-body"></div>
</div>
<div class="briefing-card hm-activity-card">
<div class="hm-card-head">
<h4>Activiteitenlog</h4>
<a href="/agents" class="hm-card-link">Agents →</a>
</div>
<ul id="briefing-activity-log" class="hm-activity-timeline"></ul>
</div>
<div class="briefing-horizon-col">
<div class="horizon-card short">
<h4>⚡ Korte termijn</h4>
<ul id="briefing-actions-list"></ul>
</div>
<div class="horizon-card long">
<h4>🎯 Lange termijn</h4>
<ul id="briefing-longterm-list"></ul>
</div>
</div>
</div>
</section>
<section class="hm-dash-section" data-widget="retail">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Retail</div>
<h3 class="hm-section-title">Retail operatie</h3>
<div class="hm-dash-two-col">
<div class="hm-chart-panel">
<div class="hm-chart-head"><h4>Trending food retail</h4><small><a href="/marketing">Marketing Hub →</a></small></div>
<div id="briefing-retail"></div>
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>
<div class="hm-chart-panel">
<div class="hm-chart-head"><h4>Sales milestones</h4><small>live DB</small></div>
<div id="briefing-milestones"></div>
</div>
</div>
<div class="hm-chart-panel hm-dash-trends-only" style="margin-top:1rem">
<div class="hm-chart-head"><h4>Markt highlights</h4><small>M&A · retail</small></div>
<div id="briefing-food-highlights" class="hm-highlight-grid"></div>
<div class="hm-ceo-brand-grid" style="margin-top:0.9rem">
<article class="hm-ceo-brand-card brand-foodlinkk" id="ceo-b2b">
<header><span class="hm-ceo-brand-icon">🌍</span><h3>Foodlinkk B2B</h3></header>
<ul class="hm-ceo-brand-list"><li class="muted">Laden…</li></ul>
</article>
<article class="hm-ceo-brand-card brand-cucina" id="ceo-b2c">
<header><span class="hm-ceo-brand-icon">🍽️</span><h3>Cucina B2C</h3></header>
<ul class="hm-ceo-brand-list"><li class="muted">Laden…</li></ul>
</article>
</div>
</section>
<section class="hm-dash-section hm-dash-full hm-dash-analytics" data-widget="analytics">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Analytics</div>
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Data & analytics</h3>
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Pipeline</div>
<div class="hm-neo-charts">
<div class="hm-chart-panel hm-analytics-panel">
<div class="hm-chart-head"><h4>Pipeline</h4><small>live deals</small></div>
<div class="hm-chart-wrap"><canvas id="chart-pipeline"></canvas></div>
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>
<div class="hm-chart-panel hm-analytics-panel page-viz-panel viz-mode-chart">
<div class="hm-chart-head"><h4>NAS sentiment</h4><small id="viz-label-sentiment">neo-bars</small></div>
<div class="page-viz-body">
<div class="hm-chart-wrap page-viz-canvas"><canvas id="chart-sentiment"></canvas></div>
<div id="viz-sentiment-alt" class="viz-alt"></div>
</div>
</div>
<div class="hm-chart-panel hm-analytics-panel">
<div class="hm-chart-head"><h4>NAS top woorden</h4><small>corpus</small></div>
<div class="hm-chart-wrap"><canvas id="chart-words"></canvas></div>
<div class="hm-eq hm-eq-cyan"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
<div class="hm-chart-head"><h4>Pipeline</h4><small id="ceo-pipeline-sub">revenue + CRM</small></div>
<div class="hm-chart-wrap hm-pipeline-host"><canvas id="chart-pipeline" hidden></canvas></div>
</div>
</div>
<!-- hidden stubs so older renderers don't throw -->
<canvas id="chart-sentiment" hidden></canvas>
<div id="viz-sentiment-alt" hidden></div>
<canvas id="chart-words" hidden></canvas>
</section>
<div class="hub-grid hm-dash-feed" data-widget="feed">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Feed</div>
<section class="panel hm-dash-full">
<h2>Projecten <a href="/projects" class="btn btn-sm" style="float:right">Alle projecten →</a></h2>
<div id="dash-projects-mini"><p class="muted">Laden…</p></div>
</section>
<section class="hm-dash-section hm-dash-full hm-ceo-mid" data-widget="briefing">
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Briefing</div>
<h3 class="hm-section-title">Herman briefing</h3>
<div class="hm-ceo-mid-grid">
<article class="hm-ceo-panel hm-ceo-focus">
<header><h3>🎯 Focus vandaag</h3></header>
<ul id="ceo-focus-list" class="hm-ceo-action-list"><li class="muted">Laden…</li></ul>
</article>
<article class="hm-ceo-panel hm-ceo-trends">
<header><h3>📈 Trends</h3></header>
<ul id="ceo-trends-list" class="hm-ceo-trend-list"><li class="muted">Laden…</li></ul>
</article>
</div>
<details class="hm-ceo-memo" id="ceo-memo-wrap">
<summary class="hm-ceo-memo-label">Dagmemo <small>klik om te openen</small></summary>
<div id="briefing-summary-text" class="hm-ceo-memo-body"></div>
</details>
</section>
<section class="hm-dash-section hm-dash-full hm-ceo-news" data-widget="news">
<details class="hm-ceo-news-details">
<summary>📰 Laatste nieuws <small>max 5 · <a href="/?tab=rss" @click.prevent="setHermanTab('rss')">alle RSS →</a></small></summary>
<div id="ceo-news-list" class="hm-ceo-news-list"></div>
</details>
</section>
<div class="hub-grid hm-dash-feed" data-widget="feed" hidden>
<div id="dash-projects-mini"></div>
</div>
</div>
@@ -263,8 +229,8 @@
{% endblock %}
{% 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=19"></script>
<script src="/static/js/dashboard-layout.js?v=11"></script>
<script src="/static/js/briefing-charts.js?v=29"></script>
<script>
localStorage.setItem('foodlinkk-persona', 'ceo');
const INITIAL_BRIEFING = {{ briefing_payload | tojson }};
@@ -282,7 +248,11 @@ function dashboardBriefing() {
_prefs: null, _setDragMode: null, _saveTimer: null,
applyDashboardPrefs(prefs) {
this._prefs = prefs;
window._dashboardKpis = prefs.dashboard_kpis || DashboardLayout.DEFAULT_KPIS;
window._dashboardKpis = (DashboardLayout.ceoKpis ? DashboardLayout.ceoKpis(prefs) : (prefs.dashboard_kpis || DashboardLayout.DEFAULT_KPIS));
if (!localStorage.getItem('rc-kpi-migrated-v2')) {
DashboardLayout.saveDashboardConfig(null, null, window._dashboardKpis);
localStorage.setItem('rc-kpi-migrated-v2', '1');
}
const dash = document.getElementById('briefing-dashboard');
const order = DashboardLayout.visibleLayout(prefs);
DashboardLayout.applyOrder(dash, order);
@@ -359,7 +329,7 @@ function dashboardBriefing() {
this._prefs = prefs;
this._vizMode = prefs.global_viz_mode || 'neo-bars';
window._dashboardVizMode = this._vizMode;
window._dashboardKpis = prefs.dashboard_kpis || DashboardLayout.DEFAULT_KPIS;
window._dashboardKpis = (DashboardLayout.ceoKpis ? DashboardLayout.ceoKpis(prefs) : (prefs.dashboard_kpis || DashboardLayout.DEFAULT_KPIS));
const dash = document.getElementById('briefing-dashboard');
this.applyDashboardPrefs(prefs);
this._setDragMode = DashboardLayout.initDrag(dash, (order) => {
@@ -391,7 +361,8 @@ function dashboardBriefing() {
await this.loadProjectsMini();
if (this.liveDigest) {
BriefingCharts.renderLiveSummary(this.liveDigest, false);
} else if (this.content) {
}
if (this.content) {
BriefingCharts.renderText(dash, this.content, this.createdAt);
}
window._refreshBriefingLive = (t) => this.refreshLive(t);
@@ -405,8 +376,6 @@ function dashboardBriefing() {
await fetch('/api/retail/rss/refresh', { method: 'POST' });
await this.loadRssTab();
if (this._stats) {
BriefingCharts.renderRetail(document.getElementById('briefing-retail'), this._stats);
BriefingCharts.renderFoodHighlights(document.getElementById('briefing-food-highlights'), this._stats);
BriefingCharts.renderKpis(document.getElementById('briefing-kpis'), this._stats, window._dashboardKpis);
}
if (toast) Cockpit.toast('RSS bijgewerkt (' + (this.rssTotal || 0) + ' items)', 'success');
@@ -509,7 +478,6 @@ function dashboardBriefing() {
} catch (e) {}
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 || []);
this.updateLiveMeta(r.at, this.liveDigest);
}
if (toast !== false) Cockpit.toast('Live data bijgewerkt', 'success');
@@ -567,7 +535,6 @@ function dashboardBriefing() {
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 || []);
document.getElementById('briefing-meta').textContent = 'Rapport: ' + this.createdAt.substring(0, 19).replace('T', ' ') + ' UTC';
Cockpit.toast('Dagrapport klaar!', 'success');
} catch (e) {
+141 -63
View File
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}Revenue Cockpit · Cucina{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/revenue-cockpit.css?v=6" />
<link rel="stylesheet" href="/static/css/revenue-cockpit.css?v=12" />
{% endblock %}
{% block content %}
<div class="rc-page" :class="{ 'rc-mode-sheet': viewMode === 'sheet' }" x-data="revenueCockpit()" x-init="init()">
@@ -41,17 +41,36 @@
<span class="rc-sum-label">Deals</span>
<strong x-text="filteredProjects().length + '/' + projects.length"></strong>
</div>
<div class="rc-visual-toggle" role="group" aria-label="Weergave">
<button type="button" class="rc-vis-btn" :class="{ active: visualMode === 'table' }" @click="setVisualMode('table')">Tabel</button>
<button type="button" class="rc-vis-btn" :class="{ active: visualMode === 'bars' }" @click="setVisualMode('bars')">Staven</button>
<button type="button" class="rc-vis-btn" :class="{ active: visualMode === 'cards' }" @click="setVisualMode('cards')">Kaarten</button>
</div>
<input type="search" class="rc-filter-search rc-filter-inline" placeholder="Zoek deal…" x-model="filterQ" />
<div class="rc-style-dots">
<button type="button" class="rc-dot-btn" :class="{ active: !filterStyle }" @click="filterStyle = ''" title="Alle">○</button>
<template x-for="s in styleFilters" :key="'d-' + s.id">
<button type="button" class="rc-dot-btn" :class="['rc-dot-' + s.id, { active: filterStyle === s.id }]"
@click="filterStyle = filterStyle === s.id ? '' : s.id" :title="s.label"></button>
<div class="rc-style-chips">
<button type="button" class="rc-chip" :class="{ active: !filterStyle }" @click="filterStyle = ''">Alle</button>
<template x-for="s in styleFilters" :key="'c-' + s.id">
<button type="button" class="rc-chip" :class="['rc-chip-' + s.id, { active: filterStyle === s.id }]"
@click="filterStyle = filterStyle === s.id ? '' : s.id"
x-text="s.label"></button>
</template>
</div>
</div>
<div class="rc-visual-wrap">
<div class="rc-goals-banner" x-show="goals.vision_text || goals.horizon_text || goals.tagline">
<span x-text="goals.vision_text"></span>
<span class="rc-goals-sep" x-show="goals.horizon_text">·</span>
<span x-text="goals.horizon_text"></span>
<span class="rc-goals-sep" x-show="goals.mid_text">·</span>
<span x-text="goals.mid_text"></span>
<em x-show="goals.tagline" x-text="goals.tagline"></em>
</div>
<!-- TABEL -->
<div class="rc-visual-wrap" x-show="visualMode === 'table'" x-cloak>
<table class="rc-visual-table" cellspacing="0" cellpadding="0">
<colgroup>
<col class="rc-col-deal" />
@@ -60,15 +79,6 @@
<col class="rc-col-donut" />
<col class="rc-col-steps" />
</colgroup>
<!-- Excel rij 1: goals -->
<tr class="rc-visual-goals">
<td class="rc-vcell" x-text="goals.vision_text || '—'"></td>
<td class="rc-vcell" x-text="goals.horizon_text || '—'"></td>
<td class="rc-vcell" x-text="goals.mid_text || '—'"></td>
<td class="rc-vcell rc-vtagline" colspan="2" x-text="goals.tagline || '—'"></td>
</tr>
<tr class="rc-visual-head">
<td class="rc-vcell">Deal</td>
<td class="rc-vcell rc-num">Marge mnd</td>
@@ -76,84 +86,152 @@
<td class="rc-vcell rc-num">Aandeel</td>
<td class="rc-vcell">Next steps</td>
</tr>
<template x-for="row in visualRows()" :key="row.source_row + '-' + row.name">
<template x-for="row in visualRows()" :key="'t-' + row.source_row + '-' + row.name">
<tr class="rc-visual-row"
:class="['rc-style-' + (row.row_style || 'white'), 'rc-brand-' + (row.brand || 'none'), { 'rc-row-open': expandedKey === projectKey(row) }]"
:class="['rc-style-' + (row.row_style || 'white'), { 'rc-row-open': selectedKey === projectKey(row) }]"
@click="toggleExpand(row)">
<td class="rc-vcell rc-vdeal">
<span class="rc-deal-name" x-text="row.name"></span>
<span class="rc-brand-tag" x-show="row.logoCell === 'cucina'">Cucina</span>
<span class="rc-brand-tag rc-brand-tag-fl" x-show="row.logoCell === 'foodlinkk'">Foodlinkk</span>
</td>
<td class="rc-vcell rc-vnum">
<span class="rc-vamount" x-text="row.margin_month != null ? fmtEuroCompact(row.margin_month) : '—'"></span>
<div class="rc-mini-bar" x-show="row.margin_month">
<div class="rc-mini-bar-fill" :style="'width:' + marginBarPct(row, 'margin_month') + '%;background:' + styleColor(row.row_style)"></div>
</div>
</td>
<td class="rc-vcell rc-vnum">
<span class="rc-vamount" x-text="row.margin_year != null ? fmtEuroCompact(row.margin_year) : '—'"></span>
<div class="rc-mini-bar" x-show="row.margin_year">
<div class="rc-mini-bar-fill rc-mini-bar-year" :style="'width:' + marginBarPct(row, 'margin_year') + '%'"></div>
</div>
</td>
<td class="rc-vcell rc-vdonut">
<div class="rc-donut" x-show="row.margin_month" :style="donutStyle(row)">
<span class="rc-donut-label" x-text="sharePct(row) + '%'"></span>
</div>
<span class="rc-donut-empty" x-show="!row.margin_month">—</span>
</td>
<td class="rc-vcell rc-vsteps">
<span x-text="truncate(row.next_steps, 72) || '—'"></span>
<span class="rc-expand-hint" x-show="row.next_steps && row.next_steps.length > 72">↵</span>
</td>
</tr>
<tr class="rc-expand-row" x-show="expandedKey === projectKey(row)" x-cloak>
<td class="rc-expand-cell" colspan="5">
<div class="rc-expand-inner">
<div class="rc-expand-chart">
<div class="rc-bar-chart">
<div class="rc-bar-col">
<div class="rc-bar-stack" :style="'height:' + marginBarPct(row, 'margin_month') + '%'"></div>
<span>Mnd</span>
</div>
<div class="rc-bar-col">
<div class="rc-bar-stack rc-bar-year" :style="'height:' + marginBarPct(row, 'margin_year') + '%'"></div>
<span>Jr</span>
</div>
</div>
<div class="rc-expand-stats">
<div><span>Marge mnd</span><strong x-text="fmtEuro(row.margin_month)"></strong></div>
<div><span>Marge jr</span><strong x-text="fmtEuro(row.margin_year)"></strong></div>
<div><span>Portfolio</span><strong x-text="sharePct(row) + '%'"></strong></div>
</div>
</div>
<div class="rc-expand-body">
<h4>Next steps</h4>
<p x-text="row.next_steps || 'Geen next steps in Excel'"></p>
<div class="rc-expand-task" x-show="row.db_id">
<div class="rc-inline">
<select x-model="taskForm.agent_name">
<template x-for="a in agents" :key="a"><option :value="a" x-text="a"></option></template>
</select>
<input type="text" x-model="taskForm.title" placeholder="Taak…" @click.stop />
<button type="button" class="btn btn-sm btn-pulse" @click.stop="assignTask()">→</button>
</div>
</div>
</div>
</div>
</td>
</tr>
</template>
</table>
</div>
<!-- STAVEN -->
<div class="rc-bars-view" x-show="visualMode === 'bars'" x-cloak>
<template x-for="row in rankedRows()" :key="'b-' + row.source_row + '-' + row.name">
<button type="button" class="rc-bar-row"
:class="['rc-style-' + (row.row_style || 'white'), { active: selectedKey === projectKey(row) }]"
@click="toggleExpand(row)">
<div class="rc-bar-row-top">
<span class="rc-bar-name" x-text="row.name"></span>
<span class="rc-bar-amt" x-text="row.margin_month != null ? fmtEuroCompact(row.margin_month) : '—'"></span>
<span class="rc-bar-share" x-text="sharePct(row) + '%'"></span>
</div>
<div class="rc-bar-track">
<div class="rc-bar-fill" :style="'width:' + marginBarPct(row, 'margin_month') + '%;background:' + styleColor(row.row_style)"></div>
</div>
</button>
</template>
</div>
<!-- KAARTEN -->
<div class="rc-cards-view" x-show="visualMode === 'cards'" x-cloak>
<template x-for="row in cardRows()" :key="'k-' + row.source_row + '-' + row.name">
<article class="rc-card"
:class="['rc-style-' + (row.row_style || 'white'), { active: selectedKey === projectKey(row) }]"
@click="toggleExpand(row)">
<div class="rc-card-head">
<h3 x-text="row.name"></h3>
<span class="rc-chip rc-chip-static" :class="'rc-chip-' + (row.row_style || 'white')" x-text="styleLabel(row.row_style)"></span>
</div>
<div class="rc-card-metrics">
<div>
<span>mnd</span>
<strong x-text="row.margin_month != null ? fmtEuroCompact(row.margin_month) : '—'"></strong>
</div>
<div x-show="row.margin_year != null">
<span>jr</span>
<strong x-text="fmtEuroCompact(row.margin_year)"></strong>
</div>
<div>
<span>aandeel</span>
<strong x-text="sharePct(row) + '%'"></strong>
</div>
</div>
<div class="rc-card-bar" aria-hidden="true">
<div :style="'width:' + marginBarPct(row, 'margin_month') + '%;background:' + styleColor(row.row_style)"></div>
</div>
<p class="rc-card-steps" x-text="truncate(row.next_steps, 100) || 'Geen next step'"></p>
</article>
</template>
</div>
<!-- DETAIL PANEEL -->
<aside class="rc-detail" x-show="selectedProject" x-cloak>
<div class="rc-detail-head">
<div>
<h3 x-text="selectedProject?.name"></h3>
<span class="rc-chip rc-chip-static" :class="'rc-chip-' + (selectedProject?.row_style || 'white')"
x-text="styleLabel(selectedProject?.row_style)"></span>
</div>
<button type="button" class="rc-detail-close" @click="selectedKey = null; expandedKey = null">✕</button>
</div>
<div class="rc-detail-grid">
<div class="rc-detail-stat">
<span>Marge / mnd</span>
<strong x-text="fmtEuro(selectedProject?.margin_month)"></strong>
</div>
<div class="rc-detail-stat" x-show="selectedProject?.margin_year != null">
<span>Marge / jr</span>
<strong x-text="fmtEuro(selectedProject?.margin_year)"></strong>
</div>
<div class="rc-detail-stat">
<span>Portfolio</span>
<strong x-text="sharePct(selectedProject || {}) + '%'"></strong>
</div>
<div class="rc-detail-ring" :style="donutStyle(selectedProject || {})">
<span x-text="sharePct(selectedProject || {}) + '%'"></span>
</div>
</div>
<div class="rc-detail-compare">
<div class="rc-detail-compare-col">
<div class="rc-detail-vbar" :style="'height:' + marginBarPct(selectedProject || {}, 'margin_month') + '%;background:' + styleColor(selectedProject?.row_style)"></div>
<span>Mnd</span>
</div>
<div class="rc-detail-compare-col" x-show="selectedProject?.margin_year != null">
<div class="rc-detail-vbar rc-detail-vbar-year" :style="'height:' + marginBarPct(selectedProject || {}, 'margin_year') + '%'"></div>
<span>Jr</span>
</div>
</div>
<div class="rc-detail-status">
<span class="rc-detail-label">Zet status</span>
<div class="rc-style-chips rc-style-chips-assign">
<template x-for="s in styleFilters" :key="'as-' + s.id">
<button type="button" class="rc-chip" :class="['rc-chip-' + s.id, { active: selectedProject?.row_style === s.id }]"
@click="setCockpitStyle(s.id)" x-text="s.label"></button>
</template>
</div>
<p class="rc-detail-hint" x-show="selectedProject && !selectedProject.db_id">Sync DB om status permanent te bewaren</p>
</div>
<div class="rc-detail-steps">
<h4>Next steps</h4>
<p x-text="selectedProject?.next_steps || 'Geen next steps in Excel'"></p>
</div>
<div class="rc-detail-task" x-show="selectedProject?.db_id">
<div class="rc-inline">
<select x-model="taskForm.agent_name">
<template x-for="a in agents" :key="a"><option :value="a" x-text="a"></option></template>
</select>
<input type="text" x-model="taskForm.title" placeholder="Taak…" />
<button type="button" class="btn btn-sm btn-pulse" @click="assignTask()">→</button>
</div>
</div>
</aside>
</div>
<!-- Bewerken: originele Excel -->
@@ -258,5 +336,5 @@
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/revenue-cockpit.js?v=6"></script>
<script src="/static/js/revenue-cockpit.js?v=12"></script>
{% endblock %}