/* Ordrar, redesign 2026-09.
 *
 * Datakällor (läs backend innan du ändrar fältnamn):
 *   GET /v1/admin/orders                 src/routes/admin.orders.ts
 *       limit, offset, status (open|cancelled|all), search (ordernr, PO, e-post),
 *       domain, delivery_from, delivery_to, sort, sort_dir
 *   GET /v1/admin/orders/:shopifyId      samma fil, med lineItems
 *   GET /v1/admin/opter/deliveries       src/routes/admin.opter.ts + src/lib/deliveryView.ts
 *       overdue, domain, delivery_from, delivery_to, q, limit (max 500), offset
 *       Ordrar utan leveransdatum följer med i datumfiltret (includeUndated).
 *   GET /v1/admin/opter/deliveries/:id   detalj med kollin, 404 för avbrutna
 *   GET /v1/admin/fortnox/reconciliation-gaps   src/routes/admin.fortnox.ts
 *       Passerat leveransdatum, senaste 180 dagarna, utan lyckad eller pågående
 *       faktura. Högst 200 rader, sorterade på leveransdatum fallande.
 *   GET /v1/admin/finance/invoices       de 200 senaste fakturasynkarna (för förloppet)
 *
 * Belopp är Shopifys total_price och visas som exkl. moms.
 */

const RX_ORD_PER_PAGE = 50;
const RX_ORD_SHOPIFY_URL = 'https://admin.shopify.com/store/stakk-ab/orders/';

function rxOrdIso(d) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

function rxOrdAddDays(iso, n) {
  const d = rxParseDate(iso);
  d.setDate(d.getDate() + n);
  return rxOrdIso(d);
}

/** SQLite-tider ('2026-09-23 13:37:00') tolkas som ISO. */
function rxOrdStamp(v) {
  if (!v) return '';
  const s = String(v).replace(' ', 'T');
  const day = rxDay(s);
  const time = rxTime(s);
  return day ? `${day} ${time}` : '';
}

function rxOrdSum(rows) {
  return rows.reduce((a, r) => a + (Number(r.totalPrice) || 0), 0);
}

/** Normaliserad rad från en order i /v1/admin/orders (lista eller detalj). */
function rxOrdFromOrder(o) {
  return {
    id: o.shopifyId,
    name: o.shopifyName,
    po: o.poNumber,
    customer: o.clientName || o.emailDomain,
    email: o.email,
    domain: o.emailDomain,
    deliveryDate: o.deliveryDate,
    fulfillmentStatus: o.fulfillmentStatus,
    shippedTime: o.ongoingShippedTime,
    cancelledAt: o.cancelledAt,
    totalPrice: o.totalPrice,
    createdAt: o.createdAt,
    hasOrder: true,
  };
}

function rxOrdMerge(r, o) {
  return o ? { ...r, ...rxOrdFromOrder(o) } : r;
}

function rxOrdFs(s) {
  const v = String(s || '').toLowerCase();
  if (v === 'fulfilled') return 'fulfilled';
  if (v === 'partial' || v === 'partially_fulfilled') return 'partial';
  return 'open';
}

/** Leveransstatus i Shopify, dvs. om ordern är skickad från lager. */
function rxOrdShipped(r) {
  if (!r || !r.hasOrder) return null;
  if (r.cancelledAt) return { label: 'Avbruten', tone: 'danger' };
  const fs = rxOrdFs(r.fulfillmentStatus);
  if (fs === 'fulfilled') return { label: 'Skickad', tone: 'ok' };
  if (fs === 'partial') return { label: 'Delvis skickad', tone: 'warn' };
  return { label: 'Ej skickad', tone: 'neutral' };
}

const RX_ORD_INVOICE_STATUS = {
  failed: 'fakturering misslyckades',
  skipped: 'fakturering hoppades över',
};

/**
 * Avstämning mot faktura, härledd ur reconciliation-gaps. Endpointen täcker
 * 180 dagar bakåt och högst 200 ordrar, så utanför det är svaret okänt.
 */
function rxOrdInvoice(r, gi, today) {
  if (!r) return null;
  if (!gi.loaded) return gi.failed ? { label: 'Okänd', tone: 'neutral', title: 'Avstämningen kunde inte hämtas' } : null;
  if (r.cancelledAt) return null;
  const gap = gi.byId[r.id];
  if (gap) {
    const why = gap.invoiceStatus ? (RX_ORD_INVOICE_STATUS[gap.invoiceStatus] || `status ${gap.invoiceStatus}`) : 'ingen faktura i Fortnox';
    return { label: 'Ej avstämd', tone: 'warn', title: `Leveransdatum har passerat, ${why}` };
  }
  if (!r.deliveryDate || r.deliveryDate >= today) return { label: 'Ej aktuell', tone: 'neutral', title: 'Leveransdatum har inte passerat än', faint: true };
  if (r.deliveryDate < rxOrdAddDays(today, -180)) return { label: 'Okänd', tone: 'neutral', title: 'Avstämningen täcker bara 180 dagar bakåt', faint: true };
  if (gi.capped && gi.oldest && r.deliveryDate <= gi.oldest) return { label: 'Okänd', tone: 'neutral', title: 'Avstämningslistan är begränsad till 200 ordrar', faint: true };
  return { label: 'Avstämd', tone: 'ok', title: 'Faktura skapad eller på väg i Fortnox' };
}

/** Senaste fakturasynken per order. Hämtas en gång och sparas i två minuter. */
const rxOrdInvoiceCache = { at: 0, promise: null };
function rxOrdInvoices() {
  if (!rxOrdInvoiceCache.promise || Date.now() - rxOrdInvoiceCache.at > 120000) {
    rxOrdInvoiceCache.at = Date.now();
    const p = apiFetch('/v1/admin/finance/invoices').then(d => {
      const m = {};
      (d.records || []).forEach(x => { if (!m[x.shopifyOrderId]) m[x.shopifyOrderId] = x; });
      return m;
    });
    p.catch(() => { if (rxOrdInvoiceCache.promise === p) rxOrdInvoiceCache.promise = null; });
    rxOrdInvoiceCache.promise = p;
  }
  return rxOrdInvoiceCache.promise;
}

