<div id="wds-driver-picklist" style="max-width:900px;margin:0 auto;padding:16px;font-family:-apple-system,system-ui,sans-serif;">
  <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">
    <div>
      <div style="font-size:20px;font-weight:700;">WDS Driver Picklist</div>
      <div id="wds-env" style="opacity:.7;font-size:12px;margin-top:4px;"></div>
      <div id="wds-order" style="opacity:.85;font-size:12px;margin-top:4px;"></div>
      <div id="wds-token" style="opacity:.7;font-size:12px;margin-top:4px;"></div>
    </div>
    <div style="border:1px solid #ddd;border-radius:12px;padding:10px 12px;font-size:12px;min-width:220px;">
      <div><strong>Status</strong></div>
      <div id="wds-status" style="opacity:.8;margin-top:4px;"></div>
    </div>
  </div>

  <div id="wds-order-input" style="margin-top:14px;display:none;border:1px solid #ddd;border-radius:12px;padding:12px;">
    <div style="font-weight:700;margin-bottom:8px;">Enter orderId + token</div>
    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
      <input id="wds-orderId" style="padding:10px;border:1px solid #ddd;border-radius:10px;width:360px;max-width:100%;" placeholder="Paste orderId here" />
      <input id="wds-tokenInput" style="padding:10px;border:1px solid #ddd;border-radius:10px;width:360px;max-width:100%;" placeholder="Paste token here" />
      <button id="wds-load" style="padding:10px 12px;border-radius:10px;border:1px solid #111;background:#fff;cursor:pointer;">Load Picklist</button>
    </div>
    <div style="opacity:.7;font-size:12px;margin-top:8px;">
      Tip: open the driver link generated from the client shopping list page. It includes ?orderId=...&token=...
    </div>
  </div>

  <div style="margin-top:14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
    <button id="wds-refresh" style="padding:10px 12px;border-radius:10px;border:1px solid #111;background:#fff;cursor:pointer;">Refresh</button>
    <button id="wds-undo" style="padding:10px 12px;border-radius:10px;border:1px solid #111;background:#fff;cursor:pointer;">Undo Last</button>
    <span style="font-size:12px;opacity:.75;">Store locationId:</span>
    <strong id="wds-loc" style="font-size:12px;"></strong>
  </div>

  <div id="wds-list" style="margin-top:14px;"></div>

  <div id="wds-scanner" style="display:none;border:1px solid #ddd;border-radius:12px;padding:12px;margin-top:14px;">
    <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">
      <div style="font-weight:700;">Scanner <span id="wds-scan-item" style="font-weight:400;opacity:.7;"></span></div>
      <button id="wds-close-scan" style="padding:8px 10px;border-radius:10px;border:1px solid #bbb;background:#fff;cursor:pointer;">Close</button>
    </div>
    <video id="wds-video" playsinline autoplay muted
      style="width:100%;max-height:420px;object-fit:cover;border-radius:12px;border:1px solid #eee;margin-top:10px;">
    </video>
    <div style="opacity:.7;font-size:12px;margin-top:8px;">Point the camera at the barcode. It will auto-submit.</div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/umd/index.min.js"></script>

