/* Leveranser: status från Opter (Fraktlogistik). Bara avläsning.
 *
 * Endpoints (src/routes/admin.opter.ts, läsmodell src/lib/deliveryView.ts):
 *   GET  /v1/admin/opter/deliveries          summary + page + deliveries
 *        query: q, delivery_from, delivery_to, overdue=true, limit (max 500), offset
 *        Inget statusfilter finns i API:t; statusfilter görs på det hämtade
 *        underlaget (de 500 senast daterade ordrarna) och sägs ut i vyn.
 *   GET  /v1/admin/opter/deliveries/:id      delivery + packages
 *   GET  /v1/admin/opter/events?shopifyId=   råa händelser för tidslinjen
 *   GET  /v1/admin/opter/unmatched           okopplade händelser
 *   GET  /v1/admin/opter/config              eventCount, lastEventAt, portalVisible m.m.
 *   POST /v1/admin/opter/link                { opterOrderReference, shopifyId }, bara efter bekräftelse
 */

const rxDelPerPage = 50;
const rxDelBaseLimit = 500;
const rxDelFlow = ['awaiting_status', 'booked', 'picked_up', 'in_transit', 'arrived', 'partially_delivered', 'delivered', 'deviation', 'shopify_shipped'];

// Två av chippen slår ihop flera statusar, eftersom summeringen från servern
// räknar dem i samma hink. De behöver därför egna filter: ett `status:x` kan
// bara peka på en status, och utan detta gick chippen inte att klicka på.
const rxDelGroups = {
  moving: ['picked_up', 'in_transit', 'arrived'],
  rest:   ['booked', 'partially_delivered'],
};

// Vilka statusar ett filter ska be servern om. Alla statusfilter går till
// servern, inte till underlaget i webbläsaren: listan sorterar ordrar utan
// leveransdatum sist, så de faller utanför de 500 rader vi hämtar, och det är
// precis de ordrarna Opter rapporterar om. Chippen sa "5 i rörelse" över en
// lista med 3 innan detta.
function rxDelStatusesFor(filter) {
  if (rxDelGroups[filter]) return rxDelGroups[filter];
  if (filter && filter.startsWith('status:')) return [filter.slice(7)];
  return null;
}

/** SQLite datetime('now') är UTC utan zon ('2026-09-23 11:37:00'). */
function rxDelUtc(v) {
  if (!v) return null;
  return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(v) ? v.replace(' ', 'T') + 'Z' : v;
}

function rxDelParams(obj) {
  const p = new URLSearchParams();
  Object.keys(obj).forEach(k => {
    const v = obj[k];
    if (v !== undefined && v !== null && v !== '') p.set(k, String(v));
  });
  return p.toString();
}

function rxDelDays(n) { return `${n} ${n === 1 ? 'dag' : 'dagar'}`; }

/** Avvikelse enligt Opter, eller leverans på annat datum än förväntat i Shopify. */
function rxDelHasDeviation(r) { return r.dateDeviationDays != null || r.status === 'deviation'; }

/** Leverans mot förväntat datum. tone: ok | late | muted */
function rxDelVsPromised(d, long) {
  if (!d.deliveryDate) return { text: 'Inget datum', tone: 'muted' };
  if (d.deliveredAt) {
    const diff = rxDayDiff(d.deliveryDate, d.deliveredAt);
    if (diff == null) return { text: '', tone: 'muted' };
    if (diff === 0) return { text: 'Samma dag', tone: 'ok' };
    if (diff < 0) return { text: long ? `${rxDelDays(-diff)} före` : `${-diff} d före`, tone: 'ok' };
    return { text: long ? `${rxDelDays(diff)} sen` : `${diff} d sen`, tone: 'late' };
  }
  if (d.overdue) {
    return { text: long ? `${rxDelDays(d.daysLate)} sen, ej levererad` : `${d.daysLate} d sen`, tone: 'late' };
  }
  const ahead = rxDayDiff(rxToday(), d.deliveryDate);
  if (ahead === 0) return { text: 'Idag', tone: 'muted' };
  if (ahead === 1) return { text: 'Imorgon', tone: 'muted' };
  if (ahead != null && ahead > 1) return { text: `Om ${ahead} d`, tone: 'muted' };
  return { text: '', tone: 'muted' };
}

function rxDelToneStyle(tone) {
  if (tone === 'ok') return { fontWeight: 600, color: 'var(--rx-ok)' };
  if (tone === 'late') return { fontWeight: 600, color: 'var(--rx-danger)' };
  return { color: 'var(--rx-ink-3)' };
}

function rxDelWhen(v) {
  if (!v) return '';
  return `${rxDay(v)} kl ${rxTime(v)}`;
}

function rxDelLc(v) { return String(v || '').toLowerCase(); }

/** Tidslinjen byggs av leveransraden plus de råa händelserna, när de finns. */
function rxDelSteps(d, events) {
  const evs = (events || []).filter(e => !rxDelLc(e.event_mapping_code).startsWith('undo'));
  const latest = pred => {
    const m = evs.filter(pred).sort((a, b) => String(b.event_date_time || '').localeCompare(String(a.event_date_time || '')));
    return m[0] || null;
  };
  const dep = latest(e => e.event_kind === 'status' && rxDelLc(e.event_mapping_code) === 'departure');
  const scan = latest(e => e.event_kind === 'packagescan' && rxDelLc(e.event_mapping_code).includes('terminal'));
  const arr = latest(e => e.event_kind === 'status' && rxDelLc(e.event_mapping_code) === 'arrival');
  const pod = latest(e => e.event_kind === 'pod');
  const devs = evs.filter(e => e.event_kind === 'deviation');
  const join = parts => parts.filter(Boolean).join(' · ');

  const pickedAt = d.pickedUpAt || (dep && dep.event_date_time);
  const deliveredAt = d.deliveredAt || (pod && pod.event_date_time);
  const moved = !!(pickedAt || scan || arr || deliveredAt);
  const steps = [];

  steps.push({
    label: 'Hämtad',
    state: pickedAt ? 'done' : 'todo',
    meta: pickedAt
      ? join([rxDelWhen(pickedAt), dep && dep.event_location, d.transporterResource && `resurs ${d.transporterResource}`])
      : 'Ej rapporterad',
  });
  steps.push({
    label: 'På väg',
    state: scan ? 'done' : 'todo',
    meta: scan
      ? join([rxDelWhen(scan.event_date_time), scan.event_location, 'terminalscan'])
      : (moved ? 'Ingen terminalscan rapporterad' : 'Ej rapporterad'),
  });
  steps.push({
    label: 'Framme',
    state: arr ? 'done' : 'todo',
    meta: arr ? join([rxDelWhen(arr.event_date_time), arr.event_location]) : 'Ej rapporterad',
  });
  steps.push({
    label: d.status === 'partially_delivered' ? 'Delvis levererad' : 'Levererad',
    state: deliveredAt ? 'done' : 'todo',
    meta: deliveredAt
      ? join([rxDelWhen(deliveredAt), pod && pod.event_location,
        (d.podSigneeName || (pod && pod.pod_signee_name)) && `signerad av ${d.podSigneeName || pod.pod_signee_name}`])
      : 'Ej rapporterad',
  });
  if (devs.length > 0) {
    devs.forEach(e => steps.push({
      label: 'Avvikelse',
      state: 'now',
      meta: join([rxDelWhen(e.event_date_time), e.event_mapping_code, e.event_location]),
    }));
  } else if (d.status === 'deviation') {
    steps.push({ label: 'Avvikelse', state: 'now', meta: join([d.deviationCode, d.deviationNote]) });
  }
  return steps;
}

