/* ===========================================================
   DTF.uk · Ops — Partner shop view (location-limited access)
   A fulfilment partner (e.g. DTF Studio) sees ONLY the orders
   handed to their shop, with prices / payments / customer PII
   stripped server-side. They can view + upload artwork, run
   artwork services, create Royal Mail labels and dispatch.
   Admins get the same view as a read-through PREVIEW.
   =========================================================== */
const { useState: useStateSh, useEffect: useEffectSh, useMemo: useMemoSh } = React;

const MINT = "#18f4af";
const P_STATUS = {
  pending:    { label: "Pending",    bg: "#eef1f4", fg: "#5b6673" },
  printing:   { label: "Printing",   bg: "#fff4d6", fg: "#8a6d1a" },
  printed:    { label: "Printed",    bg: "#e3efff", fg: "#1f5fae" },
  dispatched: { label: "Dispatched", bg: "#dcf7ec", fg: "#0f7a52" },
};
// Shops only need one optional step before dispatch — "printed". (Orders that came
// in as "printing" from the admin board are shown collapsed to "printed" in the flow.)
const P_FLOW = ["pending", "printed", "dispatched"];

// Craft-aware wording. An embroidery shop must never see print/DTF language —
// its work is "embroider" and a finished job is "Embroidered". Everything keys
// off the shop's `craft` field ("print" | "embroidery").
function craftOf(slug) {
  const s = ((DTF && DTF.myShops) || []).find(x => x.slug === slug)
    || (DTF && DTF.shopsById && DTF.shopsById[slug]) || {};
  return s.craft === "embroidery" ? "embroidery" : "print";
}
function craftWords(craft) {
  const emb = craft === "embroidery";
  return {
    emb,
    doneLabel:   emb ? "Embroidered" : "Printed",
    markDone:    emb ? "Mark embroidered" : "Mark printed",
    markNotDone: emb ? "Mark not embroidered" : "Mark not printed",
    doneLower:   emb ? "embroidered" : "printed",
    verbThese:   emb ? "embroider these jobs" : "print these jobs",
  };
}
// Status label, relabelled for embroidery ("Printed"/"Printing" → "Embroidered").
function statusLabel(status, craft) {
  const m = P_STATUS[status] || P_STATUS.pending;
  if (craft === "embroidery" && (status === "printed" || status === "printing")) return "Embroidered";
  return m.label;
}
function PBadge({ status, craft }) {
  const m = P_STATUS[status] || P_STATUS.pending;
  return <span className="badge" style={{ background: m.bg, color: m.fg, borderColor: "transparent" }}>{statusLabel(status, craft)}</span>;
}

// Map a redacted shop line item (title/quantity/variantTitle) onto the shape the
// shared admin helpers expect (name/qty/variant) so we can reuse ItemProps + the
// metres calc (DTF.itemMetres) without duplicating any of that logic here.
const shItem = (li) => ({ ...li, name: li.title, qty: li.currentQuantity != null ? li.currentQuantity : li.quantity, orderedQty: li.quantity, removed: !!li.removed || li.currentQuantity === 0, variant: li.variantTitle });

// Print type — same rule the admin board uses: "UV" if the item title says so,
// otherwise a normal DTF transfer.
const isUvTitle = (t) => /\buv\b/i.test(t || "");
function orderKind(o) {
  const its = o.lineItems || o.items || [];
  return { uv: its.some(li => isUvTitle(li.title || li.name)), dtf: its.some(li => !isUvTitle(li.title || li.name)) };
}
function KindTags({ o, craft }) {
  if (craft === "embroidery") return null;   // no DTF/UV print tags for embroidery shops
  const k = orderKind(o);
  return <>{k.uv && <span className="kind-tag kind-tag--uv">UV</span>}{k.dtf && <span className="kind-tag kind-tag--dtf">DTF</span>}</>;
}

// Shipping service the customer paid for — surfaced in the partner list + detail
// so the shop can double-check it before making a label (Special Delivery in
// particular must NOT be shipped on a slower service). Derived from the order's
// shipping title / delivery method.
function svcInfo(o) {
  if (o && (o.deliveryMethodType === "PICK_UP" || o.deliveryMethodType === "RETAIL")) return { label: "Collection", cls: "svc--collect", sd: false };
  const t = String((o && (o.shippingTitle || o.deliveryMethodType)) || "").toLowerCase();
  if (!t) return { label: "—", cls: "svc--std", sd: false };
  if (/special delivery|guaranteed|1\s*pm|13[:.]00|by 1|9\s*am/.test(t)) return { label: "Special Delivery", cls: "svc--sd", sd: true };
  if (/48|2nd|second/.test(t)) return { label: "Tracked 48", cls: "svc--48", sd: false };
  if (/24|1st|first|next[- ]day|tracked/.test(t)) return { label: "Tracked 24", cls: "svc--24", sd: false };
  if (/international|intl/.test(t)) return { label: "International", cls: "svc--intl", sd: false };
  return { label: o.shippingTitle || "Standard", cls: "svc--std", sd: false };
}
function SvcBadge({ o, full }) {
  const s = svcInfo(o);
  return <span className={"svc-badge " + s.cls} title={o.shippingTitle ? "Service: " + o.shippingTitle : s.label}>{s.sd ? "★ " : ""}{full && o.shippingTitle ? o.shippingTitle : s.label}</span>;
}

// Item line-item props are rendered by the shared <ItemProps> component (from
// views-orders.jsx) so the shop view matches the admin order view exactly —
// aligned label/value rows, design previews, downloadable print-ready files.

async function shGet(url) {
  const r = await fetch(url);
  if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || (r.status + " " + r.statusText));
  return r.json();
}
async function shSend(url, method, body) {
  const r = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}) });
  if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || (r.status + " " + r.statusText));
  return r.json();
}

