/* Kunder > Kundregister (redesign 2026-09).
 *
 * Källor (alla läsande):
 *   GET /v1/admin/clients                        kundposter: id, name, emailDomain, creditLimitSek
 *   GET /v1/admin/reports/sales?days=90          byClient: emailDomain, orderCount, revenueSek (exkl. moms), lastOrderAt; summary.totalRevenueSek
 *   GET /v1/admin/reports/customer-analysis      customers: emailDomain, lastOrderAt (senaste order oavsett period)
 *   GET /v1/admin/fortnox/ar?status=open         invoices: customer_number, customer_name, due_date, balance (inkl. moms)
 *   GET /v1/admin/finance/customer-dso?days=180  customers: customerNumber, avgDso, avgDaysLate, paidCount
 *   GET /v1/admin/finance/fortnox-by-customer    customers: customerNumber, customerName, emailDomain (via customer_mapping)
 *
 * Koppling Shopify-domän > kundpost > Fortnox-kund:
 *   1. Domän mot kundpost: exakt e-postdomän.
 *   2. Kundpost mot Fortnox: först kundnummer som faktureringen redan har
 *      kopplat till domänen (customer_mapping). Annars namn efter
 *      normalisering (gemener, utan AB/Sweden/Sverige/i Sverige och
 *      skiljetecken), lika eller prefix på ordgräns. En kund kan få flera
 *      kundnummer (t.ex. ChargeBuddy AB och Chargebuddy Energy AB). Det blir
 *      bara "Ej kopplad" när ett kundnummer matchar flera kundposter.
 */

const RX_CUS_STRIP = new Set(['ab', 'aktiebolag', 'publ', 'sweden', 'sverige', 'hb', 'kb']);
const RX_CUS_DSO_DAYS = 180;

function rxCusNorm(name) {
  if (!name) return '';
  const s = String(name).toLowerCase().replace(/&/g, ' och ').replace(/[^a-z0-9åäöéü]+/g, ' ').trim();
  const words = s.split(' ').filter(Boolean);
  const out = [];
  for (let i = 0; i < words.length; i++) {
    const w = words[i];
    if (w === 'i' && words[i + 1] === 'sverige') continue;
    if (RX_CUS_STRIP.has(w)) continue;
    out.push(w);
  }
  return out.join(' ');
}

function rxCusNameMatch(a, b) {
  if (!a || !b) return false;
  if (a === b) return true;
  if (a.replace(/ /g, '') === b.replace(/ /g, '')) return true;
  const short = a.length <= b.length ? a : b;
  const long = a.length <= b.length ? b : a;
  if (short.replace(/ /g, '').length < 4) return false;
  return long.startsWith(short + ' ');
}