/* ── Små byggstenar ─────────────────────────────────────── */
function RxDelFact({ label, children, mono }) {
  return <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, fontSize: 13, padding: '6px 0', borderBottom: '1px solid var(--rx-divider)' }}>
    <span className="rx-muted">{label}</span>
    <span className={mono ? 'rx-mono' : ''} style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>{children}</span>
  </div>;
}

function RxDelNotReported() {
  return <span className="rx-faint">Ej rapporterad</span>;
}

function RxDelSubhead({ children }) {
  return <h3 style={{ margin: 0, fontSize: 12, fontWeight: 600, color: 'var(--rx-ink-3)', textTransform: 'uppercase', letterSpacing: '.05em' }}>{children}</h3>;
}

/** Horisontella staplar: items [{ label, n, color }] */
function RxDelBars({ items }) {
  const max = Math.max(1, ...items.map(i => i.n));
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
    {items.map(i => <div key={i.label} style={{ display: 'grid', gridTemplateColumns: '130px 1fr 36px', alignItems: 'center', gap: 10, fontSize: 13 }}>
      <span className="rx-muted">{i.label}</span>
      <div style={{ height: 10, borderRadius: 999, background: 'var(--rx-divider)', overflow: 'hidden' }}>
        <div style={{ width: `${Math.round(i.n / max * 100)}%`, height: '100%', borderRadius: 999, background: i.color || 'var(--rx-accent)' }} />
      </div>
      <span className="rx-num" style={{ fontWeight: 600 }}>{fmtInt(i.n)}</span>
    </div>)}
  </div>;
}

/** Tidsaxel 06 till 18 med hämtad och levererad. */
function RxDelDayAxis({ from, to }) {
  const pos = v => {
    const t = rxParseDate(v);
    if (!t) return null;
    const h = t.getHours() + t.getMinutes() / 60;
    return Math.max(0, Math.min(100, (h - 6) / 12 * 100));
  };
  const outside = v => {
    const t = rxParseDate(v);
    if (!t) return false;
    const h = t.getHours() + t.getMinutes() / 60;
    return h < 6 || h > 18;
  };
  if (!from && !to) return <div className="rx-muted" style={{ fontSize: 13 }}>Ingen hämtning eller leverans rapporterad.</div>;
  const sameDay = from && to && rxDayDiff(from, to) === 0;
  const a = from && (sameDay || !to) ? pos(from) : null;
  const b = to ? pos(to) : null;
  const close = a != null && b != null && Math.abs(b - a) < 14;
  const label = (p, text, color, shift) => <div style={{ position: 'absolute', left: `${p}%`, top: 0, transform: `translateX(${shift})`, fontSize: 11.5, fontWeight: 600, color, whiteSpace: 'nowrap' }}>{text}</div>;
  const dot = (p, color) => <div style={{ position: 'absolute', left: `${p}%`, top: 14, width: 14, height: 14, marginLeft: -7, borderRadius: 999, background: '#fff', border: `3px solid ${color}`, boxSizing: 'border-box' }} />;
  const notes = [];
  if (from && to && !sameDay) notes.push(`Hämtad ${rxDayTime(from)}, levererad en annan dag.`);
  if ((from && outside(from)) || (to && outside(to))) notes.push('Minst en tidpunkt ligger utanför 06 till 18 och visas vid kanten.');
  return <div>
    <div style={{ position: 'relative', height: 46 }} aria-label={`Hämtad ${from ? rxDayTime(from) : 'ej rapporterad'}, levererad ${to ? rxDayTime(to) : 'ej rapporterad'}`} role="img">
      <div style={{ position: 'absolute', left: 0, right: 0, top: 16, height: 10, borderRadius: 999, background: 'var(--rx-divider)' }} />
      {a != null && b != null && <div style={{ position: 'absolute', left: `${Math.min(a, b)}%`, width: `${Math.max(1, Math.abs(b - a))}%`, top: 16, height: 10, borderRadius: 999, background: 'var(--rx-accent)' }} />}
      {a != null && dot(a, '#8D86EA')}
      {b != null && dot(b, 'var(--rx-accent)')}
      {a != null && label(a, rxTime(from), 'var(--rx-ink-2)', close ? '-100%' : '-50%')}
      {b != null && label(b, rxTime(to), 'var(--rx-accent-ink)', close ? '0%' : '-50%')}
      <div style={{ position: 'absolute', left: 0, top: 32, fontSize: 11, color: '#9C9AA3' }}>06</div>
      <div style={{ position: 'absolute', left: '50%', top: 32, transform: 'translateX(-50%)', fontSize: 11, color: '#9C9AA3' }}>12</div>
      <div style={{ position: 'absolute', right: 0, top: 32, fontSize: 11, color: '#9C9AA3' }}>18</div>
    </div>
    {notes.map(n => <div key={n} className="rx-muted" style={{ fontSize: 12, marginTop: 4 }}>{n}</div>)}
  </div>;
}

/* ── Uppladdning av OrderXML ────────────────────────────────
 *
 * Opter skickar två format och de bär olika saker. Händelserna i JSON ger
 * kvittens men varken pris eller vårt ordernummer. OrderXML ger extraId1 med
 * vårt ordernummer och totalPrice med fraktkostnaden.
 *
 * Kan Fraktlogistik inte rikta sitt OrderXML-flöde mot webhooken går filen att
 * mata in här i stället. Samma tolkning och samma skrivningar som webhooken,
 * så resultatet blir identiskt.
 */
function RxDelOrderXmlUpload({ onDone }) {
  const [state, setState] = React.useState(null);

  async function läsFil(fil) {
    if (!fil) return;
    setState({ tone: 'info', text: `Läser ${fil.name}…` });
    try {
      const svar = await apiFetch('/v1/admin/opter/orderxml', {
        method: 'POST',
        headers: { 'content-type': 'application/xml' },
        body: await fil.text(),
      });
      setState({ tone: svar.shopifyId ? 'ok' : 'warn', result: svar });
      if (onDone) onDone();
    } catch (err) {
      setState({ tone: 'err', text: err.message });
    }
  }

  const r = state && state.result;
  const ton = !state ? null
    : state.tone === 'ok' ? 'var(--rx-ok)'
    : state.tone === 'err' ? 'var(--rx-danger)'
    : state.tone === 'warn' ? 'var(--rx-warn)'
    : 'var(--rx-muted)';

  // Kvittensen ligger i verktygsraden, alltså en flexrad. Den hålls därför på
  // en rad med detaljerna i title, i stället för en callout som blir
  // hoptryckt bredvid länkarna.
  const kvittens = !state ? null
    : state.text ? state.text
    : r && r.shopifyId
      ? `Kopplad via ${r.matchedOn}` +
        (r.totalPrice != null ? `, ${Number(r.totalPrice).toLocaleString('sv-SE')} kr` : '') +
        (r.backfilled && r.backfilled.events > 0
          ? `, ${r.backfilled.events} händelser kopplades` : '')
      : 'Ingen order matchade';

  const förklaring = !r ? undefined
    : r.shopifyId
      ? `OrderXML ${r.internalOrderId} kopplad till ordern.` +
        (r.readyForInvoicing === false
          ? ' Priset är ännu inte fastställt hos Fraktlogistik.' : '')
      : `OrderXML ${r.internalOrderId} sparad, men ingen order matchade. ` +
        'Kontrollera att extraId1 innehåller vårt ordernummer.';

  return <>
    <label className="rx-link" style={{ cursor: 'pointer' }}
      title="Läser en OrderXML från Opter. Kopplar ordern på extraId1 och hämtar fraktkostnaden.">
      Ladda upp OrderXML
      <input type="file" accept=".xml,text/xml,application/xml" style={{ display: 'none' }}
        onChange={e => { läsFil(e.target.files && e.target.files[0]); e.target.value = ''; }} />
    </label>
    {state && <span style={{ fontSize: 12, color: ton, whiteSpace: 'nowrap' }} title={förklaring}>
      {kvittens}
      <button onClick={() => setState(null)} aria-label="Stäng meddelandet"
        style={{ background: 'transparent', border: 0, cursor: 'pointer', color: 'inherit', marginLeft: 4 }}>×</button>
    </span>}
  </>;
}

