/* ============================================================================
   ENERVECO UI SHELL — gedeeld design-system voor alle portaalpagina's
   ----------------------------------------------------------------------------
   Laden vóór de paginascript:
     <script type="text/babel" src="/ui/shell.jsx"></script>
   Alles is bereikbaar via window.EUI  (géén andere globals).
   ============================================================================ */
(() => {
const { useState, useEffect, useMemo, useCallback, useRef } = React;

/* ─── Design tokens ─────────────────────────────────────────────────────── */
const C = {
  primary: '#2b4f26',        // diep teal — merkkleur
  primaryDark: '#1a3117',
  primaryLight: '#e9f0e7',   // lichte tint (achtergrond voor selecties)
  primaryStrong: '#3d6d35',
  accent: '#e8a838',         // amber
  accentDark: '#b57e1a',
  bg: '#f2f4f3',
  card: '#ffffff',
  border: '#e3e7e5',
  borderSoft: '#eef1ef',
  text: '#16211f',
  textMuted: '#5f6f6c',
  textLight: '#93a19d',
  success: '#1e8a6e',
  warning: '#d69e2e',
  danger: '#c53030',
  info: '#2b6cb0',
  sidebar: '#1a3117',
  sidebarText: '#c3d2ce',
  sidebarActive: '#e8a838',
  headerBg: '#fbfcfb',
};

/* ─── Statusmodel (één bron van waarheid, slug → label + kleur) ─────────── */
const STATUS_META = {
  nieuw:                        { label: 'Nieuw',                         color: '#8a98a5' },
  in_behandeling:               { label: 'In behandeling',                color: '#4299e1' },
  offerte_verzonden:            { label: 'Offerte verzonden',             color: '#d69e2e' },
  goedgekeurd:                  { label: 'Goedgekeurd',                   color: '#38a169' },
  opgestart_outsourcing:        { label: 'Opgestart — outsourcing',       color: '#f59e0b' },
  opgestart_enerveco:           { label: 'Opgestart — Enerveco',          color: '#14b8a6' },
  outsourcing_nakijken:         { label: 'Outsourcing klaar — nakijken',  color: '#7c5cd6' },
  outsourcing_nagekeken:        { label: 'Outsourcing nagekeken',         color: '#5d9e7b' },  /* v7.18 */
  advies_gestuurd:              { label: 'Advies gestuurd',               color: '#319795' },
  startverklaring_ingediend:    { label: 'Startverklaring ingediend',     color: '#3182ce' },
  stavingsstukken_opgevraagd:   { label: 'Stavingsstukken opgevraagd',    color: '#dd6b20' },
  voorbereiding_aangifte:       { label: 'Voorbereiding aangifte',        color: '#805ad5' },
  voorlopige_aangifte_verzonden:{ label: 'Voorlopige aangifte verzonden', color: '#6b46c1' },
  definitief_ingediend:         { label: 'Definitief ingediend',          color: '#2f855a' },
  afgewezen:                    { label: 'Afgewezen',                     color: '#c53030' },
  vervallen:                    { label: 'Vervallen',                     color: '#718096' },
};
const STATUS_ORDER = Object.keys(STATUS_META);
const normStatus = (s) => {
  if (!s) return 'nieuw';
  const k = String(s).toLowerCase().replace(/[\s-]+/g, '_');
  return STATUS_META[k] ? k : (s === 'Nieuw' ? 'nieuw' : k);
};
const statusLabel = (s) => (STATUS_META[normStatus(s)] || {}).label || s || '—';
const statusColor = (s) => (STATUS_META[normStatus(s)] || {}).color || '#8a98a5';

const INVOICE_STATUS_COLORS = {
  Verzonden: '#4299e1', Betaald: '#1e8a6e', Vervallen: '#c53030', Creditnota: '#8a98a5',
};

/* ─── DIENSTEN + MIJLPALEN + FACTUURFASEN (v7.3) ─────────────────────────────
   Eén bron van waarheid voor het Projectenoverzicht (index.html) én de mobiele
   app (app.html). Vroeger stond deze logica in beide bestanden apart. */

const DIENSTEN = {
  epb:        { label: 'EPB',        volledig: 'EPB-verslaggeving',       color: '#2b4f26' },
  ventilatie: { label: 'Ventilatie', volledig: 'Ventilatieverslaggeving', color: '#2f7d6b' },
  epc:        { label: 'EPC',        volledig: 'EPC (bestaande woning)',  color: '#b06f1a' },
  riolering:  { label: 'Riolering',  volledig: 'Rioleringskeuring',       color: '#3a6ea8' },
  /* v7.10 — wordt in onderaanneming uitgevoerd; facturatie loopt wel via ons. */
  veiligheid: { label: 'Veiligheid', volledig: 'Veiligheidscoördinatie (onderaanneming)', color: '#7c5cd6' },
  /* v7.11 — blowerdoortest, eveneens in onderaanneming. */
  luchtdichtheid: { label: 'Luchtdichtheid', volledig: 'Luchtdichtheidsmeting — blowerdoortest (onderaanneming)', color: '#0e7490' },
};
const DIENST_KEYS = Object.keys(DIENSTEN);

/** CSV ('epb,ventilatie') → array; tolerant voor null, spaties en hoofdletters. */
const parseDiensten = (csv) => String(csv || '')
  .split(',').map(s => s.trim().toLowerCase()).filter(k => DIENSTEN[k]);
const formatDiensten = (arr) => (arr || []).filter(k => DIENSTEN[k]).join(',');
/** Heeft dit dossier deze dienst? Zonder ingevulde diensten tellen we EPB mee. */
const heeftDienst = (rec, key) => {
  const d = parseDiensten(rec && rec.diensten);
  if (!d.length) return key === 'epb';
  return d.includes(key);
};

/* ── Factuurfasen ──────────────────────────────────────────────────────────
   Een factuur kan meerdere fasen dekken ('advies,startverklaring' komt in de
   echte data het vaakst voor). Kolom 'fases' (migratie 028) is leidend; is die
   leeg, dan leiden we hem af uit trigger_status en de omschrijving. */
const FACTUUR_FASEN = {
  advies:          { kort: 'Advies',      lang: 'Advies / EPB-simulatie',        dienst: 'epb' },
  startverklaring: { kort: 'Startverkl.', lang: 'Startverklaring',               dienst: 'epb' },
  aangifte:        { kort: 'Aangifte',    lang: '(Voorlopige) EPB-aangifte',     dienst: 'epb' },
  epc:             { kort: 'EPC',         lang: 'EPC-keuring',                   dienst: 'epc' },
  riolering:       { kort: 'Riolering',   lang: 'Keuring privéwaterafvoer',      dienst: 'riolering' },
  veiligheid:      { kort: 'Veiligheid',  lang: 'Veiligheidscoördinatie',        dienst: 'veiligheid' },
  luchtdichtheid:  { kort: 'Luchtdicht.', lang: 'Luchtdichtheidsmeting (blowerdoortest)', dienst: 'luchtdichtheid' },
  epb:             { kort: 'EPB',         lang: 'EPB globaal (geen fase vermeld)', dienst: 'epb' },
};
const FASE_KEYS = ['advies', 'startverklaring', 'aangifte', 'epc', 'riolering', 'veiligheid', 'luchtdichtheid'];

const _TRIGGER_FASE = {
  advies_gestuurd: 'advies',
  startverklaring_ingediend: 'startverklaring',
  voorlopige_aangifte_verzonden: 'aangifte',
  epc: 'epc',
  riolering: 'riolering',
};
const _FASE_PATRONEN = [
  ['riolering',       /riool|rioler|waterafvoer|priv[eé]\s?water|keuring\s+priv/i],
  ['veiligheid',      /veiligheidsco/i],
  ['epc',             /\bepc\b|energieprestatiecertificaat/i],
  ['advies',          /advies|simulatie|studie/i],
  ['startverklaring', /start\s?verklaring|starverklaring|voorontwerp|\bvvo\b/i],
  ['luchtdichtheid',  /blower\s?door|luchtdicht/i],
  ['aangifte',        /aangifte|as-?built/i],
];

/** Welke fasen dekt deze factuur? → array met sleutels uit FACTUUR_FASEN. */
function factuurFasen(inv) {
  if (!inv) return [];
  const opgeslagen = String(inv.fases || '').split(',').map(s => s.trim()).filter(s => FACTUUR_FASEN[s]);
  if (opgeslagen.length) return opgeslagen;
  const uit = new Set();
  if (inv.trigger_status && _TRIGGER_FASE[inv.trigger_status]) uit.add(_TRIGGER_FASE[inv.trigger_status]);
  const tekst = [inv.description, inv.notes].filter(Boolean).join(' | ');
  if (tekst) for (const [key, re] of _FASE_PATRONEN) if (re.test(tekst)) uit.add(key);
  if (!uit.size && /\bepb\b/i.test(tekst)) uit.add('epb');
  return [...uit];
}

/** Betalingstoestand van een set facturen: betaald > vervallen > verzonden > aangemaakt. */
function _factuurToestand(facturen, nu) {
  if (!facturen.length) return null;
  if (facturen.some(i => i.status === 'Betaald')) return 'betaald';
  if (facturen.some(i => i.status === 'Verzonden' && i.date_due && new Date(i.date_due) < nu)) return 'vervallen';
  if (facturen.some(i => i.status === 'Verzonden')) return 'verzonden';
  return 'aangemaakt';
}
/** Toestand van één factuurfase. 'epb' (globaal) telt mee voor alle EPB-fasen. */
function faseToestand(facturen, fase, nu) {
  const nuD = nu || new Date();
  const isEpbFase = FACTUUR_FASEN[fase] && FACTUUR_FASEN[fase].dienst === 'epb' && fase !== 'epb';
  const rel = (facturen || []).filter(i => {
    const f = factuurFasen(i);
    return f.includes(fase) || (isEpbFase && f.includes('epb'));
  });
  const st = _factuurToestand(rel, nuD);
  if (!st) return null;
  // Globale EPB-factuur zonder fase → apart gemarkeerd (glob), zodat de matrix
  // eerlijk toont dat er wél gefactureerd is, maar niet per fase.
  const enkelGlobaal = rel.every(i => factuurFasen(i).includes('epb') && !factuurFasen(i).includes(fase));
  return enkelGlobaal ? st + ':glob' : st;
}
/** Facturen die aan géén enkele kolom toegewezen konden worden. */
const nietToegewezenFacturen = (facturen) =>
  (facturen || []).filter(i => !factuurFasen(i).length);

/* ── Mijlpalen ─────────────────────────────────────────────────────────────
   EPB-mijlpalen zijn afgeleid (status-volgorde + adviesrapport + facturen);
   ventilatie, EPC en riolering zijn echte datums op het project. */
const MIJLPAAL_VOLGORDE = ['nieuw','in_behandeling','offerte_verzonden','goedgekeurd',
  'opgestart_outsourcing','opgestart_enerveco','outsourcing_nakijken','outsourcing_nagekeken','advies_gestuurd','startverklaring_ingediend',
  'stavingsstukken_opgevraagd','voorbereiding_aangifte','voorlopige_aangifte_verzonden','definitief_ingediend'];

/** Kolommen van de Mijlpalen-matrix, gegroepeerd per dienst. */
const MIJLPAAL_KOLOMMEN = {
  epb: [
    { key: 'mAdv', kort: 'Advies',      titel: 'Advies / EPB-simulatie afgerond' },
    { key: 'mSv',  kort: 'Startverkl.', titel: 'Startverklaring ingediend bij VEKA' },
    { key: 'mVa',  kort: 'Voorl. aang.',titel: 'Voorlopige aangifte verzonden' },
    { key: 'mEa',  kort: 'Eindaang.',   titel: 'Definitieve EPB-aangifte ingediend' },
  ],
  ventilatie: [
    { key: 'mVvo', veld: 'datum_vvo', kort: 'VVO', titel: 'Ventilatievoorontwerp opgemaakt' },
    { key: 'mVpv', veld: 'datum_vpv', kort: 'VPV', titel: 'Ventilatieprestatieverslag geregistreerd' },
  ],
  epc: [
    { key: 'mEpcB', veld: 'datum_epc_bezoek',    kort: 'Bezoek', titel: 'Plaatsbezoek / opname uitgevoerd' },
    { key: 'mEpcO', veld: 'datum_epc_opgemaakt', kort: 'Attest', titel: 'EPC opgemaakt en geregistreerd' },
  ],
  riolering: [
    { key: 'mRioK', veld: 'datum_riool_keuring', kort: 'Keuring', titel: 'Keuring privéwaterafvoer uitgevoerd' },
    { key: 'mRioA', veld: 'datum_riool_attest',  kort: 'Attest',  titel: 'Keuringsattest afgeleverd' },
  ],
};

/**
 * Berekent alle mijlpaal- en factuurvlaggen voor één dossier.
 * @param p         project (of pseudo-project uit een aanvraag zonder dossier)
 * @param a         gekoppelde aanvraag (mag null zijn)
 * @param facturen  facturen van dit dossier
 * @param adviesMap index van adviesrapporten ('p<id>' / 'a<id>')
 */
function berekenMijlpalen(p, a, facturen, adviesMap, nu) {
  const nuD = nu || new Date();
  const st = normStatus(p.status);
  const i = MIJLPAAL_VOLGORDE.indexOf(st);
  const na = (mijlpaal) => i >= 0 && i >= MIJLPAAL_VOLGORDE.indexOf(mijlpaal);
  const heeftAdvies = !!(adviesMap && (adviesMap['p' + p.id] || (a && adviesMap['a' + a.id])));
  const uit = {
    mAdv: heeftAdvies || na('advies_gestuurd') || !!faseToestand(facturen, 'advies', nuD),
    mSv:  na('startverklaring_ingediend')      || !!faseToestand(facturen, 'startverklaring', nuD),
    mVa:  na('voorlopige_aangifte_verzonden')  || !!faseToestand(facturen, 'aangifte', nuD),
    mEa:  st === 'definitief_ingediend' || !!p.datum_definitieve_aangifte_ingediend,
    mVvo: !!p.datum_vvo,  mVpv: !!p.datum_vpv,
    mEpcB: !!p.datum_epc_bezoek, mEpcO: !!p.datum_epc_opgemaakt,
    mRioK: !!p.datum_riool_keuring, mRioA: !!p.datum_riool_attest,
  };
  for (const fase of FASE_KEYS) uit['f_' + fase] = faseToestand(facturen, fase, nuD);
  uit._losseFacturen = nietToegewezenFacturen(facturen);
  return uit;
}

/* ─── ADRESVERGELIJKING (v7.3) ───────────────────────────────────────────────
   Losse opdrachten (aanvragen zonder dossiernummer, meestal uit de Zenfactuur-
   import) horen vaak bij een dossier dat wél bestaat: "Noeveren 132, 2850 Boom"
   is hetzelfde pand als dossier 26021 "Noeveren 132 te boom". Hiermee stelt het
   Overzicht een koppeling voor — koppelen doet de gebruiker zelf, met één klik. */

// Vlaamse straatnaam-uitgangen: zo weten we waar de straat ophoudt, ook als er
// geen huisnummer in staat ("Hoevenstraat, Wommelgem").
const _STRAAT_SUFFIX = /(straat|laan|lei|baan|weg|dreef|plein|pad|kaai|markt|wijk|park|hof|berg|veld|steenweg|singel|kade|ring)$/;
const _ADRES_STOP = new Set(['te', 'bus', 'nr', 'no', 'lot', 'lots', 'appartement', 'app']);

function ontleedAdres(adres) {
  const ruw = String(adres || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
    .replace(/[^a-z0-9]+/g, ' ').trim();
  if (!ruw || ruw === 'null') return null;
  const tokens = ruw.split(' ').filter(t => t && !_ADRES_STOP.has(t));
  const postcode = tokens.find(t => /^\d{4}$/.test(t)) || null;
  const huisnr = tokens.find(t => /^\d{1,4}[a-z]?$/.test(t) && t !== postcode) || null;
  let straat = '';
  const eind = tokens.findIndex(t => _STRAAT_SUFFIX.test(t));
  if (eind >= 0) straat = tokens.slice(0, eind + 1).join(' ');
  else {
    const eersteCijfer = tokens.findIndex(t => /^\d/.test(t));
    straat = (eersteCijfer > 0 ? tokens.slice(0, eersteCijfer) : tokens.slice(0, 1)).join(' ');
  }
  return straat ? { straat, huisnr, postcode, ruw } : null;
}

/** Zelfde pand? → null, of {zekerheid, reden}. Verschillend nummer of andere
    postcode betekent altijd een ánder pand (Dublinstraat lot 27 ≠ lot 28). */
function vergelijkAdres(a, b) {
  if (!a || !b || a.straat !== b.straat) return null;
  if (a.postcode && b.postcode && a.postcode !== b.postcode) return null;
  if (a.huisnr && b.huisnr && a.huisnr !== b.huisnr) return null;
  if (a.huisnr && b.huisnr) {
    return { zekerheid: 'zeker', reden: 'zelfde straat en huisnummer' + (a.postcode ? ' en postcode' : '') };
  }
  return { zekerheid: 'waarschijnlijk', reden: 'zelfde straat — één van beide zonder huisnummer' };
}

/** Zoekt het waarschijnlijke dossier bij een adres. Bij twijfel: niets. */
function zoekDossier(adres, projecten) {
  const a = ontleedAdres(adres);
  if (!a) return null;
  const treffers = [];
  for (const p of projecten || []) {
    if (!p.dossier_nr) continue;
    const m = vergelijkAdres(a, ontleedAdres(p.werfadres));
    if (m) treffers.push({ project: p, ...m });
  }
  if (!treffers.length) return null;
  const zekere = treffers.filter(t => t.zekerheid === 'zeker');
  const keuze = zekere.length ? zekere : treffers;
  return keuze.length === 1 ? keuze[0] : null;   // meerdere kandidaten → niet gokken
}

const URGENCY_META = {
  overdue:     { label: 'Verstreken',        color: '#c53030', bg: '#fde8e8' },
  critical:    { label: 'Kritiek (<30 d)',   color: '#c2410c', bg: '#ffedd5' },
  imminent:    { label: 'Binnen 90 d',       color: '#b45309', bg: '#fef3c7' },
  approaching: { label: 'Binnen 180 d',      color: '#1d4ed8', bg: '#dbeafe' },
  ok:          { label: 'Op schema',         color: '#166534', bg: '#dcfce7' },
  completed:   { label: 'Ingediend',         color: '#374151', bg: '#e5e7eb' },
  unknown:     { label: 'Geen datums',       color: '#6b7280', bg: '#f3f4f6' },
};

/* ─── API-client (cookie-sessie) ────────────────────────────────────────── */
const api = {
  async call(method, url, body) {
    const opts = { method, credentials: 'include', headers: {} };
    if (body !== undefined) {
      opts.headers['Content-Type'] = 'application/json';
      opts.body = JSON.stringify(body);
    }
    const r = await fetch(url, opts);
    if (r.status === 401) {
      if (!url.includes('/api/me')) window.location.href = '/';
      return null;
    }
    try { return await r.json(); } catch { return null; }
  },
  get:   (url)       => api.call('GET', url),
  post:  (url, body) => api.call('POST', url, body),
  put:   (url, body) => api.call('PUT', url, body),
  patch: (url, body) => api.call('PATCH', url, body),
  del:   (url)       => api.call('DELETE', url),
};

/* ─── Formatters ────────────────────────────────────────────────────────── */
const fmtEuro = (n, { dash = true } = {}) => {
  const v = Number(n);
  if (!isFinite(v) || (dash && v === 0)) return '—';
  return '€ ' + v.toLocaleString('nl-BE', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
};
const fmtDate = (d) => {
  if (!d) return '—';
  const t = new Date(String(d).replace(' ', 'T'));
  return isNaN(t) ? '—' : t.toLocaleDateString('nl-BE', { day: '2-digit', month: '2-digit', year: 'numeric' });
};
const fmtDateShort = (d) => {
  if (!d) return '—';
  const t = new Date(String(d).replace(' ', 'T'));
  return isNaN(t) ? '—' : t.toLocaleDateString('nl-BE', { day: '2-digit', month: '2-digit', year: '2-digit' });
};
const relTime = (d) => {
  if (!d) return '';
  const t = new Date(String(d).replace(' ', 'T'));
  if (isNaN(t)) return '';
  const s = (Date.now() - t.getTime()) / 1000;
  if (s < 90) return 'zonet';
  if (s < 3600) return Math.round(s / 60) + ' min geleden';
  if (s < 86400 * 2) return Math.round(s / 3600) + ' uur geleden';
  return Math.round(s / 86400) + ' dagen geleden';
};

/* ─── Viewport hook ─────────────────────────────────────────────────────── */
const MOBILE_BP = 768, TABLET_BP = 1080;
function useViewport() {
  const [w, setW] = useState(window.innerWidth);
  useEffect(() => {
    let raf = null;
    const onR = () => { if (!raf) raf = requestAnimationFrame(() => { raf = null; setW(window.innerWidth); }); };
    window.addEventListener('resize', onR);
    return () => window.removeEventListener('resize', onR);
  }, []);
  return { width: w, isMobile: w < MOBILE_BP, isTablet: w >= MOBILE_BP && w < TABLET_BP, isDesktop: w >= TABLET_BP };
}

/* ─── Iconen (inline SVG, stroke-based) ─────────────────────────────────── */
const ICON_PATHS = {
  dashboard: <><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></>,
  inbox_tray: <><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></>,
  mail: <><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 6L2 7"/></>,
  folder: <><path d="M4 4h5l2 3h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/></>,
  euro: <><path d="M18.5 5.5a7.5 7.5 0 1 0 0 13"/><path d="M3 10h10M3 14h10"/></>,
  doc: <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h5"/></>,
  settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>,
  logout: <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></>,
  search: <><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></>,
  plus: <><path d="M12 5v14M5 12h14"/></>,
  refresh: <><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></>,
  upload: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/></>,
  download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></>,
  calendar: <><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></>,
  bell: <><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></>,
  check: <><path d="M20 6 9 17l-5-5"/></>,
  x: <><path d="M18 6 6 18M6 6l12 12"/></>,
  chevron_left: <><path d="m15 18-6-6 6-6"/></>,
  chevron_right: <><path d="m9 18 6-6-6-6"/></>,
  external: <><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></>,
  map: <><path d="M9 18l-6 3V6l6-3 6 3 6-3v15l-6 3-6-3z"/><path d="M9 3v15M15 6v15"/></>,
  table: <><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18"/></>,
  archive: <><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></>,
  building: <><path d="M3 21h18"/><path d="M5 21V7l7-4 7 4v14"/><path d="M9 10h1M9 14h1M14 10h1M14 14h1"/></>,
  user: <><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></>,
  clock: <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
  warning: <><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></>,
  filter: <><path d="M22 3H2l8 9.46V19l4 2v-8.54z"/></>,
  paperclip: <><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></>,
  send: <><path d="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/></>,
  home: <><path d="M3 9.5 12 3l9 6.5V20a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22v-8h6v8"/></>,
  eye: <><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></>,
  lock: <><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></>,
  phone: <><rect x="6" y="2" width="12" height="20" rx="2.5"/><path d="M11 18h2"/></>,
  edit: <><path d="M17 3a2.83 2.83 0 0 1 4 4L7.5 20.5 2 22l1.5-5.5z"/></>,
};

function Icon({ name, size = 18, style = {}, strokeWidth = 2 }) {
  const paths = ICON_PATHS[name];
  if (!paths) return null;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
         strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
         style={{ flexShrink: 0, verticalAlign: '-3px', ...style }} aria-hidden="true">
      {paths}
    </svg>
  );
}

/* ─── Basiscomponenten ──────────────────────────────────────────────────── */
function Btn({ children, onClick, variant, small, disabled, style = {}, title, type = 'button', icon }) {
  const base = {
    padding: small ? '6px 12px' : '9px 16px',
    borderRadius: 8, border: 'none', cursor: disabled ? 'not-allowed' : 'pointer',
    fontSize: small ? 12.5 : 13.5, fontWeight: 600, fontFamily: 'inherit',
    display: 'inline-flex', alignItems: 'center', gap: 7,
    transition: 'background .15s, color .15s, box-shadow .15s',
    opacity: disabled ? 0.55 : 1, whiteSpace: 'nowrap', lineHeight: 1.35,
  };
  const variants = {
    primary: { background: C.primary, color: '#fff' },
    accent:  { background: C.accent, color: C.primaryDark },
    danger:  { background: '#fde8e8', color: C.danger },
    ghost:   { background: 'transparent', color: C.textMuted, border: `1.5px solid ${C.border}` },
    default: { background: '#ebf1e9', color: C.primary, border: `1.5px solid #d5dfd1` },
  };
  return (
    <button type={type} title={title} disabled={disabled} onClick={onClick}
            style={{ ...base, ...(variants[variant] || variants.default), ...style }}>
      {icon && <Icon name={icon} size={small ? 14 : 16} />}{children}
    </button>
  );
}

function Badge({ color = '#8a98a5', children, style = {} }) {
  return (
    <span style={{ padding: '3px 10px', borderRadius: 999, fontSize: 11, fontWeight: 600,
                   background: color + '22', color, whiteSpace: 'nowrap', ...style }}>
      {children}
    </span>
  );
}

function StatusBadge({ status, small, style = {} }) {
  const meta = STATUS_META[normStatus(status)] || { label: status || '—', color: '#8a98a5' };
  return (
    <span style={{ padding: small ? '2px 8px' : '3px 10px', borderRadius: 999,
                   fontSize: small ? 10.5 : 11, fontWeight: 600,
                   background: meta.color + '1f', color: meta.color, whiteSpace: 'nowrap', ...style }}>
      {meta.label}
    </span>
  );
}

/* ─── Diensten / mijlpalen · gedeelde weergavecomponenten (v7.3) ─────────── */

/** Dienstlabels van een dossier. Met onToggle worden het aanklikbare keuzes. */
function DienstChips({ diensten, onToggle, small, style = {} }) {
  const gekozen = new Set(parseDiensten(diensten));
  return (
    <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', ...style }}>
      {DIENST_KEYS.map(k => {
        const aan = gekozen.has(k);
        const d = DIENSTEN[k];
        return (
          <span key={k} title={d.volledig + (onToggle ? (aan ? ' — klik om weg te halen' : ' — klik om toe te voegen') : '')}
                onClick={onToggle ? (e) => { e.stopPropagation(); onToggle(k, !aan); } : undefined}
                style={{
                  padding: small ? '1.5px 7px' : '3px 10px', borderRadius: 999,
                  fontSize: small ? 10 : 11.5, fontWeight: 700, whiteSpace: 'nowrap',
                  cursor: onToggle ? 'pointer' : 'default', userSelect: 'none',
                  background: aan ? d.color + '1f' : 'transparent',
                  color: aan ? d.color : '#a9b5b1',
                  border: `1.5px solid ${aan ? d.color + '55' : '#dfe5e2'}`,
                  opacity: aan || onToggle ? 1 : .55,
                }}>
            {aan ? '✓ ' : ''}{d.label}
          </span>
        );
      })}
    </div>
  );
}

/** Eén mijlpaalvakje. nvt = dienst hoort niet bij dit dossier → grijs streepje. */
function MijlpaalCel({ aan, titel, nvt, onClick, bezig }) {
  if (!aan && nvt) return <span title="Deze dienst hoort niet bij dit dossier" style={{ color: '#e6eae8' }}>·</span>;
  if (!onClick) {
    return aan
      ? <span title={titel} style={{ color: '#1e8a6e', fontWeight: 800, fontSize: 14 }}>✓</span>
      : <span style={{ color: '#d3dcd8' }}>—</span>;
  }
  return (
    <button onClick={(e) => { e.stopPropagation(); onClick(); }} disabled={bezig}
            title={aan ? titel + ' — klik om te wissen' : 'Klik om als afgewerkt te markeren'}
            style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 14,
                     fontWeight: 800, color: aan ? '#1e8a6e' : (nvt ? '#e6eae8' : '#c9d4d0'),
                     padding: '2px 8px', borderRadius: 6 }}>
      {aan ? '✓' : '·'}
    </button>
  );
}

/** Eén factuurvakje. Toestand uit faseToestand(); ':glob' = EPB zonder fase.
    Met onClick wordt het vakje bewerkbaar vanuit het overzicht zelf. */
function FactuurCel({ toestand, nvt, onClick, bezig }) {
  const map = {
    betaald:    { t: '✓', c: '#1e8a6e',  titel: 'Gefactureerd en betaald' },
    verzonden:  { t: '✓', c: '#d69e2e',  titel: 'Factuur verzonden — nog niet betaald' },
    vervallen:  { t: '!', c: C.danger,   titel: 'Factuur vervallen' },
    aangemaakt: { t: '·', c: C.textLight, titel: 'Factuur aangemaakt, nog niet verzonden' },
  };
  const globaal = !!toestand && toestand.endsWith(':glob');
  const kern = globaal ? toestand.slice(0, -5) : toestand;
  const m = kern ? (map[kern] || map.aangemaakt) : null;

  const inhoud = m ? m.t : (nvt ? '·' : '—');
  const kleur  = m ? m.c : (nvt ? '#e6eae8' : '#d3dcd8');
  const titel  = m
    ? (globaal ? m.titel + ' — via een globale EPB-factuur (geen fase vermeld)' : m.titel) +
      (onClick ? '\nKlik om aan te passen.' : '')
    : (nvt ? 'Deze dienst hoort niet bij dit dossier'
           : 'Nog niet gefactureerd' + (onClick ? ' — klik om een factuur toe te voegen' : ''));

  const stijl = { color: kleur, fontWeight: 800, fontSize: 14,
                  opacity: globaal ? .55 : 1,
                  borderBottom: globaal ? `1.5px dotted ${kleur}` : 'none' };
  if (!onClick) return <span title={titel} style={stijl}>{inhoud}</span>;
  return (
    <button onClick={(e) => { e.stopPropagation(); onClick(); }} disabled={bezig} title={titel}
            style={{ ...stijl, border: 'none', background: 'transparent', cursor: 'pointer',
                     padding: '2px 8px', borderRadius: 6, fontFamily: 'inherit' }}>
      {inhoud}
    </button>
  );
}

function Card({ title, subtitle, action, children, style = {}, bodyStyle = {} }) {
  return (
    <div style={{ background: C.card, borderRadius: 14, border: `1px solid ${C.border}`,
                  boxShadow: '0 1px 2px rgba(26,49,23,.04)', ...style }}>
      {(title || action) && (
        <div style={{ padding: '14px 20px', borderBottom: `1px solid ${C.borderSoft}`,
                      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
          <div>
            <div style={{ fontWeight: 700, fontSize: 14.5, color: C.text }}>{title}</div>
            {subtitle && <div style={{ fontSize: 12, color: C.textMuted, marginTop: 2 }}>{subtitle}</div>}
          </div>
          {action}
        </div>
      )}
      <div style={{ padding: '16px 20px', ...bodyStyle }}>{children}</div>
    </div>
  );
}

const inputBase = {
  padding: '8px 11px', border: `1.5px solid ${C.border}`, borderRadius: 8,
  background: '#fbfcfb', fontSize: 13.5, fontFamily: 'inherit', color: C.text,
  outline: 'none', width: '100%', boxSizing: 'border-box',
};
function Input(props) { return <input {...props} style={{ ...inputBase, ...(props.style || {}) }} />; }
function Select({ children, ...props }) {
  return <select {...props} style={{ ...inputBase, cursor: 'pointer', ...(props.style || {}) }}>{children}</select>;
}
function Field({ label, children, style = {} }) {
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0, ...style }}>
      <span style={{ fontSize: 10.5, fontWeight: 700, textTransform: 'uppercase',
                     letterSpacing: '.5px', color: C.textMuted }}>{label}</span>
      {children}
    </label>
  );
}

