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:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user