/* ── Tabell ─────────────────────────────────────────────── */
function RxDelTable({ rows, selectedId, onSelect }) {
  return <div style={{ overflowX: 'auto' }}>
    <table className="rx-table">
      <thead><tr>
        <th>Order</th>
        <th>Kundens ordernr</th>
        <th>Förväntat</th>
        <th>Status</th>
        <th>Hämtad</th>
        <th>Levererad</th>
        <th className="rx-num">Mot förväntat</th>
        <th className="rx-num" title="Levererade kollin av registrerade, från Opter">Kollin</th>
      </tr></thead>
      <tbody>
        {rows.map(d => {
          const vs = rxDelVsPromised(d, false);
          const sel = d.shopifyId === selectedId;
          return <tr key={d.shopifyId} tabIndex={0}
            className={`rx-row--click${sel ? ' rx-row--selected' : ''}`}
            aria-selected={sel}
            aria-label={`Visa leverans för order ${d.orderName || d.shopifyId}`}
            onClick={() => onSelect(d.shopifyId)}
            onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(d.shopifyId); } }}>
            <td className="rx-mono" style={{ fontWeight: 600 }}>{d.orderName || d.shopifyId}</td>
            <td className="rx-mono rx-muted">{d.poNumber || <span className="rx-faint">Saknas</span>}</td>
            <td style={{ whiteSpace: 'nowrap' }}>{d.deliveryDate ? rxDay(d.deliveryDate) : <span className="rx-faint">Inget datum</span>}</td>
            <td><RxDeliveryPill status={d.status} label={d.statusLabel} cancelled={d.cancelled} isReturn={d.isReturn} isRma={d.isRma} /></td>
            <td style={{ whiteSpace: 'nowrap' }}>{d.pickedUpAt ? rxDayTime(d.pickedUpAt) : <RxDelNotReported />}</td>
            <td style={{ whiteSpace: 'nowrap' }}>{d.deliveredAt
              ? <span style={{ fontWeight: 600, color: 'var(--rx-ok)' }}>{rxDayTime(d.deliveredAt)}</span>
              : <RxDelNotReported />}</td>
            <td className="rx-num" style={{ whiteSpace: 'nowrap', ...rxDelToneStyle(vs.tone) }}>{vs.text}</td>
            <td className="rx-num">{d.packageCount > 0
              ? <span title={`${d.packageCount} registrerade, ${d.scannedPackageCount} avlästa, ${d.deliveredPackageCount} levererade`}>{d.deliveredPackageCount}/{d.packageCount}</span>
              : <span className="rx-faint">Inga</span>}</td>
          </tr>;
        })}
      </tbody>
    </table>
  </div>;
}

function RxDelPager({ page, total, onPage }) {
  const pages = Math.max(1, Math.ceil(total / rxDelPerPage));
  if (total <= rxDelPerPage) return null;
  const start = (page - 1) * rxDelPerPage + 1;
  const end = Math.min(page * rxDelPerPage, total);
  return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '10px 16px', borderTop: '1px solid var(--rx-divider)', fontSize: 13 }}>
    <span className="rx-muted">Visar {fmtInt(start)} till {fmtInt(end)} av {fmtInt(total)}</span>
    <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
      <button type="button" className="btn btn--sm" disabled={page <= 1} onClick={() => onPage(page - 1)}>Föregående</button>
      <span className="rx-muted">Sida {fmtInt(page)} av {fmtInt(pages)}</span>
      <button type="button" className="btn btn--sm" disabled={page >= pages} onClick={() => onPage(page + 1)}>Nästa</button>
    </div>
  </div>;
}