/**
 * Hämtar orderdata för rader som bara har id och leveransdatum (försenade och
 * ej avstämda). Frågar ordrarna inom radernas datumintervall i stället för en
 * gång per rad.
 */
async function rxOrdOrdersByRange(ids, dates, domain) {
  const need = new Set(ids);
  const out = {};
  const ds = dates.filter(Boolean).sort();
  if (!ds.length) return out;
  let offset = 0;
  for (let i = 0; i < 6 && need.size > 0; i++) {
    const p = new URLSearchParams({ status: 'all', limit: '250', offset: String(offset), delivery_from: ds[0], delivery_to: ds[ds.length - 1], sort: 'deliveryDate', sort_dir: 'desc' });
    if (domain) p.set('domain', domain);
    const d = await apiFetch('/v1/admin/orders?' + p);
    (d.orders || []).forEach(o => { if (need.has(o.shopifyId)) { out[o.shopifyId] = o; need.delete(o.shopifyId); } });
    offset += 250;
    if (offset >= d.total) break;
  }
  return out;
}

/**
 * Opter-status för synliga rader. Ett anrop för radernas datumintervall och
 * ett för ordrar utan datum (delivery_from långt fram släpper bara igenom
 * odaterade, eftersom deliveryView tar med dem i datumfiltret). Det som ändå
 * saknas slås upp styckvis, högst tio.
 */
async function rxOrdOpterFor(rows, domain) {
  const byId = {};
  let failed = false;
  const live = rows.filter(r => !r.cancelledAt);
  if (!live.length) return { byId, failed };
  const want = new Set(live.map(r => r.id));
  const dated = live.map(r => r.deliveryDate).filter(Boolean).sort();
  const reqs = [];
  const base = () => { const p = new URLSearchParams({ limit: '500' }); if (domain) p.set('domain', domain); return p; };
  if (dated.length) { const p = base(); p.set('delivery_from', dated[0]); p.set('delivery_to', dated[dated.length - 1]); reqs.push(p); }
  if (live.some(r => !r.deliveryDate)) { const p = base(); p.set('delivery_from', '9999-12-31'); reqs.push(p); }
  const res = await Promise.all(reqs.map(p => apiFetch('/v1/admin/opter/deliveries?' + p).catch(() => { failed = true; return null; })));
  res.forEach(d => { if (d) (d.deliveries || []).forEach(x => { if (want.has(x.shopifyId)) byId[x.shopifyId] = x; }); });
  const missing = live.filter(r => !byId[r.id]).slice(0, 10);
  const singles = await Promise.all(missing.map(r =>
    apiFetch('/v1/admin/opter/deliveries/' + encodeURIComponent(r.id)).then(d => d.delivery).catch(() => null)));
  missing.forEach((r, i) => { if (singles[i]) byId[r.id] = singles[i]; });
  return { byId, failed };
}

/**
 * Ordrar utan leveransdatum. Listan kan inte filtrera på det, men SQLite
 * sorterar NULL först vid stigande ordning, så de odaterade ligger först när
 * vi sorterar på leveransdatum. Vi läser tills första daterade ordern.
 */
async function rxOrdLoadUndated(o) {
  const out = [];
  for (let i = 0; i < 8; i++) {
    const p = new URLSearchParams({ limit: '250', offset: String(i * 250), status: o.status, sort: 'deliveryDate', sort_dir: 'asc' });
    if (o.search) p.set('search', o.search);
    if (o.domain) p.set('domain', o.domain);
    const d = await apiFetch('/v1/admin/orders?' + p);
    let hitDated = false;
    for (const x of (d.orders || [])) {
      if (x.deliveryDate) { hitDated = true; break; }
      out.push(rxOrdFromOrder(x));
    }
    if (hitDated || (d.orders || []).length < 250) {
      out.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
      return { rows: out, truncated: false };
    }
  }
  out.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
  return { rows: out, truncated: true };
}

function rxOrdGapRows(gaps, byDomain, search, domain) {
  const q = (search || '').toLowerCase();
  const dom = (domain || '').toLowerCase();
  return ((gaps && gaps.gaps) || []).map(g => {
    const email = String(g.customerEmail || '').toLowerCase();
    const d = email.includes('@') ? email.split('@').pop() : '';
    return {
      id: g.shopifyId, name: g.orderName, email, domain: d, customer: byDomain[d] || d,
      deliveryDate: g.deliveryDate, totalPrice: g.totalPrice, createdAt: g.createdAt,
      invoiceStatus: g.invoiceStatus, hasOrder: false,
    };
  }).filter(r => (!dom || r.domain === dom) &&
    (!q || [r.name, r.email, r.customer].some(v => String(v || '').toLowerCase().includes(q))));
}