function rxCusTodayIso() {
  const t = rxToday();
  return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`;
}

function rxCusDate(v) {
  const d = rxParseDate(v);
  if (!d) return '';
  const base = `${d.getDate()}/${d.getMonth() + 1}`;
  return d.getFullYear() === new Date().getFullYear() ? base : `${base} ${d.getFullYear()}`;
}

function rxCusPct(x) {
  if (x == null || !isFinite(x)) return '';
  return `${(x * 100).toLocaleString('sv-SE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %`;
}

/** Slår ihop källorna till en rad per e-postdomän. Källor som saknas är null. */
function rxCusBuild({ clients, sales, analysis, ar, dso, fbc }) {
  const todayIso = rxCusTodayIso();

  // Fortnox-kunder, nyckel = kundnummer
  const fx = new Map();
  const addFx = (num, name) => {
    if (num == null || num === '') return;
    const k = String(num);
    if (!fx.has(k)) fx.set(k, { num: k, name: name || '' });
    else if (!fx.get(k).name && name) fx.get(k).name = name;
  };
  (ar?.invoices || []).forEach(i => addFx(i.customer_number, i.customer_name));
  (dso?.customers || []).forEach(c => addFx(c.customerNumber, c.customerName));
  (fbc?.customers || []).forEach(c => addFx(c.customerNumber, c.customerName));
  fx.forEach(f => { f.norm = rxCusNorm(f.name); });

  // Öppna kundfordringar per kundnummer
  const arBy = new Map();
  (ar?.invoices || []).forEach(i => {
    const k = String(i.customer_number ?? '');
    if (!k) return;
    const bal = Number(i.balance) || 0;
    if (bal <= 0) return;
    const a = arBy.get(k) || { open: 0, openCount: 0, overdue: 0, overdueCount: 0, oldestOverdue: null };
    a.open += bal; a.openCount += 1;
    if (i.due_date && i.due_date < todayIso) {
      a.overdue += bal; a.overdueCount += 1;
      if (!a.oldestOverdue || i.due_date < a.oldestOverdue) a.oldestOverdue = i.due_date;
    }
    arBy.set(k, a);
  });

  const dsoBy = new Map();
  (dso?.customers || []).forEach(c => dsoBy.set(String(c.customerNumber), c));

  // Steg 1: kundnummer som faktureringen redan kopplat till en domän
  const mapped = new Map();
  (fbc?.customers || []).forEach(c => {
    if (!c.emailDomain || c.customerNumber == null) return;
    const d = String(c.emailDomain).toLowerCase();
    if (!mapped.has(d)) mapped.set(d, new Set());
    mapped.get(d).add(String(c.customerNumber));
  });
  const taken = new Set();
  mapped.forEach(s => s.forEach(n => taken.add(n)));

  // Steg 2: namnmatchning för kundposter utan kundnummer
  const clientList = clients || [];
  const links = new Map();
  const cands = new Map();
  const reverse = new Map();
  clientList.forEach(c => {
    const d = String(c.emailDomain || '').toLowerCase();
    const m = mapped.get(d);
    if (m && m.size) { links.set(c.id, { method: 'mapping', nums: [...m] }); return; }
    const n = rxCusNorm(c.name);
    const list = [];
    fx.forEach(f => { if (!taken.has(f.num) && rxCusNameMatch(n, f.norm)) list.push(f.num); });
    cands.set(c.id, list);
    list.forEach(num => reverse.set(num, (reverse.get(num) || 0) + 1));
  });
  clientList.forEach(c => {
    if (links.has(c.id)) return;
    const list = cands.get(c.id) || [];
    // En kund kan ha flera kundnummer i Fortnox (t.ex. ChargeBuddy AB och
    // Chargebuddy Energy AB). Alla kandidater kopplas så länge ingen av dem
    // också matchar en annan kundpost; bara då är det tvetydigt.
    if (!list.length) links.set(c.id, { method: 'none', nums: [] });
    else if (list.every(num => reverse.get(num) === 1)) links.set(c.id, { method: 'name', nums: list });
    else links.set(c.id, { method: 'ambiguous', nums: [], names: list.map(n => fx.get(n)?.name || n) });
  });

  // Rader: domäner med ordrar senaste 90 dagarna plus alla kundposter
  const byDomain = new Map();
  const ensure = d => {
    if (!byDomain.has(d)) byDomain.set(d, { domain: d, client: null, revenue90: 0, orders90: 0, lastOrderAt: null });
    return byDomain.get(d);
  };
  let noEmailRevenue = 0;
  (sales?.byClient || []).forEach(s => {
    const d = String(s.emailDomain || '').toLowerCase();
    if (!d || d === 'unknown') { noEmailRevenue += Number(s.revenueSek) || 0; return; }
    const r = ensure(d);
    r.revenue90 += Number(s.revenueSek) || 0;
    r.orders90 += Number(s.orderCount) || 0;
    if (s.lastOrderAt && (!r.lastOrderAt || s.lastOrderAt > r.lastOrderAt)) r.lastOrderAt = s.lastOrderAt;
  });
  clientList.forEach(c => {
    const d = String(c.emailDomain || '').toLowerCase();
    if (!d) return;
    ensure(d).client = c;
  });
  const lastAll = new Map();
  (analysis?.customers || []).forEach(a => { if (a.emailDomain) lastAll.set(String(a.emailDomain).toLowerCase(), a.lastOrderAt); });

  const linkedNums = new Set();
  const rows = [...byDomain.values()].map(r => {
    if (!r.lastOrderAt && lastAll.has(r.domain)) r.lastOrderAt = lastAll.get(r.domain);
    const link = r.client ? links.get(r.client.id) : null;
    const nums = link ? link.nums : [];
    nums.forEach(n => linkedNums.add(n));
    const agg = { open: 0, openCount: 0, overdue: 0, overdueCount: 0, oldestOverdue: null };
    nums.forEach(n => {
      const a = arBy.get(n);
      if (!a) return;
      agg.open += a.open; agg.openCount += a.openCount;
      agg.overdue += a.overdue; agg.overdueCount += a.overdueCount;
      if (a.oldestOverdue && (!agg.oldestOverdue || a.oldestOverdue < agg.oldestOverdue)) agg.oldestOverdue = a.oldestOverdue;
    });
    // Betalningsmönster, viktat på antal betalda fakturor
    let w = 0, sumDso = 0, wLate = 0, sumLate = 0;
    nums.forEach(n => {
      const c = dsoBy.get(n);
      if (!c) return;
      const pc = Number(c.paidCount) || 0;
      if (c.avgDso != null && pc > 0) { w += pc; sumDso += c.avgDso * pc; }
      if (c.avgDaysLate != null && pc > 0) { wLate += pc; sumLate += c.avgDaysLate * pc; }
    });
    return {
      ...r,
      link,
      fortnox: nums.map(n => ({ num: n, name: fx.get(n)?.name || '' })),
      ar: agg,
      paidCount: w,
      avgDso: w > 0 ? Math.round(sumDso / w) : null,
      avgDaysLate: wLate > 0 ? Math.round(sumLate / wLate) : null,
      name: r.client ? r.client.name : r.domain,
    };
  });
  rows.sort((a, b) => (b.revenue90 - a.revenue90) || (b.ar.open - a.ar.open) || a.name.localeCompare(b.name, 'sv'));

  let unlinkedFxCount = 0, unlinkedFxSum = 0;
  arBy.forEach((a, n) => { if (!linkedNums.has(n) && a.open > 0) { unlinkedFxCount += 1; unlinkedFxSum += a.open; } });

  return { rows, noEmailRevenue, unlinkedFxCount, unlinkedFxSum };
}

function rxCusPaysText(late) {
  if (late == null) return null;
  if (late < 0) return `Betalar i snitt ${rxPlural(-late, 'dag', 'dagar')} före förfallodag`;
  if (late === 0) return 'Betalar i snitt på förfallodagen';
  return `Betalar i snitt ${rxPlural(late, 'dag', 'dagar')} efter förfallodag`;
}

function rxCusIsAnomaly(row) {
  return row.avgDaysLate != null && row.avgDaysLate < 0 && row.ar.overdueCount >= 3;
}

function RxCusDash() {
  return <span className="rx-faint" aria-label="saknas">·</span>;
}

/** Status för Fortnox-kolumnerna för en rad: 'ok' | 'loading' | 'error' | 'noclient' | 'unlinked' */
function rxCusFxState(row, st) {
  if (!row.client) return 'noclient';
  if (st.fbc.loading) return 'loading';
  if (!row.link || row.link.nums.length === 0) return 'unlinked';
  return 'ok';
}

function RxCusUnlinked({ row }) {
  const title = !row.client
    ? 'Domänen saknar kundpost och kan inte kopplas till ett kundnummer i Fortnox.'
    : row.link?.method === 'ambiguous'
      ? `Flera möjliga Fortnox-kunder: ${row.link.names.join(', ')}`
      : 'Ingen säker matchning mot en kund i Fortnox.';
  return <span className="rx-faint" title={title}>Ej kopplad</span>;
}

function RxCusArCell({ row, st, field }) {
  const s = rxCusFxState(row, st);
  if (s === 'noclient' || s === 'unlinked') return <RxCusUnlinked row={row} />;
  if (s === 'loading' || st.ar.loading) return <span className="rx-faint">…</span>;
  if (st.ar.error) return <span className="rx-faint" title={String(st.ar.error?.message || st.ar.error)}>Kunde inte hämtas</span>;
  if (field === 'open') {
    if (!row.ar.openCount) return <span className="rx-muted">0 kr</span>;
    return <span>{fmtSek(row.ar.open)}</span>;
  }
  if (!row.ar.overdueCount) return <span className="rx-muted">0 kr</span>;
  return <span style={{ fontWeight: 600, color: 'var(--rx-warn)' }}>{fmtSek(row.ar.overdue)}</span>;
}

function RxCusDsoCell({ row, st }) {
  const s = rxCusFxState(row, st);
  if (s === 'noclient' || s === 'unlinked') return <RxCusUnlinked row={row} />;
  if (s === 'loading' || st.dso.loading) return <span className="rx-faint">…</span>;
  if (st.dso.error) return <span className="rx-faint" title={String(st.dso.error?.message || st.dso.error)}>Kunde inte hämtas</span>;
  if (row.avgDso == null) return <span className="rx-faint" title={`Inga betalda fakturor senaste ${RX_CUS_DSO_DAYS} dagarna`}>Ingen historik</span>;
  return <span>{rxPlural(row.avgDso, 'dag', 'dagar')}</span>;
}

function RxCusLimitCell({ row, st }) {
  if (st.clients.error) return <span className="rx-faint">Kunde inte hämtas</span>;
  if (!row.client) return <span className="rx-faint">Skapa kundpost först</span>;
  if (Number(row.client.creditLimitSek) > 0) return <span>{fmtSek(row.client.creditLimitSek)}</span>;
  return <RxPill tone="warn">Saknas</RxPill>;
}

function RxCusCard({ row, st, totalRevenue, onNavigate }) {
  const fxs = rxCusFxState(row, st);
  const linked = fxs === 'ok';
  const arOk = linked && !st.ar.loading && !st.ar.error;
  const dsoOk = linked && !st.dso.loading && !st.dso.error;
  const share = totalRevenue > 0 ? row.revenue90 / totalRevenue : null;
  const pays = rxCusPaysText(row.avgDaysLate);
  const fxUnavailable = !linked ? 'Ej kopplad' : null;

  return <RxPanel title={row.name} sub={row.client ? row.domain : 'saknar kundpost'}>
    <div className="rx-grid rx-grid--2" style={{ gap: 10 }}>
      <RxKpi label="Omsättning 90 d" value={rxShortSek(row.revenue90)}
        sub={share != null ? `${rxCusPct(share)} av total, exkl. moms` : 'exkl. moms'} />
      <RxKpi label="Ordrar 90 d" value={fmtInt(row.orders90)}
        sub={row.lastOrderAt ? `senaste ${rxCusDate(row.lastOrderAt)}` : 'ingen order registrerad'} />
      <RxKpi label="Öppna kundfordringar" value={arOk ? rxShortSek(row.ar.open) : (fxUnavailable || (st.ar.error ? 'Fel' : '…'))}
        sub={arOk ? `${rxPlural(row.ar.openCount, 'faktura', 'fakturor')}, inkl. moms` : null} />
      <RxKpi label="Förfallet" tone={arOk && row.ar.overdueCount ? 'warn' : undefined}
        value={arOk ? rxShortSek(row.ar.overdue) : (fxUnavailable || (st.ar.error ? 'Fel' : '…'))}
        sub={arOk && row.ar.overdueCount ? `${rxPlural(row.ar.overdueCount, 'faktura', 'fakturor')}, äldsta ${rxCusDate(row.ar.oldestOverdue)}` : (arOk ? 'inget förfallet' : null)} />
    </div>

    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div className="rx-section-title" style={{ fontSize: 14 }}>Betalningsmönster</div>
      {!linked && <div className="rx-muted" style={{ fontSize: 13 }}>
        {!row.client ? 'Domänen saknar kundpost, så inga fakturor i Fortnox kan kopplas.' :
          row.link?.method === 'ambiguous' ? `Flera möjliga Fortnox-kunder (${row.link.names.join(', ')}). Ingen koppling görs.` :
          st.fbc.loading ? 'Hämtar koppling mot Fortnox…' : 'Ingen säker matchning mot en kund i Fortnox.'}
      </div>}
      {linked && st.dso.loading && <div className="rx-muted" style={{ fontSize: 13 }}>Hämtar…</div>}
      {linked && st.dso.error && <div className="rx-muted" style={{ fontSize: 13 }}>Kunde inte hämta betalningshistorik.</div>}
      {dsoOk && row.avgDso == null && <div className="rx-muted" style={{ fontSize: 13 }}>Inga betalda fakturor senaste {RX_CUS_DSO_DAYS} dagarna.</div>}
      {dsoOk && row.avgDso != null && <>
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13.5 }}>
          <span className="rx-muted">Genomsnittlig kredittid</span>
          <span style={{ fontWeight: 600 }}>{rxPlural(row.avgDso, 'dag', 'dagar')}</span>
        </div>
        {pays && <div style={{ fontSize: 13.5 }}>{pays}</div>}
        <div className="rx-faint" style={{ fontSize: 12 }}>
          Baserat på {rxPlural(row.paidCount, 'betald faktura', 'betalda fakturor')} senaste {RX_CUS_DSO_DAYS} dagarna.
        </div>
      </>}
      {dsoOk && arOk && rxCusIsAnomaly(row) && <RxCallout tone="warn" title="Avvikelse.">
        Historiskt betalar kunden i snitt {rxPlural(-row.avgDaysLate, 'dag', 'dagar')} före förfallodag, men {rxPlural(row.ar.overdueCount, 'faktura', 'fakturor')} är nu förfallna.
      </RxCallout>}
      {linked && <div className="rx-faint" style={{ fontSize: 12 }}>
        {row.link.method === 'mapping' ? 'Kopplad via kundnummer från faktureringen' : 'Kopplad på kundnamn'}
        {': '}{row.fortnox.map(f => `${f.name || 'namn saknas'} (kundnr ${f.num})`).join(', ')}
      </div>}
    </div>

    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div className="rx-section-title" style={{ fontSize: 14 }}>Kreditgräns</div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 13.5 }}>
        {st.clients.error ? <span className="rx-muted">Kunde inte hämtas</span> :
          !row.client ? <span className="rx-muted">Skapa kundpost först</span> :
          Number(row.client.creditLimitSek) > 0 ? <span style={{ fontWeight: 600 }}>{fmtSek(row.client.creditLimitSek)}</span> :
          <RxPill tone="warn">Ingen gräns satt</RxPill>}
        <button type="button" className="rx-link" style={{ marginLeft: 'auto' }} onClick={() => onNavigate && onNavigate('clients/kredit')}>
          {row.client ? 'Ändra kreditgräns' : 'Skapa kundpost'}
        </button>
      </div>
      {arOk && Number(row.client?.creditLimitSek) > 0 && <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        <RxBar pct={row.ar.open / row.client.creditLimitSek * 100} tone={row.ar.open > row.client.creditLimitSek ? 'warn' : undefined} />
        <span className="rx-faint" style={{ fontSize: 12 }}>{rxCusPct(row.ar.open / row.client.creditLimitSek)} utnyttjat av öppna kundfordringar</span>
      </div>}
    </div>

    <div style={{ borderTop: '1px solid var(--rx-divider)', paddingTop: 12 }}>
      <button type="button" className="rx-link" onClick={() => onNavigate && onNavigate('orders')}>Visa ordrar</button>
    </div>
  </RxPanel>;
}