/* ── Sidopanel ──────────────────────────────────────────── */
function RxDelDetail({ shopifyId, reloadKey, onOpenOrder, onClose }) {
  const q = useRxFetch(`/v1/admin/opter/deliveries/${encodeURIComponent(shopifyId)}`, [reloadKey]);
  const ev = useRxFetch(`/v1/admin/opter/events?${rxDelParams({ shopifyId, limit: 200 })}`, [reloadKey]);
  const closeBtn = <button type="button" className="btn btn--icon btn--ghost" aria-label="Stäng panelen" onClick={onClose}>{I.close}</button>;

  if (q.loading && !q.data) return <RxPanel title="Leverans" actions={closeBtn}><RxLoading /></RxPanel>;
  if (q.error) return <RxPanel title="Leverans" actions={closeBtn}><RxError error={q.error} onRetry={q.reload} /></RxPanel>;
  if (!q.data) return null;

  const d = q.data.delivery;
  const packages = q.data.packages || [];
  const events = ev.data ? ev.data.events : [];
  const vs = rxDelVsPromised(d, true);
  const transit = rxDuration(d.pickedUpAt, d.deliveredAt);
  const hasDeviation = d.deviationCode || d.deviationNote;

  return <RxPanel title={d.orderName || d.shopifyId} actions={closeBtn}>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: -6 }}>
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
        <RxDeliveryPill status={d.status} label={d.statusLabel} cancelled={d.cancelled} isReturn={d.isReturn} isRma={d.isRma} />
        {d.overdue && <RxPill tone="danger">Försenad {rxDelDays(d.daysLate)}</RxPill>}
        {d.dateDeviationDays != null && <RxPill tone="warn">Avvikelse mot förväntat datum</RxPill>}
      </div>
      <span className="rx-muted" style={{ fontSize: 13 }}>
        Kundens ordernr <span className="rx-mono">{d.poNumber || 'saknas'}</span>
        {d.deliveryDate ? <>, förväntat {rxDay(d.deliveryDate)}</> : ', inget förväntat datum'}
      </span>
    </div>

    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      <RxDelSubhead>Leveranstid</RxDelSubhead>
      <RxDelDayAxis from={d.pickedUpAt} to={d.deliveredAt} />
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 8 }}>
        <div style={{ background: 'var(--rx-surface-alt)', borderRadius: 10, padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 2 }}>
          <span className="rx-muted" style={{ fontSize: 12 }}>Transporttid</span>
          <span style={{ fontSize: 16, fontWeight: 650, fontVariantNumeric: 'tabular-nums' }}>{transit || <span className="rx-faint" style={{ fontSize: 13, fontWeight: 500 }}>Ej rapporterad</span>}</span>
        </div>
        <div style={{ background: vs.tone === 'ok' ? '#F1F8F4' : vs.tone === 'late' ? 'var(--rx-danger-bg)' : 'var(--rx-surface-alt)', borderRadius: 10, padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 2 }}>
          <span style={{ fontSize: 12, ...rxDelToneStyle(vs.tone), fontWeight: 400 }}>Mot förväntat datum</span>
          <span style={{ fontSize: 16, fontWeight: 650, ...rxDelToneStyle(vs.tone) }}>{vs.text || 'Ej rapporterad'}</span>
        </div>
      </div>
    </div>

    {d.dateDeviationDays != null && <RxCallout tone="warn" title="Avvikelse mot förväntat leveransdatum.">
      Opter rapporterade leverans {rxDay(d.deliveredAt)}, {rxDelDays(Math.abs(d.dateDeviationDays))} {d.dateDeviationDays > 0 ? 'efter' : 'före'} förväntat
      datum {rxDay(d.deliveryDate)}. Förväntat datum kommer från orderns tagg i Shopify, så om kunden har ändrat önskat leveransdatum syns det här.
    </RxCallout>}

    {hasDeviation && <RxCallout tone="warn" title="Avvikelse.">
      {[d.deviationCode, d.deviationNote].filter(Boolean).join(': ')}
    </RxCallout>}

    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      <RxDelSubhead>Händelser från Opter</RxDelSubhead>
      {ev.loading && !ev.data ? <RxLoading label="Hämtar händelser…" /> : <RxTimeline steps={rxDelSteps(d, events)} />}
      {ev.error && <span className="rx-muted" style={{ fontSize: 12 }}>Kunde inte hämta de enskilda händelserna. Tidslinjen bygger på leveransraden.</span>}
    </div>

    <div>
      <RxDelFact label="Fraktsedel" mono>{d.freightBillNumber || <RxDelNotReported />}</RxDelFact>
      <RxDelFact label="Opter-referens" mono>{d.opterOrderReference || <RxDelNotReported />}</RxDelFact>
      <RxDelFact label="Kollin">{d.packageCount > 0
        ? `${fmtInt(d.deliveredPackageCount)} av ${fmtInt(d.packageCount)} levererade, ${fmtInt(d.scannedPackageCount)} avlästa`
        : <span className="rx-faint">Inga registrerade</span>}</RxDelFact>
      <RxDelFact label="Innehåll">{rxPlural(d.skuCount, 'artikel', 'artiklar')}, {fmtInt(d.unitCount)} st</RxDelFact>
      {/* Ett fastställt pris ur OrderXML och en uppskattning ur en händelse är
          inte samma uppgift, så källan skrivs ut i stället för att döljas. */}
      <RxDelFact label="Fraktkostnad">{(() => {
        const belopp = d.freightCost != null ? d.freightCost : d.transportCost;
        if (belopp == null) return <RxDelNotReported />;
        const fastställt = d.freightCostSource === 'orderxml';
        return <span title={fastställt
          ? 'Pris från Opters OrderXML'
          : 'Uppskattning från en Opter-händelse, inte ett fastställt pris'}>
          {Number(belopp).toLocaleString('sv-SE')} kr
          <span className="rx-muted" style={{ fontSize: 11, marginLeft: 6 }}>
            {fastställt ? 'fastställt' : 'uppskattat'}
          </span>
        </span>;
      })()}</RxDelFact>
      <RxDelFact label="Koldioxid">{d.transportCo2 != null
        ? <span title="Uppskattad av Opter, enheten anges inte i händelsen">{Number(d.transportCo2).toLocaleString('sv-SE')}</span>
        : <RxDelNotReported />}</RxDelFact>
      {/* Leveransbeviset. Bilden ligger bakom en egen endpoint, inte i
          leveransraden: den är flera kilobyte och skulle göra listan tung. */}
      <RxDelFact label="Kvittens">{d.podSigneeName
        ? <>{d.podSigneeName}
            {d.hasPodSignature && <> · <a className="rx-link" target="_blank" rel="noreferrer"
              href={`/v1/admin/opter/deliveries/${encodeURIComponent(d.shopifyId)}/signature`}
              title="Signaturen som mottagaren skrev vid leverans">Visa signatur</a></>}
            {!d.hasPodSignature && d.podSignatureUrl && <> · <a className="rx-link" target="_blank" rel="noreferrer"
              href={d.podSignatureUrl} title="Signaturen hos Opter">Signatur hos Opter</a></>}
          </>
        : <RxDelNotReported />}</RxDelFact>
      <RxDelFact label="Senaste händelse">{d.lastEventAt
        ? [rxDayTime(d.lastEventAt), d.lastEventLocation].filter(Boolean).join(', ')
        : <RxDelNotReported />}</RxDelFact>
    </div>

    {packages.length > 0 && <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <RxDelSubhead>Kollin</RxDelSubhead>
      {packages.map((p, i) => <div key={p.barcode || i} style={{ display: 'flex', justifyContent: 'space-between', gap: 10, fontSize: 12.5, padding: '6px 0', borderBottom: '1px solid var(--rx-divider)' }}>
        <div style={{ display: 'flex', flexDirection: 'column', minWidth: 0 }}>
          <span className="rx-mono">{p.barcode || 'Ingen streckkod'}</span>
          <span className="rx-muted" style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
            {[p.sku, p.text, p.quantity != null ? `${fmtInt(p.quantity)} st` : null].filter(Boolean).join(', ')}
          </span>
        </div>
        <span style={{ whiteSpace: 'nowrap', textAlign: 'right' }}>
          {p.deliveredAt
            ? <span style={{ color: 'var(--rx-ok)', fontWeight: 600 }}>Levererat {rxDayTime(p.deliveredAt)}</span>
            : p.lastScanAt ? <span className="rx-muted">Avläst {rxDayTime(p.lastScanAt)}</span>
              : <span className="rx-faint">Ej avläst</span>}
        </span>
      </div>)}
    </div>}

    {onOpenOrder && <button type="button" className="btn btn--primary" onClick={() => onOpenOrder(d.shopifyId)}>Öppna order</button>}
  </RxPanel>;
}