/** Laddar en sida för aktuell vy. Server-sidade vyer pagineras av API:t. */
async function rxOrdLoadRows(o) {
  const PER = RX_ORD_PER_PAGE;
  if (o.view === 'active' || o.view === 'today') {
    const p = new URLSearchParams({ limit: String(PER), offset: String((o.page - 1) * PER), status: o.status, sort: o.sort.key, sort_dir: o.sort.dir });
    if (o.search) p.set('search', o.search);
    if (o.domain) p.set('domain', o.domain);
    if (o.view === 'today') { p.set('delivery_from', o.today); p.set('delivery_to', o.today); }
    const d = await apiFetch('/v1/admin/orders?' + p);
    const rows = (d.orders || []).map(rxOrdFromOrder);
    return { rows, total: d.total, sum: rxOrdSum(rows), sumScope: d.total <= rows.length ? 'all' : 'page', truncated: false };
  }
  if (o.view === 'overdue') {
    const p = new URLSearchParams({ overdue: 'true', limit: String(PER), offset: String((o.page - 1) * PER) });
    if (o.search) p.set('q', o.search);
    if (o.domain) p.set('domain', o.domain);
    const d = await apiFetch('/v1/admin/opter/deliveries?' + p);
    const list = d.deliveries || [];
    const base = list.map(x => ({ id: x.shopifyId, name: x.orderName, po: x.poNumber, deliveryDate: x.deliveryDate, hasOrder: false }));
    const found = await rxOrdOrdersByRange(base.map(r => r.id), base.map(r => r.deliveryDate), o.domain).catch(() => ({}));
    const rows = base.map(r => rxOrdMerge(r, found[r.id]));
    const opterById = {};
    list.forEach(x => { opterById[x.shopifyId] = x; });
    return { rows, total: d.page ? d.page.total : list.length, sum: rxOrdSum(rows), sumScope: 'page', sumPartial: rows.some(r => !r.hasOrder), truncated: false, opterById };
  }
  // Klient-sidade vyer: hela urvalet hämtas en gång och pagineras här.
  const key = JSON.stringify([o.view, o.search, o.domain, o.status, Object.keys(o.clientsByDomain || {}).length]);
  const c = o.cache;
  if (!(c.key === key && c.rows && c.src === o.gaps)) {
    if (o.view === 'undated') {
      const r = await rxOrdLoadUndated(o);
      c.rows = r.rows; c.truncated = r.truncated;
    } else {
      c.rows = rxOrdGapRows(o.gaps, o.clientsByDomain, o.search, o.domain);
      c.truncated = ((o.gaps && o.gaps.count) || 0) >= 200;
    }
    c.key = key; c.src = o.gaps;
  }
  const all = c.rows;
  let rows = all.slice((o.page - 1) * PER, o.page * PER);
  if (o.view === 'gaps' && rows.length) {
    const found = await rxOrdOrdersByRange(rows.map(r => r.id), rows.map(r => r.deliveryDate), o.domain).catch(() => ({}));
    rows = rows.map(r => rxOrdMerge(r, found[r.id]));
  }
  return { rows, total: all.length, sum: rxOrdSum(all), sumScope: 'all', truncated: c.truncated };
}

/** Förloppet i sidopanelen. Bara verkliga tidpunkter; resten är 'todo'. */
function rxOrdSteps(m, o, d, inv, gi, today) {
  const steps = [];
  steps.push({
    label: 'Skapad i Shopify', state: m.createdAt ? 'done' : 'todo',
    meta: [m.createdAt && rxOrdStamp(m.createdAt), m.deliveryDate ? `leverans utlovad ${rxDay(m.deliveryDate)}` : 'inget leveransdatum'].filter(Boolean).join(' · '),
  });
  if (m.cancelledAt) steps.push({ label: 'Avbruten', state: 'now', meta: rxOrdStamp(m.cancelledAt) });

  const fs = rxOrdFs(m.fulfillmentStatus);
  if (m.shippedTime) {
    steps.push({ label: 'Skickad från lager', state: 'done', meta: [rxOrdStamp(m.shippedTime), o && o.ongoingWayBill && `fraktsedel ${o.ongoingWayBill}`].filter(Boolean).join(' · ') });
  } else if (m.hasOrder && fs === 'fulfilled') {
    steps.push({ label: 'Skickad från lager', state: 'done', meta: 'Skickad enligt Shopify, klockslag saknas' });
  } else if (m.hasOrder && fs === 'partial') {
    steps.push({ label: 'Delvis skickad från lager', state: 'now', meta: 'Enligt Shopify' });
  } else {
    steps.push({ label: 'Skickad från lager', state: 'todo', meta: 'Rapporteras av lagret (Ongoing) och Shopify' });
  }

  const st = d ? d.status : null;
  const moved = ['picked_up', 'in_transit', 'arrived', 'partially_delivered', 'delivered'].includes(st);
  if (d && d.pickedUpAt) {
    steps.push({ label: 'Hämtad', state: 'done', meta: [rxOrdStamp(d.pickedUpAt), d.transporterResource].filter(Boolean).join(' · ') });
  } else if (moved) {
    steps.push({ label: 'Hämtad', state: 'done', meta: 'Klockslag saknas från Opter' });
  } else {
    steps.push({ label: 'Hämtad', state: 'todo', meta: 'Rapporteras av Opter' });
  }

  if (st === 'delivered') {
    steps.push({ label: 'Levererad', state: 'done', meta: [d.deliveredAt && rxOrdStamp(d.deliveredAt), d.lastEventLocation, d.podSigneeName && `signerad av ${d.podSigneeName}`].filter(Boolean).join(' · ') || 'Levererad enligt Opter' });
  } else if (st === 'partially_delivered') {
    steps.push({ label: 'Delvis levererad', state: 'now', meta: [d.deliveredAt && rxOrdStamp(d.deliveredAt), d.packageCount ? `${d.deliveredPackageCount} av ${d.packageCount} kollin` : '', d.lastEventLocation, d.podSigneeName && `signerad av ${d.podSigneeName}`].filter(Boolean).join(' · ') });
  } else if (st === 'deviation') {
    steps.push({ label: 'Avvikelse', state: 'now', meta: [d.deviationNote || d.deviationCode, d.lastEventAt && rxOrdStamp(d.lastEventAt), d.lastEventLocation].filter(Boolean).join(' · ') || 'Avvikelse rapporterad av Opter' });
  } else if (d && d.overdue) {
    steps.push({ label: 'Levererad', state: 'now', meta: `Försenad ${rxPlural(d.daysLate, 'dag', 'dagar')}. Klockslag, plats och signatur rapporteras av Opter` });
  } else {
    steps.push({ label: 'Levererad', state: 'todo', meta: 'Klockslag, plats och signatur rapporteras av Opter' });
  }

  const rec = inv.rec;
  if (m.cancelledAt) {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'todo', meta: 'Avbruten order faktureras inte' });
  } else if (rec && rec.status === 'success' && rec.invoiceNumber) {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'done', meta: [`faktura ${rec.invoiceNumber}`, rec.createdAt && `skapad ${rxOrdStamp(rec.createdAt)}`, rec.sentAt && `skickad ${rxOrdStamp(rec.sentAt)}`].filter(Boolean).join(' · ') });
  } else if (rec && rec.status === 'pending') {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'now', meta: 'Fakturan skapas i Fortnox' });
  } else if (rec && rec.status === 'failed') {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'now', meta: ['Fakturering misslyckades', rec.errorMessage].filter(Boolean).join(': ') });
  } else if (rec && rec.status === 'skipped') {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'todo', meta: 'Fakturering hoppades över' });
  } else if (gi.byId[m.id]) {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'now', meta: 'Ej avstämd: leveransdatum har passerat utan faktura i Fortnox' });
  } else if (inv.loading) {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'todo', meta: 'Hämtar fakturastatus' });
  } else if (m.deliveryDate && m.deliveryDate < today) {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'todo', meta: inv.failed ? 'Fakturastatus kunde inte hämtas' : 'Finns inte bland de 200 senaste fakturorna i Fortnox' });
  } else {
    steps.push({ label: 'Fakturerad i Fortnox', state: 'todo', meta: 'Stäms av mot Fortnox när leveransdatumet passerat' });
  }
  return steps;
}

