/* =============================================================
   CoreIQ Office — Pack levels (build-1 web leg)
   1. 'Pack review' screen: the pack-fact-unconfirmed worklist +
      the uom_review_queue subtab (sweep items needing a human).
   2. window.PackLevelsCard — the product page's Pack levels card
      (levels, SELL/ORDER badges, add-outer editor, conversion entry).
   3. window.PackConversionWizard — the 4-step 'Sell as singles…' modal.
   4. window.PackBarcodesPanel — level-grouped Barcodes tab with CRUD.

   Vocabulary (spec ruling): 'Each', 'Outer of N', 'Carton of N';
   never the bare word 'pack' as a level name; SELL = 'Sold at till as',
   ORDER = 'Ordered from supplier as'; always show the equivalence line.

   No-fake-data: every number here traces to the pack API, the product
   detail payload, or the operator's own input.
   ============================================================= */
const { useState: pkUseState, useEffect: pkUseEffect, useMemo: pkUseMemo } = React;

const pkMoney = (c) => "$" + (Number(c || 0) / 100).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 });

// barcode_type grammar '<symbology>[;units=N]' (spec §Algorithms C). Unknown → units 1.
function pkParseBarcodeType(bt) {
  const s = String(bt || "").trim();
  if (!s) return { symbology: "ean13", units: 1 };
  const parts = s.split(";");
  let units = 1;
  for (let i = 1; i < parts.length; i++) {
    const m = /^units=(\d+)$/.exec(parts[i].trim());
    if (m) units = Math.max(1, parseInt(m[1], 10) || 1);
  }
  return { symbology: (parts[0] || "ean13").trim().toLowerCase(), units };
}
const PK_SYM_LABEL = { ean13: "EAN-13", ean8: "EAN-8", upc: "UPC", itf14: "ITF-14", code128: "Code 128", internal: "Internal" };
const pkSymLabel = (s) => PK_SYM_LABEL[s] || String(s || "").toUpperCase();

// Factor "in words" for the consent dialog (spec: consent shows the factor spelt out).
function pkWords(n) {
  const ones = ["zero","one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve","thirteen","fourteen","fifteen","sixteen","seventeen","eighteen","nineteen"];
  const tens = ["","","twenty","thirty","forty","fifty","sixty","seventy","eighty","ninety"];
  n = Math.trunc(Number(n));
  if (!isFinite(n) || n < 0 || n > 999) return String(n);
  if (n < 20) return ones[n];
  if (n < 100) return tens[Math.floor(n / 10)] + (n % 10 ? "-" + ones[n % 10] : "");
  return ones[Math.floor(n / 100)] + " hundred" + (n % 100 ? " and " + pkWords(n % 100) : "");
}

// Worklist suggestion chip text ('CH2 lists P5' idiom).
function pkSuggestionText(it) {
  if (!it || !it.suggestionUnits || Number(it.suggestionUnits) < 2) return null;
  if (it.suggestionSource === "ch2-price-file") return "CH2 lists P" + it.suggestionUnits;
  if (it.suggestionSource === "invoice-uom") return "Invoice UOM suggests ×" + it.suggestionUnits;
  return "Suggests ×" + it.suggestionUnits;
}

const PK_STATUS = {
  unconfirmed:     { label: "Unconfirmed",             bg: "var(--hold-bg)", fg: "var(--hold-text)" },
  confirmed_base:  { label: "Confirmed · single unit", bg: "var(--paid-bg)", fg: "var(--paid-text)" },
  confirmed_outer: { label: "Confirmed · has outer",   bg: "var(--paid-bg)", fg: "var(--paid-text)" },
  converted:       { label: "Converted",               bg: "var(--brand-bg)", fg: "var(--brand)" },
};
function PkStatusBadge({ status }) {
  const cfg = PK_STATUS[status] || { label: status || "—", bg: "var(--bg-subtle)", fg: "var(--text-subtle)" };
  return <span style={{fontSize:10, fontWeight:700, padding:"2px 8px", borderRadius:10, background:cfg.bg, color:cfg.fg, whiteSpace:"nowrap"}}>{cfg.label}</span>;
}
function PkChip({ children, tone }) {
  const bg = tone === "warn" ? "var(--hold-bg)" : "var(--brand-bg)";
  const fg = tone === "warn" ? "var(--hold-text)" : "var(--brand)";
  return <span style={{display:"inline-flex", alignItems:"center", gap:4, fontSize:"var(--fs-11)", fontWeight:600, padding:"2px 9px", borderRadius:99, background:bg, color:fg, whiteSpace:"nowrap"}}>{children}</span>;
}
function PkBadge({ kind }) {
  // SELL = 'Sold at till as' · ORDER = 'Ordered from supplier as' (fixed vocabulary)
  const sell = kind === "sell";
  return (
    <span title={sell ? "Sold at till as" : "Ordered from supplier as"}
      style={{fontSize:9, fontWeight:700, letterSpacing:"0.05em", padding:"2px 7px", borderRadius:6,
              background: sell ? "var(--paid-bg)" : "var(--navy-100, var(--bg-subtle))",
              color: sell ? "var(--paid-text)" : "var(--text)", whiteSpace:"nowrap"}}>
      {sell ? "SELL" : "ORDER"}
    </span>
  );
}