<script>
(() => {
  // ===== CONFIG =====
  const API_BASE = "https://wds-bsd-api.onrender.com";
  const ACCEPTED_REFRESH_DELAY_MS = 600;
  const SCANNER_VERSION = "1.3";
  const SUB_PRICE_WARN_PCT = 0.25;
  const SUB_MIN_NAME_MATCH_SCORE = 1;
  // ==================

  const envEl = document.getElementById("wds-env");
  const orderEl = document.getElementById("wds-order");
  const tokenEl = document.getElementById("wds-token");
  const statusEl = document.getElementById("wds-status");
  const locEl = document.getElementById("wds-loc");
  const listEl = document.getElementById("wds-list");

  const refreshBtn = document.getElementById("wds-refresh");
  const undoBtn = document.getElementById("wds-undo");

  const orderInputWrap = document.getElementById("wds-order-input");
  const orderIdInput = document.getElementById("wds-orderId");
  const tokenInput = document.getElementById("wds-tokenInput");
  const loadBtn = document.getElementById("wds-load");

  const scannerWrap = document.getElementById("wds-scanner");
  const closeScanBtn = document.getElementById("wds-close-scan");
  const scanItemEl = document.getElementById("wds-scan-item");
  const videoEl = document.getElementById("wds-video");

  envEl.textContent = API_BASE + " • Scanner " + SCANNER_VERSION;

 const esc = s => String(s ?? "").replace(/[&<>"']/g, m => ({({
    "&":"&",
    "<":"<",
    ">":">",
    '"':""",
    "'":"'"
  }[m]));

  const setStatus = msg => statusEl.textContent = msg || "";

  function getParam(name) {
    const u = new URL(window.location.href);
    return u.searchParams.get(name);
  }

  function setUrlParams(orderId, token) {
    const u = new URL(window.location.href);
    if (orderId) u.searchParams.set("orderId", orderId);
    if (token) u.searchParams.set("token", token);
    window.history.replaceState({}, "", u.toString());
  }

  let busy = false;
  let scanLock = false;
  let refreshTimer = null;

  async function fetchWithRetry(url, options = {}, tries = 3) {
    let lastErr;

    for (let i = 0; i < tries; i++) {
      try {
        const r = await fetch(url, options);
        if (!r.ok) throw new Error(await r.text());
        return r;
      } catch (e) {
        lastErr = e;
        await new Promise(res => setTimeout(res, 300 * Math.pow(3, i)));
      }
    }

    throw lastErr;
  }

  async function apiGet(path) {
    const r = await fetchWithRetry(API_BASE + path, { method: "GET" }, 3);
    return r.json();
  }

  async function apiPost(path, body) {
    const r = await fetchWithRetry(API_BASE + path, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body || {})
    }, 3);

    return r.json();
  }

  function digitsOnly(s) {
    return String(s || "").replace(/\D/g, "");
  }

  function upcCheckDigit(d11) {
    let odd = 0;
    let even = 0;

    for (let i = 0; i < 11; i++) {
      const n = parseInt(d11[i], 10);
      if (i % 2 === 0) odd += n;
      else even += n;
    }

    const total = odd * 3 + even;
    return String((10 - (total % 10)) % 10);
  }

  function normalizeSet(code) {
    const d = digitsOnly(code);
    if (!d) return new Set();

    const out = new Set([d]);

    [14, 13, 12, 11].forEach(n => {
      if (d.length > n) out.add(d.slice(-n));
    });

    if (d.length >= 11) {
      out.add(d.slice(0, 11));
      out.add(d.slice(-11));
    }

    if (d.length === 12) {
      out.add("0" + d);
      out.add(d.slice(0, 11));
    }

    if (d.length === 13 && d.startsWith("0")) {
      out.add(d.slice(1));
      out.add(d.slice(1, 12));
    }

    if (d.length === 11) {
      const chk = upcCheckDigit(d);
      const upc12 = d + chk;
      out.add(upc12);
      out.add("0" + upc12);
    }

    return out;
  }

  function codesMatchLocal(scanned, expected) {
    const a = normalizeSet(scanned);
    const b = normalizeSet(expected);
    for (const x of a) if (b.has(x)) return true;
    return false;
  }

  function itemMeta(item) {
    return item?.item_meta_json || item?.item_meta || {};
  }

  function normalizedText(v) {
    return String(v || "")
      .toLowerCase()
      .replace(/[^a-z0-9\s]/g, " ")
      .replace(/\s+/g, " ")
      .trim();
  }

  function hasOrganicText(v) {
    return /\borganic\b/i.test(String(v || ""));
  }

  function organicStateFromItem(item) {
    const meta = itemMeta(item);
    const combined = [
      item?.name,
      item?.brand,
      item?.size,
      meta.brand,
      meta.size,
      meta.notes,
      meta.itemDepartment,
      meta.substitutionNotes
    ].filter(Boolean).join(" ");

    return hasOrganicText(combined) ? "organic" : "not_organic";
  }

  function extractSizeInfo(text) {
    const s = String(text || "").toLowerCase();
    const match = s.match(/(\d+(?:\.\d+)?)\s?(fl oz|oz|ounce|ounces|lb|lbs|pound|pounds|ct|count|pk|pack|gal|gallon|ml|l)\b/);

    if (!match) return null;

    let value = Number(match[1]);
    let unit = match[2];

    if (!Number.isFinite(value)) return null;

    unit = unit
      .replace("ounces", "oz")
      .replace("ounce", "oz")
      .replace("lbs", "lb")
      .replace("pounds", "lb")
      .replace("pound", "lb")
      .replace("count", "ct")
      .replace("pack", "pk")
      .replace("gallon", "gal");

    return { value, unit };
  }

  function sizeCloseEnough(originalSize, substituteSize) {
    if (!originalSize || !substituteSize) {
      return {
        ok: true,
        warning: "Size could not be fully verified."
      };
    }

    if (originalSize.unit !== substituteSize.unit) {
      return {
        ok: false,
        warning: `Size unit differs: original ${originalSize.value} ${originalSize.unit}, substitute ${substituteSize.value} ${substituteSize.unit}.`
      };
    }

    const ratio = substituteSize.value / originalSize.value;

    if (ratio >= 0.80 && ratio <= 1.25) {
      return { ok: true, warning: "" };
    }

    return {
      ok: false,
      warning: `Size differs more than expected: original ${originalSize.value} ${originalSize.unit}, substitute ${substituteSize.value} ${substituteSize.unit}.`
    };
  }

  function productKeywords(name) {
    const stop = new Set([
      "the", "and", "or", "with", "for", "each", "fresh", "kroger",
      "simple", "truth", "private", "selection", "organic", "original",
      "brand", "pack", "ct", "oz", "lb"
    ]);

    return normalizedText(name)
      .split(" ")
      .filter(w => w.length >= 3 && !stop.has(w))
      .slice(0, 8);
  }

  function nameMatchScore(originalName, substituteName) {
    const a = productKeywords(originalName);
    const b = new Set(productKeywords(substituteName));

    return a.reduce((sum, word) => sum + (b.has(word) ? 1 : 0), 0);
  }

  function priceWarning(originalItem, substituteProduct) {
    const meta = itemMeta(originalItem);

    const originalPrice =
      Number(meta.effectivePrice ?? meta.unitPrice ?? meta.regularPrice ?? originalItem.effectivePrice ?? originalItem.unitPrice);

    const substitutePrice =
      Number(substituteProduct?.effectivePrice ?? substituteProduct?.price?.effective ?? substituteProduct?.regularPrice);

    if (!Number.isFinite(originalPrice) || !Number.isFinite(substitutePrice) || originalPrice <= 0) {
      return "";
    }

    const pctDiff = (substitutePrice - originalPrice) / originalPrice;

    if (pctDiff > SUB_PRICE_WARN_PCT) {
      return `Substitute appears more than ${Math.round(SUB_PRICE_WARN_PCT * 100)}% higher in price.`;
    }

    return "";
  }

  function evaluateSubstitution(originalItem, substituteProduct, substituteQty = 1) {
    const meta = itemMeta(originalItem);
    const originalName = originalItem?.name || "";
    const substituteName = substituteProduct?.name || substituteProduct?.description || "Substitute item";

    const warnings = [];
    const approvals = [];

    const score = nameMatchScore(originalName, substituteName);
    if (score < SUB_MIN_NAME_MATCH_SCORE) {
      warnings.push("Substitute may not be the same type of item.");
      approvals.push("type_check");
    }

    const originalOrganic = organicStateFromItem(originalItem);
    const substituteOrganic = hasOrganicText([
      substituteName,
      substituteProduct?.brand,
      substituteProduct?.size,
      Array.isArray(substituteProduct?.categories) ? substituteProduct.categories.join(" ") : ""
    ].filter(Boolean).join(" ")) ? "organic" : "not_organic";

    if (originalOrganic === "organic" && substituteOrganic !== "organic") {
      warnings.push("Original item appears organic, but substitute does not appear organic.");
      approvals.push("organic_downgrade");
    }

    if (originalOrganic !== "organic" && substituteOrganic === "organic") {
      warnings.push("Substitute appears organic while original was not organic. Usually acceptable, but may cost more.");
    }

    const originalSize = extractSizeInfo([originalItem?.size, meta.size, originalName].filter(Boolean).join(" "));
    const substituteSize = extractSizeInfo([substituteProduct?.size, substituteName].filter(Boolean).join(" "));
    const sizeCheck = sizeCloseEnough(originalSize, substituteSize);

    if (sizeCheck.warning) {
      warnings.push(sizeCheck.warning);
      if (!sizeCheck.ok) approvals.push("size_difference");
    }

    const pWarn = priceWarning(originalItem, substituteProduct);
    if (pWarn) {
      warnings.push(pWarn);
      approvals.push("price_increase");
    }

    const originalQty = Math.max(1, parseInt(originalItem?.qty_required || originalItem?.qty || 1, 10));
    const pickedQty = Math.max(1, parseInt(substituteQty || 1, 10));

    if (pickedQty < originalQty) {
      warnings.push(`Substitute quantity ${pickedQty} is less than original requested quantity ${originalQty}.`);
      approvals.push("quantity_short");
    }

    return {
      ok: approvals.length === 0,
      needsApproval: approvals.length > 0,
      warnings,
      approvals,
      score,
      originalOrganic,
      substituteOrganic,
      originalSize,
      substituteSize,
      substituteName
    };
  }

  async function lookupKrogerProductByUpc(upc) {
    const code = digitsOnly(upc);
    const locationIdNow = picklist?.locationId || locEl.textContent || "";

    if (!code || !locationIdNow) return null;

    try {
      const json = await apiGet(`/v1/kroger/products?term=${encodeURIComponent(code)}&locationId=${encodeURIComponent(locationIdNow)}&limit=5`);
      const data = Array.isArray(json?.data) ? json.data : [];
      return data[0] || null;
    } catch (e) {
      return null;
    }
  }

  function buildSubstitutionNotes(originalItem, substituteProduct, scannedCode, evaluation, substituteQty) {
    const warnings = evaluation?.warnings?.length
      ? evaluation.warnings.join(" | ")
      : "No substitution warnings.";

    return [
      "Substitute scanned.",
      `Original: ${originalItem?.name || ""}`,
      `Substitute: ${substituteProduct?.name || substituteProduct?.description || "Unknown item"}`,
      `Substitute UPC: ${scannedCode || ""}`,
      `Substitute Qty: ${substituteQty || 1}`,
      `Validation: ${evaluation?.ok ? "accepted" : "needs review"}`,
      `Warnings: ${warnings}`
    ].join(" ");
  }

  let orderId = getParam("orderId");
  let token = getParam("token");
  let picklist = null;

  const ZXing = window.ZXing;
  const reader = new ZXing.BrowserMultiFormatReader();
  let scanTarget = null;

  function tokenQS() {
    return "token=" + encodeURIComponent(token || "");
  }

  function picklistPath() {
    return `/v1/orders/${encodeURIComponent(orderId)}/picklist?${tokenQS()}&_=${Date.now()}`;
  }

  function requireTokenUI() {
    if (!orderId || !token) {
      orderInputWrap.style.display = "block";
      listEl.innerHTML = '<div style="opacity:.7;">Missing orderId or token. Enter both above.</div>';
      setStatus("");
      return true;
    }

    orderInputWrap.style.display = "none";
    return false;
  }

  async function loadPicklist() {
    if (busy) return;
    if (requireTokenUI()) return;

    busy = true;
    refreshBtn.disabled = true;
    undoBtn.disabled = true;

    orderEl.innerHTML = `<strong>orderId:</strong> ${esc(orderId)}`;
    tokenEl.innerHTML = `<strong>token:</strong> ${esc(token).slice(0, 8)}…`;

    try {
      setStatus("Loading picklist…");
      picklist = await apiGet(picklistPath());
      locEl.textContent = picklist.locationId || "";
      renderPicklist();
      setStatus("");
    } catch (e) {
      setStatus("Error: " + e.message);
      listEl.innerHTML = '<div style="color:#b00;">Failed to load picklist.</div>';
    } finally {
      busy = false;
      refreshBtn.disabled = false;
      undoBtn.disabled = false;
    }
  }

  async function refreshPicklistDirect(statusAfter = "") {
    if (requireTokenUI()) return;

    picklist = await apiGet(picklistPath());
    locEl.textContent = picklist.locationId || "";
    renderPicklist();

    if (statusAfter) setStatus(statusAfter);
  }

  async function saveValidatedSubstitution(originalItem, scannedCode, substituteProduct, substituteQty) {
    const evaluation = evaluateSubstitution(originalItem, substituteProduct || {}, substituteQty);

    const substituteName =
      substituteProduct?.name ||
      substituteProduct?.description ||
      prompt("Substitute item name:", "") ||
      "Substitute item";

    const warningsText = evaluation.warnings.length
      ? evaluation.warnings.join("\n")
      : "No major substitution warnings.";

    const okToSave = confirm(
      `Substitution Check\n\n` +
      `Original: ${originalItem?.name || ""}\n` +
      `Substitute: ${substituteName}\n` +
      `UPC: ${scannedCode || ""}\n` +
      `Qty: ${substituteQty || 1}\n\n` +
      `${warningsText}\n\n` +
      `Save this substitute?`
    );

    if (!okToSave) {
      setStatus("Substitution cancelled.");
      return;
    }

    const notes = buildSubstitutionNotes(
      originalItem,
      { ...substituteProduct, name: substituteName },
      scannedCode,
      evaluation,
      substituteQty
    );

    await apiPost(
      `/v1/orders/${encodeURIComponent(orderId)}/items/${encodeURIComponent(originalItem.order_item_id)}/substitution?${tokenQS()}`,
      {
        unavailable: true,
        substitution_name: substituteName,
        substitution_upc: scannedCode || null,
        substitution_notes: notes
      }
    );

    await refreshPicklistDirect(
      evaluation.ok
        ? "Substitution saved."
        : "Substitution saved with warnings."
    );
  }

  function scheduleAcceptedRefresh() {
    if (refreshTimer) {
      clearTimeout(refreshTimer);
      refreshTimer = null;
    }

    setStatus("Scan accepted. Updating picklist…");

    refreshTimer = setTimeout(async () => {
      try {
        await refreshPicklistDirect("Picklist updated.");
      } catch (e) {
        setStatus("Scan accepted, but refresh failed. Tap Refresh.");
      } finally {
        refreshTimer = null;
      }
    }, ACCEPTED_REFRESH_DELAY_MS);
  }

  function remainingQtyForItem(item) {
    const required = parseInt(item?.qty_required || 1, 10) || 1;
    const picked = parseInt(item?.qty_picked || 0, 10) || 0;
    return Math.max(0, required - picked);
  }

  async function saveShortageOrAlternate(target, pickedQty, remainingQty) {
    const shortageQty = Math.max(0, remainingQty - pickedQty);

    const wantsAlternate = confirm(
      `You picked ${pickedQty} of ${remainingQty}. ` +
      `There are ${shortageQty} unit(s) short.\n\n` +
      `Is an alternate/substitute needed?\n\n` +
      `OK = Add alternate/substitution notes\n` +
      `Cancel = No alternate right now`
    );

    if (!wantsAlternate) {
      await apiPost(
        `/v1/orders/${encodeURIComponent(orderId)}/items/${encodeURIComponent(target.order_item_id)}/substitution?${tokenQS()}`,
        {
          unavailable: false,
          substitution_name: null,
          substitution_upc: null,
          substitution_notes: `Short ${shortageQty} unit(s). Picked ${pickedQty} of ${remainingQty}. No alternate selected yet.`
        }
      );
      return;
    }

    await startSubstituteScanner(target);
  }

  async function confirmPickedQuantity(target) {
    const remaining = Math.max(1, parseInt(target.remaining_qty || 1, 10) || 1);

    if (remaining <= 1) return 1;

    const raw = prompt(
      `${target.name || "This item"} needs ${remaining} unit(s).\n\n` +
      `How many correct units did you pick?`,
      String(remaining)
    );

    if (raw === null) {
      setStatus("Scan cancelled before quantity confirmation.");
      return null;
    }

    let pickedQty = parseInt(raw, 10);

    if (!Number.isFinite(pickedQty) || pickedQty < 1) {
      pickedQty = 1;
    }

    pickedQty = Math.min(pickedQty, remaining);

    if (pickedQty < remaining) {
      const moreAvailable = confirm(
        `You entered ${pickedQty} of ${remaining}.\n\n` +
        `Are the remaining ${remaining - pickedQty} exact unit(s) available to pick later?\n\n` +
        `OK = Yes, leave item open\n` +
        `Cancel = No, request alternate/substitution`
      );

      if (!moreAvailable) {
        await saveShortageOrAlternate(target, pickedQty, remaining);
      }
    }

    return pickedQty;
  }

  function renderPicklist() {
    const items = (picklist && picklist.items) ? picklist.items : [];
    listEl.innerHTML = "";

    if (!items.length) {
      listEl.innerHTML = '<div style="opacity:.7;">No items in this order.</div>';
      return;
    }

    items.forEach(item => {
      const remainingQty = remainingQtyForItem(item);
      const done = remainingQty <= 0;
      const unavailable = !!item.unavailable;
      const meta = itemMeta(item);
      const substitutionsAllowed = meta.allowSubstitutions !== false;

      const row = document.createElement("div");
      row.style.border = "1px solid #ddd";
      row.style.borderRadius = "12px";
      row.style.padding = "12px";
      row.style.marginBottom = "10px";
      row.style.opacity = done ? "0.75" : "1.0";

      row.innerHTML = `
        <div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;">
          <div style="min-width:240px;flex:1;">
            <div style="font-weight:700;">${esc(item.name)}</div>

            <div style="font-size:12px;opacity:.75;margin-top:4px;">
              <div><strong>Picked:</strong> ${esc(item.qty_picked)}/${esc(item.qty_required)} ${done ? "✅" : ""}</div>
              ${remainingQty > 0 ? `<div><strong>Remaining:</strong> ${esc(remainingQty)}</div>` : ``}
              ${item.upc ? `<div><strong>UPC:</strong> ${esc(item.upc)}</div>` : ``}
              ${item.plu ? `<div><strong>PLU:</strong> ${esc(item.plu)}</div>` : ``}
              ${substitutionsAllowed ? `<div><strong>Substitutions:</strong> Allowed</div>` : `<div><strong>Substitutions:</strong> Not allowed</div>`}
              ${unavailable ? `<div style="color:#b00;"><strong>Unavailable / Issue Recorded</strong></div>` : ``}
              ${item.substitution_name ? `<div><strong>Sub:</strong> ${esc(item.substitution_name)}</div>` : ``}
              ${item.substitution_upc ? `<div><strong>Sub UPC:</strong> ${esc(item.substitution_upc)}</div>` : ``}
              ${item.substitution_notes ? `<div><strong>Notes:</strong> ${esc(item.substitution_notes)}</div>` : ``}
            </div>
          </div>

          <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
            ${item.upc && !done ? `<button data-scan style="padding:10px 12px;border-radius:10px;border:1px solid #111;background:#fff;cursor:pointer;">Scan to Confirm</button>` : ``}
            ${item.plu && !done ? `<button data-plu style="padding:10px 12px;border-radius:10px;border:1px solid #bbb;background:#fff;cursor:pointer;">Enter PLU</button>` : ``}
            ${substitutionsAllowed && !done ? `<button data-sub-scan style="padding:10px 12px;border-radius:10px;border:1px solid #444;background:#fff;cursor:pointer;">Scan Substitute</button>` : ``}
            <button data-sub style="padding:10px 12px;border-radius:10px;border:1px solid #bbb;background:#fff;cursor:pointer;">Unavailable / Substitute</button>
          </div>
        </div>
      `;

      const scanBtn = row.querySelector("[data-scan]");
      if (scanBtn) scanBtn.onclick = () => startScanner(item, "barcode");

      const subScanBtn = row.querySelector("[data-sub-scan]");
      if (subScanBtn) subScanBtn.onclick = () => startSubstituteScanner(item);

      const pluBtn = row.querySelector("[data-plu]");
      if (pluBtn) {
        pluBtn.onclick = async () => {
          const val = prompt(`Enter PLU for: ${item.name}`, item.plu || "");
          if (!val) return;

          const qtyToSubmit = await confirmPickedQuantity({
            ...item,
            remaining_qty: remainingQtyForItem(item)
          });

          if (qtyToSubmit === null) return;

          await submitScan(item.order_item_id, val.trim(), "plu", val.trim(), item.upc || "", qtyToSubmit);
        };
      }

      const subBtn = row.querySelector("[data-sub]");
      subBtn.onclick = async () => {
        const meta = itemMeta(item);
        const substitutionsAllowed = meta.allowSubstitutions !== false;

        if (!substitutionsAllowed) {
          const noteOnly = prompt(
            "Customer did not allow substitutions. Add unavailable note:",
            item.substitution_notes || "Customer did not allow substitutions."
          );

          if (noteOnly === null) return;

          try {
            setStatus("Saving unavailable note…");

            await apiPost(
              `/v1/orders/${encodeURIComponent(orderId)}/items/${encodeURIComponent(item.order_item_id)}/substitution?${tokenQS()}`,
              {
                unavailable: true,
                substitution_name: null,
                substitution_upc: null,
                substitution_notes: noteOnly || "Customer did not allow substitutions."
              }
            );

            await refreshPicklistDirect("Unavailable note saved.");
          } catch (e) {
            setStatus("Error: " + e.message);
          }

          return;
        }

        const useSub = confirm(
          "Substitutions are allowed.\n\n" +
          "Did you pick an alternate/substitute item?\n\n" +
          "OK = Enter substitute details\n" +
          "Cancel = Mark unavailable / add note only"
        );

        if (!useSub) {
          const notes = prompt("Unavailable or shortage notes:", item.substitution_notes || "");
          if (notes === null) return;

          try {
            setStatus("Saving note…");

            await apiPost(
              `/v1/orders/${encodeURIComponent(orderId)}/items/${encodeURIComponent(item.order_item_id)}/substitution?${tokenQS()}`,
              {
                unavailable: true,
                substitution_name: null,
                substitution_upc: null,
                substitution_notes: notes || "Item unavailable. Substitute not selected."
              }
            );

            await refreshPicklistDirect("Unavailable note saved.");
          } catch (e) {
            setStatus("Error: " + e.message);
          }

          return;
        }

        const subUpc = prompt("Substitute UPC, if available:", item.substitution_upc || "");
        const subName = prompt("Substitute name:", item.substitution_name || "");
        const subQtyRaw = prompt("Substitute quantity:", String(remainingQtyForItem(item) || 1));

        if (subName === null && subUpc === null) return;

        const subQty = Math.max(1, parseInt(subQtyRaw || "1", 10) || 1);

        let substituteProduct = null;
        if (subUpc) {
          setStatus("Looking up substitute item…");
          substituteProduct = await lookupKrogerProductByUpc(subUpc);
        }

        if (!substituteProduct) {
          substituteProduct = {
            name: subName || "Substitute item",
            upc: subUpc || "",
            size: "",
            brand: ""
          };
        }

        await saveValidatedSubstitution(item, subUpc || "", substituteProduct, subQty);
      };

      listEl.appendChild(row);
    });
  }

  async function submitScan(orderItemId, codeToSend, mode, rawScanned, expectedUpc, qtyToSubmit = 1) {
    if (busy) return;

    busy = true;
    refreshBtn.disabled = true;
    undoBtn.disabled = true;

    try {
      setStatus("Submitting scan…");

      const safeQty = Math.max(1, parseInt(qtyToSubmit || 1, 10));

      const res = await apiPost(`/v1/orders/${encodeURIComponent(orderId)}/scan?${tokenQS()}`, {
        order_item_id: orderItemId,
        scanned_code: codeToSend,
        qty: safeQty,
        mode
      });

      if (!res.accepted) {
        alert(
          "Rejected: " + (res.reason || "not accepted") +
          "\nExpected: " + (expectedUpc || "(none)") +
          "\nScanned:  " + (rawScanned || "(none)") +
          "\nSent:    " + (codeToSend || "(none)")
        );

        await refreshPicklistDirect("Rejected: " + (res.reason || "not accepted"));
        return;
      }

      if (picklist && Array.isArray(picklist.items)) {
        const localItem = picklist.items.find(x => String(x.order_item_id) === String(orderItemId));

        if (localItem) {
          if (typeof res.qty_picked === "number") {
            localItem.qty_picked = res.qty_picked;
          } else {
            const currentPicked = parseInt(localItem.qty_picked || 0, 10) || 0;
            const required = parseInt(localItem.qty_required || 1, 10) || 1;
            localItem.qty_picked = Math.min(required, currentPicked + safeQty);
          }

          renderPicklist();
        }
      }

      scheduleAcceptedRefresh();
    } catch (e) {
      setStatus("Error: " + e.message);
    } finally {
      busy = false;
      refreshBtn.disabled = false;
      undoBtn.disabled = false;
    }
  }

  async function getRearCameraDeviceId() {
    try {
      const devices = await ZXing.BrowserCodeReader.listVideoInputDevices();

      if (!devices || !devices.length) return null;

      const rear = devices.find(d =>
        /back|rear|environment/i.test(d.label || "")
      );

      return (rear || devices[devices.length - 1]).deviceId || null;
    } catch (e) {
      return null;
    }
  }

  async function startSubstituteScanner(item) {
    if (scanLock) return;

    const remaining = remainingQtyForItem(item);
    const substituteQtyRaw = prompt(
      `${item.name || "This item"} needs ${remaining || 1} remaining unit(s).\n\n` +
      `How many substitute units did you pick?`,
      String(remaining || 1)
    );

    if (substituteQtyRaw === null) {
      setStatus("Substitute scan cancelled.");
      return;
    }

    const substituteQty = Math.max(1, parseInt(substituteQtyRaw || "1", 10) || 1);

    scanItemEl.textContent = `— Substitute for ${item.name || "item"}`;
    scannerWrap.style.display = "block";
    videoEl.setAttribute("autoplay", "true");
    videoEl.setAttribute("muted", "true");
    videoEl.setAttribute("playsinline", "true");

    try {
      const rearCameraId = await getRearCameraDeviceId();

      await reader.decodeFromVideoDevice(rearCameraId, videoEl, async (result) => {
        if (!result) return;
        if (scanLock) return;

        scanLock = true;

        const scannedCode = result.getText();
        stopScanner();

        setTimeout(() => {
          scanLock = false;
        }, 1200);

        setStatus("Looking up substitute item…");

        const substituteProduct = await lookupKrogerProductByUpc(scannedCode);

        await saveValidatedSubstitution(
          item,
          scannedCode,
          substituteProduct || {
            name: "",
            upc: scannedCode,
            size: "",
            brand: ""
          },
          substituteQty
        );
      });
    } catch (e) {
      alert("Camera error. Confirm Safari camera permission and that this page is HTTPS.");
      stopScanner();
      scanLock = false;
    }
  }

  async function startScanner(item, mode) {
    if (scanLock) return;

    scanTarget = {
      order_item_id: item.order_item_id,
      mode,
      name: item.name,
      expected_upc: item.upc || "",
      qty_required: item.qty_required || 1,
      qty_picked: item.qty_picked || 0,
      remaining_qty: remainingQtyForItem(item)
    };

    scanItemEl.textContent = `— ${item.name}`;
    scannerWrap.style.display = "block";
    videoEl.setAttribute("autoplay", "true");
    videoEl.setAttribute("muted", "true");
    videoEl.setAttribute("playsinline", "true");

    try {
      const rearCameraId = await getRearCameraDeviceId();

      await reader.decodeFromVideoDevice(rearCameraId, videoEl, async (result) => {
        if (!result) return;
        if (scanLock) return;

        scanLock = true;

        const targetId = scanTarget?.order_item_id;
        const targetMode = scanTarget?.mode;
        const expectedUpc = scanTarget?.expected_upc || "";
        const rawScanned = result.getText();

        stopScanner();

        setTimeout(() => {
          scanLock = false;
        }, 1200);

        if (!targetId || !targetMode) {
          setStatus("Error: scan target missing. Try again.");
          return;
        }

        let codeToSend = rawScanned;
        if (expectedUpc && codesMatchLocal(rawScanned, expectedUpc)) {
          codeToSend = expectedUpc;
        }

        const qtyToSubmit = await confirmPickedQuantity(scanTarget);

        if (qtyToSubmit === null) {
          return;
        }

        await submitScan(targetId, codeToSend, targetMode, rawScanned, expectedUpc, qtyToSubmit);
      });
    } catch (e) {
      alert("Camera error. Confirm Safari camera permission and that this page is HTTPS.");
      stopScanner();
      scanLock = false;
    }
  }

  function stopScanner() {
    try { reader.reset(); } catch {}
    scannerWrap.style.display = "none";
    scanTarget = null;
  }

  async function undoLast() {
    if (busy) return;
    if (requireTokenUI()) return;

    const ok = confirm("Undo the last accepted scan?");
    if (!ok) return;

    busy = true;
    refreshBtn.disabled = true;
    undoBtn.disabled = true;

    try {
      setStatus("Undoing last scan…");
      const res = await apiPost(`/v1/orders/${encodeURIComponent(orderId)}/undo-last?${tokenQS()}`, {});
      await refreshPicklistDirect(res.message || "Undone.");
    } catch (e) {
      setStatus("Error: " + e.message);
    } finally {
      busy = false;
      refreshBtn.disabled = false;
      undoBtn.disabled = false;
    }
  }

  closeScanBtn.onclick = stopScanner;
  refreshBtn.onclick = loadPicklist;
  undoBtn.onclick = undoLast;

  loadBtn.onclick = () => {
    const oid = (orderIdInput.value || "").trim();
    const tok = (tokenInput.value || "").trim();
    if (!oid || !tok) return;

    orderId = oid;
    token = tok;

    setUrlParams(orderId, token);
    loadPicklist();
  };

  loadPicklist();
})();
</script>