function RxOrdPager({ page, pages, onPage }) {
  if (pages <= 1) return null;
  return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, padding: '10px 14px', borderTop: '1px solid var(--rx-divider)', fontSize: 13 }}>
    <span className="rx-muted">Sida {fmtInt(page)} av {fmtInt(pages)}</span>
    <button type="button" className="btn btn--sm" disabled={page <= 1} onClick={() => onPage(page - 1)}>Föregående</button>
    <button type="button" className="btn btn--sm" disabled={page >= pages} onClick={() => onPage(page + 1)}>Nästa</button>
  </div>;
}

function RxOrdSortTh({ label, k, sort, onSort, enabled, num }) {
  if (!enabled) return <th className={num ? 'rx-num' : undefined}>{label}</th>;
  const on = sort.key === k;
  return <th className={num ? 'rx-num' : undefined} aria-sort={on ? (sort.dir === 'asc' ? 'ascending' : 'descending') : 'none'}>
    <button type="button" onClick={() => onSort(k)}
      style={{ border: 0, background: 'none', padding: 0, font: 'inherit', color: on ? 'var(--rx-ink)' : 'inherit', cursor: 'pointer', fontWeight: on ? 600 : 500 }}>
      {label}{on ? (sort.dir === 'asc' ? ' ▲' : ' ▼') : ''}
    </button>
  </th>;
}