// =================================================================
// PACK REVIEW — worklist + review-queue screen (LIVE · this pharmacy)
// =================================================================
function PackReviewScreen({ initialTab }) {
  const { setScreen, setScreenState, pushToast } = window.useOffice();
  const [tab, setTab] = pkUseState(initialTab || "worklist");

  const [items, setItems] = pkUseState(null);
  const [wlErr, setWlErr] = pkUseState(null);
  const [queue, setQueue] = pkUseState(null);
  const [quErr, setQuErr] = pkUseState(null);
  const [sweeping, setSweeping] = pkUseState(false);

  pkUseEffect(() => {
    let alive = true;
    window.OfficeAPI.packWorklist()
      .then((rows) => { if (alive) setItems(rows || []); })
      .catch((e) => { if (alive) setWlErr(String((e && e.message) || e)); });
    window.OfficeAPI.packReviewQueue()
      .then((rows) => { if (alive) setQueue(rows || []); })
      .catch((e) => { if (alive) setQuErr(String((e && e.message) || e)); });
    return () => { alive = false; };
  }, []);

  const openProduct = (pid) => {
    setScreenState((st) => ({ ...st, activeProductId: pid, productFrom: "packReview" }));
    setScreen("productDetail");
  };

  const unconfirmed = (items || []).filter((it) => it.status === "unconfirmed").length;
  const openQueue = (queue || []).filter((it) => (it.status || "open") === "open").length;

  function ack(it) {
    const id = it.id;
    window.OfficeAPI.ackReviewItem(id)
      .then((updated) => {
        setQueue((q) => (q || []).map((r) => (r.id === id ? (updated || { ...r, status: "dismissed" }) : r)));
        pushToast({ kind: "paid", icon: "check", title: "Review item acknowledged" });
      })
      .catch((e) => pushToast({ kind: "void", icon: "alert", title: "Couldn't acknowledge", meta: String((e && e.message) || e) }));
  }

  function runSweep() {
    setSweeping(true);
    window.OfficeAPI.packSweep()
      .then((r) => {
        setSweeping(false);
        pushToast({ kind: "paid", icon: "check-circle", title: "Sweep complete",
          meta: `checked ${r.checked} · late ${r.lateFound} · compensated ${r.compensated} · queued ${r.queued}` });
        return window.OfficeAPI.packReviewQueue().then((rows) => setQueue(rows || []));
      })
      .catch((e) => { setSweeping(false); pushToast({ kind: "void", icon: "alert", title: "Sweep failed", meta: String((e && e.message) || e) }); });
  }

  return (
    <div className="off-screen">
      <header className="off-screen-header">
        <div className="off-screen-header-meta">
          <span className="off-screen-eyebrow">Catalogue &amp; stock · live from Aurora</span>
          <h2 className="off-screen-title">Pack review</h2>
        </div>
      </header>

      <div className="off-screen-body">
        <div className="tabset" style={{marginBottom:16}}>
          <button className="tabset-item" aria-selected={tab === "worklist"} onClick={() => setTab("worklist")}>
            Worklist{items != null ? <span style={{marginLeft:6, fontSize:"var(--fs-11)", color:"var(--text-subtle)"}}>{unconfirmed}</span> : null}
          </button>
          <button className="tabset-item" aria-selected={tab === "queue"} onClick={() => setTab("queue")}>
            Review queue{queue != null ? <span style={{marginLeft:6, fontSize:"var(--fs-11)", color:"var(--text-subtle)"}}>{openQueue}</span> : null}
          </button>
        </div>

        {tab === "worklist" && (
          <div className="off-card">
            <div className="off-card-header">
              <div>
                <h3 className="off-card-title">Pack facts to confirm</h3>
                <div className="off-card-meta">
                  {items == null && !wlErr ? "loading…" :
                   wlErr ? "" :
                   `${items.length} product${items.length === 1 ? "" : "s"} · ${unconfirmed} unconfirmed — a human must confirm each structure (price-file UOMs are ambiguous)`}
                </div>
              </div>
            </div>
            {wlErr && (
              <div style={{padding:"12px 4px", fontSize:"var(--fs-13)", color:"var(--void-text)", display:"flex", alignItems:"center", gap:8}}>
                <window.OIcon name="alert"/> Couldn't load the worklist: {wlErr}
              </div>
            )}
            {!wlErr && items != null && items.length === 0 && (
              <div className="off-empty" style={{padding:32}}>
                <div className="empty-icon"><window.OIcon name="check-circle" width={20} height={20}/></div>
                <div className="empty-title">All pack facts confirmed</div>
                <div style={{fontSize:"var(--fs-13)", color:"var(--text-subtle)"}}>Nothing is waiting for review.</div>
              </div>
            )}
            {!wlErr && items != null && items.length > 0 && (
              <div className="off-grid" style={{gridTemplateColumns:"minmax(220px,1.6fr) 130px minmax(150px,1fr) 170px 90px"}}>
                <div className="off-grid-headcell">Product</div>
                <div className="off-grid-headcell">SKU</div>
                <div className="off-grid-headcell">Pack suggestion</div>
                <div className="off-grid-headcell">Status</div>
                <div className="off-grid-headcell"></div>
                {items.map((it) => {
                  const sug = pkSuggestionText(it);
                  return (
                    <div key={it.productId} className="off-grid-row" style={{cursor:"pointer"}} onClick={() => openProduct(it.productId)}>
                      <div className="off-grid-cell off-cell-strong" style={{overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}>{it.name || it.sku}</div>
                      <div className="off-grid-cell off-cell-mono" style={{fontSize:"var(--fs-12)"}}>{it.sku || "—"}</div>
                      <div className="off-grid-cell">{sug ? <PkChip tone="warn">{sug}</PkChip> : <span className="off-muted">—</span>}</div>
                      <div className="off-grid-cell"><PkStatusBadge status={it.status}/></div>
                      <div className="off-grid-cell" style={{justifyContent:"flex-end", color:"var(--brand)", fontSize:"var(--fs-12)", fontWeight:600}}>Open ›</div>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        )}

        {tab === "queue" && (
          <div className="off-card">
            <div className="off-card-header">
              <div>
                <h3 className="off-card-title">Conversion review queue</h3>
                <div className="off-card-meta">movements the compensation sweep refused to guess about — evidence shown, human decides</div>
              </div>
              <button className="btn btn--ghost btn--sm" disabled={sweeping} onClick={runSweep}>
                {sweeping ? <span className="spinner"/> : <window.OIcon name="refresh"/>} {sweeping ? "Sweeping…" : "Run sweep now"}
              </button>
            </div>
            {quErr && (
              <div style={{padding:"12px 4px", fontSize:"var(--fs-13)", color:"var(--void-text)", display:"flex", alignItems:"center", gap:8}}>
                <window.OIcon name="alert"/> Couldn't load the review queue: {quErr}
              </div>
            )}
            {!quErr && queue != null && queue.length === 0 && (
              <div className="off-empty" style={{padding:32}}>
                <div className="empty-icon"><window.OIcon name="check-circle" width={20} height={20}/></div>
                <div className="empty-title">Review queue is empty</div>
                <div style={{fontSize:"var(--fs-13)", color:"var(--text-subtle)"}}>The sweep has nothing that needs a human.</div>
              </div>
            )}
            {!quErr && queue != null && queue.length > 0 && queue.map((it) => {
              const status = it.status || "open";
              const detail = it.detail != null ? (typeof it.detail === "string" ? it.detail : JSON.stringify(it.detail)) : null;
              return (
                <div key={it.id} className="alert-row" style={{cursor:"default", alignItems:"flex-start"}}>
                  <div style={{flex:1, minWidth:0}}>
                    <div style={{display:"flex", alignItems:"center", gap:8, flexWrap:"wrap"}}>
                      <span style={{fontSize:"var(--fs-13)", fontWeight:600}}>{String(it.reason || "").replace(/_/g, " ") || "—"}</span>
                      <span style={{fontSize:10, fontWeight:700, padding:"2px 8px", borderRadius:10,
                        background: status === "open" ? "var(--hold-bg)" : "var(--bg-subtle)",
                        color: status === "open" ? "var(--hold-text)" : "var(--text-subtle)"}}>{status}</span>
                    </div>
                    <div style={{fontSize:"var(--fs-12)", color:"var(--text-subtle)", fontFamily:"var(--font-mono)", marginTop:2, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}>
                      movement {it.movementId || it.movement_id || "—"} · event {it.eventId || it.event_id || "—"} · {String(it.createdAt || it.created_at || "").replace("T", " ").slice(0, 16)}
                    </div>
                    {detail && <div style={{fontSize:"var(--fs-11)", color:"var(--text-subtle)", fontFamily:"var(--font-mono)", marginTop:4, whiteSpace:"pre-wrap", wordBreak:"break-word"}}>{detail}</div>}
                  </div>
                  {status === "open" && (
                    <button className="btn btn--ghost btn--sm" onClick={() => ack(it)}><window.OIcon name="check"/> Ack</button>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

// =================================================================
// PACK LEVELS CARD — lives on the product page (Details tab)
// =================================================================
function PackLevelsCard({ productId, product, barcodes, onChanged }) {
  const { pushToast } = window.useOffice();
  const [pack, setPack] = pkUseState(null);
  const [packErr, setPackErr] = pkUseState(null);
  const [wlItem, setWlItem] = pkUseState(null);
  const [wizardOpen, setWizardOpen] = pkUseState(false);

  // add-outer inline editor
  const [adding, setAdding] = pkUseState(false);
  const [qtyStr, setQtyStr] = pkUseState("");
  const [nameStr, setNameStr] = pkUseState("");
  const [nameTouched, setNameTouched] = pkUseState(false);
  const [orderAt, setOrderAt] = pkUseState("outer"); // ORDER radio: outer (default) | base
  const [saving, setSaving] = pkUseState(false);
  const [saveErr, setSaveErr] = pkUseState(null);

  const reload = () => {
    setPackErr(null);
    return window.OfficeAPI.packProduct(productId)
      .then(setPack)
      .catch((e) => setPackErr(String((e && e.message) || e)));
  };
  pkUseEffect(() => {
    let alive = true;
    setPack(null); setPackErr(null); setWlItem(null);
    window.OfficeAPI.packProduct(productId)
      .then((d) => { if (alive) setPack(d); })
      .catch((e) => { if (alive) setPackErr(String((e && e.message) || e)); });
    window.OfficeAPI.packWorklist()
      .then((rows) => { if (alive) setWlItem((rows || []).find((it) => it.productId === productId) || null); })
      .catch(() => {}); // chip only — absence is honest
    return () => { alive = false; };
  }, [productId]);

  const levels = (pack && pack.levels) || [];
  const events = (pack && pack.events) || [];
  const settings = (pack && pack.settings) || {};
  const converted = settings.status === "converted" || events.length > 0;
  const outer = levels.find((l) => l.seq > 0) || null;
  const base = levels.find((l) => l.seq === 0) || null;
  const qty = parseInt(qtyStr, 10);
  const qtyOk = Number.isInteger(qty) && qty >= 2 && qty <= 10000;
  const autoName = qtyOk ? "Outer of " + qty : "Outer of N";
  const effName = (nameTouched ? nameStr : autoName).trim();

  // Primary (sell-level) code from the synced barcodes on file — the implicit
  // base row's code when no persisted levels exist yet.
  const primaryBc = (barcodes || []).find((b) => b.isPrimary) || (barcodes || []).find((b) => pkParseBarcodeType(b.barcodeType).units === 1) || null;

  const suggestion = pkSuggestionText(wlItem);
  // Factor precedence: the human-CONFIRMED outer beats the price-file
  // suggestion beats the legacy carton_size number.
  const suggestedFactor =
    (outer && Number(outer.qtyOfParent) >= 2 && Number(outer.qtyOfParent)) ||
    (wlItem && Number(wlItem.suggestionUnits) >= 2 && Number(wlItem.suggestionUnits)) ||
    (Number(product && product.cartonSize) >= 2 && Number(product.cartonSize)) || null;

  function saveOuter() {
    setSaveErr(null);
    if (!qtyOk) { setSaveErr("Units per outer must be a whole number between 2 and 10,000."); return; }
    if (!effName || effName.length > 40) { setSaveErr("Level name must be 1–40 characters."); return; }
    if (effName.toLowerCase() === "pack") {
      // Spec vocabulary ruling: the bare word 'pack' is banned as a level name.
      setSaveErr("'Pack' is ambiguous as a level name — call it '" + ("Outer of " + (qtyOk ? qty : "N")) + "' instead.");
      return;
    }
    setSaving(true);
    // BUILD-1 rule (pinned contract): for UNCONVERTED products isSell rides the
    // OUTER — today's truth: the stock on record is counted in outers. The
    // conversion event (never this endpoint) is what moves selling to the base.
    const input = {
      levels: [
        { name: "Each", qtyOfParent: null, isSell: false, isOrder: orderAt === "base" },
        { name: effName, qtyOfParent: qty, isSell: true, isOrder: orderAt === "outer" },
      ],
      confirm: true,
    };
    window.OfficeAPI.packStructure(productId, input)
      .then(() => {
        setSaving(false); setAdding(false); setQtyStr(""); setNameStr(""); setNameTouched(false);
        pushToast({ kind: "paid", icon: "check", title: "Pack structure confirmed", meta: "1 " + effName + " = " + qty + " Each" });
        return reload().then(() => { onChanged && onChanged(); });
      })
      .catch((e) => { setSaving(false); setSaveErr(String((e && e.message) || e)); });
  }

  function confirmBaseOnly() {
    setSaveErr(null);
    setSaving(true);
    window.OfficeAPI.packStructure(productId, { levels: [], confirm: true })
      .then(() => {
        setSaving(false);
        setWlItem((it) => (it ? { ...it, status: "confirmed_base" } : it));
        pushToast({ kind: "paid", icon: "check", title: "Confirmed — sold as single units", meta: "No outer; structure stays factor 1." });
        return reload().then(() => { onChanged && onChanged(); });
      })
      .catch((e) => { setSaving(false); setSaveErr(String((e && e.message) || e)); });
  }

  const levelRow = (name, containsTxt, isSell, isOrder, barcode, key, note) => (
    <div key={key} className="alert-row" style={{cursor:"default"}}>
      <div style={{flex:"0 0 170px", fontSize:"var(--fs-13)", fontWeight:600}}>
        {name}
        {note && <div style={{fontSize:"var(--fs-11)", color:"var(--text-subtle)", fontWeight:400}}>{note}</div>}
      </div>
      <div style={{flex:"0 0 160px", fontSize:"var(--fs-12)", color:"var(--text-subtle)"}}>{containsTxt}</div>
      <div style={{flex:"0 0 110px", display:"flex", gap:5}}>
        {isSell && <PkBadge kind="sell"/>}
        {isOrder && <PkBadge kind="order"/>}
      </div>
      <div style={{flex:1, fontFamily:"var(--font-mono)", fontSize:"var(--fs-12)", color: barcode ? "var(--text)" : "var(--text-subtle)"}}>
        {barcode || "no barcode"}
      </div>
    </div>
  );

  const lastEvent = events.length > 0 ? events[0] : null;
  const convertedSince = lastEvent && lastEvent.executedAt ? String(lastEvent.executedAt).slice(0, 10) : null;

  return (
    <div className="off-card" style={{marginTop:16}}>
      <div className="off-card-header">
        <div>
          <h3 className="off-card-title">Pack levels</h3>
          <div className="off-card-meta">how this product is sold at the till and ordered from the supplier</div>
        </div>
        {suggestion && !converted && <PkChip tone="warn">{suggestion}</PkChip>}
      </div>

      {packErr && (
        <div style={{padding:"10px 4px", fontSize:"var(--fs-13)", color:"var(--void-text)", display:"flex", alignItems:"center", gap:8}}>
          <window.OIcon name="alert"/> Pack levels unavailable: {packErr}
        </div>
      )}
      {!packErr && pack == null && <div style={{padding:"10px 4px", fontSize:"var(--fs-13)", color:"var(--text-subtle)"}}>Loading pack structure…</div>}

      {!packErr && pack != null && (<>
        {levels.length === 0
          ? levelRow("Each", "single unit", true, true, primaryBc ? primaryBc.value : null, "implicit-base", "the unit sold at the till today")
          : levels.map((l) => {
              const parent = l.seq === 0 ? null : (levels.find((x) => x.seq === l.seq - 1) || null);
              return levelRow(
                l.name,
                l.seq === 0 ? "single unit" : "contains " + l.qtyOfParent + " × " + (parent ? parent.name : "Each"),
                !!l.isSell, !!l.isOrder, l.barcode || null, l.id || ("lv" + l.seq), null);
            })}

        {(outer || (adding && qtyOk)) && (
          <div style={{padding:"8px 4px 2px", fontSize:"var(--fs-12)", color:"var(--text-subtle)"}}>
            1 {outer ? outer.name : effName} = {outer ? outer.qtyOfParent : qty} Each
          </div>
        )}

        {/* ---- add-outer inline editor (unconverted products only) ---- */}
        {!converted && !outer && !adding && (
          <div style={{display:"flex", gap:8, paddingTop:10, flexWrap:"wrap"}}>
            <button className="btn btn--ghost btn--sm" onClick={() => { setAdding(true); setQtyStr(suggestedFactor ? String(suggestedFactor) : ""); }}>
              <window.OIcon name="plus"/> Add outer level
            </button>
            {wlItem && wlItem.status === "unconfirmed" && (
              <button className="btn btn--ghost btn--sm" disabled={saving} onClick={confirmBaseOnly}>
                <window.OIcon name="check"/> Confirm — sold as single units (no outer)
              </button>
            )}
          </div>
        )}
        {!converted && adding && (
          <div style={{marginTop:10, padding:"12px 14px", border:"1px dashed var(--border-subtle, var(--line))", borderRadius:10, display:"flex", flexDirection:"column", gap:10}}>
            <div style={{display:"flex", gap:12, flexWrap:"wrap", alignItems:"flex-end"}}>
              <div>
                <label className="field-label" style={{fontSize:"var(--fs-11)"}}>Units inside one outer</label>
                <input className="input" style={{width:110, height:32}} inputMode="numeric" autoFocus value={qtyStr}
                  onChange={(e) => setQtyStr(e.target.value.replace(/[^\d]/g, ""))} placeholder="e.g. 5"/>
              </div>
              <div>
                <label className="field-label" style={{fontSize:"var(--fs-11)"}}>Level name</label>
                <input className="input" style={{width:180, height:32}} value={nameTouched ? nameStr : autoName}
                  onChange={(e) => { setNameTouched(true); setNameStr(e.target.value); }}/>
              </div>
              <div>
                <label className="field-label" style={{fontSize:"var(--fs-11)"}}>Ordered from supplier as</label>
                <div style={{display:"flex", gap:12, height:32, alignItems:"center"}}>
                  <label style={{display:"inline-flex", alignItems:"center", gap:5, fontSize:"var(--fs-13)", cursor:"pointer"}}>
                    <input type="radio" name="pk-order-level" checked={orderAt === "outer"} onChange={() => setOrderAt("outer")}/> {qtyOk ? autoName : "the outer"}
                  </label>
                  <label style={{display:"inline-flex", alignItems:"center", gap:5, fontSize:"var(--fs-13)", cursor:"pointer"}}>
                    <input type="radio" name="pk-order-level" checked={orderAt === "base"} onChange={() => setOrderAt("base")}/> Each
                  </label>
                </div>
              </div>
            </div>
            {qtyOk && <div style={{fontSize:"var(--fs-12)", color:"var(--text-subtle)"}}>1 {effName} = {qty} Each · today the till sells the {effName}; converting to sell singles is a separate, journalled step below.</div>}
            {saveErr && <div style={{fontSize:"var(--fs-12)", color:"var(--void-text)"}}>{saveErr}</div>}
            <div style={{display:"flex", gap:8}}>
              <button className="btn btn--primary btn--sm" disabled={!qtyOk || saving} onClick={saveOuter}>
                {saving ? <span className="spinner"/> : <window.OIcon name="check"/>} Confirm structure
              </button>
              <button className="btn btn--ghost btn--sm" disabled={saving} onClick={() => { setAdding(false); setSaveErr(null); }}>Cancel</button>
            </div>
          </div>
        )}
        {!converted && !adding && saveErr && <div style={{fontSize:"var(--fs-12)", color:"var(--void-text)", paddingTop:6}}>{saveErr}</div>}

        {/* ---- conversion entry point ---- */}
        <div style={{borderTop:"1px solid var(--border-subtle, var(--line))", marginTop:12, paddingTop:12}}>
          {converted ? (
            <div style={{fontSize:"var(--fs-13)", color:"var(--text-subtle)", display:"flex", alignItems:"center", gap:8}}>
              <window.OIcon name="check-circle" style={{color:"var(--paid)"}}/>
              <span>
                Sold as Each{convertedSince ? " since " + convertedSince : ""}
                {outer ? " · ordered as " + outer.name : ""}
              </span>
            </div>
          ) : (outer || suggestedFactor) ? (
            <button className="btn btn--secondary btn--sm" onClick={() => setWizardOpen(true)}>
              <window.OIcon name="box"/> Sell as singles…
            </button>
          ) : (
            <div style={{fontSize:"var(--fs-12)", color:"var(--text-subtle)"}}>
              No outer on record and no pack suggestion — add an outer level above before converting to singles.
            </div>
          )}
        </div>
      </>)}

      {wizardOpen && (
        <PackConversionWizard
          productId={productId}
          product={product}
          barcodes={barcodes}
          outerName={outer ? outer.name : (suggestedFactor ? "Outer of " + suggestedFactor : "Outer")}
          initialFactor={suggestedFactor || ""}
          onClose={() => setWizardOpen(false)}
          onConverted={() => { setWizardOpen(false); reload(); onChanged && onChanged(); }}
        />
      )}
    </div>
  );
}
window.PackLevelsCard = PackLevelsCard;

// =================================================================
// THE WIZARD — 'Sell as singles…' (4 steps + success), sup-modal idiom
// =================================================================
const PK_CHECKS = [
  { code: "unsettled_event",      label: "No prior unsettled conversion of this product" },
  { code: "recent_till_activity", label: "No till stock activity in the last 15 minutes" },
  { code: "negative_stock",       label: "No site holds negative stock" },
  { code: "stored_in_robot",      label: "Not stored in the ROWA robot" },
  { code: "sold_by_weight",       label: "Not sold by weight" },
  { code: "no_cost",              label: "Cost price on file" },
];

function PackConversionWizard({ productId, product, barcodes, outerName, initialFactor, onClose, onConverted }) {
  const { pushToast, activeUser } = window.useOffice();
  const [step, setStep] = pkUseState(1);
  const [factorStr, setFactorStr] = pkUseState(String(initialFactor || ""));
  const [pf, setPf] = pkUseState(null);          // preflight result for the CURRENT factor
  const [pfLoading, setPfLoading] = pkUseState(false);
  const [pfErr, setPfErr] = pkUseState(null);
  const [counts, setCounts] = pkUseState({});     // siteId -> string (operator-typed, starts EMPTY)
  const [sellStr, setSellStr] = pkUseState("");   // dollars text; parses to integer cents
  const [sellTouched, setSellTouched] = pkUseState(false);
  const [armStr, setArmStr] = pkUseState("");     // type-the-factor-to-arm
  const [submitting, setSubmitting] = pkUseState(false);
  const [submitErr, setSubmitErr] = pkUseState(null);
  const [doneEvent, setDoneEvent] = pkUseState(null);

  const factor = parseInt(factorStr, 10);
  const factorOk = Number.isInteger(factor) && factor >= 2 && factor <= 10000;

  // Pre-flight is advisory + read-only: re-run whenever the factor changes.
  pkUseEffect(() => {
    if (!factorOk) { setPf(null); setPfErr(null); return undefined; }
    let alive = true;
    setPfLoading(true); setPfErr(null);
    const t = setTimeout(() => {
      window.OfficeAPI.packPreflight(productId, factor)
        .then((r) => { if (alive) { setPf(r); setPfLoading(false); } })
        .catch((e) => { if (alive) { setPf(null); setPfErr(String((e && e.message) || e)); setPfLoading(false); } });
    }, 350);
    return () => { alive = false; clearTimeout(t); };
  }, [productId, factor, factorOk]);

  const sites = (pf && pf.sites) || [];
  const blockers = (pf && pf.blockers) || [];
  const costPerSingle = sites.length > 0 ? sites[0].costPerSingleCents : null;
  const tiered = sites.length > 0 && sites[0].suggestedSellCents ? sites[0].suggestedSellCents.tiered : null;
  const divide = sites.length > 0 && sites[0].suggestedSellCents ? sites[0].suggestedSellCents.divide : null;

  // D1 (owner ruling): show BOTH ÷N and tiered-.99 suggestions; PRE-FILL with
  // tiered; the human must confirm or edit — never silently committed.
  pkUseEffect(() => {
    if (step === 3 && !sellTouched && tiered != null && sellStr === "") {
      setSellStr((tiered / 100).toFixed(2));
    }
  }, [step, tiered]);

  const sellCents = Math.round((parseFloat(sellStr) || 0) * 100);
  const sellOk = Number.isInteger(sellCents) && sellCents > 0;
  // GST comes from the pre-flight envelope (server truth), never a product-row guess.
  const taxable = !!(pf && pf.taxable);
  const sellEx = taxable ? sellCents / 1.1 : sellCents;
  const gp = sellOk && costPerSingle != null && sellEx > 0 ? ((sellEx - costPerSingle) / sellEx) * 100 : null;

  const countOk = (v) => /^\d+$/.test(String(v || "").trim());
  const allCounted = sites.every((s) => countOk(counts[s.siteId]));

  const executedBy = ((activeUser && (activeUser.first + " " + (activeUser.last || "")).trim()) || "Office user");

  // The consent block — rendered verbatim AND sent verbatim as consentText.
  const consentText = pkUseMemo(() => {
    if (!factorOk || !pf) return "";
    const L = [];
    L.push("Pack conversion — " + ((product && (product.fullName || product.name)) || productId));
    L.push("1 " + outerName + " becomes " + factor + " Each (" + pkWords(factor) + " singles per " + outerName + ").");
    sites.forEach((s) => {
      const c = parseInt(counts[s.siteId], 10);
      L.push((s.siteName || s.siteId) + ": stock on record " + s.onHandOldUnits + " → set to the shelf count of " + c + " Each (expected " + s.expectedSingles + ").");
    });
    if (pf.currentCostCents != null && costPerSingle != null) {
      L.push("Cost: " + pkMoney(pf.currentCostCents) + " per " + outerName + " → " + pkMoney(costPerSingle) + " per Each.");
    }
    L.push("Sell price: " + pkMoney(sellCents) + " per Each" + (gp != null ? " (GP " + gp.toFixed(1) + "%" + (taxable ? ", GST-inclusive price" : "") + ")" : "") + ", entered by " + executedBy + ".");
    const codes = (barcodes || []).map((b) => b.value);
    L.push(codes.length
      ? "Barcode plan: " + codes.join(", ") + " stay attached; each scan rings 1 Each after conversion — do not sell sealed outers until the till update ships."
      : "Barcode plan: no barcodes on file — the product will not scan; add the single's barcode on the Barcodes tab.");
    L.push(pf.hasLabelTemplate
      ? "A new shelf label will be queued for every converted site."
      : "No label template exists yet — print the new shelf label from Print labels after converting.");
    L.push("The counts above are physical shelf counts taken now (count-anchored).");
    return L.join("\n");
  }, [factorOk, pf, counts, sellCents, gp, outerName, factor, executedBy]);

  const armed = armStr.trim() === String(factor);

  function convert() {
    if (!armed || submitting) return;
    setSubmitting(true); setSubmitErr(null);
    window.OfficeAPI.packConvert(productId, {
      factor,
      counts: sites.map((s) => ({ siteId: s.siteId, countedSingles: parseInt(counts[s.siteId], 10) })),
      sellPriceCents: sellCents,
      consentText,
      executedBy,
    })
      .then((event) => {
        setSubmitting(false);
        setDoneEvent(event || {});
        pushToast({ kind: "paid", icon: "check-circle", title: "Converted to singles", meta: "1 " + outerName + " → " + factor + " Each" });
      })
      .catch((e) => { setSubmitting(false); setSubmitErr(String((e && e.message) || e)); });
  }

  const stepTitle = doneEvent ? "Converted" :
    step === 1 ? "Step 1 of 4 · Checks" :
    step === 2 ? "Step 2 of 4 · Count the shelf" :
    step === 3 ? "Step 3 of 4 · Price" : "Step 4 of 4 · Confirm";

  return (
    <div className="sup-modal-overlay" onClick={submitting ? undefined : onClose}>
      <div className="sup-modal" style={{maxWidth:620}} onClick={(e) => e.stopPropagation()}>
        <div className="sup-modal-head">
          <div className="sup-modal-head-ic"><window.OIcon name="box"/></div>
          <div style={{flex:1}}>
            <div className="sup-modal-title">Sell as singles</div>
            <div className="sup-modal-sub">{(product && (product.fullName || product.name)) || productId} · {stepTitle}</div>
          </div>
          <button className="off-topbar-btn" onClick={onClose} disabled={submitting}><window.OIcon name="x"/></button>
        </div>

        <div className="sup-modal-body" style={{maxHeight:"62vh"}}>
          {/* ---------- SUCCESS ---------- */}
          {doneEvent && (<>
            <div style={{display:"flex", alignItems:"center", gap:10, color:"var(--paid-text)", fontSize:"var(--fs-14)", fontWeight:600}}>
              <window.OIcon name="check-circle" width={20} height={20}/> Conversion committed — 1 {outerName} → {factor} Each
            </div>
            {(doneEvent.sites || []).length > 0 && (
              <div className="off-grid" style={{gridTemplateColumns:"1fr 110px 110px 90px"}}>
                <div className="off-grid-headcell">Site</div>
                <div className="off-grid-headcell" style={{justifyContent:"flex-end"}}>Before</div>
                <div className="off-grid-headcell" style={{justifyContent:"flex-end"}}>Counted</div>
                <div className="off-grid-headcell" style={{justifyContent:"flex-end"}}>Drift</div>
                {doneEvent.sites.map((s, i) => (
                  <div key={i} className="off-grid-row">
                    <div className="off-grid-cell off-cell-strong">{s.siteName || s.siteId}</div>
                    <div className="off-grid-cell off-cell-num">{s.beforeQty}</div>
                    <div className="off-grid-cell off-cell-num off-cell-strong">{s.countedSingles}</div>
                    <div className="off-grid-cell off-cell-num off-muted">{s.driftCents != null ? pkMoney(s.driftCents) : "—"}</div>
                  </div>
                ))}
              </div>
            )}
            <div className="sup-modal-hint">{(pf && pf.hasLabelTemplate ? "New shelf labels are queued in Print labels. " : "No label template exists yet — print the new shelf labels from Print labels. ")}Break open all outers, restock as singles and swap the labels now — sealed outers must not be sold until the till update ships.</div>
          </>)}

          {/* ---------- STEP 1 · CHECKS ---------- */}
          {!doneEvent && step === 1 && (<>
            <div>
              <label className="sup-field-label">Singles inside one {outerName}</label>
              <input className="input" style={{width:120}} inputMode="numeric" value={factorStr}
                onChange={(e) => { setFactorStr(e.target.value.replace(/[^\d]/g, "")); setCounts({}); setSellTouched(false); setSellStr(""); setArmStr(""); }}/>
              {!factorOk && factorStr !== "" && <div className="sup-modal-hint" style={{marginTop:4, color:"var(--void-text)"}}>Enter a whole number of 2 or more.</div>}
            </div>
            {!factorOk && <div className="sup-modal-hint">Enter how many sellable singles one {outerName} contains — the checks run against that factor.</div>}
            {factorOk && pfLoading && <div style={{display:"flex", alignItems:"center", gap:8, fontSize:"var(--fs-13)", color:"var(--text-subtle)"}}><span className="spinner"/> Running checks…</div>}
            {factorOk && !pfLoading && pfErr && (
              <div className="sup-alert sup-alert--bad"><window.OIcon name="alert"/> <span>{pfErr}</span></div>
            )}
            {factorOk && !pfLoading && pf && (<>
              <div style={{display:"flex", flexDirection:"column", gap:6}}>
                {PK_CHECKS.map((c) => {
                  const hit = blockers.find((b) => b.code === c.code);
                  return (
                    <div key={c.code} style={{display:"flex", alignItems:"flex-start", gap:8, fontSize:"var(--fs-13)"}}>
                      <window.OIcon name={hit ? "close-circle" : "check-circle"} style={{color: hit ? "var(--void)" : "var(--paid)", flexShrink:0, marginTop:2}}/>
                      <span style={{color: hit ? "var(--void-text)" : "var(--text)"}}>{hit ? hit.message : c.label}</span>
                    </div>
                  );
                })}
                {blockers.filter((b) => !PK_CHECKS.some((c) => c.code === b.code)).map((b, i) => (
                  <div key={"x" + i} style={{display:"flex", alignItems:"flex-start", gap:8, fontSize:"var(--fs-13)"}}>
                    <window.OIcon name="close-circle" style={{color:"var(--void)", flexShrink:0, marginTop:2}}/>
                    <span style={{color:"var(--void-text)"}}>{b.message}</span>
                  </div>
                ))}
              </div>
              <div className="sup-modal-hint">
                Today: cost {pf.currentCostCents != null ? pkMoney(pf.currentCostCents) : "—"} · sell {pf.currentSellCents != null ? pkMoney(pf.currentSellCents) : "—"} per {outerName}.
                {" "}These checks are advisory — every guard re-runs on the server at execution.
              </div>
            </>)}
          </>)}

          {/* ---------- STEP 2 · COUNT ---------- */}
          {!doneEvent && step === 2 && (<>
            <div className="sup-modal-hint">Count the shelf in singles at every site — the ledger is set to YOUR count, not to arithmetic. 1 {outerName} = {factor} Each.</div>
            {sites.length === 0 && <div style={{fontSize:"var(--fs-13)", color:"var(--text-subtle)"}}>No site has stock movement records for this product — there is nothing to count; the conversion still journals per-site.</div>}
            {sites.map((s) => {
              const v = counts[s.siteId] || "";
              const entered = countOk(v) ? parseInt(v, 10) : null;
              const variance = entered != null ? entered - s.expectedSingles : null;
              return (
                <div key={s.siteId} style={{display:"flex", flexDirection:"column", gap:4, padding:"10px 12px", border:"1px solid var(--border-subtle, var(--line))", borderRadius:10}}>
                  <div style={{fontSize:"var(--fs-13)", fontWeight:600}}>{s.siteName || s.siteId}</div>
                  <div style={{fontSize:"var(--fs-12)", color:"var(--text-subtle)"}}>
                    On record: {s.onHandOldUnits} × {outerName} = {s.expectedSingles} Each expected. Count the shelf:
                  </div>
                  <div style={{display:"flex", alignItems:"center", gap:10}}>
                    <input className="input" style={{width:120, height:34}} inputMode="numeric" placeholder="counted singles"
                      value={v} onChange={(e) => setCounts((c) => ({ ...c, [s.siteId]: e.target.value.replace(/[^\d]/g, "") }))}/>
                    <span style={{fontSize:"var(--fs-12)", color:"var(--text-subtle)"}}>expected {s.expectedSingles}</span>
                    {variance != null && variance !== 0 && (
                      <span style={{fontSize:"var(--fs-12)", color:"var(--hold-text)", fontWeight:600}}>
                        variance {variance > 0 ? "+" : ""}{variance} Each — will be recorded as a count variance
                      </span>
                    )}
                  </div>
                </div>
              );
            })}
          </>)}

          {/* ---------- STEP 3 · PRICE ---------- */}
          {!doneEvent && step === 3 && (<>
            <div style={{fontSize:"var(--fs-14)"}}>
              Cost per Each becomes <b>{costPerSingle != null ? pkMoney(costPerSingle) : "—"}</b>
              {pf && pf.currentCostCents != null ? <span className="off-muted"> (was {pkMoney(pf.currentCostCents)} per {outerName})</span> : null}.
            </div>
            <div>
              <label className="sup-field-label">New sell price per Each — your decision, never automatic</label>
              <div style={{display:"flex", gap:8, marginBottom:8, flexWrap:"wrap"}}>
                {divide != null && (
                  <button className="btn btn--ghost btn--sm" onClick={() => { setSellTouched(true); setSellStr((divide / 100).toFixed(2)); }}>
                    ÷{factor} → {pkMoney(divide)}
                  </button>
                )}
                {tiered != null && (
                  <button className="btn btn--ghost btn--sm" onClick={() => { setSellTouched(true); setSellStr((tiered / 100).toFixed(2)); }}>
                    Tiered .99 → {pkMoney(tiered)}
                  </button>
                )}
              </div>
              <div style={{display:"flex", alignItems:"center", gap:8}}>
                <span style={{color:"var(--text-subtle)"}}>$</span>
                <input className="input" style={{width:130, height:36, fontSize:"var(--fs-15)", fontWeight:600}} inputMode="decimal"
                  value={sellStr} onChange={(e) => { setSellTouched(true); setSellStr(e.target.value.replace(/[^\d.]/g, "")); }}/>
                {gp != null && (
                  <span style={{fontSize:"var(--fs-13)", fontWeight:600, color: gp >= 0 ? "var(--paid-text)" : "var(--void-text)"}}>
                    GP {gp.toFixed(1)}%{taxable ? " (GST-incl. price)" : ""}
                  </span>
                )}
              </div>
              {!sellOk && sellStr !== "" && <div className="sup-modal-hint" style={{color:"var(--void-text)", marginTop:4}}>Enter a price above $0.00.</div>}
            </div>
          </>)}

          {/* ---------- STEP 4 · CONFIRM (consent dialog) ---------- */}
          {!doneEvent && step === 4 && (<>
            <div style={{whiteSpace:"pre-wrap", fontSize:"var(--fs-13)", lineHeight:1.6, background:"var(--bg-subtle)", border:"1px solid var(--border-subtle, var(--line))", borderRadius:10, padding:"12px 14px"}}>
              {consentText}
            </div>
            <div>
              <label className="sup-field-label">Type the factor ({factor}) to arm the conversion</label>
              <input className="input" style={{width:120}} inputMode="numeric" value={armStr}
                onChange={(e) => setArmStr(e.target.value.replace(/[^\d]/g, ""))} placeholder={String(factor)}/>
            </div>
            {submitErr && <div className="sup-alert sup-alert--bad"><window.OIcon name="alert"/> <span>{submitErr}</span></div>}
          </>)}
        </div>

        <div className="sup-modal-foot">
          <div className="sup-modal-hint" style={{flex:1}}>
            {doneEvent ? "" : "Org-wide, journalled, forward-only — there is no undo; a regretted split is reversed by a new merge event."}
          </div>
          {doneEvent ? (
            <button className="btn btn--primary" onClick={() => { onConverted && onConverted(); }}>Done</button>
          ) : (<>
            {step > 1 && <button className="btn btn--secondary" disabled={submitting} onClick={() => { setStep(step - 1); setSubmitErr(null); }}>Back</button>}
            {step < 4 && (
              <button className="btn btn--primary"
                disabled={step === 1 ? !(factorOk && pf && pf.ok && blockers.length === 0 && !pfLoading)
                        : step === 2 ? !allCounted
                        : !sellOk}
                onClick={() => setStep(step + 1)}>
                Next
              </button>
            )}
            {step === 4 && (
              <button className="btn btn--primary" disabled={!armed || submitting} onClick={convert}>
                {submitting ? <span className="spinner"/> : <window.OIcon name="check"/>} {submitting ? "Converting…" : "Convert"}
              </button>
            )}
          </>)}
        </div>
      </div>
    </div>
  );
}
window.PackConversionWizard = PackConversionWizard;

// =================================================================
// BARCODES TAB — level-grouped list + add/delete (synced barcodes CRUD)
// =================================================================
function PackBarcodesPanel({ productId, barcodes, onBarcodes, onReload }) {
  const { pushToast } = window.useOffice();
  const [pack, setPack] = pkUseState(null);
  const [adding, setAdding] = pkUseState(false);
  const [value, setValue] = pkUseState("");
  const [isPrimary, setIsPrimary] = pkUseState(false);
  const [busy, setBusy] = pkUseState(false);
  const [err, setErr] = pkUseState(null);

  pkUseEffect(() => {
    let alive = true;
    window.OfficeAPI.packProduct(productId)
      .then((d) => { if (alive) setPack(d); })
      .catch(() => {}); // levels only name the groups below; the list renders without them
    return () => { alive = false; };
  }, [productId]);

  const levels = (pack && pack.levels) || [];
  // The add form attaches at the SELLING unit only (units=1): the till fleet
  // can't resolve outer scans yet, so outer codes are not offered here.

  // Group rows by parsed grammar level.
  const rows = (barcodes || []).map((b) => ({ ...b, parsed: pkParseBarcodeType(b.barcodeType) }));
  const groups = [];
  rows.forEach((r) => {
    let g = groups.find((x) => x.units === r.parsed.units);
    if (!g) { g = { units: r.parsed.units, rows: [] }; groups.push(g); }
    g.rows.push(r);
  });
  groups.sort((a, b) => a.units - b.units);
  const levelName = (u) => {
    if (u === 1) return "Each";
    const lv = levels.find((l) => Number(l.unitsInBase) === u);
    return lv ? lv.name : "Outer of " + u;
  };

  const digits = value.replace(/[^\d]/g, "");
  const valueOk = digits.length >= 6 && digits.length <= 14;

  function submit() {
    if (!valueOk || busy) return;
    setBusy(true); setErr(null);
    // Always the selling unit (units=1) — outer codes wait on the till fleet update.
    window.OfficeAPI.addBarcode(productId, { value: digits, units: 1, isPrimary })
      .then((list) => {
        setBusy(false); setAdding(false); setValue(""); setIsPrimary(false);
        if (list && onBarcodes) onBarcodes(list); else if (onReload) onReload();
        pushToast({ kind: "paid", icon: "barcode", title: "Barcode attached", meta: digits + " · " + levelName(1) });
      })
      .catch((e) => { setBusy(false); setErr(String((e && e.message) || e)); });
  }

  function remove(b) {
    if (!b.id) return;
    if (!window.confirm("Remove barcode " + b.value + "? Tills stop resolving it once the change syncs.")) return;
    window.OfficeAPI.deleteBarcode(productId, b.id)
      .then(() => { onReload && onReload(); pushToast({ kind: "paid", icon: "check", title: "Barcode removed", meta: b.value }); })
      .catch((e) => pushToast({ kind: "void", icon: "alert", title: "Couldn't remove barcode", meta: String((e && e.message) || e) }));
  }

  return (
    <div className="off-card">
      <div className="off-card-header">
        <div>
          <h3 className="off-card-title">Barcodes</h3>
          <div className="off-card-meta">{(barcodes || []).length} on file · grouped by pack level · a value owns exactly one level</div>
        </div>
        <button className="btn btn--ghost btn--sm" onClick={() => { setAdding((v) => !v); setErr(null); }}>
          <window.OIcon name="plus"/> Add barcode
        </button>
      </div>

      {adding && (
        <div style={{marginBottom:12, padding:"12px 14px", border:"1px dashed var(--border-subtle, var(--line))", borderRadius:10, display:"flex", flexDirection:"column", gap:10}}>
          <div style={{display:"flex", gap:12, flexWrap:"wrap", alignItems:"flex-end"}}>
            <div>
              <label className="sup-field-label">Value (scan or type)</label>
              <input className="input" style={{width:200, height:34, fontFamily:"var(--font-mono)"}} autoFocus inputMode="numeric"
                value={value} onChange={(e) => setValue(e.target.value)} placeholder="e.g. 9312345678904"
                onKeyDown={(e) => { if (e.key === "Enter") submit(); }}/>
            </div>
            <div>
              <label className="sup-field-label">Level</label>
              <div className="input" style={{height:34, display:"inline-flex", alignItems:"center", fontSize:"var(--fs-13)", color:"var(--text)"}}>Each — 1 unit per scan</div>
            </div>
            <label style={{display:"inline-flex", alignItems:"center", gap:6, fontSize:"var(--fs-13)", height:34, cursor:"pointer"}}>
              <input type="checkbox" checked={isPrimary} onChange={(e) => setIsPrimary(e.target.checked)}/>
              Primary
            </label>
          </div>
          <div className="sup-modal-hint" style={{color:"var(--text-subtle)"}}>Outer barcodes arrive with the till fleet update.</div>
          {!valueOk && digits.length > 0 && <div className="sup-modal-hint" style={{color:"var(--void-text)"}}>Barcode values are 6–14 digits.</div>}
          {err && <div className="sup-alert sup-alert--bad"><window.OIcon name="alert"/> <span>{err}</span></div>}
          <div style={{display:"flex", gap:8}}>
            <button className="btn btn--primary btn--sm" disabled={!valueOk || busy} onClick={submit}>
              {busy ? <span className="spinner"/> : <window.OIcon name="barcode"/>} Attach
            </button>
            <button className="btn btn--ghost btn--sm" disabled={busy} onClick={() => { setAdding(false); setErr(null); }}>Cancel</button>
          </div>
        </div>
      )}

      {(barcodes || []).length === 0 && !adding && (
        <div style={{padding:"12px 4px", fontSize:"var(--fs-13)", color:"var(--text-subtle)"}}>
          No barcodes on file — the product resolves by SKU only at the till.
        </div>
      )}

      {groups.map((g) => (
        <div key={g.units} style={{marginBottom:6}}>
          <div style={{fontSize:"var(--fs-11)", color:"var(--text-subtle)", textTransform:"uppercase", letterSpacing:"0.06em", fontWeight:600, padding:"8px 0 2px"}}>
            {levelName(g.units)}{g.units > 1 ? " · one scan = " + g.units + " units" : " · one scan = 1 unit"}
          </div>
          {g.rows.map((b, i) => (
            <div key={b.id || i} className="alert-row" style={{cursor:"default"}}>
              <div style={{flex:1, fontFamily:"var(--font-mono)", fontSize:"var(--fs-13)"}}>{b.value}</div>
              <PkChip>{pkSymLabel(b.parsed.symbology)}{b.parsed.units > 1 ? " · outer of " + b.parsed.units : ""}</PkChip>
              {b.isPrimary && <span style={{fontSize:10, fontWeight:700, color:"var(--brand)", marginLeft:8}}>PRIMARY</span>}
              {b.id && (
                <button className="off-topbar-btn" title="Remove barcode" style={{width:26, height:26, marginLeft:8}} onClick={() => remove(b)}>
                  <window.OIcon name="trash" width={13} height={13}/>
                </button>
              )}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}
window.PackBarcodesPanel = PackBarcodesPanel;

// -----------------------------------------------------------------
// Register
// -----------------------------------------------------------------
function PackReviewWorklistScreen() { return <PackReviewScreen initialTab="worklist"/>; }
function PackReviewQueueScreen() { return <PackReviewScreen initialTab="queue"/>; }
window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, {
  packReview: PackReviewWorklistScreen,
  packReviewQueue: PackReviewQueueScreen,
});