function RxCustomersPage({ onNavigate }) {
  const clientsQ = useRxFetch('/v1/admin/clients');
  const salesQ = useRxFetch('/v1/admin/reports/sales?days=90');
  const analysisQ = useRxFetch('/v1/admin/reports/customer-analysis');
  const arQ = useRxFetch('/v1/admin/fortnox/ar?status=open');
  const dsoQ = useRxFetch(`/v1/admin/finance/customer-dso?days=${RX_CUS_DSO_DAYS}`);
  const fbcQ = useRxFetch('/v1/admin/finance/fortnox-by-customer');
  const [q, setQ] = React.useState('');
  const [filter, setFilter] = React.useState('all');
  const [selected, setSelected] = React.useState(null);

  const st = { clients: clientsQ, sales: salesQ, analysis: analysisQ, ar: arQ, dso: dsoQ, fbc: fbcQ };

  const model = React.useMemo(() => rxCusBuild({
    clients: clientsQ.data, sales: salesQ.data, analysis: analysisQ.data,
    ar: arQ.data, dso: dsoQ.data, fbc: fbcQ.data,
  }), [clientsQ.data, salesQ.data, analysisQ.data, arQ.data, dsoQ.data, fbcQ.data]);

  const clientsOk = !!clientsQ.data;
  const totalRevenue = Number(salesQ.data?.summary?.totalRevenueSek) || 0;
  const rows = model.rows;
  const noClient = clientsOk ? rows.filter(r => !r.client && r.orders90 > 0) : [];
  const withOverdue = rows.filter(r => r.client && r.link?.nums.length && r.ar.overdueCount > 0);
  const top2 = salesQ.data ? rows.filter(r => r.revenue90 > 0).slice(0, 2) : [];
  const top2Share = totalRevenue > 0 ? top2.reduce((s, r) => s + r.revenue90, 0) / totalRevenue : null;

  let arTotal = 0, overdueTotal = 0, overdueCount = 0;
  if (arQ.data) {
    const todayIso = rxCusTodayIso();
    (arQ.data.invoices || []).forEach(i => {
      const b = Number(i.balance) || 0;
      if (b <= 0) return;
      arTotal += b;
      if (i.due_date && i.due_date < todayIso) { overdueTotal += b; overdueCount += 1; }
    });
  }

  const filtered = React.useMemo(() => {
    const ql = q.trim().toLowerCase();
    return rows.filter(r => {
      if (filter === 'overdue' && !(r.client && r.link?.nums.length && r.ar.overdueCount > 0)) return false;
      if (filter === 'noclient' && !(clientsOk && !r.client)) return false;
      if (!ql) return true;
      return r.domain.includes(ql) || r.name.toLowerCase().includes(ql) ||
        r.fortnox.some(f => (f.name || '').toLowerCase().includes(ql) || f.num.includes(ql));
    });
  }, [rows, q, filter, clientsOk]);

  const sel = rows.find(r => r.domain === selected) || filtered[0] || null;
  const baseLoading = clientsQ.loading || salesQ.loading;

  const kpiVal = (qry, v) => qry.loading ? '…' : qry.error ? 'Fel' : v;

  return <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
    <div className="rx-grid rx-grid--4">
      <RxKpi label="Omsättning 90 d" value={kpiVal(salesQ, rxShortSek(totalRevenue))}
        sub={salesQ.data ? `${rxPlural(Number(salesQ.data.summary?.totalOrders) || 0, 'order', 'ordrar')}, Shopify exkl. moms` : 'Shopify exkl. moms'} />
      <RxKpi label="Kundkoncentration" value={kpiVal(salesQ, top2Share != null ? rxCusPct(top2Share) : 'Ingen försäljning')}
        sub={top2.length ? `två största: ${top2.map(r => `${r.name} ${rxCusPct(r.revenue90 / totalRevenue)}`).join(', ')}` : 'andel av omsättning 90 d'} />
      <RxKpi label="Kundfordringar" value={kpiVal(arQ, rxShortSek(arTotal))}
        sub={arQ.data ? `${rxPlural((arQ.data.invoices || []).filter(i => Number(i.balance) > 0).length, 'öppen faktura', 'öppna fakturor')}, Fortnox inkl. moms` : 'Fortnox inkl. moms'} />
      <RxKpi label="Förfallet" tone={overdueTotal > 0 ? 'warn' : undefined} value={kpiVal(arQ, rxShortSek(overdueTotal))}
        sub={arQ.data ? `${rxPlural(overdueCount, 'faktura', 'fakturor')}, inkl. moms` : 'inkl. moms'} />
    </div>

    {clientsOk && salesQ.data && noClient.length > 0 && <RxCallout tone="warn"
      title={`${rxPlural(noClient.length, 'e-postdomän', 'e-postdomäner')} har ordrar senaste 90 dagarna men ingen kundpost.`}
      action={<button type="button" className="rx-link" onClick={() => onNavigate && onNavigate('clients/kredit')}>Granska och skapa kundpost</button>}>
      Deras kundfordringar och kreditgräns kan inte kopplas till något kundnummer i Fortnox.
    </RxCallout>}

    {clientsQ.error && <RxError error={clientsQ.error} onRetry={clientsQ.reload} />}
    {salesQ.error && <RxError error={salesQ.error} onRetry={salesQ.reload} />}
    {fbcQ.error && <RxCallout tone="warn" title="Kopplingen via kundnummer kunde inte hämtas.">
      Kunder kopplas bara på namn tills den går att hämta igen.
    </RxCallout>}

    <RxFilterRow value={filter} onChange={setFilter} filters={[
      { key: 'all', label: 'Alla', count: rows.length },
      { key: 'overdue', label: 'Med förfallet', count: arQ.data && !fbcQ.loading ? withOverdue.length : null, tone: withOverdue.length ? 'warn' : undefined },
      { key: 'noclient', label: 'Utan kundpost', count: clientsOk ? rows.filter(r => !r.client).length : null },
    ]}>
      <div style={{ marginLeft: 'auto' }}>
        <RxSearch value={q} onChange={setQ} placeholder="Sök kund, domän eller kundnummer" label="Sök i kundregistret" />
      </div>
    </RxFilterRow>

    <div className="rx-split">
      <div className="rx-split__main">
        <RxPanel flush title="Kundregister" sub="Sorterat på omsättning senaste 90 dagarna">
          {baseLoading ? <RxLoading /> : rows.length === 0 ? <RxEmpty title="Inga kunder">Det finns inga kundposter och inga ordrar senaste 90 dagarna.</RxEmpty> :
          filtered.length === 0 ? <RxEmpty title="Inga kunder matchar">Ändra sökning eller filter.</RxEmpty> :
          <div style={{ overflowX: 'auto' }}>
            <table className="rx-table">
              <thead><tr>
                <th scope="col">Kund</th>
                <th scope="col" className="rx-num">Omsättning 90 d</th>
                <th scope="col" className="rx-num">Ordrar 90 d</th>
                <th scope="col">Senaste order</th>
                <th scope="col" className="rx-num">Kundfordringar</th>
                <th scope="col" className="rx-num">Förfallet</th>
                <th scope="col" className="rx-num">Kredittid</th>
                <th scope="col" className="rx-num">Kreditgräns</th>
              </tr></thead>
              <tbody>
                {filtered.map(r => {
                  const isSel = sel && sel.domain === r.domain;
                  return <tr key={r.domain} className={`rx-row--click${isSel ? ' rx-row--selected' : ''}`} onClick={() => setSelected(r.domain)}>
                    <td>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                        <span aria-hidden="true" style={{ width: 30, height: 30, borderRadius: 8, flexShrink: 0, display: 'grid', placeItems: 'center', fontSize: 13, fontWeight: 700,
                          color: r.client ? '#2F25A8' : 'var(--rx-ink-4)', background: r.client ? '#E6E4FA' : 'var(--rx-neutral-bg)' }}>
                          {(r.name || '?').charAt(0).toUpperCase()}
                        </span>
                        <div style={{ display: 'flex', flexDirection: 'column', minWidth: 0 }}>
                          <button type="button" aria-pressed={isSel} onClick={e => { e.stopPropagation(); setSelected(r.domain); }}
                            style={{ border: 0, background: 'none', padding: 0, font: 'inherit', fontWeight: 600, color: 'var(--rx-ink)', textAlign: 'left', cursor: 'pointer' }}>
                            {r.name}
                          </button>
                          {r.client
                            ? <span className="rx-muted" style={{ fontSize: 12 }}>{r.domain}</span>
                            : clientsOk && <span style={{ fontSize: 12, color: 'var(--rx-warn)' }}>saknar kundpost</span>}
                        </div>
                      </div>
                    </td>
                    <td className="rx-num">
                      {salesQ.error ? <span className="rx-faint">Kunde inte hämtas</span> : <>
                        <div style={{ fontWeight: 600 }}>{fmtSek(r.revenue90)}</div>
                        {totalRevenue > 0 && r.revenue90 > 0 && <div className="rx-muted" style={{ fontSize: 12 }}>{rxCusPct(r.revenue90 / totalRevenue)}</div>}
                      </>}
                    </td>
                    <td className="rx-num">{salesQ.error ? <RxCusDash /> : fmtInt(r.orders90)}</td>
                    <td className="rx-muted">{r.lastOrderAt ? rxCusDate(r.lastOrderAt) : (analysisQ.loading ? '…' : <span className="rx-faint">Ingen order</span>)}</td>
                    <td className="rx-num"><RxCusArCell row={r} st={st} field="open" /></td>
                    <td className="rx-num"><RxCusArCell row={r} st={st} field="overdue" /></td>
                    <td className="rx-num"><RxCusDsoCell row={r} st={st} /></td>
                    <td className="rx-num"><RxCusLimitCell row={r} st={st} /></td>
                  </tr>;
                })}
              </tbody>
            </table>
          </div>}
        </RxPanel>
        <div className="rx-faint" style={{ fontSize: 12, display: 'flex', flexDirection: 'column', gap: 2 }}>
          <span>Omsättning från Shopify exkl. moms. Kundfordringar och förfallet från Fortnox inkl. moms. Kredittid är snittet från fakturadatum till betalning, senaste {RX_CUS_DSO_DAYS} dagarna.</span>
          {arQ.data && !fbcQ.loading && model.unlinkedFxCount > 0 && <span>
            {rxPlural(model.unlinkedFxCount, 'kund', 'kunder')} i Fortnox med öppna kundfordringar ({fmtSek(model.unlinkedFxSum)}) är inte kopplade till någon kundpost.
          </span>}
          {model.noEmailRevenue > 0 && <span>Ordrar utan e-postadress ({fmtSek(model.noEmailRevenue)}) ingår i totalen men visas inte som egen rad.</span>}
        </div>
      </div>
      <div className="rx-split__side">
        {sel ? <RxCusCard row={sel} st={st} totalRevenue={totalRevenue} onNavigate={onNavigate} />
          : !baseLoading && <RxPanel><RxEmpty>Välj en kund i listan för att se kundkortet.</RxEmpty></RxPanel>}
      </div>
    </div>
  </div>;
}