function RxOrdPanel({ row, opter, gapsInfo, today, onClose, onOpenOrder }) {
  const id = row.id;
  const detail = useRxFetch(`/v1/admin/orders/${encodeURIComponent(id)}`);
  const opterDetail = useRxFetch(row.cancelledAt ? null : `/v1/admin/opter/deliveries/${encodeURIComponent(id)}`);
  const [inv, setInv] = React.useState({ loading: true, rec: null, failed: false });

  React.useEffect(() => {
    let cancelled = false;
    setInv({ loading: true, rec: null, failed: false });
    rxOrdInvoices()
      .then(map => { if (!cancelled) setInv({ loading: false, rec: map[id] || null, failed: false }); })
      .catch(() => { if (!cancelled) setInv({ loading: false, rec: null, failed: true }); });
    return () => { cancelled = true; };
  }, [id]);

  React.useEffect(() => {
    const fn = e => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', fn);
    return () => document.removeEventListener('keydown', fn);
  }, [onClose]);

  const o = detail.data;
  const m = o ? rxOrdFromOrder(o) : row;
  const d = (opterDetail.data && opterDetail.data.delivery) || opter || null;
  const packages = opterDetail.data ? (opterDetail.data.packages || []) : null;
  const shipped = rxOrdShipped(m);
  const invoice = rxOrdInvoice(m, gapsInfo, today);
  const steps = rxOrdSteps(m, o, d, inv, gapsInfo, today);
  const opterErr = opterDetail.error && opterDetail.error.status !== 404 ? opterDetail.error : null;

  const dt = { fontSize: 12.5, color: 'var(--rx-ink-3)' };
  const dd = { margin: 0, fontSize: 13.5, color: 'var(--rx-ink)' };

  return <RxPanel>
    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <h2 className="rx-mono" style={{ fontSize: 18, fontWeight: 650, margin: 0, color: 'var(--rx-ink)' }}>{m.name || id}</h2>
          {shipped && <RxPill tone={shipped.tone}>{shipped.label}</RxPill>}
        </div>
        <div className="rx-muted" style={{ fontSize: 13, marginTop: 4 }}>
          {m.customer || ''}{m.po ? ` · kundens ordernr ${m.po}` : ''}
        </div>
      </div>
      <button type="button" className="btn btn--icon btn--ghost" aria-label="Stäng sidopanelen" title="Stäng (Esc)" onClick={onClose}>{I.close}</button>
    </div>

    <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
      <button type="button" className="btn btn--primary" onClick={() => onOpenOrder && onOpenOrder(id)}>Öppna hela ordern</button>
      <a className="btn" href={RX_ORD_SHOPIFY_URL + encodeURIComponent(id)} target="_blank" rel="noreferrer">{I.external}<span>Shopify</span></a>
    </div>

    {detail.error && <RxError error={detail.error} onRetry={detail.reload} />}

    <dl style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '6px 14px', margin: 0, alignItems: 'baseline' }}>
      <dt style={dt}>Leveransdatum</dt>
      <dd style={dd}>{m.deliveryDate ? <span>{rxDay(m.deliveryDate, { relative: true })} <span className="rx-muted rx-mono">{m.deliveryDate}</span></span> : <span className="rx-muted">Saknas</span>}</dd>
      <dt style={dt}>Ordervärde</dt>
      <dd style={dd}>{m.totalPrice != null ? <span><strong className="rx-num">{fmtSek(m.totalPrice)}</strong> <span className="rx-muted">exkl. moms</span></span> : <span className="rx-muted">Hämtar</span>}</dd>
      {m.email && <><dt style={dt}>E-post</dt><dd style={{ ...dd, wordBreak: 'break-all' }} className="rx-mono">{m.email}</dd></>}
      <dt style={dt}>Status i Opter</dt>
      <dd style={dd}>{d ? <RxDeliveryPill status={d.status} /> : <span className="rx-muted">{m.cancelledAt ? 'Avbruten order' : opterDetail.loading ? 'Hämtar' : 'Ingen status'}</span>}</dd>
      {d && d.freightBillNumber && <><dt style={dt}>Fraktsedel</dt><dd style={dd} className="rx-mono">{d.freightBillNumber}</dd></>}
      {packages && packages.length > 0 && <><dt style={dt}>Kollin</dt><dd style={dd}>{fmtInt(packages.length)}{d && d.deliveredPackageCount ? `, ${fmtInt(d.deliveredPackageCount)} levererade` : ''}</dd></>}
      <dt style={dt}>Faktura</dt>
      <dd style={dd}>{invoice ? <span title={invoice.title}><RxPill tone={invoice.tone}>{invoice.label}</RxPill></span> : <span className="rx-muted">{m.cancelledAt ? 'Avbruten order' : 'Hämtar'}</span>}</dd>
    </dl>

    {opterErr && <RxCallout tone="warn" title="Opter-status kunde inte hämtas.">{String(opterErr.message || '')}</RxCallout>}

    <div>
      <div className="rx-section-title" style={{ marginBottom: 10 }}>Förlopp</div>
      <RxTimeline steps={steps} />
    </div>

    <div>
      <div className="rx-section-title" style={{ marginBottom: 8 }}>Orderrader</div>
      {detail.loading && <RxLoading />}
      {o && (!o.lineItems || o.lineItems.length === 0) && <div className="rx-muted" style={{ fontSize: 13 }}>Ordern har inga rader.</div>}
      {o && o.lineItems && o.lineItems.length > 0 && <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column' }}>
        {o.lineItems.map((li, i) => <li key={i} style={{ display: 'flex', gap: 10, alignItems: 'baseline', padding: '7px 0', borderTop: i ? '1px solid var(--rx-divider)' : 0 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13.5 }}>{li.title}</div>
            <div className="rx-mono rx-muted">{li.sku ? `Art.nr ${li.sku}` : 'Artikelnummer saknas'}</div>
          </div>
          <div className="rx-num" style={{ fontSize: 13.5, fontWeight: 600, whiteSpace: 'nowrap' }}>{fmtInt(li.quantity)} st</div>
        </li>)}
      </ul>}
    </div>
  </RxPanel>;
}

const RX_ORD_STATUS_LABEL = { open: 'Alla aktiva', cancelled: 'Alla avbrutna', all: 'Alla ordrar' };