function Spinner({ label = 'Laden…' }) {
  return (
    <div style={{ padding: 48, textAlign: 'center', color: C.textMuted, fontSize: 14 }}>
      <div className="eui-spin" style={{ width: 26, height: 26, border: `3px solid ${C.border}`,
        borderTopColor: C.primary, borderRadius: '50%', margin: '0 auto 12px' }} />
      {label}
    </div>
  );
}

function EmptyState({ icon = 'folder', title, hint }) {
  return (
    <div style={{ padding: '44px 20px', textAlign: 'center', color: C.textMuted }}>
      <div style={{ width: 52, height: 52, borderRadius: 14, background: C.bg, display: 'flex',
                    alignItems: 'center', justifyContent: 'center', margin: '0 auto 14px', color: C.textLight }}>
        <Icon name={icon} size={24} />
      </div>
      <div style={{ fontWeight: 700, fontSize: 14, color: C.text }}>{title}</div>
      {hint && <div style={{ fontSize: 12.5, marginTop: 5 }}>{hint}</div>}
    </div>
  );
}

/* ─── Navigatie ─────────────────────────────────────────────────────────── */
const NAV_ITEMS = [
  { id: 'dashboard',    icon: 'dashboard',  label: 'Dashboard' },
  { id: 'aanvragen',    icon: 'inbox_tray', label: 'Aanvragen', href: '/aanvragen.html' },
  { id: 'projects',     icon: 'folder',     label: 'Projecten' },
  // 'Overzicht' stond hier ook, maar dat was dezelfde pagina als Projecten met
  // de Mijlpalen-weergave afgedwongen — en Projecten heeft die knop al bovenaan
  // staan (en onthoudt je keuze). De route /?page=overzicht blijft wel werken,
  // zodat bestaande links en de knop in de mobiele app niet breken.
  { id: 'invoices',     icon: 'euro',       label: 'Facturatie' },
  { id: 'offerte',      icon: 'doc',        label: 'Offerte Generator', href: '/offerte.html', newTab: true },
  { id: 'inbox',        icon: 'mail',       label: 'Inbox' },
  { id: 'instellingen', icon: 'settings',   label: 'Instellingen' },
  { id: 'snel',         icon: 'phone',      label: 'Mobile', href: '/app.html' },
];