/* ---------------- Partner standalone shell ---------------- */
function ShopApp({ shops, route, go, userEmail }) {
  const list = shops || [];
  const slug = (route.view === "shop" && route.slug) ? route.slug : (list[0] && list[0].slug) || null;
  const shop = list.find(s => s.slug === slug) || list[0] || null;

  // Keep partners on a valid shop URL (they can't reach admin routes anyway).
  useEffectSh(() => {
    if (route.view !== "shop" || !route.slug) { if (slug) go({ view: "shop", slug }); }
  }, [route.view, route.slug]);

  // A newer frontend build has been deployed — surface the same reload prompt the
  // admin app shows (DTF.versionWatch polls /api/version, which partners can hit).
  const [updateReady, setUpdateReady] = useStateSh(!!(DTF && DTF._updateReady));
  useEffectSh(() => {
    if (DTF && DTF._updateReady) { setUpdateReady(true); return; }
    const onUpd = () => setUpdateReady(true);
    window.addEventListener("dtf:update-available", onUpd);
    if (DTF && DTF.versionWatch) DTF.versionWatch.start();
    return () => { window.removeEventListener("dtf:update-available", onUpd); if (DTF && DTF.versionWatch) DTF.versionWatch.stop(); };
  }, []);

  if (!shop) return (
    <div className="app"><div className="main" style={{ gridColumn: "1 / -1" }}><div className="content">
      <div className="empty" style={{ padding: 40 }}>No shop is assigned to your account yet. Ask the studio to hand orders over to your shop.</div>
    </div></div></div>
  );

  const name = route.view === "shop" ? route.id : null;
  return (
    <div className="shopapp">
      <header className="shop-top">
        {(() => {
          const initials = (shop.name || "").split(/\s+/).filter(Boolean).slice(0, 2).map(w => w[0]).join("").toUpperCase() || "SH";
          const sub = shop.craft === "embroidery" ? "Embroidery partner" : "Fulfilment · DTF.uk";
          return <div className="shop-brand"><span className="shop-mark" style={{ background: MINT }}>{initials}</span>
            <div><div className="shop-title">{shop.name}</div><div className="shop-sub">{sub}</div></div></div>;
        })()}
        <div className="shop-top__right">
          {list.length > 1 &&
            <select className="rm-in" value={slug} onChange={e => go({ view: "shop", slug: e.target.value })}>
              {list.map(s => <option key={s.slug} value={s.slug}>{s.name}</option>)}
            </select>}
          <span className="shop-user" title={userEmail}>{userEmail}</span>
          <button className="btn btn--ghost btn--sm" onClick={() => { fetch("/api/logout", { method: "POST" }).finally(() => location.reload()); }}>Sign out</button>
        </div>
      </header>
      {updateReady && (
        <div className="update-bar" role="alert">
          <Icon name="spark" />
          <span>A new version of the app is available.</span>
          <button className="btn btn--sm update-bar__btn" onClick={() => location.reload()}>Reload to update</button>
        </div>
      )}
      <main className="shop-main">
        {name
          ? <ShopOrderDetail slug={slug} name={name} go={go} routeTab={route.tab} routeFilter={route.filter} />
          : <ShopView slug={slug} go={go} routeTab={route.tab} routeFilter={route.filter} />}
      </main>
    </div>
  );
}