function RxOrdersPage({ onNavigate, onOpenOrder }) {
  const today = rxOrdIso(new Date());
  const [view, setView] = React.useState('active');
  const [searchInput, setSearchInput] = React.useState('');
  const [search, setSearch] = React.useState('');
  const [domain, setDomain] = React.useState('');
  const [status, setStatus] = React.useState('open');
  const [page, setPage] = React.useState(1);
  const [sort, setSort] = React.useState({ key: 'createdAt', dir: 'desc' });
  const [nonce, setNonce] = React.useState(0);
  const [list, setList] = React.useState({ loading: true, error: null, rows: [], total: 0, sum: 0, sumScope: 'page', truncated: false });
  const [opter, setOpter] = React.useState({ loading: true, byId: {}, failed: false });
  const [counts, setCounts] = React.useState({});
  const [picked, setPicked] = React.useState({});
  const [active, setActive] = React.useState(null);
  const cache = React.useRef({ key: null, rows: null, truncated: false, src: null });

  const clients = useRxFetch('/v1/admin/clients');
  const gapsFetch = useRxFetch('/v1/admin/fortnox/reconciliation-gaps');

  React.useEffect(() => {
    const t = setTimeout(() => setSearch(searchInput.trim()), 300);
    return () => clearTimeout(t);
  }, [searchInput]);

  React.useEffect(() => { setPage(1); }, [view, search, domain, status, sort.key, sort.dir]);

  const clientList = React.useMemo(() => {
    const l = Array.isArray(clients.data) ? clients.data.slice() : [];
    l.sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'sv'));
    return l;
  }, [clients.data]);
  const clientsByDomain = React.useMemo(() => {
    const m = {};
    clientList.forEach(c => { if (c.emailDomain) m[String(c.emailDomain).toLowerCase()] = c.name; });
    return m;
  }, [clientList]);

  const gapsInfo = React.useMemo(() => {
    const g = gapsFetch.data;
    if (!g) return { loaded: false, failed: !!gapsFetch.error, byId: {}, capped: false, oldest: null };
    const byId = {};
    let oldest = null;
    (g.gaps || []).forEach(x => {
      byId[x.shopifyId] = x;
      if (x.deliveryDate && (!oldest || x.deliveryDate < oldest)) oldest = x.deliveryDate;
    });
    return { loaded: true, failed: false, byId, capped: (g.count || 0) >= 200, oldest };
  }, [gapsFetch.data, gapsFetch.error]);

  const gapsDep = view === 'gaps' ? gapsFetch.data : null;
  const clientsDep = view === 'gaps' ? clientsByDomain : null;

  // Listan för aktuell vy och sida, följt av Opter-status för raderna.
  React.useEffect(() => {
    if (view === 'gaps' && !gapsFetch.data) {
      setList(s => ({ ...s, loading: !gapsFetch.error, rows: [], total: 0 }));
      return;
    }
    let cancelled = false;
    setList(s => ({ ...s, loading: true, error: null }));
    setOpter(s => ({ ...s, loading: true }));
    (async () => {
      const res = await rxOrdLoadRows({ view, search, domain, status, page, sort, today, gaps: gapsFetch.data, clientsByDomain, cache: cache.current });
      if (cancelled) return;
      setList({ loading: false, error: null, sumPartial: false, ...res });
      if (res.opterById) { setOpter({ loading: false, byId: res.opterById, failed: false }); return; }
      const op = await rxOrdOpterFor(res.rows, domain);
      if (!cancelled) setOpter({ loading: false, byId: op.byId, failed: op.failed });
    })().catch(e => {
      if (cancelled) return;
      setList(s => ({ ...s, loading: false, error: e, rows: [] }));
      setOpter({ loading: false, byId: {}, failed: false });
    });
    return () => { cancelled = true; };
  }, [view, search, domain, status, page, sort.key, sort.dir, gapsDep, clientsDep, gapsFetch.error, nonce]);

  // Antal per sparad vy. Följer kund- och statusfiltret men inte sökningen.
  React.useEffect(() => {
    let cancelled = false;
    const dq = domain ? `&domain=${encodeURIComponent(domain)}` : '';
    const st = `status=${status}`;
    const tasks = [
      ['active', apiFetch(`/v1/admin/orders?limit=1&${st}${dq}`).then(d => ({ n: d.total }))],
      ['today', apiFetch(`/v1/admin/orders?limit=1&${st}&delivery_from=${today}&delivery_to=${today}${dq}`).then(d => ({ n: d.total }))],
      ['overdue', apiFetch(`/v1/admin/opter/deliveries?overdue=true&limit=1${dq}`).then(d => ({ n: d.summary ? d.summary.overdue : (d.page ? d.page.total : 0) }))],
      ['undated', apiFetch(`/v1/admin/orders?limit=250&${st}&sort=deliveryDate&sort_dir=asc${dq}`).then(d => {
        const l = d.orders || [];
        let n = 0;
        for (const x of l) { if (x.deliveryDate) break; n++; }
        return { n, plus: n === 250 };
      })],
    ];
    Promise.all(tasks.map(([k, p]) => p.then(v => [k, v]).catch(() => [k, null]))).then(entries => {
      if (cancelled) return;
      const out = {};
      entries.forEach(([k, v]) => { out[k] = v; });
      setCounts(out);
    });
    return () => { cancelled = true; };
  }, [domain, status, today, nonce]);

  const gapCount = React.useMemo(() => {
    if (!gapsFetch.data) return null;
    const n = rxOrdGapRows(gapsFetch.data, clientsByDomain, '', domain).length;
    return { n, plus: (gapsFetch.data.count || 0) >= 200 };
  }, [gapsFetch.data, clientsByDomain, domain]);

  const views = [
    { key: 'active', label: RX_ORD_STATUS_LABEL[status], count: counts.active },
    { key: 'today', label: 'Levereras idag', count: counts.today },
    { key: 'overdue', label: 'Försenade', count: counts.overdue, warn: true },
    { key: 'undated', label: 'Utan leveransdatum', count: counts.undated, warn: true },
    { key: 'gaps', label: 'Ej avstämda mot faktura', count: gapCount, warn: true },
  ];

  const statusApplies = view === 'active' || view === 'today' || view === 'undated';
  const sortable = view === 'active' || view === 'today';
  const pages = Math.max(1, Math.ceil((list.total || 0) / RX_ORD_PER_PAGE));
  const rows = list.rows || [];
  const pickedIds = Object.keys(picked);
  const allOnPage = rows.length > 0 && rows.every(r => picked[r.id]);

  const reload = () => { cache.current.key = null; setNonce(n => n + 1); };
  const onSort = k => setSort(s => s.key === k ? { key: k, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key: k, dir: k === 'shopifyName' ? 'asc' : 'desc' });

  const togglePick = r => setPicked(p => {
    const n = { ...p };
    if (n[r.id]) delete n[r.id];
    else n[r.id] = { ...r, opter: opter.byId[r.id] || null };
    return n;
  });
  const togglePage = () => setPicked(p => {
    const n = { ...p };
    if (allOnPage) rows.forEach(r => { delete n[r.id]; });
    else rows.forEach(r => { n[r.id] = { ...r, opter: opter.byId[r.id] || null }; });
    return n;
  });

  const exportCsv = () => {
    const out = Object.values(picked).map(r => {
      const s = rxOrdShipped(r);
      const inv = rxOrdInvoice(r, gapsInfo, today);
      const op = r.opter;
      return {
        order: r.name || r.id,
        kund: r.customer || '',
        epost: r.email || '',
        po: r.po || '',
        leverans: r.deliveryDate || '',
        lager: s ? s.label : '',
        opter: op ? ((RX_DELIVERY_STATUS[op.status] && RX_DELIVERY_STATUS[op.status].label) || op.statusLabel || op.status) : '',
        faktura: inv ? inv.label : '',
        belopp: r.totalPrice != null ? String(r.totalPrice).replace('.', ',') : '',
      };
    });
    if (!out.length || typeof downloadCsv !== 'function') return;
    downloadCsv(`ordrar-${today}.csv`, out, [
      { key: 'order', label: 'Order' }, { key: 'kund', label: 'Kund' }, { key: 'epost', label: 'E-post' },
      { key: 'po', label: 'Kundens ordernr' }, { key: 'leverans', label: 'Leveransdatum' },
      { key: 'lager', label: 'Från lager' }, { key: 'opter', label: 'Status i Opter' },
      { key: 'faktura', label: 'Faktura' }, { key: 'belopp', label: 'Belopp exkl. moms (SEK)' },
    ]);
  };

  const openRow = r => setActive(r);
  const closePanel = React.useCallback(() => setActive(null), []);

  const onRowKey = (e, r) => {
    if (e.target !== e.currentTarget) return;
    if (e.key === 'Enter') { e.preventDefault(); openRow(r); }
    else if (e.key === ' ') { e.preventDefault(); togglePick(r); }
    else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
      e.preventDefault();
      const next = e.key === 'ArrowDown' ? e.currentTarget.nextElementSibling : e.currentTarget.previousElementSibling;
      if (next && next.dataset && next.dataset.id) {
        next.focus();
        if (active) {
          const nr = rows.find(x => x.id === next.dataset.id);
          if (nr) setActive(nr);
        }
      }
    }
  };

  const countText = c => {
    if (!c) return null;
    return `${fmtInt(c.n)}${c.plus ? '+' : ''}`;
  };

  let hits = '';
  if (!list.error && !(list.loading && !rows.length)) {
    hits = `${rxPlural(list.total || 0, 'order', 'ordrar')}${list.truncated ? '+' : ''}`;
    if (rows.length) {
      hits += ` · ${fmtSek(list.sum)} exkl. moms${list.sumScope === 'page' ? ' på sidan' : ''}`;
      if (list.sumPartial) hits += ', belopp saknas för vissa';
    }
  }

  const viewNote = {
    today: `Ordrar med leveransdatum idag, ${rxDay(today)}.`,
    overdue: 'Leveransdatumet har passerat utan leverans enligt Opter. Ordrar som saknar status från Opter räknas också som försenade. Sökningen gäller ordernr, kundens ordernr och Opter-referens.',
    undated: 'Ordrar som saknar leveransdatum i Shopify. Listan kan inte filtreras på det i API:t, så urvalet läses i leveransdatumordning och stannar vid första daterade ordern.',
    gaps: 'Ordrar med passerat leveransdatum de senaste 180 dagarna som saknar lyckad eller pågående faktura i Fortnox. Listan omfattar högst 200 ordrar. Sökningen gäller ordernr, kund och e-post.',
  }[view];

  return <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
    <nav className="rx-tabs" aria-label="Sparade vyer">
      {views.map(v => {
        const on = v.key === view;
        const ct = countText(v.count);
        return <button key={v.key} type="button" aria-pressed={on} className={`rx-tab${on ? ' rx-tab--active' : ''}`} onClick={() => setView(v.key)}>
          {v.label}
          {ct != null && <span className={`rx-count${v.warn && v.count && v.count.n > 0 ? ' rx-count--warn' : on ? ' rx-count--accent' : ''}`}>{ct}</span>}
        </button>;
      })}
    </nav>

    {viewNote && <div className="rx-muted" style={{ fontSize: 13 }}>{viewNote}</div>}
    {view === 'undated' && list.truncated && <RxCallout tone="warn" title="Urvalet är begränsat.">Visar de första 2 000 ordrarna utan leveransdatum. Använd sökning eller kundfilter för att snäva in.</RxCallout>}

    <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
      <RxSearch value={searchInput} onChange={setSearchInput} label="Sök ordrar" placeholder="Sök ordernr, kundens ordernr, kund eller e-post" />
      <select className="select" aria-label="Kund" value={domain} onChange={e => setDomain(e.target.value)} style={{ height: 36 }}>
        <option value="">Alla kunder</option>
        {clientList.map(c => <option key={c.id} value={c.emailDomain}>{c.name}</option>)}
      </select>
      <select className="select" aria-label="Status" value={status} onChange={e => setStatus(e.target.value)} disabled={!statusApplies}
        title={statusApplies ? undefined : 'Vyn visar bara aktiva ordrar'} style={{ height: 36 }}>
        <option value="open">Aktiva</option>
        <option value="cancelled">Avbrutna</option>
        <option value="all">Alla statusar</option>
      </select>
      {(searchInput || domain || status !== 'open') && <button type="button" className="rx-link" onClick={() => { setSearchInput(''); setDomain(''); setStatus('open'); }}>Rensa filter</button>}
      <span className="rx-muted" style={{ marginLeft: 'auto', fontSize: 13, fontVariantNumeric: 'tabular-nums' }} aria-live="polite">{hits}</span>
    </div>

    {pickedIds.length > 0 && <div role="region" aria-label="Åtgärder för valda ordrar"
      style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '8px 12px', borderRadius: 10, background: 'var(--rx-accent-softer)', border: '1px solid #DAD7F7', fontSize: 13 }}>
      <strong>{rxPlural(pickedIds.length, 'vald', 'valda')}</strong>
      <button type="button" className="btn btn--sm" onClick={exportCsv}>{I.download}<span>Exportera som CSV</span></button>
      <button type="button" className="btn btn--sm" onClick={() => onNavigate && onNavigate('deliveries/plan')}
        title="Plocklistan skrivs ut per leveransdatum och kund under Leveranser">Plocklista under Leveranser</button>
      <button type="button" className="rx-link" style={{ marginLeft: 'auto' }} onClick={() => setPicked({})}>Avmarkera</button>
    </div>}

    {opter.failed && <RxCallout tone="warn" title="Status från Opter kunde inte hämtas för alla ordrar.">
      <button type="button" className="rx-link" onClick={reload}>Försök igen</button>
    </RxCallout>}

    <div className="rx-split">
      <div className="rx-split__main">
        <RxPanel flush>
          {list.error && <div style={{ padding: 14 }}><RxError error={list.error} onRetry={reload} /></div>}
          {view === 'gaps' && gapsFetch.error && <div style={{ padding: 14 }}><RxError error={gapsFetch.error} onRetry={gapsFetch.reload} /></div>}
          {!list.error && list.loading && rows.length === 0 && <RxLoading label="Hämtar ordrar…" />}
          {!list.error && !list.loading && rows.length === 0 && !(view === 'gaps' && gapsFetch.error) &&
            <RxEmpty title="Inga ordrar i vyn">{search || domain ? 'Inget matchar sökningen eller kundfiltret.' : view === 'gaps' ? 'Alla levererade ordrar har faktura i Fortnox.' : null}</RxEmpty>}
          {!list.error && rows.length > 0 && <div style={{ overflowX: 'auto', opacity: list.loading ? 0.6 : 1 }}>
            <table className="rx-table">
              <thead><tr>
                <th style={{ width: 36 }}>
                  <input type="checkbox" checked={allOnPage} onChange={togglePage} aria-label="Välj alla på sidan" />
                </th>
                <RxOrdSortTh label="Order" k="shopifyName" sort={sort} onSort={onSort} enabled={sortable} />
                <th>Kund</th>
                <th>Kundens ordernr</th>
                <RxOrdSortTh label="Leverans" k="deliveryDate" sort={sort} onSort={onSort} enabled={sortable} />
                <th>Från lager</th>
                <th>Status i Opter</th>
                <th>Faktura</th>
                <RxOrdSortTh label="Belopp exkl. moms" k="totalPrice" sort={sort} onSort={onSort} enabled={sortable} num />
              </tr></thead>
              <tbody>
                {rows.map(r => {
                  const isActive = active && active.id === r.id;
                  const s = rxOrdShipped(r);
                  const op = opter.byId[r.id];
                  const inv = rxOrdInvoice(r, gapsInfo, today);
                  const late = op ? op.overdue : (!!r.deliveryDate && r.deliveryDate < today && !r.cancelledAt && rxOrdFs(r.fulfillmentStatus) !== 'fulfilled' && view === 'overdue');
                  return <tr key={r.id} data-id={r.id} tabIndex={0}
                    className={`rx-row--click${isActive ? ' rx-row--selected' : ''}`}
                    onClick={() => openRow(r)} onKeyDown={e => onRowKey(e, r)}>
                    <td onClick={e => e.stopPropagation()}>
                      <input type="checkbox" checked={!!picked[r.id]} onChange={() => togglePick(r)} aria-label={`Välj order ${r.name || r.id}`} />
                    </td>
                    <td className="rx-mono" style={{ fontWeight: 600, whiteSpace: 'nowrap' }}>{r.name || r.id}</td>
                    <td>{r.customer || <span className="rx-faint">Hämtar</span>}</td>
                    <td className="rx-mono rx-muted">{r.po || ''}</td>
                    <td style={{ whiteSpace: 'nowrap' }}>
                      {r.deliveryDate
                        ? <span style={late ? { color: 'var(--rx-warn)', fontWeight: 600 } : undefined} title={late && op && op.daysLate ? `Försenad ${rxPlural(op.daysLate, 'dag', 'dagar')}` : r.deliveryDate}>
                            {rxDay(r.deliveryDate, { relative: true })}
                            {late && op && op.daysLate ? <span style={{ fontWeight: 400 }}> · {op.daysLate} d sen</span> : null}
                          </span>
                        : <span className="rx-faint">Saknas</span>}
                    </td>
                    <td>{s ? <span title={r.shippedTime ? `Skickad ${rxOrdStamp(r.shippedTime)}` : undefined}><RxPill tone={s.tone}>{s.label}</RxPill></span> : null}</td>
                    <td>{r.cancelledAt ? null
                      : op ? <span title={op.lastEventAt ? `Senaste händelse ${rxOrdStamp(op.lastEventAt)}` : undefined}><RxDeliveryPill status={op.status} /></span>
                      : <span className="rx-faint" style={{ fontSize: 12.5 }}>{opter.loading ? 'Hämtar' : 'Ej hämtad'}</span>}</td>
                    <td>{inv ? <span title={inv.title}>{inv.faint ? <span className="rx-muted" style={{ fontSize: 12.5 }}>{inv.label}</span> : <RxPill tone={inv.tone}>{inv.label}</RxPill>}</span> : null}</td>
                    <td className="rx-num" style={{ whiteSpace: 'nowrap' }}>{r.totalPrice != null ? fmtSek(r.totalPrice) : ''}</td>
                  </tr>;
                })}
              </tbody>
            </table>
          </div>}
          <RxOrdPager page={page} pages={pages} onPage={setPage} />
        </RxPanel>
        <div className="rx-muted" style={{ fontSize: 12 }}>Klick eller Enter öppnar ordern i sidopanelen. Pil upp och ned bläddrar, mellanslag väljer raden, Esc stänger panelen.</div>
      </div>
      {active && <div className="rx-split__side" style={{ position: 'sticky', top: 16 }}>
        <RxOrdPanel key={active.id} row={active} opter={opter.byId[active.id]} gapsInfo={gapsInfo} today={today}
          onClose={closePanel} onOpenOrder={onOpenOrder} />
      </div>}
    </div>
  </div>;
}
