// PartnerAdmin — /partner-admin, the fulfilment queue and the support view.
//
// AUTH IS THE /deep-dives-admin PATTERN, deliberately unchanged: there is no
// client admin login, the operator pastes the shared WEBHOOK_SECRET and every
// request carries it as x-webhook-secret for the server to check. The page is
// unlinked from the nav. It is a proven pattern in this codebase and it
// introduces no new secret to manage.
//
// The honest limit of that: the secret is SHARED, so nothing here can say which
// of the two of you did something. partner_admin_log records what changed and
// when, and its `actor` field is a free-text note rather than an identity — so
// the box asking who you are is a courtesy to your future selves, not auth.
//
// FOUR JOBS, and deliberately no more:
//   1. See which shops are live and which are sitting on an unposted order.
//   2. View any shop's dashboard exactly as they see it, to support them on the
//      phone — plus a diagnostics strip they cannot see.
//   3. Advance an order: printing → posted (with tracking), or cancel it.
//   4. Reveal or rotate a shop's dashboard link.
//
// Adding shop number ten, pausing a shop, editing a name: all still SQL. Building
// UI on a guess about how shops arrive is how it ends up wrong.
(function () {
  const { useState, useCallback } = React;

  const API = "/api/partner";

  async function call(secret, body) {
    const r = await fetch(API, {
      method: "POST",
      headers: { "Content-Type": "application/json", "x-webhook-secret": secret },
      body: JSON.stringify(body),
    });
    let json = null;
    try { json = await r.json(); } catch (e) { /* non-JSON */ }
    if (!r.ok) {
      const err = new Error((json && json.message) || (json && json.error) || "failed");
      err.status = r.status;
      throw err;
    }
    return json;
  }

  const fmt = (iso) => {
    if (!iso) return "—";
    const d = new Date(iso);
    return isNaN(d) ? "—" : d.toLocaleDateString("en-GB", { day: "numeric", month: "short" });
  };

  const gbp = (pence) => "£" + ((pence || 0) / 100).toFixed(2);

  // Hand the operator a file. Blob + <a download> because the SVG comes from the
  // database, not a URL — there is nothing to link to.
  function downloadSvg(name, svg) {
    try {
      const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }));
      const a = document.createElement("a");
      a.href = url; a.download = name;
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 4000);
      return true;
    } catch (e) { return false; }
  }

  const SURFACE_NAME = { card: "counter-card", a: "flyer-A", b: "flyer-B" };

  // The whole kit as one file, so handing artwork to a designer is one click and
  // one folder rather than three downloads to keep track of.
  //
  // Filenames stay ASCII — {CODE}_{surface}.svg and README.txt. The shop's name
  // goes inside the README, where accents round-trip exactly; in a filename they
  // would depend on the recipient's unzip, and the common one on macOS is from
  // 2009 and mangles them. See tools/check_zip.js.
  function downloadKit(shop, code, art) {
    if (!window.BBR_zip) return false;
    const files = art.map(a => ({
      name: `${code}_${SURFACE_NAME[a.surface] || a.surface}.svg`,
      text: a.svg,
    }));

    // A README travels with the files, because the two ways a printed QR fails
    // are both things a designer does to it after we hand it over.
    files.push({ name: "README.txt", text: [
      `${shop} — ambassador kit artwork`,
      `Shop code: ${code}`,
      "",
      "WHAT EACH FILE IS. One QR per printed surface. Each one is a different URL,",
      "so they are not interchangeable:",
      "",
      ...art.map(a => `  ${code}_${SURFACE_NAME[a.surface] || a.surface}.svg   ->   ${a.url}`),
      "",
      "TWO THINGS THAT STOP A PRINTED QR WORKING.",
      "",
      "  1. Do not crop into the white margin. That quiet zone is part of the",
      "     symbol, not padding, and cropping it is the commonest reason a printed",
      "     QR will not scan.",
      "  2. Use the SVG and scale it as vector. A raster QR that looks fine on",
      "     screen turns into soft-edged modules at 300dpi, which is the other",
      "     commonest reason.",
      "",
      "Every symbol here was decoded back after being generated and confirmed to",
      `resolve to ${code}. The URL above each file is what it actually reads.`,
      "",
      `Generated ${new Date().toISOString().slice(0, 10)} · The Lead-In ambassador programme`,
      "",
    ].join("\n") });

    try {
      const blob = window.BBR_zip(files);
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url; a.download = `${code}_ambassador-kit.zip`;
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 4000);
      return true;
    } catch (e) { return false; }
  }

  // What to do about each shop, in one word. The flag is decided in SQL
  // (partner_programme_report) so the table and the totals cannot disagree.
  const FLAG = {
    not_invited: ["not invited", "Send them their dashboard link."],
    no_order:    ["no order", "Invited, but has not asked for a kit."],
    kit_pending: ["kit pending", "Ordered — waiting on us to print and post it."],
    silent:      ["silent", "Kit posted, never scanned. Worth a phone call."],
    scans_only:  ["scans, no signups", "People are scanning but not signing up."],
    working:     ["working", "Scanning and referring."],
  };

  function Stat({ label, value, note }) {
    return (
      <div className="pta-stat">
        <span className="pta-slabel">{label}</span>
        <span className="pta-svalue">{value}</span>
        {note && <span className="pta-snote">{note}</span>}
      </div>
    );
  }

  function Programme({ report }) {
    if (!report) return <p className="pt-quiet">Loading…</p>;
    const t = report.totals || {};
    const o = report.orders || {};
    const sf = report.by_surface || {};
    const tm = report.timing || {};
    const scans = sf.card + sf.flyer;
    const pct = (n, d) => (d ? Math.round((n / d) * 100) + "%" : "—");

    return (
      <>
        <div className="pta-stats">
          <Stat label="Shops" value={t.shops}
                note={`${t.invited} invited · ${t.kits_posted} kits posted`} />
          <Stat label="Live" value={t.live}
                note={`${t.referring} ${t.referring === 1 ? "has" : "have"} referred someone`} />
          <Stat label="Scans" value={t.scans}
                note={scans ? `${pct(sf.card, scans)} from counter cards` : "none yet"} />
          <Stat label="Collectors" value={t.collectors}
                note={`${t.activated} ${t.activated === 1 ? "has" : "have"} added records`} />
          <Stat label="At launch" value={gbp(t.projected_pence)}
                note={`per month, at ${gbp(report.share_pence)} each`} />
        </div>

        {/* The two numbers that turn into an action rather than a chart. */}
        <div className="pta-actions-row">
          {t.silent_shops > 0 && (
            <p className="pta-alert">
              <strong>{t.silent_shops} shop{t.silent_shops === 1 ? "" : "s"} posted a kit and has never
              had a scan.</strong> A shop that never puts the card out looks exactly like a shop with no
              customers — this is the only thing that tells them apart. Worth a call.
            </p>
          )}
          {(o.placed + o.printing) > 0 && (
            <p className="pta-alert">
              <strong>{o.placed + o.printing} kit{(o.placed + o.printing) === 1 ? "" : "s"} waiting to go out.</strong>
              {o.oldest_unposted_at ? ` Oldest ordered ${fmt(o.oldest_unposted_at)}.` : ""}
            </p>
          )}
          {report.unattributed_scans > 0 && (
            <p className="pt-quiet">
              {report.unattributed_scans} scan{report.unattributed_scans === 1 ? "" : "s"} carry no shop
              code at all — pre-reprint flyers still in circulation. They can never be credited to anyone.
            </p>
          )}
          {tm.median_hours_posted_to_first_scan != null && (
            <p className="pt-quiet">
              Median {tm.median_hours_posted_to_first_scan} hours from a kit being posted to its first
              scan, across {tm.n} shop{tm.n === 1 ? "" : "s"}.
            </p>
          )}
        </div>

        <div className="pta-tablewrap">
          <table className="pta-table">
            <thead>
              <tr><th>Code</th><th>Shop</th><th>Standing</th><th>Scans</th>
                  <th>Card / flyer</th><th>Emails</th><th>Collectors</th><th>At launch</th></tr>
            </thead>
            <tbody>
              {(report.shops || []).map(sh => {
                const f = FLAG[sh.flag] || [sh.flag, ""];
                return (
                  <tr key={sh.code}>
                    <td className="mono">{sh.code}</td>
                    <td>{sh.shop}<div className="pt-dim" style={{ fontSize: ".78rem" }}>{sh.town || "—"}</div></td>
                    <td><span className={"pta-flag pta-" + sh.flag} title={f[1]}>{f[0]}</span></td>
                    <td className="mono">{sh.scans}</td>
                    <td className="mono">{sh.scans_card} / {sh.scans_flyer}</td>
                    <td className="mono">{sh.emails}</td>
                    <td className="mono">{sh.collectors}{sh.activated ? ` (${sh.activated})` : ""}</td>
                    <td className="mono">{gbp(sh.projected_pence)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
        <p className="pt-foot">
          Collectors shows the number in brackets who have actually added records — the figure that
          predicts subscribing. “At launch” is an illustration at today's {gbp(report.share_pence)}
          {" "}per subscriber, not money owed.
        </p>
      </>
    );
  }

  // The copy editor.
  //
  // One textarea per named paragraph, not one for the whole HTML: the structure
  // that holds these emails together is not editable, because one bad paste would
  // break an email that goes to a shop once and cannot be unsent.
  //
  // Preview renders through the SAME builder the send uses, with the unsaved edits
  // applied — so what you approve is what goes out.
  function Emails({ data, onSave, onPreview, busy }) {
    const [which, setWhich] = useState(0);
    const [draft, setDraft] = useState(null);
    const [preview, setPreview] = useState(null);
    const [err, setErr] = useState("");

    if (!data) return <p className="pt-quiet">Loading…</p>;
    const tpl = data.templates[which];
    const edits = (draft && draft.key === tpl.key) ? draft.copy : tpl.copy;

    const set = (k, v) => {
      setDraft({ key: tpl.key, copy: { ...edits, [k]: v } });
      setPreview(null); setErr("");
    };

    const val = (f) => (edits[f.key] != null ? edits[f.key] : "");
    const overridden = (f) => !!(edits[f.key] && edits[f.key].trim() && edits[f.key].trim() !== f.default);

    return (
      <>
        <div className="pta-emailtabs">
          {data.templates.map((t, i) => (
            <button key={t.key} className={"pta-etab" + (i === which ? " on" : "")}
                    onClick={() => { setWhich(i); setDraft(null); setPreview(null); setErr(""); }}>
              {t.name}
            </button>
          ))}
        </div>
        <p className="pt-quiet">
          {tpl.note}
          {tpl.updated_at && <> · last edited {fmt(tpl.updated_at)}{tpl.updated_by ? ` by ${tpl.updated_by}` : ""}</>}
        </p>

        <div className="pta-vars">
          <span className="pt-dim">Merge fields you can use:</span>
          {Object.keys(data.vars).map(v => (
            <code key={v} title={data.vars[v]}>{"{" + v + "}"}</code>
          ))}
        </div>

        {tpl.fields.map(f => (
          <label className="pta-copyfield" key={f.key}>
            <span className="pta-copylabel">
              {f.label}
              {overridden(f) && <em className="pta-edited">edited</em>}
              {f.must.length > 0 && <em className="pta-must">must keep {f.must.map(m => "{" + m + "}").join(" ")}</em>}
            </span>
            {f.help && <span className="pta-copyhelp">{f.help}</span>}
            <textarea rows={f.lines} value={val(f)} spellCheck="true"
                      placeholder={f.default}
                      onChange={e => set(f.key, e.target.value)} />
            {overridden(f) && (
              <button className="pt-link" onClick={() => set(f.key, "")}>
                revert to the shipped wording
              </button>
            )}
          </label>
        ))}

        {err && <p className="pt-err">{err}</p>}

        <div className="pta-copyactions">
          <button className="pt-cta pt-ghost" disabled={busy}
                  onClick={async () => {
                    setErr("");
                    const out = await onPreview(tpl.key, edits);
                    if (out.error) setErr(out.message || out.error);
                    else setPreview(out);
                  }}>
            Preview
          </button>
          <button className="pt-cta" disabled={busy}
                  onClick={async () => {
                    setErr("");
                    const out = await onSave(tpl.key, edits);
                    if (out.error) setErr(out.message || out.error);
                    else { setDraft(null); setPreview(null); }
                  }}>
            Save wording
          </button>
        </div>
        <p className="pt-quiet">
          A box left empty uses the wording shipped in the code — that is how you undo an edit.
          Saving changes what the next email says; it does not resend anything.
        </p>

        {preview && (
          <div className="pta-preview">
            <p className="pt-eyebrow">Preview · subject</p>
            <p className="pta-subject">{preview.subject}</p>
            <p className="pt-eyebrow">Body, as the shop receives it</p>
            {/* srcDoc, not innerHTML: an email body is a full document with its
                own styles, and letting it into this page's DOM would let it
                restyle the admin around it. */}
            <iframe title="Email preview" className="pta-frame" srcDoc={preview.html} />
          </div>
        )}
      </>
    );
  }

  function Pill({ status }) {
    const label = status || "no order";
    return <span className={"pta-pill " + (status || "none")}>{label}</span>;
  }

  function PartnerAdmin({ onHome }) {
    const [secret, setSecret] = useState("");
    const [authed, setAuthed] = useState(false);
    const [busy, setBusy] = useState(false);
    const [msg, setMsg] = useState("");
    const [shops, setShops] = useState([]);
    const [orders, setOrders] = useState([]);
    const [actor, setActor] = useState("");
    const [revealed, setRevealed] = useState({});
    const [viewing, setViewing] = useState(null);   // { data, diagnostics, code }
    const [tab, setTab] = useState("queue");       // queue | programme
    const [report, setReport] = useState(null);
    const [emails, setEmails] = useState(null);
    const [art, setArt] = useState({});            // code -> { art:[], command }

    const refresh = useCallback(async (sec) => {
      const out = await call(sec || secret, { action: "admin-shops" });
      setShops(out.shops || []);
      setOrders(out.orders || []);
      setAuthed(true);
    }, [secret]);

    const run = async (fn, okMsg) => {
      setBusy(true); setMsg("");
      try {
        await fn();
        if (okMsg) setMsg(okMsg);
      } catch (e) {
        setMsg(e.status === 401 ? "That secret isn't right." : (e.message || "Failed."));
      } finally {
        setBusy(false);
      }
    };

    // ---- gate ------------------------------------------------------------
    if (!authed) {
      return (
        <div className="pta-wrap" style={{ maxWidth: 460, margin: "14vh auto" }}>
          <h1 className="pt-h1" style={{ fontSize: 26 }}>Ambassador kits</h1>
          <p className="pt-quiet">
            Enter the shared secret to see the fulfilment queue and support a shop.
          </p>
          <input type="password" placeholder="WEBHOOK_SECRET" value={secret}
                 onChange={e => setSecret(e.target.value)}
                 onKeyDown={e => { if (e.key === "Enter") run(() => refresh(secret)); }}
                 style={{ width: "100%", padding: "10px 12px", marginBottom: 12, font: "inherit" }} />
          <button className="pt-cta" disabled={busy || !secret}
                  onClick={() => run(() => refresh(secret))}>
            {busy ? "Checking…" : "Unlock"}
          </button>
          {msg && <p className="pt-err">{msg}</p>}
        </div>
      );
    }

    // ---- support view ----------------------------------------------------
    //
    // The shop's own dashboard opens in a NEW TAB; only the diagnostics render
    // here. It used to be embedded in this page, which had two faults:
    //
    //   * The shop page carries the site nav and footer. One click on "The List"
    //     or "My Collection" navigated the SPA away from /partner-admin, and the
    //     admin secret lives only in React state — so looking at a shop logged
    //     you out of the queue you were working through.
    //   * It offered the shop's own action buttons ("Order more flyers",
    //     "Change this") inside a panel labelled read-only. They could only fail:
    //     preview mode has no token, so each one would 401.
    //
    // Opening the real page in its own tab fixes both, and is a better support
    // view anyway — it is exactly what the shopkeeper is looking at, live, rather
    // than a copy of it.
    if (viewing) {
      const d = viewing.diagnostics || {};
      const v = viewing.data;
      return (
        <div className="pta-wrap">
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 12 }}>
            <h1 className="pt-h1" style={{ fontSize: 26, marginBottom: 4 }}>
              {v.shop.name} <span className="pt-dim" style={{ fontSize: ".6em" }}>{v.shop.code}</span>
            </h1>
            <button className="pt-link" onClick={() => setViewing(null)}>← back to the queue</button>
          </div>

          {viewing.opened === false && (
            <p className="pta-alert">
              Your browser blocked the new tab. Open their dashboard here instead:{" "}
              <code className="pta-reveal" style={{ display: "block", marginTop: 6 }}>{viewing.link}</code>
            </p>
          )}
          {viewing.opened && (
            <p className="pt-quiet">
              Their dashboard is open in another tab — that's the live page, exactly as they see it.
              This one stays logged in.
            </p>
          )}

          <div className="pta-stats">
            <Stat label="State" value={v.state} note="what their page is showing" />
            <Stat label="Scans" value={v.funnel.scans}
                  note={`${v.funnel.scans_card} card · ${v.funnel.scans_flyer} flyer`} />
            <Stat label="Collectors" value={v.funnel.collectors}
                  note={`${v.funnel.activated} added records`} />
            <Stat label="At launch" value={gbp(v.money.projected_pence)} note="illustration" />
          </div>

          <div className="pt-panel">
            <p className="pt-eyebrow">Diagnostics — the shop does not see this</p>
            <ul style={{ margin: 0, paddingLeft: "1.2em", fontSize: ".88rem", lineHeight: 1.65 }}>
              <li>Address confirmed: <strong>{v.address ? fmt(v.address.confirmed_at) : "never"}</strong></li>
              <li>Orders on record: <strong>{(v.orders || []).length}</strong>
                {v.order ? <> · latest is <strong>{v.order.status}</strong></> : null}</li>
              <li>Reorder: <strong>{v.reorder.allowed ? "available" : v.reorder.reason}</strong></li>
              <li>
                Scans site-wide with no shop code: <strong>{d.scans_with_no_shop_anywhere}</strong>
                {d.scans_with_no_shop_anywhere > 0 && (
                  <span className="pt-dim"> — pre-reprint flyers still out there, creditable to nobody</span>
                )}
              </li>
              {(d.admin_log || []).length > 0 && (
                <li>
                  Last change by us: <strong>{d.admin_log[0].action}</strong> on {fmt(d.admin_log[0].at)}
                  {d.admin_log[0].actor ? ` (${d.admin_log[0].actor})` : ""}
                </li>
              )}
            </ul>
          </div>

          <p className="pt-foot">
            The tab that just opened holds a live credential in its address bar. Close it when
            you're done, and don't leave it up on a shared screen.
          </p>
        </div>
      );
    }

    // ---- the queue -------------------------------------------------------
    const orderFor = (code) => orders.find(o => o.shop_code === code && o.status !== "cancelled") || null;
    const pending = orders.filter(o => o.status === "placed" || o.status === "printing");

    return (
      <div className="pta-wrap">
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 12 }}>
          <h1 className="pt-h1" style={{ fontSize: 26, marginBottom: 4 }}>Ambassador kits</h1>
          <button className="pt-link" onClick={onHome}>← site</button>
        </div>
        <div className="pta-tabs" role="tablist">
          <button role="tab" aria-selected={tab === "queue"}
                  className={"pta-tab" + (tab === "queue" ? " on" : "")}
                  onClick={() => setTab("queue")}>Queue</button>
          <button role="tab" aria-selected={tab === "emails"}
                  className={"pta-tab" + (tab === "emails" ? " on" : "")}
                  onClick={() => { setTab("emails"); if (!emails) run(async () => {
                    const out = await call(secret, { action: "admin-copy", copyAction: "get" });
                    setEmails(out);
                  }); }}>Emails</button>
          <button role="tab" aria-selected={tab === "programme"}
                  className={"pta-tab" + (tab === "programme" ? " on" : "")}
                  onClick={() => { setTab("programme"); if (!report) run(async () => {
                    const out = await call(secret, { action: "admin-report" });
                    setReport(out.report);
                  }); }}>Programme</button>
        </div>

        {msg && <p className="pt-flash">{msg}</p>}

        {tab === "emails" ? (
          <Emails
            data={emails}
            busy={busy}
            onPreview={async (template, copy) => {
              try {
                return await call(secret, { action: "admin-copy", copyAction: "preview", template, copy });
              } catch (e) { return { error: "failed", message: e.message }; }
            }}
            onSave={async (template, copy) => {
              try {
                const out = await call(secret, { action: "admin-copy", copyAction: "save", template, copy, actor });
                const fresh = await call(secret, { action: "admin-copy", copyAction: "get" });
                setEmails(fresh);
                setMsg(`Saved — ${out.saved} field${out.saved === 1 ? "" : "s"} overridden.`);
                return out;
              } catch (e) { return { error: "failed", message: e.message }; }
            }}
          />
        ) : tab === "programme" ? <Programme report={report} /> : (<>

        <p className="pt-quiet">
          {shops.length} shops · {shops.filter(x => x.status === "new").length} not yet invited ·{" "}
          {pending.length} order{pending.length === 1 ? "" : "s"} waiting to go out.
          {" "}Marking an order posted is what advances the shop's own tracker, so do it when the parcel
          actually leaves.
        </p>

        <label className="pt-field" style={{ maxWidth: 260, marginBottom: 20 }}>
          <span>Who's doing this — for the log</span>
          <input value={actor} onChange={e => setActor(e.target.value)} placeholder="ric" />
        </label>

        <div className="pta-tablewrap">
          <table className="pta-table">
            <thead>
              <tr>
                <th>Code</th><th>Shop</th><th>Order</th><th>Post to</th>
                <th>Scans</th><th>Placed</th><th></th>
              </tr>
            </thead>
            <tbody>
              {shops.map(s => {
                const o = orderFor(s.code);
                return (
                  <tr key={s.code}>
                    <td className="mono">{s.code}</td>
                    <td>
                      {s.shop_name}
                      <div className="pt-dim" style={{ fontSize: ".8rem" }}>
                        {s.email}
                        {s.status === "new" && <span style={{ color: "var(--accent)" }}> · not invited yet</span>}
                      </div>
                      {revealed[s.code] && <div className="pta-reveal">{revealed[s.code]}</div>}
                      {art[s.code] && (
                        art[s.code].art.length ? (
                          <div className="pta-art">
                            <button className="pt-cta pta-kitzip"
                                    onClick={() => {
                                      if (!downloadKit(s.shop_name, s.code, art[s.code].art)) {
                                        setMsg("Could not build the zip in this browser — the individual SVGs below still work.");
                                      }
                                    }}>
                              Download the kit ({art[s.code].art.length} SVGs + README)
                            </button>
                            {art[s.code].art.map(a => (
                              <div className="pta-artrow" key={a.surface}>
                                <button className="pt-link"
                                        onClick={() => downloadSvg(`${s.code}_${SURFACE_NAME[a.surface] || a.surface}.svg`, a.svg)}>
                                  {SURFACE_NAME[a.surface] || a.surface}.svg
                                </button>
                                <code>{a.url}</code>
                                <span className="pt-dim">v{a.qr_version} · {a.modules}² · verified {a.verified_by}</span>
                              </div>
                            ))}
                          </div>
                        ) : (
                          <div className="pta-art">
                            <span className="pt-dim">Couldn't generate artwork for this shop — check the code format.</span>
                          </div>
                        )
                      )}
                    </td>
                    <td>
                      <Pill status={o && o.status} />
                      {o && (
                        <div className="pt-dim" style={{ fontSize: ".78rem", marginTop: 4 }}>
                          {o.qty_card}c · {o.qty_flyer_a}A · {o.qty_flyer_b}B
                          {o.kind === "reorder" ? " · reorder" : ""}
                        </div>
                      )}
                    </td>
                    <td style={{ fontSize: ".82rem", lineHeight: 1.5 }}>
                      {o
                        ? [o.ship_name, o.ship_line1, o.ship_line2, o.ship_town, o.ship_postcode]
                            .filter(Boolean).join(", ")
                        : <span className="pt-dim">no address yet</span>}
                      {o && o.note && (
                        <div style={{ color: "var(--accent)", marginTop: 4 }}>“{o.note}”</div>
                      )}
                    </td>
                    <td className="mono">{s.scans}</td>
                    <td className="mono" style={{ whiteSpace: "nowrap" }}>{o ? fmt(o.placed_at) : "—"}</td>
                    <td>
                      <div className="pta-rowacts">
                        <button className="pt-link" disabled={busy}
                                onClick={() => {
                                  // Opened SYNCHRONOUSLY, before any await. A
                                  // window.open() after an async gap has lost its
                                  // user-activation and browsers block it — which
                                  // would look like the button doing nothing.
                                  const win = window.open("", "_blank");
                                  run(async () => {
                                    const [view, link] = await Promise.all([
                                      call(secret, { action: "admin-view", code: s.code }),
                                      call(secret, { action: "admin-link", code: s.code }),
                                    ]);
                                    if (win) { try { win.location = link.link; } catch (e) { /* blocked */ } }
                                    setViewing({
                                      code: s.code, data: view.data, diagnostics: view.diagnostics,
                                      link: link.link, opened: !!win,
                                    });
                                  });
                                }}>
                          view
                        </button>
                        {o && o.status === "placed" && (
                          <button className="pt-link" disabled={busy}
                                  onClick={() => run(async () => {
                                    await call(secret, { action: "admin-order", orderId: o.id, status: "printing", actor });
                                    await refresh();
                                  }, `${s.code} marked at the printer.`)}>
                            printing
                          </button>
                        )}
                        {/* Available whether or not the shop ordered through the
                            dashboard. It used to require an order id, so with
                            nobody having ordered yet the button appeared nowhere
                            — and a kit sent proactively could not be recorded at
                            all. The server creates the order when there isn't
                            one, and refuses if we have no address to have posted
                            it to. Marking posted also emails the shop. */}
                        {(!o || o.status === "placed" || o.status === "printing") && (
                          <button className="pt-link" disabled={busy}
                                  onClick={() => run(async () => {
                                    if (!o && !window.confirm(
                                      `${s.code} has no order on file.\n\nMark a kit posted anyway? ` +
                                      `We'll record it against their address and email them to look out for it.`
                                    )) { setMsg("Nothing recorded."); return; }
                                    const tracking = window.prompt(`Tracking number for ${s.code}? (optional)`) || "";
                                    const out = await call(secret, {
                                      action: "admin-order",
                                      ...(o ? { orderId: o.id } : { code: s.code }),
                                      status: "posted", tracking, actor,
                                    });
                                    await refresh();
                                    setMsg(out.notified
                                      ? `${s.code} marked posted — their tracker has moved and they've been emailed.`
                                      : `${s.code} marked posted, but the email did NOT go out. Tell them it's coming.`);
                                  })}>
                            posted
                          </button>
                        )}
                        <button className="pt-link" disabled={busy}
                                onClick={() => run(async () => {
                                  const out = await call(secret, { action: "admin-link", code: s.code });
                                  setRevealed(r => ({ ...r, [s.code]: out.link }));
                                })}>
                          link
                        </button>
                        {/* Serves what tools/make_kit_qr.py has already decoded
                            back and proven. Nothing is generated here: a symbol
                            nothing has verified should never reach a printer. */}
                        <button className="pt-link" disabled={busy}
                                onClick={() => run(async () => {
                                  const out = await call(secret, { action: "admin-art", code: s.code });
                                  setArt(a => ({ ...a, [s.code]: out }));
                                  if (out.made && out.made.length) {
                                    setMsg(`Generated ${out.made.length} QR code${out.made.length === 1 ? "" : "s"} for ${s.code}.`);
                                  }
                                })}>
                          QR
                        </button>
                        {/* The launch email. TWO steps on purpose: the first is a
                            dry run that sends nothing, the second is irreversible.
                            An email handing over a live credential to a shop that
                            has heard nothing for a fortnight is not a thing to fire
                            off a single mis-click. */}
                        <button className="pt-link" disabled={busy}
                                onClick={() => run(async () => {
                                  const dry = await call(secret, { action: "admin-invite", code: s.code });
                                  const go = window.confirm(
                                    `Send the welcome email?\n\n` +
                                    `To:      ${dry.to}\n` +
                                    `Subject: ${dry.subject}\n\n` +
                                    `It contains ${s.code}'s live dashboard link. This cannot be unsent.`
                                  );
                                  if (!go) { setMsg(`Nothing sent — ${s.code} left alone.`); return; }
                                  await call(secret, { action: "admin-invite", code: s.code, confirm: true, actor });
                                  await refresh();
                                }, `Welcome email sent to ${s.shop_name}.`)}>
                          {s.status === "new" ? "invite" : "re-invite"}
                        </button>
                        <button className="pt-link" disabled={busy}
                                onClick={() => run(async () => {
                                  if (!window.confirm(
                                    `Rotate ${s.code}'s link?\n\nTheir current link stops working immediately and you'll need to email them the new one.`
                                  )) return;
                                  const out = await call(secret, { action: "admin-rotate", code: s.code, actor });
                                  setRevealed(r => ({ ...r, [s.code]: out.link }));
                                }, `${s.code} rotated — email them the new link.`)}>
                          rotate
                        </button>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        <p className="pt-foot">
          A revealed link is a live credential — it is the shop's whole login. Don't paste one into
          Slack or leave this page on a shared screen. Rotating is the fix if one gets out.
        </p>
        </>)}
      </div>
    );
  }

  window.PartnerAdmin = PartnerAdmin;
})();