/* ── Leveranstider ──────────────────────────────────────── */
function RxDelCharts({ rows, page }) {
  const delivered = rows.filter(r => r.deliveredAt && (r.status === 'delivered' || r.status === 'partially_delivered'));
  const complete = page && page.returned >= page.total;
  const scope = complete ? '' : ` bland de ${fmtInt(page ? page.returned : 0)} ordrar med senast förväntat datum, av ${fmtInt(page ? page.total : 0)} i urvalet`;
  const basis = `Baserat på ${rxPlural(delivered.length, 'rapporterad leverans', 'rapporterade leveranser')}${scope}. Fylls på när Opter rapporterar fler.`;

  const head = <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
    <h2 className="rx-section-title" style={{ margin: 0 }}>Leveranstider</h2>
    <span className="rx-muted" style={{ fontSize: 13 }}>{basis}</span>
  </div>;

  if (delivered.length === 0) {
    return <section style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      {head}
      <RxPanel><RxEmpty title="Inga rapporterade leveranser i urvalet ännu.">Diagrammen visas när Opter rapporterat leveranser med klockslag.</RxEmpty></RxPanel>
    </section>;
  }

  // Klockslag: en stapel per timme 06 till 17 (06:00 till 17:59).
  const hours = Array.from({ length: 12 }, () => 0);
  let outsideHours = 0;
  delivered.forEach(r => {
    const t = rxParseDate(r.deliveredAt);
    if (!t) return;
    const h = t.getHours();
    if (h >= 6 && h < 18) hours[h - 6]++; else outsideHours++;
  });
  const maxH = Math.max(1, ...hours);

  // Transporttid, hämtad till levererad.
  const transit = { u4: 0, u8: 0, sameLong: 0, next: 0, later: 0 };
  let noPickup = 0;
  delivered.forEach(r => {
    const a = rxParseDate(r.pickedUpAt), b = rxParseDate(r.deliveredAt);
    if (!a || !b || b < a) { noPickup++; return; }
    const days = rxDayDiff(r.pickedUpAt, r.deliveredAt);
    const hrs = (b - a) / 36e5;
    if (days === 0) {
      if (hrs < 4) transit.u4++; else if (hrs < 8) transit.u8++; else transit.sameLong++;
    } else if (days === 1) transit.next++;
    else transit.later++;
  });
  const transitItems = [
    { label: 'Under 4 h', n: transit.u4 },
    { label: '4 till 8 h', n: transit.u8 },
  ];
  if (transit.sameLong > 0) transitItems.push({ label: 'Över 8 h, samma dag', n: transit.sameLong });
  transitItems.push({ label: 'Nästa dag', n: transit.next }, { label: '2 dagar eller mer', n: transit.later });

  // Mot förväntat datum.
  const punct = { early: 0, same: 0, late: 0 };
  let undated = 0;
  delivered.forEach(r => {
    const diff = rxDayDiff(r.deliveryDate, r.deliveredAt);
    if (diff == null) { undated++; return; }
    if (diff < 0) punct.early++; else if (diff === 0) punct.same++; else punct.late++;
  });

  const card = { background: 'var(--rx-surface)', border: '1px solid var(--rx-border)', borderRadius: 14, padding: '16px 18px', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 };
  const title = { margin: 0, fontSize: 14, fontWeight: 600 };
  const foot = { fontSize: 12, color: 'var(--rx-ink-3)' };

  return <section style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
    {head}
    <div className="rx-grid rx-grid--3">
      <article style={card}>
        <h3 style={title}>Klockslag för leverans</h3>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 80, borderBottom: '1px solid var(--rx-border)' }} role="img"
          aria-label={hours.map((n, i) => `${String(i + 6).padStart(2, '0')}: ${n}`).join(', ')}>
          {hours.map((n, i) => <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', height: '100%' }}
            title={`${String(i + 6).padStart(2, '0')}:00 till ${String(i + 6).padStart(2, '0')}:59: ${n}`}>
            {n > 0 && <span style={{ fontSize: 11, fontWeight: 600, color: 'var(--rx-accent-ink)' }}>{n}</span>}
            <div style={{ width: '70%', height: `${n / maxH * 60}px`, borderRadius: '4px 4px 0 0', background: n > 0 ? 'var(--rx-accent)' : 'transparent' }} />
          </div>)}
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: '#9C9AA3' }}>
          <span>06</span><span>09</span><span>12</span><span>15</span><span>18</span>
        </div>
        {outsideHours > 0 && <span style={foot}>{rxPlural(outsideHours, 'leverans', 'leveranser')} utanför 06 till 18 visas inte.</span>}
      </article>
      <article style={card}>
        <h3 style={title}>Transporttid, hämtad till levererad</h3>
        <RxDelBars items={transitItems} />
        {noPickup > 0 && <span style={foot}>{rxPlural(noPickup, 'leverans', 'leveranser')} saknar hämtningstid och räknas inte.</span>}
      </article>
      <article style={card}>
        <h3 style={title}>Leverans mot förväntat datum</h3>
        <RxDelBars items={[
          { label: 'Före', n: punct.early, color: 'var(--rx-ok-dot)' },
          { label: 'Samma dag', n: punct.same, color: 'var(--rx-accent)' },
          { label: 'Sen', n: punct.late, color: 'var(--rx-danger)' },
        ]} />
        {undated > 0 && <span style={foot}>{rxPlural(undated, 'order', 'ordrar')} utan förväntat datum räknas inte.</span>}
      </article>
    </div>
  </section>;
}

/* ── Okopplade händelser ────────────────────────────────── */
function RxDelUnmatched({ q, onLink }) {
  if (q.loading && !q.data) return <RxPanel><RxLoading /></RxPanel>;
  if (q.error) return <RxError error={q.error} onRetry={q.reload} />;
  const list = q.data ? q.data.unmatched : [];
  return <RxPanel flush title="Okopplade Opter-händelser" sub={q.data ? rxPlural(q.data.count, 'referens', 'referenser') : ''}>
    <div className="rx-muted" style={{ fontSize: 13, padding: '0 18px 12px', lineHeight: 1.5 }}>
      Händelser som Opter skickat men som inte matchar någon order. Vanligast är att Fraktlogistik skrivit en egen referens i
      stället för ordernumret. Koppla dem till rätt order, så syns historiken i leveransvyn direkt.
    </div>
    {list.length === 0
      ? <RxEmpty title="Inga okopplade händelser.">Alla händelser från Opter har kunnat kopplas till en order.</RxEmpty>
      : <div style={{ overflowX: 'auto' }}>
        <table className="rx-table">
          <thead><tr>
            <th>Opter-referens</th>
            <th>Kundens referens</th>
            <th>Fraktsedel</th>
            <th className="rx-num">Händelser</th>
            <th>Först sedd</th>
            <th>Senast sedd</th>
            <th title="Högsta händelsekoden i bokstavsordning bland händelserna, inte nödvändigtvis den senaste">Händelsekod</th>
            <th><span style={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }}>Åtgärd</span></th>
          </tr></thead>
          <tbody>
            {list.map((u, i) => <tr key={`${u.opter_order_reference || ''}|${u.customer_order_reference || ''}|${u.freight_bill_number || ''}|${i}`}>
              <td className="rx-mono" style={{ fontWeight: 600 }}>{u.opter_order_reference || <span className="rx-faint">Saknas</span>}</td>
              <td className="rx-mono">{u.customer_order_reference || <span className="rx-faint">Saknas</span>}</td>
              <td className="rx-mono rx-muted">{u.freight_bill_number || <span className="rx-faint">Saknas</span>}</td>
              <td className="rx-num">{fmtInt(u.event_count)}</td>
              <td className="rx-muted" style={{ whiteSpace: 'nowrap' }}>{rxDayTime(rxDelUtc(u.first_seen))}</td>
              <td style={{ whiteSpace: 'nowrap' }}>{rxDayTime(rxDelUtc(u.last_seen))}</td>
              <td className="rx-mono rx-muted">{u.last_event_code || ''}</td>
              <td className="rx-num">
                {u.opter_order_reference
                  ? <button type="button" className="btn btn--sm" onClick={() => onLink(u)}
                    aria-label={`Koppla Opter-referens ${u.opter_order_reference} till order`}>Koppla till order</button>
                  : <span className="rx-faint" title="Kopplingen kräver en Opter-referens">Kan inte kopplas</span>}
              </td>
            </tr>)}
          </tbody>
        </table>
      </div>}
  </RxPanel>;
}

