function revenueCockpit() {
  return {
    loading: false,
    saving: false,
    savedAt: '',
    viewMode: 'cockpit',
    goals: { vision_text: '', horizon_text: '', mid_text: '', tagline: '' },
    projects: [],
    dbProjects: [],
    sheetRows: [],
    selectedId: null,
    selectedKey: null,
    expandedKey: null,
    meta: {},
    aggregates: {},
    filterQ: '',
    filterStyle: '',
    filterCategory: '',
    filterHasMargin: false,
    agents: ['herman', 'research', 'marketing', 'retail', 'packaging', 'sysops', 'knowledge'],
    taskForm: { agent_name: 'research', title: '', description: '' },
    styleFilters: [
      { id: 'green', label: 'Live' },
      { id: 'red', label: 'Inactief' },
      { id: 'orange', label: 'Oranje' },
      { id: 'yellow', label: 'Strategisch' },
      { id: 'blue', label: 'Blauw' },
      { id: 'white', label: 'Neutraal' },
    ],
    categoryFilters: [
      { id: 'deal', label: 'Deal' },
      { id: 'initiative', label: 'Initiatief' },
      { id: 'strategic', label: 'Strategisch' },
    ],

    get selectedRow() {
      return this.dbProjects.find((p) => p.id === this.selectedId) || null;
    },

    get selectedProject() {
      if (!this.selectedKey) return null;
      return this.projects.find((p) => this.projectKey(p) === this.selectedKey) || null;
    },

    async init() {
      localStorage.setItem('foodlinkk-persona', 'ceo');
      await this.loadLive();
      await this.loadDbQuiet();
    },

    projectKey(p) {
      return String(p.source_row || '') + '|' + (p.name || '');
    },

    fmtNum(n) {
      if (n == null || n === '' || isNaN(n)) return '';
      return Number(n).toLocaleString('nl-NL', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
    },

    fmtEuro(n) {
      if (n == null || n === '' || isNaN(n)) return '—';
      return '€ ' + Number(n).toLocaleString('nl-NL', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
    },

    fmtEuroCompact(n) {
      if (n == null || n === '' || isNaN(n)) return '—';
      const v = Number(n);
      if (v >= 1000000) return '€' + (v / 1000000).toFixed(1) + 'M';
      if (v >= 1000) return '€' + Math.round(v / 1000) + 'k';
      return '€' + Math.round(v);
    },

    fmtDate(iso) {
      if (!iso) return '';
      try {
        return new Date(iso).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' });
      } catch (e) {
        return iso;
      }
    },

    categoryLabel(c) {
      if (c === 'deal') return 'Deal';
      if (c === 'strategic') return 'Strategisch';
      return 'Initiatief';
    },

    parseNum(text) {
      const t = String(text || '').trim();
      if (!t || t === '—') return null;
      const n = parseFloat(t.replace(/\./g, '').replace(',', '.'));
      return isNaN(n) ? null : n;
    },

    filteredProjects() {
      const q = (this.filterQ || '').trim().toLowerCase();
      return this.projects.filter((p) => {
        if (this.filterStyle && (p.row_style || 'white') !== this.filterStyle) return false;
        if (this.filterCategory && p.category !== this.filterCategory) return false;
        if (this.filterHasMargin && !p.margin_month) return false;
        if (q) {
          const hay = ((p.name || '') + ' ' + (p.next_steps || '')).toLowerCase();
          if (!hay.includes(q)) return false;
        }
        return true;
      });
    },

    clearFilters() {
      this.filterQ = '';
      this.filterStyle = '';
      this.filterCategory = '';
      this.filterHasMargin = false;
    },

    truncate(s, n) {
      if (!s) return '';
      const t = String(s).replace(/\s+/g, ' ').trim();
      return t.length <= n ? t : t.slice(0, n).trim() + '…';
    },

    styleColor(style) {
      const map = {
        green: '#22c55e', red: '#ef4444', orange: '#f59e0b',
        yellow: '#eab308', blue: '#3b82f6', white: '#64748b',
      };
      return map[style] || map.white;
    },

    foodlinkkIndexFrom(list) {
      const idx = list.findIndex((p) =>
        (p.row_style === 'yellow' && /foodlinkk|total earnings|loonkosten/i.test(p.name)) ||
        /foodlinkk food marketing/i.test(p.name)
      );
      return idx >= 0 ? idx : list.length;
    },

    visualRows() {
      const items = this.filteredProjects();
      const flIdx = this.foodlinkkIndexFrom(items);
      return items.map((p, i) => {
        let logoCell = null;
        let brand = 'cucina';
        if (i >= flIdx && flIdx < items.length) brand = 'foodlinkk';
        if (i === 0) logoCell = 'cucina';
        else if (i === flIdx && flIdx < items.length) logoCell = 'foodlinkk';
        return { ...p, logoCell, brand };
      });
    },

    maxMargin(field) {
      const vals = this.filteredProjects().map((p) => p[field] || 0);
      return Math.max(...vals, 1);
    },

    marginBarPct(p, field) {
      if (!p[field]) return 0;
      return Math.min(100, Math.round((p[field] / this.maxMargin(field)) * 100));
    },

    sharePct(p) {
      const total = this.aggregates.total_margin_month || 0;
      if (!p.margin_month || !total) return 0;
      return Math.min(100, Math.round((p.margin_month / total) * 1000) / 10);
    },

    donutStyle(p) {
      const pct = this.sharePct(p);
      const color = this.styleColor(p.row_style);
      return 'background:conic-gradient(' + color + ' 0% ' + pct + '%, rgba(148,163,184,0.15) ' + pct + '% 100%)';
    },

    toggleExpand(p) {
      const key = this.projectKey(p);
      this.expandedKey = this.expandedKey === key ? null : key;
      this.selectedKey = key;
      this.taskForm.title = '';
      this.taskForm.description = p.next_steps || '';
    },

    selectProject(p) {
      this.toggleExpand(p);
    },

    mergeDbIds() {
      const byName = {};
      for (const d of this.dbProjects) {
        const k = (d.name || '').trim().toLowerCase();
        if (k) byName[k] = d.id;
      }
      this.projects = this.projects.map((p) => ({
        ...p,
        db_id: byName[(p.name || '').trim().toLowerCase()] || null,
      }));
    },

    async loadLive() {
      this.loading = true;
      try {
        const r = await fetch('/api/revenue-cockpit/live').then((x) => x.json());
        if (!r.ok) throw new Error(r.error || 'Excel laden mislukt');
        this.goals = r.goals || {};
        this.projects = r.projects || [];
        this.aggregates = {
          ...(r.aggregates || {}),
          project_count: r.project_count || this.projects.length,
        };
        this.meta = {
          source_file: r.source_file,
          sheet_name: r.sheet_name,
          file_mtime: r.file_mtime,
          parsed_at: r.parsed_at,
        };
        this.mergeDbIds();
      } catch (e) {
        if (window.Cockpit) Cockpit.toast(e.message || 'Excel laden mislukt', 'error');
      }
      this.loading = false;
    },

    async loadDbQuiet() {
      try {
        const r = await fetch('/api/revenue-cockpit/dashboard').then((x) => x.json());
        this.dbProjects = r.projects || [];
        this.mergeDbIds();
      } catch (e) {
        console.error(e);
      }
    },

    foodlinkkIndex() {
      const idx = this.dbProjects.findIndex((p) =>
        (p.row_style === 'yellow' && /foodlinkk|total earnings|loonkosten/i.test(p.name)) ||
        /foodlinkk food marketing/i.test(p.name)
      );
      return idx >= 0 ? idx : this.dbProjects.length;
    },

    buildSheetRows() {
      const flIdx = this.foodlinkkIndex();
      this.sheetRows = this.dbProjects.map((p, i) => {
        let logoCell = null;
        let logoSpan = 1;
        if (i === 0) {
          logoCell = 'cucina';
          logoSpan = flIdx > 0 ? flIdx : this.dbProjects.length;
        } else if (i === flIdx && flIdx < this.dbProjects.length) {
          logoCell = 'foodlinkk';
          logoSpan = this.dbProjects.length - flIdx;
        }
        return { ...p, logoCell, logoSpan };
      });
    },

    async switchToSheet() {
      this.viewMode = 'sheet';
      await this.loadDb();
    },

    async loadDb() {
      this.loading = true;
      try {
        const r = await fetch('/api/revenue-cockpit/dashboard').then((x) => x.json());
        this.goals = r.stats?.goals || this.goals;
        this.dbProjects = r.projects || [];
        this.buildSheetRows();
      } catch (e) {
        if (window.Cockpit) Cockpit.toast(e.message || 'DB laden mislukt', 'error');
      }
      this.loading = false;
    },

    editStart(ev) {
      const el = ev.target;
      if (el.classList.contains('rc-cell-num')) {
        const raw = el.textContent.trim();
        if (raw) el.textContent = raw.replace(/\./g, '').replace(/,(\d+)$/, '.$1');
      }
      requestAnimationFrame(() => {
        const sel = window.getSelection();
        const range = document.createRange();
        range.selectNodeContents(el);
        sel.removeAllRanges();
        sel.addRange(range);
      });
    },

    markSaved() {
      const now = new Date();
      this.savedAt = now.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
    },

    async saveGoalsField(field, el) {
      const val = el.innerText.trim();
      if (this.goals[field] === val) return;
      this.saving = true;
      try {
        const r = await fetch('/api/revenue-cockpit/goals', {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ [field]: val }),
        }).then((x) => x.json());
        this.goals = r.goals || this.goals;
        this.markSaved();
      } catch (e) {
        if (window.Cockpit) Cockpit.toast('Opslaan mislukt', 'error');
      }
      this.saving = false;
    },

    async saveProjectField(row, field, el) {
      let val;
      if (field === 'margin_month' || field === 'margin_year') {
        val = this.parseNum(el.innerText);
        el.textContent = this.fmtNum(val);
      } else {
        val = el.innerText.trim();
      }
      if (String(row[field] ?? '') === String(val ?? '')) return;

      const body = { [field]: val };
      if (field === 'margin_month' && val != null) {
        body.margin_year = val * 12;
      }

      this.saving = true;
      try {
        const r = await fetch('/api/revenue-cockpit/projects/' + row.id, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        }).then((x) => x.json());
        const updated = r.project || {};
        Object.assign(row, updated);
        row.row_style = updated.row_style || row.row_style;
        const pi = this.dbProjects.findIndex((p) => p.id === row.id);
        if (pi >= 0) this.dbProjects[pi] = { ...this.dbProjects[pi], ...updated };
        if (field === 'margin_month' && body.margin_year != null) {
          const yearCell = el.nextElementSibling;
          if (yearCell) yearCell.textContent = this.fmtNum(body.margin_year);
          row.margin_year = body.margin_year;
        }
        this.markSaved();
        await this.loadLive();
      } catch (e) {
        if (window.Cockpit) Cockpit.toast('Opslaan mislukt', 'error');
      }
      this.saving = false;
    },

    selectRow(row) {
      this.selectedId = row.id;
      this.taskForm.title = '';
      this.taskForm.description = row.next_steps || '';
    },

    async setRowStyle(style) {
      if (!this.selectedRow) return;
      try {
        await fetch('/api/revenue-cockpit/projects/' + this.selectedRow.id, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ row_style: style }),
        });
        this.selectedRow.row_style = style;
        const pi = this.dbProjects.findIndex((p) => p.id === this.selectedRow.id);
        if (pi >= 0) this.dbProjects[pi].row_style = style;
        this.buildSheetRows();
        this.markSaved();
        await this.loadLive();
      } catch (e) {
        if (window.Cockpit) Cockpit.toast('Kleur wijzigen mislukt', 'error');
      }
    },

    async assignTask() {
      const row = this.viewMode === 'sheet' ? this.selectedRow : this.selectedProject;
      const pid = row?.id || row?.db_id;
      if (!pid || !this.taskForm.title.trim()) return;
      try {
        await fetch('/api/revenue-cockpit/projects/' + pid + '/assign-task', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            agent_name: this.taskForm.agent_name,
            title: this.taskForm.title.trim(),
            description: this.taskForm.description,
            delegate_herman: true,
          }),
        });
        if (window.Cockpit) Cockpit.toast('Taak → ' + this.taskForm.agent_name, 'success');
        this.taskForm.title = '';
      } catch (e) {
        if (window.Cockpit) Cockpit.toast(e.message || 'Taak mislukt', 'error');
      }
    },

    async takeSnapshot() {
      await fetch('/api/revenue-cockpit/snapshot', { method: 'POST' });
      if (window.Cockpit) Cockpit.toast('Snapshot opgeslagen', 'success');
    },

    async importExcel() {
      if (!confirm('Sync DB uit Succes Sheet .xlsx — alle rijen worden vervangen. Doorgaan?')) return;
      this.loading = true;
      try {
        const r = await fetch('/api/revenue-cockpit/import-from-excel', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ replace: true }),
        }).then((x) => x.json());
        if (window.Cockpit) Cockpit.toast('Import: ' + (r.projects_imported || 0) + ' rijen', 'success');
        await this.loadLive();
        await this.loadDbQuiet();
        if (this.viewMode === 'sheet') await this.loadDb();
      } catch (e) {
        if (window.Cockpit) Cockpit.toast('Import mislukt', 'error');
      }
      this.loading = false;
    },
  };
}