/* --------- Admin: hand-over email settings (shown in shop preview) --------- */
// Where the "new job" email goes when an order is handed to this shop. Global
// mode (test → our own inbox, live → the shop) + this shop's own address.
function ShopNotifyControls({ slug }) {
  const [cfg, setCfg] = useStateSh(null);           // { mode, testTo, enabled }
  const [email, setEmail] = useStateSh("");
  const [savedEmail, setSavedEmail] = useStateSh("");
  const [msg, setMsg] = useStateSh("");
  useEffectSh(() => {
    shGet("/api/settings/shop-notify").then(setCfg).catch(() => setCfg({ mode: "test", testTo: "", enabled: true }));
    const s = ((DTF && DTF.myShops) || []).find(x => x.slug === slug) || (DTF && DTF.shopsById && DTF.shopsById[slug]) || {};
    setEmail(s.email || ""); setSavedEmail(s.email || "");
  }, [slug]);
  if (!cfg) return null;
  async function setMode(mode) {
    setCfg(c => ({ ...c, mode }));
    try { const j = await shSend("/api/settings/shop-notify", "PUT", { mode }); setCfg(j); } catch (e) { setMsg(e.message); }
  }
  async function saveEmail() {
    if (email === savedEmail) return;
    try { const j = await shSend("/api/shops/" + slug + "/email", "PUT", { email }); if (j && j.shops) { DTF.myShops = j.shops; if (DTF.shopsById) DTF.shopsById = Object.fromEntries(j.shops.map(s => [s.slug, s])); } setSavedEmail(email); setMsg("Saved"); setTimeout(() => setMsg(""), 1500); }
    catch (e) { setMsg(e.message); }
  }
  const live = cfg.mode === "live";
  return (
    <span style={{ display: "inline-flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
      <span className="kick">Job emails</span>
      <select className="rm-in rm-in--sm" value={cfg.mode} onChange={e => setMode(e.target.value)} title="Who receives the hand-over email when an order is assigned to this shop">
        <option value="test">Test → me{cfg.testTo ? " (" + cfg.testTo + ")" : ""}</option>
        <option value="live">Live → shop</option>
      </select>
      <input className="rm-in rm-in--sm" type="email" value={email} placeholder="shop email" style={{ width: 180, opacity: live ? 1 : 0.6 }}
             onChange={e => setEmail(e.target.value)} onBlur={saveEmail} onKeyDown={e => { if (e.key === "Enter") e.target.blur(); }}
             title="Where live hand-over emails go for this shop" />
      {msg && <span className="kick" style={{ color: msg === "Saved" ? "var(--mint-ink)" : "#c0392b" }}>{msg}</span>}
    </span>
  );
}
window.ShopNotifyControls = ShopNotifyControls;

/* ---------------- Order list (active / dispatched) ---------------- */
function ShopView({ slug, name, go, preview, shops, routeTab, routeFilter }) {
  // In admin preview this component IS the routed body; a deep-linked order
  // number renders the detail instead of the list.
  if (name) return <ShopOrderDetail slug={slug} name={name} go={go} preview={preview} routeTab={routeTab} routeFilter={routeFilter} />;

  // Tab + status filter come from the URL so each is bookmarkable and survives a
  // reload; changing them navigates (updates the hash) rather than local state.
  const tab = routeTab === "dispatched" ? "dispatched" : "active";
  const statusFilter = routeFilter === "printed" ? "printed" : routeFilter === "all" ? "all" : "pending";
  const cw = craftWords(craftOf(slug));   // craft-aware wording (embroidery vs print)
  const goTab = (t) => go({ view: "shop", slug, tab: t });
  const goFilter = (f) => go({ view: "shop", slug, tab: "todo", filter: f });

  const [orders, setOrders] = useStateSh(null);   // null = loading (current tab's rows only)
  const [counts, setCounts] = useStateSh({ active: 0, dispatched: 0 }); // both tab counts, from the server
  const [err, setErr] = useStateSh("");
  const [sel, setSel] = useStateSh(() => new Set());
  const [labelFor, setLabelFor] = useStateSh(null); // array of orders for bulk label modal
  const [busy, setBusy] = useStateSh("");
  const [carrier, setCarrierState] = useStateSh((((DTF && DTF.myShops) || []).find(s => s.slug === slug) || {}).carrier || "royalmail");
  async function setCarrier(v) { setCarrierState(v); try { const j = await shSend("/api/shops/" + slug + "/carrier", "PUT", { carrier: v }); if (j && j.shops) DTF.myShops = j.shops; } catch (e) { setErr(e.message); } }
  // Pause label creation: partner prints only, DTF.uk collects & dispatches.
  const [labelsPaused, setLabelsPausedState] = useStateSh(!!(((DTF && DTF.myShops) || []).find(s => s.slug === slug) || {}).labelsPaused);
  async function setLabelsPaused(v) { setLabelsPausedState(v); try { const j = await shSend("/api/shops/" + slug + "/labels-paused", "PUT", { paused: v }); if (j && j.shops) DTF.myShops = j.shops; } catch (e) { setErr(e.message); setLabelsPausedState(!v); } }

  // Load only the CURRENT tab's orders — the active tab queries just the still-open
  // orders (not every order ever handed to the shop), so it's fast regardless of history.
  const load = () => { setErr(""); shGet("/api/shop/orders?shop=" + encodeURIComponent(slug) + "&tab=" + tab).then(j => { setOrders(j.orders || []); if (j.counts) setCounts(j.counts); }).catch(e => { setErr(e.message); setOrders([]); }); };
  useEffectSh(() => { setOrders(null); setSel(new Set()); load(); }, [slug, tab]);
  // Switching tab/filter is a route change (component stays mounted) — clear the
  // selection so it doesn't carry across piles.
  useEffectSh(() => { setSel(new Set()); }, [tab, statusFilter]);

  // Linear metres for an order: if a "to print" artwork is marked, its actual sheet
  // length (printMetres) wins; otherwise sum the live line-item estimates.
  const lineMetres = (o) => (o.lineItems || []).filter(li => !(li.removed || li.currentQuantity === 0)).reduce((a, li) => a + DTF.itemMetres(shItem(li)), 0);
  const orderMetres = (o) => (o.printMetres > 0 ? o.printMetres : lineMetres(o));
  const sumMetres = (arr) => arr.reduce((s, o) => s + orderMetres(o), 0);

  // `orders` holds only the current tab's rows. On the active tab, hide any straggler
  // that self-heal just reclassified as dispatched.
  const tabRows = orders || [];
  const active = tab === "active" ? tabRows.filter(o => o.status !== "dispatched") : [];
  const done = tab === "dispatched" ? tabRows : [];
  // Within the To-do pile, split by print status: "to do" = not yet printed
  // (pending/printing), "printed" = printed, "all" = the whole pile.
  const toDoRows = active.filter(o => o.status !== "printed");
  const printedRows = active.filter(o => o.status === "printed");
  const activeShown = statusFilter === "all" ? active : statusFilter === "printed" ? printedRows : toDoRows;
  const rows = tab === "active" ? activeShown : done;

  const toggle = (id) => setSel(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const allSel = rows.length > 0 && rows.every(o => sel.has(o.id));
  const toggleAll = () => setSel(allSel ? new Set() : new Set(rows.map(o => o.id)));
  const selected = rows.filter(o => sel.has(o.id));

  async function bulkStatus(status) {
    setBusy(status);
    try { for (const o of selected) await shSend("/api/shop/orders/" + enc(o.id) + "/status", "POST", { status }); load(); setSel(new Set()); }
    catch (e) { setErr(e.message); } finally { setBusy(""); }
  }
  async function bulkDispatch() {
    setBusy("dispatch");
    try { for (const o of selected) await shSend("/api/shop/orders/" + enc(o.id) + "/dispatch", "POST", {}); load(); setSel(new Set()); }
    catch (e) { setErr(e.message); } finally { setBusy(""); }
  }
  // Open each selected order's label PDF (skip any without a label yet).
  function bulkPrint() {
    setErr("");
    const withLabel = selected.filter(o => o.labelReady);
    withLabel.forEach(o => window.open("/api/shop/orders/" + enc(o.id) + "/label.pdf?t=" + Date.now(), "_blank"));
    const skipped = selected.length - withLabel.length;
    if (skipped > 0) setErr(skipped + " selected order" + (skipped === 1 ? "" : "s") + " have no label yet — skipped.");
  }

  if (orders === null) return <div className="empty" style={{ padding: 40 }}><span className="spin spin--dark" /></div>;

  return (
    <div className="shop-list">
      {preview && <div className="warn-banner" style={{ marginBottom: 12, background: "var(--mint-wash)", borderColor: MINT, color: "var(--mint-ink)", alignItems: "center" }}>
        <Icon name="spark" /> Admin preview — this is exactly what the partner sees. Prices &amp; customer details are hidden.
        <span style={{ marginLeft: "auto", display: "inline-flex", gap: 12, alignItems: "center", flexWrap: "wrap" }}>
          <ShopNotifyControls slug={slug} />
          <span style={{ display: "inline-flex", gap: 6, alignItems: "center" }}>
            <span className="kick">Labels via</span>
            <select className="rm-in rm-in--sm" value={carrier} onChange={e => setCarrier(e.target.value)} disabled={labelsPaused} title={labelsPaused ? "Label creation is paused" : "How this shop creates shipping labels"}>
              <option value="royalmail">Royal Mail (Click &amp; Drop)</option>
              <option value="shopify">Shopify Shipping</option>
            </select>
          </span>
          <label style={{ display: "inline-flex", gap: 6, alignItems: "center", cursor: "pointer" }} title="Pause label creation — the studio prints only; you collect the prints and dispatch them yourself">
            <input type="checkbox" checked={labelsPaused} onChange={e => setLabelsPaused(e.target.checked)} />
            <span className="kick">Pause labels (I'll collect &amp; dispatch)</span>
          </label>
        </span>
      </div>}
      {err && <div className="warn-banner" style={{ marginBottom: 12, color: "#c0392b" }}><Icon name="spark" /> {err}</div>}

      <div className="shop-tabs">
        <button className={"shop-tab" + (tab === "active" ? " is-active" : "")} onClick={() => goTab("todo")}>To do <span className="cnt">{counts.active}</span></button>
        <button className={"shop-tab" + (tab === "dispatched" ? " is-active" : "")} onClick={() => goTab("dispatched")}>Dispatched <span className="cnt">{counts.dispatched}</span></button>
        <button className="btn btn--ghost btn--sm" style={{ marginLeft: "auto" }} onClick={load}><Icon name="refresh" /> Refresh</button>
      </div>

      {tab === "active" &&
        <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 12 }}>
          <div className="seg">
            <button className={statusFilter === "pending" ? "is-active" : ""} onClick={() => goFilter("pending")}>To do{toDoRows.length ? ` · ${toDoRows.length}` : ""}</button>
            <button className={statusFilter === "printed" ? "is-active" : ""} onClick={() => goFilter("printed")}>{cw.doneLabel}{printedRows.length ? ` · ${printedRows.length}` : ""}</button>
            <button className={statusFilter === "all" ? "is-active" : ""} onClick={() => goFilter("all")}>All{active.length ? ` · ${active.length}` : ""}</button>
          </div>
          {!cw.emb && sumMetres(rows) > 0 && (() => {
            const lbl = statusFilter === "printed" ? "Printed" : statusFilter === "all" ? "Total" : "Required";
            const tip = statusFilter === "printed" ? "DTF material already printed for the orders shown (linear metres)" : "DTF material to print for the orders shown (linear metres)";
            return <span className="shop-total-m" style={{ marginLeft: "auto" }} title={tip}><span className="kick">{lbl}</span><span className="m-badge m-badge--solid">{DTF.fmtMetres(sumMetres(rows))}</span></span>;
          })()}
        </div>}

      {tab === "active" && labelsPaused &&
        <div className="warn-banner" style={{ marginBottom: 12, background: "var(--mint-wash)", borderColor: MINT, color: "var(--mint-ink)", alignItems: "center" }}>
          <Icon name="spark" /> Label creation is paused — {cw.verbThese} and mark them {cw.doneLower}. We collect the finished work and dispatch, so no labels are made here.
        </div>}

      {tab === "active" && selected.length > 0 &&
        <div className="shop-bulk">
          <span className="kick">{selected.length} selected</span>
          {statusFilter === "printed"
            ? <button className="btn btn--ghost btn--sm" disabled={!!busy} onClick={() => bulkStatus("pending")}><Icon name="x" /> {cw.markNotDone}</button>
            : <button className="btn btn--ghost btn--sm" disabled={!!busy} onClick={() => bulkStatus("printed")}><Icon name="check" /> {cw.markDone}</button>}
          {!labelsPaused && <button className="btn btn--ghost btn--sm" disabled={!!busy} onClick={() => setLabelFor(selected)}><Icon name="truck" /> Create labels</button>}
          {!labelsPaused && <button className="btn btn--primary btn--sm" disabled={!!busy} onClick={bulkDispatch}><Icon name="truck" /> {busy === "dispatch" ? "Dispatching…" : "Mark dispatched"}</button>}
        </div>}

      {tab === "dispatched" && selected.length > 0 &&
        <div className="shop-bulk">
          <span className="kick">{selected.length} selected</span>
          <button className="btn btn--primary btn--sm" onClick={bulkPrint}><Icon name="download" /> Print selected label{selected.length === 1 ? "" : "s"}</button>
        </div>}

      {rows.length === 0
        ? <div className="empty" style={{ padding: 40 }}>{tab !== "active" ? "No dispatched orders yet." : statusFilter === "printed" ? `Nothing ${cw.doneLower} yet.` : statusFilter === "all" ? "Nothing in the pile right now." : "Nothing to do right now."}</div>
        : <table className="shop-tbl">
          <thead><tr>
            <th style={{ width: 28 }}><input type="checkbox" checked={allSel} onChange={toggleAll} /></th>
            <th>Order</th><th>Items</th><th>Service</th><th>{tab === "dispatched" ? "Dispatched" : "Placed"}</th><th>Status</th><th></th>
          </tr></thead>
          <tbody>
            {(tab === "dispatched" ? groupByDispatch(rows) : [{ key: "_all", label: null, rows }]).map(grp => {
              // Split the day's dispatched material by print type so DTF and UV are
              // totalled separately (different films / RIP queues).
              const grpM = { uv: 0, dtf: 0 };
              grp.rows.forEach(o => {
                if (o.printMetres > 0) {
                  // A marked print sheet is one number — bucket it by the order's type.
                  if ((o.lineItems || []).some(li => isUvTitle(li.title))) grpM.uv += o.printMetres; else grpM.dtf += o.printMetres;
                } else {
                  (o.lineItems || []).filter(li => !(li.removed || li.currentQuantity === 0)).forEach(li => {
                    const m = DTF.itemMetres(shItem(li));
                    if (isUvTitle(li.title)) grpM.uv += m; else grpM.dtf += m;
                  });
                }
              });
              return (
              <React.Fragment key={grp.key}>
                {grp.label && <tr className="shop-daterow"><td colSpan={7}><span className="shop-daterow__label">{grp.label}</span><span className="shop-daterow__cnt">{grp.rows.length}</span>
                  {!cw.emb && grpM.dtf > 0 && <span className="m-badge m-badge--solid shop-daterow__m" title="DTF material dispatched this day (linear metres)">DTF {DTF.fmtMetres(grpM.dtf)}</span>}
                  {!cw.emb && grpM.uv > 0 && <span className="m-badge m-badge--uv shop-daterow__m" title="UV material dispatched this day (linear metres)">UV {DTF.fmtMetres(grpM.uv)}</span>}</td></tr>}
                {grp.rows.map(o => {
                  // Removed (refunded) lines aren't made or shipped — exclude from the row.
                  const live = (o.lineItems || []).filter(li => !(li.removed || li.currentQuantity === 0));
                  const qty = live.reduce((a, li) => a + (li.currentQuantity != null ? li.currentQuantity : (li.quantity || 0)), 0);
                  const metres = orderMetres(o);
                  // Flag orders that must be dispatched TODAY (placed before today's
                  // 1pm cutoff, i.e. an earlier day or today before 13:00).
                  const co = cutoffInfo(o.createdAt);
                  const cutoff = tab === "active" && o.status !== "dispatched" && co && co.dueToday;
                  return (
                    <tr key={o.id} className={"shop-row" + (cutoff ? " shop-row--cutoff" : "")} onClick={() => go({ view: "shop", slug, id: String(o.id).replace(/^#/, ""), tab: tab === "dispatched" ? "dispatched" : "todo", filter: statusFilter })}>
                      <td onClick={e => e.stopPropagation()}><input type="checkbox" checked={sel.has(o.id)} onChange={() => toggle(o.id)} /></td>
                      <td className="mono">{o.id} <KindTags o={o} craft={cw.emb ? "embroidery" : "print"} />
                        {cutoff && <span className="badge badge--cutoff" title={co.earlierDay ? ("Placed on an earlier day at " + co.hhmm + " — overdue for same-day, dispatch today") : ("Placed " + co.hhmm + " — before the 1pm cutoff, dispatch today")}><Icon name="clock" /> {co.earlierDay ? "dispatch today" : "before 1pm"}</span>}
                        {o.group && o.group.count > 1 && <span className="badge" style={{ marginLeft: 4, background: "var(--mint-wash)", color: "var(--mint-ink)", borderColor: "transparent" }} title={"Combined: " + o.group.members.join(", ")}><Icon name="link" /> +{o.group.count - 1}</span>}
                        {o.destinationCountry && o.destinationCountry !== "GB" && <span className="badge badge--amber" style={{ marginLeft: 4 }}>INTL</span>}</td>
                      <td>{qty} item{qty === 1 ? "" : "s"} <span className="dim">· {live.map(li => li.title).slice(0, 2).join(", ")}{live.length > 2 ? "…" : ""}</span>
                        {!cw.emb && metres > 0 && <span className="m-badge m-badge--solid" style={{ marginLeft: 6 }} title="DTF material to RIP (linear metres)">{DTF.fmtMetres(metres)}</span>}</td>
                      <td onClick={e => e.stopPropagation()}><SvcBadge o={o} /></td>
                      <td className="dim">{fmtDate(tab === "dispatched" ? (o.dispatchedAt || o.createdAt) : o.createdAt)}{tab === "active" && co ? <span className={cutoff ? "" : "dim"} style={cutoff ? { color: "#b45309", fontWeight: 600 } : null}> · {co.hhmm}</span> : null}</td>
                      <td><PBadge status={o.status} craft={cw.emb ? "embroidery" : "print"} />
                        {o.status === "dispatched" && o.tracking && o.tracking.number &&
                          <div className="shop-track mono" onClick={e => e.stopPropagation()}>
                            {o.tracking.url
                              ? <a href={o.tracking.url} target="_blank" rel="noopener noreferrer">{o.tracking.number}</a>
                              : <span>{o.tracking.number}</span>}
                            {o.tracking.company ? <span className="dim"> · {o.tracking.company}</span> : null}
                          </div>}</td>
                      <td><Icon name="chevron" /></td>
                    </tr>
                  );
                })}
              </React.Fragment>
              );
            })}
          </tbody>
        </table>}

      {labelFor && <ShopLabelModal orders={labelFor} slug={slug} onClose={() => setLabelFor(null)} onDone={() => { setLabelFor(null); load(); setSel(new Set()); }} />}
    </div>
  );
}

/* ---------------- Order detail ---------------- */
function ShopOrderDetail({ slug, name, go, preview, routeTab, routeFilter }) {
  const [order, setOrder] = useStateSh(null);   // null = loading, false = error
  const [err, setErr] = useStateSh("");
  const [acting, setActing] = useStateSh("");
  const [labelOpen, setLabelOpen] = useStateSh(false);
  // The list this order was opened from — so "back" returns to that exact tab/filter.
  const listTab = routeTab === "dispatched" ? "dispatched" : "todo";
  const listFilter = routeFilter || "pending";

  const load = () => { setErr(""); shGet("/api/shop/orders/" + enc(name) + "?shop=" + encodeURIComponent(slug || "")).then(j => setOrder(j.order || false)).catch(e => { setErr(e.message); setOrder(false); }); };
  useEffectSh(() => { setOrder(null); load(); }, [name, slug]);
  // A child order resolves to its master (the parcel that ships) — keep the URL on it.
  useEffectSh(() => {
    if (order && order.group && enc(order.id) !== enc(name)) go({ view: "shop", slug, id: enc(order.id), tab: listTab, filter: listFilter });
  }, [order && order.id]);

  async function setStatus(status) { setActing(status); try { await shSend("/api/shop/orders/" + enc(name) + "/status", "POST", { status }); load(); } catch (e) { setErr(e.message); } finally { setActing(""); } }
  async function dispatch() { setActing("dispatch"); try { await shSend("/api/shop/orders/" + enc(name) + "/dispatch", "POST", {}); load(); } catch (e) { setErr(e.message); } finally { setActing(""); } }
  function printLabel() { window.open("/api/shop/orders/" + enc(name) + "/label.pdf?t=" + Date.now(), "_blank"); }

  const back = () => go({ view: "shop", slug, tab: listTab, filter: listFilter });

  if (order === null) return <div className="empty" style={{ padding: 40 }}><span className="spin spin--dark" /></div>;
  if (order === false) return <div className="shop-detail"><a className="linklike" onClick={back}>← Back</a><div className="warn-banner" style={{ marginTop: 12, color: "#c0392b" }}><Icon name="spark" /> {err || "Order not found"}</div></div>;

  const o = order;
  const st = o.status || "pending";
  const dispatched = st === "dispatched";
  // Label creation paused for this shop → print only; DTF.uk collects & dispatches.
  const labelsPaused = !!(((DTF && DTF.myShops) || []).find(s => s.slug === slug) || {}).labelsPaused;
  const cw = craftWords(craftOf(slug));   // craft-aware wording (embroidery vs print)
  // Material to RIP: if a "to print" sheet is marked, its actual length wins over
  // the per-line-item estimate (which double-counts what's ganged onto one sheet).
  const hasPrintSheet = o.printMetres > 0;
  const lineM = (o.lineItems || []).filter(li => !(li.removed || li.currentQuantity === 0)).reduce((s, li) => s + DTF.itemMetres(shItem(li)), 0);
  const orderM = hasPrintSheet ? o.printMetres : lineM;

  return (
    <div className="shop-detail">
      <div className="crumbs"><a onClick={back}>Orders</a><span className="sep">/</span><span style={{ color: "var(--txt)" }}>{o.id}</span></div>
      <div className="pagehead">
        <button className="icon-btn" onClick={back}><Icon name="back" /></button>
        <h2>{o.id}</h2>
        <PBadge status={st} craft={cw.emb ? "embroidery" : "print"} />
        <KindTags o={o} craft={cw.emb ? "embroidery" : "print"} />
        <SvcBadge o={o} full />
        {o.destinationCountry && o.destinationCountry !== "GB" && <span className="badge badge--amber">INTL · {o.destinationCountry}</span>}
        <span className="meta">{fmtDate(o.createdAt)}</span>
        <div className="actions">
          {!dispatched && st !== "printed" && <button className="btn btn--ghost btn--sm" disabled={!!acting} onClick={() => setStatus("printed")}><Icon name="check" /> {acting === "printed" ? "…" : cw.markDone}</button>}
          {!dispatched && st === "printed" && <button className="btn btn--ghost btn--sm" disabled={!!acting} onClick={() => setStatus("pending")}><Icon name="x" /> {acting === "pending" ? "…" : cw.markNotDone}</button>}
          {!dispatched && !labelsPaused && !o.labelReady && <button className="btn btn--ghost btn--sm" onClick={() => setLabelOpen(true)}><Icon name="truck" /> Create shipping label</button>}
          {o.labelReady && <button className="btn btn--primary btn--sm" onClick={printLabel} title={o.labelTracking ? "Tracking " + o.labelTracking : "Download the shipping label PDF"}><Icon name="download" /> Download shipping label</button>}
          {!dispatched && !labelsPaused && o.labelReady && <button className="btn btn--ghost btn--sm" onClick={() => setLabelOpen(true)} title="Buy a new label (replaces the current one)"><Icon name="refresh" /> Recreate</button>}
          {!dispatched && !labelsPaused && <button className="btn btn--primary btn--sm" disabled={!!acting} onClick={dispatch}><Icon name="truck" /> {acting === "dispatch" ? "Dispatching…" : "Mark dispatched"}</button>}
          {!dispatched && labelsPaused && <span className="kick" title="This shop's labels are paused — the studio collects the finished work and dispatches">{cw.emb ? "The studio will collect & dispatch" : "DTF.uk will collect & dispatch"}</span>}
        </div>
      </div>

      {/* status flow strip — collapse a legacy "printing" onto "printed" */}
      <div className="shop-flow">
        {(() => { const flowSt = st === "printing" ? "printed" : st; const at = P_FLOW.indexOf(flowSt);
          return P_FLOW.map((s, i) => {
            const done = at >= i;
            const m = P_STATUS[s];
            return <React.Fragment key={s}>
              {i > 0 && <span className={"shop-flow__bar" + (done ? " is-on" : "")} />}
              <span className={"shop-flow__step" + (done ? " is-on" : "")} style={done ? { color: m.fg } : null}>{statusLabel(s, cw.emb ? "embroidery" : "print")}</span>
            </React.Fragment>;
          });
        })()}
      </div>

      {err && <div className="warn-banner" style={{ margin: "12px 0", color: "#c0392b" }}><Icon name="spark" /> {err}</div>}
      {o.tracking && o.tracking.number && <div className="dim" style={{ marginBottom: 12 }}>Tracking: {o.tracking.url ? <a href={o.tracking.url} target="_blank" rel="noopener">{o.tracking.number}</a> : o.tracking.number}{o.tracking.company ? " · " + o.tracking.company : ""}</div>}
      {window.OrderNotes
        ? <div style={{ marginBottom: 12 }}><OrderNotes note={o.note} viewerSide="shop"
            shopNames={(((DTF && DTF.myShops) || []).find(s => s.slug === slug) || {}).name ? [(((DTF && DTF.myShops) || []).find(s => s.slug === slug) || {}).name] : (DTF.myShops || []).map(s => s.name)}
            postUrl={"/api/shop/orders/" + enc(o.id) + "/note"} onPosted={(n) => setOrder({ ...o, note: n })} /></div>
        : (o.note && <div className="order-note" style={{ marginBottom: 12 }}><Icon name="doc" /> {o.note}</div>)}

      {o.group && o.group.count > 1 &&
        <div className="warn-banner" style={{ marginBottom: 12, background: "var(--mint-wash)", borderColor: MINT, color: "var(--mint-ink)", display: "block" }}>
          <Icon name="link" /> Combined order — {o.group.count} orders ship together in one parcel. {o.id} is the master used for the shipping label; {o.group.members.filter(m => enc(m) !== enc(o.id)).join(", ")} ride along and are dispatched on the same tracking.
        </div>}

      {/* Artwork first — the studio prints from here, so keep it up top. Reuses
          the admin artwork panel (upload / services / download). For a combined
          order, one panel per member order. */}
      {window.ArtworkCard
        ? (o.members && o.members.length > 1
            ? o.members.map(m => <div key={m.id}><div className="shop-art-head mono">Artwork · {m.id}</div><ArtworkCard o={{ id: m.id }} canMark={!!preview} craft={cw.emb ? "embroidery" : "print"} /></div>)
            : <ArtworkCard o={{ id: o.id }} canMark={!!preview} craft={cw.emb ? "embroidery" : "print"} />)
        : null}

      <div className="card">
        <div className="card__head"><Icon name="box" /><h3>Items{o.group && o.group.count > 1 ? " · combined" : ""}</h3>
          {!cw.emb && orderM > 0 && <span className="right" style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
            <span className="kick">{hasPrintSheet ? "Print sheet" : "Material"}</span>
            <span className="m-badge m-badge--solid" title={hasPrintSheet ? "Length of the marked to-print sheet (linear metres)" : "DTF material to RIP (linear metres)"}>{DTF.fmtMetres(orderM)}</span></span>}</div>
        <div className="card__body">
          <table className="shop-items">
            <tbody>
              {(o.lineItems || []).map(li => { const it = shItem(li); const m = DTF.itemMetres(it); return (
                <tr key={(li._from || "") + li.id} className={it.removed ? "lineitem--removed" : ""}>
                  <td style={{ width: 52 }}>{li.image ? <a href={li.image} target="_blank" rel="noopener noreferrer"><img src={li.image} alt="" className="shop-thumb" /></a> : <span className="shop-thumb shop-thumb--ph"><Icon name="image" /></span>}</td>
                  <td>
                    <div className="shop-item__title">{li.title}
                      {it.removed
                        ? <span className="li__removed-tag" style={{ marginLeft: 6 }} title="Refunded or removed from the order">removed</span>
                        : cw.emb ? null
                        : <span className={"kind-tag " + (isUvTitle(li.title) ? "kind-tag--uv" : "kind-tag--dtf")} style={{ marginLeft: 6 }}>{isUvTitle(li.title) ? "UV" : "DTF"}</span>}
                      {li._from && li._from !== o.id && <span className="dim mono" style={{ marginLeft: 6, fontSize: 11 }}>from {li._from}</span>}</div>
                    {li.variantTitle && <div className="dim">{li.variantTitle}</div>}
                    <ItemProps it={it} orderId={li._from || o.id} />
                  </td>
                  <td className="mono" style={{ textAlign: "right", whiteSpace: "nowrap", verticalAlign: "top" }}>{it.removed ? <span className="dim">was × {it.orderedQty}</span> : <>× {it.qty}</>}
                    {!cw.emb && !hasPrintSheet && m > 0 && <div className="li__metres" style={{ marginTop: 4 }}><span className="m-badge m-badge--solid" title="DTF material for this line (linear metres)">{DTF.fmtMetres(m)}</span></div>}
                  </td>
                </tr>
              ); })}
            </tbody>
          </table>
        </div>
      </div>

      {labelOpen && <ShopLabelModal orders={[o]} slug={slug} onClose={() => setLabelOpen(false)} onDone={() => { setLabelOpen(false); load(); }} />}
    </div>
  );
}

/* ---------------- Create-label modal (single or bulk) ---------------- */
const SH_PKG = ["letter", "largeLetter", "smallParcel", "mediumParcel", "largeParcel", "parcel", "documents"];
// The Royal Mail service is fixed by the delivery method the customer paid for
// (chosen on the DTF.uk / admin side) — the shop can't change it, only print it.
// Match the order's shipping method to the closest enabled service.
function pickService(o, services, defaultId) {
  const en = services || [];
  if (!en.length) return null;
  const t = String((o && (o.shippingTitle || o.deliveryMethodType)) || "").toLowerCase();
  const intl = o && o.destinationCountry && o.destinationCountry !== "GB";
  const find = re => en.find(s => re.test(s.name));
  let svc = null;
  if (intl) svc = find(/sign|\bOTA\b/i) || find(/international|tracked|\bMPR\b/i);
  else if (/special delivery|guaranteed|1pm|9am|\bsd\b/.test(t)) svc = find(/special delivery/i);
  else if (/48|2nd|second/.test(t)) svc = find(/tracked 48|\b48\b/i);
  else if (/24|1st|first|next day|tracked|royal mail 24/.test(t)) svc = find(/tracked 24|\b24\b/i);
  return svc || en.find(s => s.id === defaultId) || en[0];
}
function ShopLabelModal({ orders, slug, onClose, onDone }) {
  // How this shop makes labels — set per shop by the admin.
  const carrier = (((DTF && DTF.myShops) || []).find(s => s.slug === slug) || {}).carrier || "royalmail";
  // Northern Ireland orders always go via Shopify Shipping (Royal Mail C&D can't
  // label NI on this account) — even for a Royal Mail shop. When every order in the
  // batch is NI, show the Shopify form.
  const allNI = orders.length > 0 && orders.every(o => o.niLabel);
  const isShopify = carrier === "shopify" || allNI;
  const [services, setServices] = useStateSh([]);
  const [rows, setRows] = useStateSh(() => orders.map(() => ({ weight: "", format: "parcel", serviceCode: "", serviceName: "", hs: "", hsDesc: "", hsValue: "", length: "35", width: "5", height: "5" })));
  const [busy, setBusy] = useStateSh(false);
  const [err, setErr] = useStateSh("");
  const [okMsg, setOkMsg] = useStateSh("");

  useEffectSh(() => {
    if (isShopify) return; // Shopify path needs no Royal Mail service catalogue
    shGet("/api/shop/services").then(d => {
      const en = d.services || [];
      setServices(en);
      // Lock each row's service to the one the admin/customer already chose for
      // that order — the shop only sets weight + parcel size.
      setRows(rs => rs.map((r, i) => {
        const svc = pickService(orders[i], en, d.defaultServiceId);
        return { ...r, serviceCode: svc ? svc.code : "", serviceName: svc ? svc.name : "", format: (svc && svc.format) || r.format || "parcel" };
      }));
    }).catch(() => {});
  }, []);
  const set = (i, patch) => setRows(rs => rs.map((r, j) => j === i ? { ...r, ...patch } : r));
  const applyAll = (patch) => setRows(rs => rs.map(r => ({ ...r, ...patch })));

  async function create() {
    setErr(""); setOkMsg(""); setBusy(true);
    const made = [];
    try {
      for (let i = 0; i < orders.length; i++) {
        const o = orders[i], r = rows[i];
        if (isShopify) {
          // Shopify supplies the package + rate; weight is an optional override.
          const body = {};
          if (Number(r.weight) > 0) body.weight = Number(r.weight);
          await shSend("/api/shop/orders/" + enc(o.id) + "/label", "POST", body);
        } else {
          if (!(Number(r.weight) > 0)) throw new Error(o.id + ": weight required");
          if (!r.serviceCode) throw new Error(o.id + ": pick a service");
          await shSend("/api/shop/orders/" + enc(o.id) + "/label", "POST", {
            weight: Number(r.weight), format: r.format, serviceCode: r.serviceCode,
            hs: r.hs, hsDesc: r.hsDesc, hsValue: r.hsValue,
          });
        }
        made.push(o.id);
      }
      // Open each generated label PDF (cache-bust so a prior 404 is never re-served).
      made.forEach(id => window.open("/api/shop/orders/" + enc(id) + "/label.pdf?t=" + Date.now(), "_blank"));
      setOkMsg(made.length + " label" + (made.length === 1 ? "" : "s") + " created");
      onDone && onDone();
    } catch (e) { setErr(e.message + (made.length ? " (" + made.length + " already created)" : "")); }
    finally { setBusy(false); }
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal modal--wide" onClick={e => e.stopPropagation()}>
        <div className="modal__head"><h3>Create {orders.length === 1 ? "label" : orders.length + " labels"} — {isShopify ? "Shopify Shipping" : "Royal Mail"}</h3><button className="icon-btn" onClick={onClose}><Icon name="x" /></button></div>
        <div className="modal__body">
          <div className="shop-lbl">
            {allNI && carrier !== "shopify" && <div className="warn-banner" style={{ marginBottom: 8, background: "var(--mint-wash)", borderColor: MINT, color: "var(--mint-ink)" }}><Icon name="spark" /> Northern Ireland order — Royal Mail can't label NI on this account, so this uses Shopify Shipping.</div>}
            {isShopify && <div className="dim" style={{ fontSize: 12, marginBottom: 4 }}>Buys a Shopify Shipping label — Shopify picks the package and the delivery service the customer paid for. Weight is optional (uses the items' weights if blank).</div>}
            {orders.length > 1 &&
              <div className="shop-lbl__all">
                <span className="kick">Apply to all</span>
                <input className="shop-lin" type="number" placeholder="weight (g)" onChange={e => applyAll({ weight: e.target.value })} />
                {!isShopify && <select className="shop-lin" defaultValue="parcel" onChange={e => applyAll({ format: e.target.value })}>{SH_PKG.map(f => <option key={f} value={f}>{f}</option>)}</select>}
                <span className="dim" style={{ fontSize: 12 }}>Service is set per order by DTF.uk</span>
              </div>}
            {orders.map((o, i) => {
              const intl = o.destinationCountry && o.destinationCountry !== "GB";
              return (
                <div className="shop-lbl__row" key={o.id}>
                  {orders.length > 1 && <div className="shop-lbl__ord mono">{o.id}{intl && <span className="badge badge--amber" style={{ marginLeft: 6 }}>INTL</span>}</div>}
                  <div className="shop-lbl__grid">
                    <label className="shop-field"><span>Weight (g){isShopify && <span className="dim" style={{ fontWeight: 400, textTransform: "none", letterSpacing: 0 }}> · optional</span>}</span><input className="shop-lin" type="number" placeholder={isShopify ? "auto" : ""} value={rows[i].weight} onChange={e => set(i, { weight: e.target.value })} /></label>
                    {isShopify ? <label className="shop-field" style={{ flex: 2 }}><span>Package &amp; service</span><input className="shop-lin" value="Set by Shopify (customer's chosen delivery)" disabled /></label> : <>
                      <label className="shop-field"><span>Parcel size</span><select className="shop-lin" value={rows[i].format} onChange={e => set(i, { format: e.target.value })}>{SH_PKG.map(f => <option key={f} value={f}>{f}</option>)}</select></label>
                      <label className="shop-field"><span>Service <span className="dim" style={{ fontWeight: 400, textTransform: "none", letterSpacing: 0 }}>· set by DTF.uk</span></span><select className="shop-lin" value={rows[i].serviceCode} disabled title="The delivery service is fixed by the order — chosen on the DTF.uk side">{rows[i].serviceCode ? services.map(s => <option key={s.id} value={s.code}>{s.name}</option>) : <option value="">Loading…</option>}</select></label>
                      {intl && <label className="shop-field"><span>HS code</span><input className="shop-lin" value={rows[i].hs} onChange={e => set(i, { hs: e.target.value })} /></label>}
                      {intl && <label className="shop-field"><span>Value (£)</span><input className="shop-lin" type="number" value={rows[i].hsValue} onChange={e => set(i, { hsValue: e.target.value })} /></label>}
                    </>}
                  </div>
                </div>
              );
            })}
          </div>
          {err && <div className="warn-banner" style={{ marginTop: 12, color: "#c0392b" }}><Icon name="spark" /> {err}</div>}
          {okMsg && <div className="kick" style={{ color: "var(--mint-ink)", marginTop: 12 }}>{okMsg} ✓</div>}
        </div>
        <div className="modal__foot">
          <button className="btn btn--ghost" onClick={onClose}>Close</button>
          <button className="btn btn--primary" disabled={busy} onClick={create}><Icon name="truck" /> {busy ? "Creating…" : "Create + print"}</button>
        </div>
      </div>
    </div>
  );
}

/* ---------------- Admin: hand an order over to a shop ---------------- */
// Rendered in the admin OrderDetail actions. Reads the current assignment and
// lets an admin hand the order to a partner shop (or take it back).
function HandoverControl({ o }) {
  const [shops, setShops] = useStateSh((DTF && DTF.myShops) || []);
  const [slug, setSlug] = useStateSh(undefined); // undefined = loading, "" = none
  const [busy, setBusy] = useStateSh(false);
  const [resent, setResent] = useStateSh("");
  const name = o.id;
  useEffectSh(() => { shGet("/api/orders/" + enc(name) + "/shop").then(j => setSlug(j.shop || "")).catch(() => setSlug("")); }, [name]);
  // Self-load the partner-shop list if it wasn't ready when this rendered — e.g. the
  // order page opened via a direct link / hard-reload before app boot finished
  // fetching it. Without this the hand-over control could silently stay hidden.
  useEffectSh(() => {
    if (shops.length) return;
    shGet("/api/shop/mine").then(j => { const list = (j && j.shops) || []; if (list.length) { setShops(list); DTF.myShops = list; } }).catch(() => {});
  }, []);
  if (!shops.length || slug === undefined) return null;

  async function change(next) {
    setBusy(true); setResent("");
    try {
      const r = next
        ? await shSend("/api/shops/" + next + "/assign", "POST", { names: [name] })
        : await shSend("/api/shops/" + (slug || shops[0].slug) + "/unassign", "POST", { names: [name] });
      setSlug(next);
      // Refresh the global assignment map (whole map is returned) so the orders-list
      // location tag updates immediately — incl. any combined-group siblings.
      if (r && r.orders) DTF.shopAssignments = r.orders;
      // Surface where the hand-over email went (or why it didn't) right after assign.
      if (next && r && r.notify) setResent(notifyMsg(r.notify));
    } catch (e) { /* surfaced via disabled state only */ }
    finally { setBusy(false); }
  }
  // Re-send the job email on demand — for orders assigned before Live mode, or if
  // the shop lost the first one. Ignores the "only on fresh assign" guard.
  async function resend() {
    setBusy(true); setResent("");
    try { setResent(notifyMsg(await shSend("/api/shops/" + slug + "/notify", "POST", { names: [name] }))); }
    catch (e) { setResent("Failed: " + e.message); }
    finally { setBusy(false); }
  }
  return (
    <span className="handover" title="Hand this order to a fulfilment partner">
      <Icon name="box" />
      <select className="rm-in rm-in--sm" value={slug} disabled={busy} onChange={e => change(e.target.value)}>
        <option value="">Not handed over</option>
        {shops.map(s => <option key={s.slug} value={s.slug}>Hand to {s.name}</option>)}
      </select>
      {slug && <a className="linklike" style={{ fontSize: 11 }} href={"#/shop/" + slug + "/" + enc(name)}>view in shop ↗</a>}
      {slug && <button className="linklike" style={{ fontSize: 11 }} disabled={busy} onClick={resend} title="Send the hand-over email to the shop now">resend job email</button>}
      {resent && <span className="dim" style={{ fontSize: 11 }}>{resent}</span>}
    </span>
  );
}
// Human summary of a notifyShopAssignment report for the admin.
function notifyMsg(n) {
  if (!n) return "";
  if (n.skipped === "disabled") return "Job emails are off (enable them to notify the shop)";
  if (n.skipped === "no recipient") return "No shop email set — add one to send";
  if (n.error) return "Failed: " + n.error;
  const bad = (n.results || []).find(r => r && r.error);
  if (bad) return "Failed: " + bad.error;
  if (n.sent) return "Sent to " + (n.to || "shop") + (n.mode === "test" ? " (test mode — went to your inbox)" : "");
  return "Nothing to send";
}
window.HandoverControl = HandoverControl;

function enc(name) { return encodeURIComponent(String(name).replace(/^#/, "")); }

// Group dispatched orders into day buckets (newest first). Each bucket:
// { key, label ("Today"/"Yesterday"/"05 Aug 2026"), rows }. Falls back to the
// order's createdAt for older orders shipped before we tracked a dispatch date.
function groupByDispatch(rows) {
  const dayKey = (iso) => { if (!iso) return "unknown"; try { return new Date(iso).toISOString().slice(0, 10); } catch { return "unknown"; } };
  const buckets = new Map();
  for (const o of rows) {
    const iso = o.dispatchedAt || o.createdAt;
    const k = dayKey(iso);
    if (!buckets.has(k)) buckets.set(k, { key: k, iso, rows: [] });
    buckets.get(k).rows.push(o);
  }
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const label = (k, iso) => {
    if (k === "unknown") return "Dispatch date unknown";
    const d = new Date(k + "T00:00:00");
    const diff = Math.round((today - d) / 86400000);
    if (diff === 0) return "Today";
    if (diff === 1) return "Yesterday";
    return fmtDate(iso);
  };
  return [...buckets.values()]
    .sort((a, b) => (a.key === "unknown" ? 1 : b.key === "unknown" ? -1 : (a.key < b.key ? 1 : -1)))
    .map(b => ({ key: b.key, label: label(b.key, b.iso), rows: b.rows }));
}

function fmtDate(iso) {
  if (!iso) return "—";
  try { const d = new Date(iso); return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" }); } catch { return iso; }
}

// Same-day dispatch cutoff = 13:00 UK time. An order must go out TODAY if it was
// placed before today's 13:00 cutoff — i.e. any earlier day (already overdue for
// same-day), OR today before 13:00. All times in Europe/London so it's correct in
// both BST and GMT.
const CUTOFF_MIN = 13 * 60;
const LONDON_DAY = (dt) => new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/London", year: "numeric", month: "2-digit", day: "2-digit" }).format(dt); // YYYY-MM-DD
function cutoffInfo(iso) {
  if (!iso) return null;
  try {
    const d = new Date(iso);
    const p = new Intl.DateTimeFormat("en-GB", { timeZone: "Europe/London", hourCycle: "h23", hour: "2-digit", minute: "2-digit" }).formatToParts(d);
    const h = (+p.find(x => x.type === "hour").value) % 24;
    const m = +p.find(x => x.type === "minute").value;
    const min = h * 60 + m;
    const orderDay = LONDON_DAY(d), today = LONDON_DAY(new Date());
    const earlierDay = orderDay < today;                       // placed a previous day → overdue for same-day
    const before1pmToday = orderDay === today && min < CUTOFF_MIN;
    return {
      min, hhmm: String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0"),
      earlierDay, before1pmToday,
      dueToday: earlierDay || before1pmToday,                   // must dispatch today
    };
  } catch { return null; }
}

window.ShopApp = ShopApp;
window.ShopView = ShopView;