function RxDelLinkModal({ item, onClose, onLinked }) {
  const [term, setTerm] = React.useState(item.customer_order_reference || '');
  const [debounced, setDebounced] = React.useState(term.trim());
  const [manualId, setManualId] = React.useState('');
  const [chosen, setChosen] = React.useState(null);
  const [step, setStep] = React.useState('pick');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [result, setResult] = React.useState(null);

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

  const search = useRxFetch(debounced.length >= 2
    ? '/v1/admin/opter/deliveries?' + rxDelParams({ q: debounced, limit: 10 })
    : null, []);
  const hits = search.data ? search.data.deliveries : [];

  const submit = async () => {
    if (!chosen) return;
    setBusy(true); setErr(null);
    try {
      const r = await apiFetch('/v1/admin/opter/link', {
        method: 'POST',
        body: JSON.stringify({ opterOrderReference: item.opter_order_reference, shopifyId: chosen.shopifyId }),
      });
      setResult(r);
      setStep('done');
      onLinked();
    } catch (e) {
      const m = e && e.message;
      setErr(m === 'order_not_found' ? 'Ingen order med det id:t finns.'
        : m === 'validation_error' ? 'Uppgifterna är ofullständiga.'
          : `Kopplingen misslyckades: ${m || 'okänt fel'}`);
    } finally {
      setBusy(false);
    }
  };

  const chosenName = chosen ? (chosen.orderName || `Shopify-id ${chosen.shopifyId}`) : '';

  let footer;
  if (step === 'pick') {
    footer = <>
      <button type="button" className="btn btn--ghost" onClick={onClose}>Avbryt</button>
      <button type="button" className="btn btn--primary" disabled={!chosen} onClick={() => { setErr(null); setStep('confirm'); }}>Fortsätt</button>
    </>;
  } else if (step === 'confirm') {
    footer = <>
      <button type="button" className="btn btn--ghost" disabled={busy} onClick={() => setStep('pick')}>Tillbaka</button>
      <button type="button" className="btn btn--primary" disabled={busy} onClick={submit}>{busy ? 'Kopplar…' : 'Koppla till order'}</button>
    </>;
  } else {
    footer = <button type="button" className="btn btn--primary" onClick={onClose}>Stäng</button>;
  }

  return <Modal open onClose={busy ? () => {} : onClose} width={560}
    title="Koppla Opter-händelser till order"
    subtitle={`Opter-referens ${item.opter_order_reference}${item.customer_order_reference ? `, kundens referens ${item.customer_order_reference}` : ''}`}
    footer={footer}>
    {step === 'pick' && <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 }}>
        <span className="rx-muted">Sök order på ordernummer, kundens ordernr eller Shopify-id</span>
        <input className="input" type="search" value={term} autoFocus
          onChange={e => { setTerm(e.target.value); }} placeholder="Till exempel 41131 eller EVS12734" />
      </label>
      {debounced.length >= 2 && <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 240, overflowY: 'auto' }}>
        {search.loading && <RxLoading label="Söker…" />}
        {search.error && <RxError error={search.error} onRetry={search.reload} />}
        {!search.loading && !search.error && hits.length === 0 && <span className="rx-muted" style={{ fontSize: 13 }}>Ingen order matchar sökningen.</span>}
        {!search.loading && hits.map(h => {
          const on = chosen && chosen.shopifyId === h.shopifyId;
          return <button key={h.shopifyId} type="button" aria-pressed={!!on}
            onClick={() => { setChosen({ shopifyId: h.shopifyId, orderName: h.orderName, poNumber: h.poNumber }); setManualId(''); }}
            style={{ display: 'flex', alignItems: 'center', gap: 10, textAlign: 'left', font: 'inherit', fontSize: 13, padding: '8px 10px', borderRadius: 8, cursor: 'pointer',
              border: `1px solid ${on ? '#C9C4F2' : 'var(--rx-border)'}`, background: on ? 'var(--rx-accent-softer)' : '#fff' }}>
            <span className="rx-mono" style={{ fontWeight: 600, minWidth: 70 }}>{h.orderName || h.shopifyId}</span>
            <span className="rx-mono rx-muted" style={{ flex: 1 }}>{h.poNumber || ''}</span>
            <span className="rx-muted">{h.deliveryDate ? rxDay(h.deliveryDate) : ''}</span>
            <RxDeliveryPill status={h.status} label={h.statusLabel} cancelled={h.cancelled} isReturn={h.isReturn} isRma={h.isRma} />
          </button>;
        })}
      </div>}
      <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 }}>
        <span className="rx-muted">Eller ange Shopify-order-id direkt</span>
        <input className="input rx-mono" value={manualId} placeholder="Shopify-order-id"
          onChange={e => {
            const v = e.target.value;
            setManualId(v);
            setChosen(v.trim() ? { shopifyId: v.trim(), orderName: null, poNumber: null } : null);
          }} />
      </label>
      {chosen && <span style={{ fontSize: 13 }}>Vald order: <strong>{chosenName}</strong></span>}
    </div>}

    {step === 'confirm' && <div style={{ display: 'flex', flexDirection: 'column', gap: 12, fontSize: 13.5, lineHeight: 1.5 }}>
      <p style={{ margin: 0 }}>
        Koppla Opter-referens <strong className="rx-mono">{item.opter_order_reference}</strong> till order <strong>{chosenName}</strong>
        {chosen && chosen.poNumber ? <> (kundens ordernr <span className="rx-mono">{chosen.poNumber}</span>)</> : null}?
      </p>
      <p style={{ margin: 0 }} className="rx-muted">
        {rxPlural(Number(item.event_count) || 0, 'händelse', 'händelser')}, statusen och kollina för referensen knyts till ordern och
        syns direkt i leveransvyn. Kopplingen går inte att ångra härifrån.
      </p>
      {err && <RxCallout tone="warn">{err}</RxCallout>}
    </div>}

    {step === 'done' && result && <RxCallout title="Kopplat.">
      {rxPlural(result.events || 0, 'händelse', 'händelser')}, {rxPlural(result.status || 0, 'statusrad', 'statusrader')} och {rxPlural(result.packages || 0, 'kolli', 'kollin')} knöts till order {chosenName}.
    </RxCallout>}
  </Modal>;
}