/** Navigeer naar een pagina van de hoofdapp (index.html). */
function goPage(id) {
  const item = NAV_ITEMS.find(m => m.id === id);
  if (item && item.href) {
    if (item.newTab) window.open(item.href, '_blank'); else window.location.href = item.href;
    return;
  }
  window.location.href = '/?page=' + id;
}

/* ─── Layout (sidebar + topbar) ─────────────────────────────────────────── */
function Layout({ children, currentUser, active, page, setPage, badges = {}, fullBleed = false, topExtra }) {
  const { isMobile } = useViewport();
  const [collapsed, setCollapsed] = useState(() => localStorage.getItem('sidebar_collapsed') === '1');
  const [drawerOpen, setDrawerOpen] = useState(false);
  const activeId = active || page;

  useEffect(() => { localStorage.setItem('sidebar_collapsed', collapsed ? '1' : '0'); }, [collapsed]);
  useEffect(() => { if (!isMobile) setDrawerOpen(false); }, [isMobile]);

  const nav = (m) => {
    setDrawerOpen(false);
    if (m.href) { if (m.newTab) window.open(m.href, '_blank'); else window.location.href = m.href; return; }
    if (setPage) {
      setPage(m.id);
      try { history.replaceState(null, '', '/?page=' + m.id); } catch {}
    } else {
      window.location.href = '/?page=' + m.id;
    }
  };

  const doLogout = async () => {
    await api.post('/api/logout');
    window.location.href = '/';
  };

  const sidebarWidth = collapsed ? 64 : 224;
  const showSidebar = !isMobile || drawerOpen;

  const sidebar = (
    <div style={{
      width: sidebarWidth, background: C.sidebar, color: C.sidebarText,
      display: 'flex', flexDirection: 'column', flexShrink: 0,
      transition: 'width .18s ease', overflow: 'hidden',
      ...(isMobile ? { position: 'fixed', inset: '0 auto 0 0', zIndex: 2500, width: 240,
                       boxShadow: drawerOpen ? '8px 0 40px rgba(0,0,0,.35)' : 'none' } : {}),
    }}>
      {/* brand — het échte Enerveco-logo (lichte variant voor donkere achtergrond) */}
      <div style={{ padding: collapsed && !isMobile ? '16px 6px' : '16px 20px', display: 'flex',
                    flexDirection: 'column', alignItems: collapsed && !isMobile ? 'center' : 'flex-start', gap: 6,
                    borderBottom: '1px solid rgba(245,241,234,.08)' }}>
        <img src="/ui/logo-light.png" srcSet="/ui/logo-light.png 1x, /ui/logo-light@2x.png 2x" alt="Enerveco"
             style={{ height: collapsed && !isMobile ? 34 : 56, width: 'auto', display: 'block' }} />
        {(!collapsed || isMobile) && (
          <div style={{ fontSize: 10, letterSpacing: '.14em', textTransform: 'uppercase', color: 'rgba(195,210,206,.65)' }}>Projectbeheer</div>
        )}
      </div>

      {/* nav */}
      <nav style={{ flex: 1, padding: '12px 10px', display: 'flex', flexDirection: 'column', gap: 3, overflowY: 'auto' }}>
        {NAV_ITEMS.map(m => {
          const isActive = activeId === m.id;
          const badge = badges[m.id];
          return (
            <button key={m.id} onClick={() => nav(m)} title={m.label}
              style={{
                display: 'flex', alignItems: 'center', gap: 12,
                padding: collapsed && !isMobile ? '11px 0' : '10px 12px',
                justifyContent: collapsed && !isMobile ? 'center' : 'flex-start',
                borderRadius: 9, border: 'none', cursor: 'pointer', width: '100%',
                background: isActive ? 'rgba(232,168,56,.14)' : 'transparent',
                color: isActive ? '#f0c268' : C.sidebarText,
                fontSize: 13.5, fontWeight: isActive ? 700 : 500, fontFamily: 'inherit',
                position: 'relative', transition: 'background .12s, color .12s',
              }}
              onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'rgba(245,241,234,.06)'; }}
              onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}
            >
              {isActive && <span style={{ position: 'absolute', left: 0, top: 8, bottom: 8, width: 3,
                                          borderRadius: 3, background: C.sidebarActive }} />}
              <Icon name={m.icon} size={18} />
              {(!collapsed || isMobile) && <span style={{ flex: 1, textAlign: 'left', whiteSpace: 'nowrap' }}>{m.label}</span>}
              {(!collapsed || isMobile) && badge ? (
                <span style={{ background: C.danger, color: '#fff', fontSize: 10.5, fontWeight: 700,
                               borderRadius: 999, padding: '1px 7px' }}>{badge}</span>
              ) : null}
              {m.newTab && (!collapsed || isMobile) && <Icon name="external" size={12} style={{ opacity: .5 }} />}
            </button>
          );
        })}
      </nav>

      {/* footer */}
      <div style={{ padding: '12px 10px', borderTop: '1px solid rgba(245,241,234,.08)' }}>
        {(!collapsed || isMobile) && currentUser && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 12px 10px' }}>
            <div style={{ width: 30, height: 30, borderRadius: '50%', background: 'rgba(232,168,56,.18)',
                          color: '#f0c268', display: 'flex', alignItems: 'center', justifyContent: 'center',
                          fontSize: 12, fontWeight: 700, flexShrink: 0 }}>
              {(currentUser.name || currentUser.username || '?').slice(0, 1).toUpperCase()}
            </div>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 12.5, fontWeight: 600, color: '#f5f1ea', overflow: 'hidden',
                            textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{currentUser.name || currentUser.username}</div>
              <div style={{ fontSize: 10.5, color: 'rgba(195,210,206,.6)' }}>{currentUser.role === 'admin' ? 'Beheerder' : 'Gebruiker'}</div>
            </div>
          </div>
        )}
        <button onClick={doLogout}
          style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%',
                   padding: collapsed && !isMobile ? '10px 0' : '9px 12px',
                   justifyContent: collapsed && !isMobile ? 'center' : 'flex-start',
                   borderRadius: 9, border: 'none', cursor: 'pointer',
                   background: 'transparent', color: '#e59898', fontSize: 13, fontWeight: 600, fontFamily: 'inherit' }}
          onMouseEnter={e => e.currentTarget.style.background = 'rgba(197,48,48,.15)'}
          onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
          <Icon name="logout" size={17} />
          {(!collapsed || isMobile) && 'Uitloggen'}
        </button>
        {!isMobile && (
          <button onClick={() => setCollapsed(!collapsed)}
            style={{ marginTop: 4, display: 'flex', alignItems: 'center', justifyContent: 'center',
                     width: '100%', padding: '7px 0', borderRadius: 9, border: 'none', cursor: 'pointer',
                     background: 'transparent', color: 'rgba(195,210,206,.5)', fontFamily: 'inherit' }}
            title={collapsed ? 'Menu uitklappen' : 'Menu inklappen'}>
            <Icon name={collapsed ? 'chevron_right' : 'chevron_left'} size={16} />
          </button>
        )}
      </div>
    </div>
  );

  return (
    <div className="eui-app-root" style={{ display: 'flex', overflow: 'hidden', background: C.bg }}>
      {showSidebar && sidebar}
      {isMobile && drawerOpen && (
        <div onClick={() => setDrawerOpen(false)}
             style={{ position: 'fixed', inset: 0, background: 'rgba(26,49,23,.5)', zIndex: 2400 }} />
      )}
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
        {/* topbar — weggelaten op desktop-fullBleed zonder extra inhoud */}
        {(isMobile || topExtra) && (
        <div style={{ background: C.headerBg, borderBottom: `1px solid ${C.border}`,
                      padding: isMobile ? '10px 14px' : '10px 26px',
                      display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
          {isMobile && (
            <button onClick={() => setDrawerOpen(true)}
              style={{ background: 'none', border: 'none', cursor: 'pointer', color: C.text, padding: 4 }}
              aria-label="Menu openen">
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
            </button>
          )}
          <div style={{ flex: 1, minWidth: 0 }} />
          {topExtra}
        </div>
        )}
        {/* content */}
        <div style={{ flex: 1, overflow: fullBleed ? 'hidden' : 'auto', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
          {fullBleed ? children : (
            <div style={{ padding: isMobile ? '18px 14px' : '26px 32px', maxWidth: 1520, width: '100%', margin: '0 auto', boxSizing: 'border-box' }}>
              {children}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function PageHeader({ title, subtitle, actions }) {
  return (
    <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
                  gap: 16, flexWrap: 'wrap', marginBottom: 20 }}>
      <div>
        <h1 style={{ margin: 0, fontSize: 23, fontWeight: 800, color: C.text, letterSpacing: '-.01em' }}>{title}</h1>
        {subtitle && <div style={{ fontSize: 13, color: C.textMuted, marginTop: 3 }}>{subtitle}</div>}
      </div>
      {actions && <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>{actions}</div>}
    </div>
  );
}

/* ─── Login screen ──────────────────────────────────────────────────────── */
function LoginScreen({ onLogin }) {
  const [user, setUser] = useState('');
  const [pass, setPass] = useState('');
  const [showPass, setShowPass] = useState(false);
  const [err, setErr] = useState('');
  const [loading, setLoading] = useState(false);
  const submit = async (e) => {
    e.preventDefault(); setLoading(true); setErr('');
    const r = await api.post('/api/login', { username: user, password: pass });
    setLoading(false);
    if (r && r.token) onLogin(r.user);
    else setErr((r && r.error) || 'Inloggen mislukt');
  };
  return (
    <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
                  background: `linear-gradient(135deg, #1a3117 0%, #2b4f26 55%, #3d6d35 100%)`, padding: 16 }}>
      <form onSubmit={submit} style={{ background: '#fff', borderRadius: 18, padding: '44px 38px',
                                       width: 390, maxWidth: '100%', boxShadow: '0 24px 70px rgba(0,0,0,.35)' }}>
        <div style={{ textAlign: 'center', marginBottom: 30 }}>
          <img src="/ui/logo.png" srcSet="/ui/logo.png 1x, /ui/logo@2x.png 2x" alt="Enerveco"
               style={{ height: 86, width: 'auto', display: 'inline-block', marginBottom: 10 }} />
          <div style={{ fontSize: 12.5, color: C.textMuted, letterSpacing: '.06em', textTransform: 'uppercase' }}>Projectbeheer</div>
        </div>
        <Field label="Gebruikersnaam" style={{ marginBottom: 14 }}>
          <Input value={user} onChange={e => setUser(e.target.value)} autoFocus autoComplete="username" />
        </Field>
        <Field label="Wachtwoord" style={{ marginBottom: 20 }}>
          <div style={{ position: 'relative' }}>
            <Input type={showPass ? 'text' : 'password'} value={pass} onChange={e => setPass(e.target.value)}
                   autoComplete="current-password" style={{ paddingRight: 40 }} />
            <button type="button" onClick={() => setShowPass(!showPass)}
                    aria-label={showPass ? 'Wachtwoord verbergen' : 'Wachtwoord tonen'}
                    style={{ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)',
                             background: 'none', border: 'none', cursor: 'pointer', color: C.textLight, padding: 4 }}>
              <Icon name="eye" size={16} />
            </button>
          </div>
        </Field>
        {err && <div style={{ background: '#fde8e8', color: C.danger, borderRadius: 8, padding: '9px 12px',
                              fontSize: 12.5, marginBottom: 14, fontWeight: 600 }}>{err}</div>}
        <Btn type="submit" variant="primary" disabled={loading}
             style={{ width: '100%', justifyContent: 'center', padding: '11px 0', fontSize: 14 }}>
          {loading ? 'Bezig…' : 'Inloggen'}
        </Btn>
      </form>
    </div>
  );
}

/* ─── Export ────────────────────────────────────────────────────────────── */
window.EUI = {
  C, STATUS_META, STATUS_ORDER, normStatus, statusLabel, statusColor,
  INVOICE_STATUS_COLORS, URGENCY_META,
  DIENSTEN, DIENST_KEYS, parseDiensten, formatDiensten, heeftDienst,
  FACTUUR_FASEN, FASE_KEYS, factuurFasen, faseToestand, nietToegewezenFacturen,
  MIJLPAAL_VOLGORDE, MIJLPAAL_KOLOMMEN, berekenMijlpalen,
  DienstChips, MijlpaalCel, FactuurCel,
  ontleedAdres, vergelijkAdres, zoekDossier,
  api, fmtEuro, fmtDate, fmtDateShort, relTime,
  useViewport, MOBILE_BP, TABLET_BP,
  Icon, Btn, Badge, StatusBadge, Card, Input, Select, Field, Spinner, EmptyState,
  NAV_ITEMS, goPage, Layout, PageHeader, LoginScreen,
};
})();