/* ── Sidan ──────────────────────────────────────────────── */
function RxDeliveriesPage({ onNavigate, onOpenOrder }) {
  const [section, setSection] = React.useState('status');
  const [filter, setFilter] = React.useState('all');
  const [page, setPage] = React.useState(1);
  const [searchInput, setSearchInput] = React.useState('');
  const [search, setSearch] = React.useState('');
  const [from, setFrom] = React.useState('');
  const [to, setTo] = React.useState('');
  const [selectedId, setSelectedId] = React.useState(null);
  const [linkItem, setLinkItem] = React.useState(null);
  const [nonce, setNonce] = React.useState(0);

  React.useEffect(() => {
    const t = setTimeout(() => setSearch(searchInput.trim()), 300);
    return () => clearTimeout(t);
  }, [searchInput]);
  React.useEffect(() => { setPage(1); }, [filter, search, from, to]);

  const common = { q: search, delivery_from: from, delivery_to: to };
  // Underlaget: de 500 senast daterade ordrarna (API:ts max). Ger nyckeltal,
  // diagram, statusfilter och de första sidorna av "Alla" i ett anrop.
  const basePath = '/v1/admin/opter/deliveries?' + rxDelParams({ ...common, limit: rxDelBaseLimit });
  // Makulerade hämtas från servern av samma skäl som försenade: de saknar
  // ofta leveransdatum, sorteras därför sist, och hade legat utanför
  // underlagets 500 rader. Räknaren hade visat 1 och tabellen varit tom.
  const statusFilter = rxDelStatusesFor(filter);
  const needServer = filter === 'overdue' || filter === 'cancelled' || filter === 'returns' || filter === 'rma' || !!statusFilter
    || (filter === 'all' && page * rxDelPerPage > rxDelBaseLimit);
  const listPath = needServer
    ? '/v1/admin/opter/deliveries?' + rxDelParams({
        ...common,
        overdue: filter === 'overdue' ? 'true' : '',
        cancelled: filter === 'cancelled' ? 'true' : '',
        returns: filter === 'returns' ? 'true' : '',
        rma: filter === 'rma' ? 'true' : '',
        status: statusFilter ? statusFilter.join(',') : '',
        limit: rxDelPerPage,
        offset: (page - 1) * rxDelPerPage,
      })
    : null;

  const baseQ = useRxFetch(basePath, [nonce]);
  const listQ = useRxFetch(listPath, [nonce]);
  const cfgQ = useRxFetch('/v1/admin/opter/config', []);
  const unmatchedQ = useRxFetch('/v1/admin/opter/unmatched?limit=100', [nonce]);

  const base = baseQ.data;
  const summary = base ? base.summary : null;
  const baseRows = base ? base.deliveries : [];
  const basePage = base ? base.page : null;
  const baseComplete = !!basePage && basePage.returned >= basePage.total;
  const cfg = cfgQ.data;
  const unmatchedCount = unmatchedQ.data ? unmatchedQ.data.count : null;

  // Antal per status. Summeringen slår ihop Hämtad, På väg och Framme och
  // räknar inte Bokad eller Delvis levererad separat, så exakta antal per
  // steg finns bara när hela urvalet ryms i underlaget.
  const chips = React.useMemo(() => {
    if (!summary) return [];
    // Makulerade räknas för sig i båda vägarna, precis som i summeringen på
    // servern. En makulerad order som är Levererad ska inte göra "Levererad"
    // till en siffra vi inte står bakom.
    const makulerade = baseComplete
      ? baseRows.filter(r => r.cancelled).length
      : (summary.cancelledWithEvents || 0);
    const makuleradChip = { key: 'cancelled', label: 'Makulerad med händelser', dot: '#A12C1E', count: makulerade, filter: 'cancelled' };
    // Returer står utanför flödet: de går tillbaka till lagret, inte ut till
    // kund, och räknas därför inte i någon av statushinkarna.
    const returer = baseComplete ? baseRows.filter(r => r.isReturn).length : (summary.returns || 0);
    const returChip = { key: 'returns', label: 'Retur till lagret', dot: '#C2760A', count: returer, filter: 'returns' };
    // RMA: retur eller reklamation som lagts upp som en order, 1 kr eller
    // taggad RMA. Kan transporteras och ha riktiga händelser, men är aldrig
    // en leverans som kan bli försenad.
    const rma = baseComplete ? baseRows.filter(r => r.isRma).length : (summary.rma || 0);
    const rmaChip = { key: 'rma', label: 'RMA', dot: '#6E6C77', count: rma, filter: 'rma' };

    if (baseComplete) {
      const c = {};
      rxDelFlow.forEach(k => { c[k] = 0; });
      baseRows.filter(r => !r.cancelled && !r.isReturn && !r.isRma).forEach(r => { c[r.status] = (c[r.status] || 0) + 1; });
      const flow = rxDelFlow.map(k => ({ key: k, label: RX_DELIVERY_STATUS[k].label, dot: RX_DELIVERY_STATUS[k].dot, count: c[k], filter: `status:${k}` }));
      return [...flow, ...(returer ? [returChip] : []), ...(rma ? [rmaChip] : []),
        ...(makulerade ? [makuleradChip] : [])];
    }
    const rest = Math.max(0, summary.total - summary.awaitingStatus - summary.delivered - summary.deviations - summary.inTransit - (summary.shopifyShipped || 0) - makulerade);
    return [
      { key: 'awaiting_status', label: 'Inväntar status', dot: RX_DELIVERY_STATUS.awaiting_status.dot, count: summary.awaitingStatus, filter: 'status:awaiting_status' },
      { key: 'rest', label: 'Bokad eller delvis levererad', dot: RX_DELIVERY_STATUS.booked.dot, count: rest, filter: 'rest' },
      { key: 'moving', label: 'Hämtad, på väg eller framme', dot: RX_DELIVERY_STATUS.in_transit.dot, count: summary.inTransit, filter: 'moving' },
      { key: 'delivered', label: 'Levererad', dot: RX_DELIVERY_STATUS.delivered.dot, count: summary.delivered, filter: 'status:delivered' },
      { key: 'deviation', label: 'Avvikelse', dot: RX_DELIVERY_STATUS.deviation.dot, count: summary.deviations, filter: 'status:deviation' },
      { key: 'shopify_shipped', label: 'Skickad enligt Shopify', dot: RX_DELIVERY_STATUS.shopify_shipped.dot, count: summary.shopifyShipped || 0, filter: 'status:shopify_shipped' },
      ...(returer ? [returChip] : []),
      ...(rma ? [rmaChip] : []),
      ...(makulerade ? [makuleradChip] : []),
    ];
  }, [base]);

  // Raderna i tabellen.
  let rows = [], total = 0, tableLoading = false, tableError = null, reloadTable = baseQ.reload;
  const statusKey = filter.startsWith('status:') ? filter.slice(7) : null;
  if (needServer) {
    rows = listQ.data ? listQ.data.deliveries : [];
    total = listQ.data ? listQ.data.page.total : 0;
    tableLoading = listQ.loading && !listQ.data;
    tableError = listQ.error;
    reloadTable = listQ.reload;
  } else if (filter === 'datedev') {
    // Enda filtret som är kvar i webbläsaren. Datumavvikelsen räknas ur
    // raderna själva och finns inte som kolumn att filtrera på i SQL.
    const all = baseRows.filter(rxDelHasDeviation);
    total = all.length;
    rows = all.slice((page - 1) * rxDelPerPage, page * rxDelPerPage);
    tableLoading = baseQ.loading && !base;
    tableError = baseQ.error;
  } else {
    rows = baseRows.slice((page - 1) * rxDelPerPage, page * rxDelPerPage);
    total = summary ? summary.total : 0;
    tableLoading = baseQ.loading && !base;
    tableError = baseQ.error;
  }
  // Upplysningen om att bara underlaget filtrerats gäller nu enbart
  // datumavvikelsen. Statusfiltren går till servern och ser hela urvalet, så
  // en not om 500 rader hade varit direkt missvisande där.
  const clientNote = filter === 'datedev' && base && !baseComplete;

  const lowDelivered = summary && summary.total > 0 && summary.delivered / summary.total < 0.05;
  const noEvents = cfg && cfg.eventCount === 0;

  const filters = [
    { key: 'all', label: 'Alla', count: summary ? summary.total : null },
    { key: 'overdue', label: 'Försenade', count: summary ? summary.overdue : null, tone: summary && summary.overdue > 0 ? 'warn' : undefined },
    { key: 'status:delivered', label: 'Levererade', count: summary ? summary.delivered : null },
    { key: 'datedev', label: 'Avvikelse mot förväntat datum', count: base ? baseRows.filter(rxDelHasDeviation).length : null, tone: base && baseRows.some(rxDelHasDeviation) ? 'warn' : undefined },
  ];

  const reloadAll = () => setNonce(n => n + 1);

  return <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
    {/* Källa och läge */}
    <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', justifyContent: 'space-between' }}>
      <span className="rx-muted" style={{ fontSize: 13 }}>
        Opter (Fraktlogistik), bara avläsning
        {cfg && cfg.lastEventAt ? `. Senaste händelse mottagen ${rxDayTime(rxDelUtc(cfg.lastEventAt))}` : ''}
        {cfg ? `. ${rxPlural(cfg.eventCount, 'händelse', 'händelser')} mottagna totalt.` : ''}
      </span>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        {cfg && <RxPill tone={cfg.portalVisible ? 'info' : 'neutral'}
          title={cfg.portalVisible ? 'Kunderna ser leveransstatusen i partnerportalen.' : 'Styrs av PORTAL_DELIVERY_STATUS_ENABLED. Bara personal ser statusen.'}>
          {cfg.portalVisible ? 'Kundportalen: visar status' : 'Kundportalen: avstängd'}
        </RxPill>}
        <RxDelOrderXmlUpload onDone={() => setNonce(n => n + 1)} />
        <a className="rx-link" href="/v1/admin/opter/events?format=xml&limit=200"
          title="Laddar ner de 200 senast mottagna händelserna som XML, med hela originalmeddelandet">Exportera händelser (XML)</a>
      </div>
    </div>

    {cfg && cfg.inboundWebhookOpen && <RxCallout tone="warn" title="Webhooken tar emot oautentiserade anrop.">
      Så länge OPTER_WEBHOOK_ALLOW_UNAUTHENTICATED är satt i Railway kan vem som helst som känner till adressen skicka leveransstatus på era ordrar.
    </RxCallout>}

    <RxFilterRow
      filters={[
        { key: 'status', label: 'Status' },
        { key: 'unmatched', label: 'Okopplade Opter-händelser', count: unmatchedCount, tone: unmatchedCount ? 'warn' : undefined },
      ]}
      value={section} onChange={setSection} />

    {section === 'unmatched' && <RxDelUnmatched q={unmatchedQ} onLink={setLinkItem} />}

    {section === 'status' && <>
      {baseQ.error && !base && <RxError error={baseQ.error} onRetry={baseQ.reload} />}

      {summary && <div className="rx-grid rx-grid--6">
        <RxKpi label="Ordrar i urvalet" value={fmtInt(summary.total)} sub="Ej avbrutna" />
        <RxKpi label="Levererade" value={fmtInt(summary.delivered)} tone={summary.delivered > 0 ? 'ok' : undefined}
          sub={summary.total > 0 ? `${(summary.delivered / summary.total * 100).toLocaleString('sv-SE', { maximumFractionDigits: 1 })} % av urvalet` : null} />
        <RxKpi label="På väg" value={fmtInt(summary.inTransit)} sub="Hämtad, på väg eller framme" />
        <RxKpi label="Inväntar status" value={fmtInt(summary.awaitingStatus)} sub="Ingen händelse från Opter" />
        <RxKpi label="Försenade" value={fmtInt(summary.overdue)} tone={summary.overdue > 0 ? 'warn' : undefined} sub="Förväntat datum passerat" />
        <RxKpi label="Avvikelser" value={fmtInt(summary.deviations)} tone={summary.deviations > 0 || baseRows.some(r => r.dateDeviationDays != null) ? 'warn' : undefined}
          sub={(() => { const n = baseRows.filter(r => r.dateDeviationDays != null).length; return n ? `Från Opter. Plus ${rxPlural(n, 'leverans', 'leveranser')} på annat datum än förväntat.` : 'Från Opter'; })()} />
      </div>}
      {!summary && baseQ.loading && <RxLoading label="Hämtar leveransstatus…" />}

      {noEvents
        ? <RxCallout tone="warn" title="Inga händelser har tagits emot från Opter ännu.">
          Alla ordrar står därför som Inväntar status. Fraktlogistik behöver slå på statusflödet till oss
          {cfg.inboundWebhookSecretSet ? '.' : ', och OPTER_WEBHOOK_SECRET saknas i miljön så endpointen avvisar allt som kommer in.'}
        </RxCallout>
        : lowDelivered && <RxCallout tone="warn"
          title={`Opter har rapporterat ${rxPlural(summary.delivered, 'leverans', 'leveranser')} i urvalet.`}
          action={<button type="button" className="rx-link" onClick={() => setSection('unmatched')}>
            Visa okopplade händelser{unmatchedCount ? ` (${fmtInt(unmatchedCount)})` : ''}
          </button>}>
          {fmtInt(summary.awaitingStatus)} ordrar inväntar fortfarande status, och därför räknas {fmtInt(summary.overdue)} som försenade.
          Händelser vars referens inte matchar någon order hamnar bland okopplade händelser och kan kopplas manuellt där.
        </RxCallout>}

      {chips.length > 0 && <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }} aria-label="Statusflöde">
        {chips.map((c, i) => {
          const on = c.filter && filter === c.filter;
          const inner = <>
            <span style={{ width: 8, height: 8, borderRadius: 999, background: c.dot }} />
            {c.label}
            <span className="rx-count">{fmtInt(c.count)}</span>
          </>;
          const style = { display: 'inline-flex', alignItems: 'center', gap: 7, height: 30, padding: '0 11px', borderRadius: 999, fontSize: 12.5, color: 'var(--rx-ink)', font: 'inherit',
            background: on ? 'var(--rx-accent-softer)' : '#fff', border: `1px solid ${on ? '#C9C4F2' : 'var(--rx-border)'}` };
          return <React.Fragment key={c.key}>
            {c.filter
              ? <button type="button" aria-pressed={!!on} style={{ ...style, cursor: 'pointer', fontSize: 12.5 }}
                onClick={() => setFilter(on ? 'all' : c.filter)}>{inner}</button>
              : <span style={style}>{inner}</span>}
            {i < chips.length - 1 && <span aria-hidden="true" style={{ fontSize: 13, color: '#B5B2BB' }}>›</span>}
          </React.Fragment>;
        })}
      </div>}

      <div className="rx-split">
        <div className="rx-split__main">
          <RxFilterRow filters={filters} value={filter} onChange={setFilter}>
            <RxSearch value={searchInput} onChange={setSearchInput}
              placeholder="Sök ordernr, kundens ordernr, Opter-referens eller fraktsedel" />
            <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
              <span className="rx-muted">Förväntat från</span>
              <input className="input" type="date" value={from} onChange={e => setFrom(e.target.value)} style={{ width: 140 }} />
            </label>
            <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
              <span className="rx-muted">till</span>
              <input className="input" type="date" value={to} onChange={e => setTo(e.target.value)} style={{ width: 140 }} />
            </label>
            {(from || to) && <button type="button" className="rx-link" onClick={() => { setFrom(''); setTo(''); }}>Rensa datum</button>}
          </RxFilterRow>

          <RxPanel flush>
            {tableError ? <div style={{ padding: 16 }}><RxError error={tableError} onRetry={reloadTable} /></div>
              : tableLoading ? <RxLoading />
                : rows.length === 0
                  ? <RxEmpty title={search ? `Ingen order matchar "${search}".` : 'Inga ordrar i urvalet.'}>
                    {search
                      ? 'Sökningen gäller ordernummer, kundens ordernr, Opter-referens och fraktsedel.'
                      : filter === 'overdue' ? 'Inga ordrar har passerat förväntat datum utan leverans.' : 'Ändra filtret eller datumintervallet.'}
                  </RxEmpty>
                  : <RxDelTable rows={rows} selectedId={selectedId} onSelect={setSelectedId} />}
            {!tableError && !tableLoading && <RxDelPager page={page} total={total} onPage={setPage} />}
            {clientNote && <div className="rx-muted" style={{ padding: '10px 16px', fontSize: 12.5, borderTop: '1px solid var(--rx-divider)' }}>
              Avvikelsen mot förväntat datum räknas ur de {fmtInt(basePage.returned)} ordrar med senast förväntat datum, av {fmtInt(basePage.total)} i urvalet.
              Sök eller begränsa datumen för att se äldre ordrar.
            </div>}
            {!clientNote && rows.length > 0 && <div className="rx-faint" style={{ padding: '8px 16px', fontSize: 12, borderTop: '1px solid var(--rx-divider)' }}>
              Sorterat på förväntat datum, senast först. Ordrar utan datum ligger sist.
            </div>}
          </RxPanel>
        </div>

        <div className="rx-split__side">
          {selectedId
            ? <RxDelDetail key={selectedId} shopifyId={selectedId} reloadKey={nonce} onOpenOrder={onOpenOrder} onClose={() => setSelectedId(null)} />
            : <RxPanel><RxEmpty title="Ingen order vald.">Välj en order i tabellen för att se leveranstid, händelser och kollin.</RxEmpty></RxPanel>}
        </div>
      </div>

      {base && <RxDelCharts rows={baseRows} page={basePage} />}
    </>}

    {linkItem && <RxDelLinkModal item={linkItem} onClose={() => setLinkItem(null)} onLinked={reloadAll} />}
  </div>;
}
