/* ===========================================================
   DTF.uk · Ops — app shell, login, routing
   =========================================================== */
const { useState, useEffect, useRef, useMemo } = React;

function Login({ onLogin }) {
  const [email, setEmail] = useState("");
  const [pw, setPw] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  async function submit(e){
    e.preventDefault(); setErr(""); setBusy(true);
    try {
      const r = await fetch("/api/login", { method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify({ email, password: pw }) });
      const j = await r.json();
      if (!r.ok || !j.authed) throw new Error(j.error || "Sign in failed");
      onLogin();
    } catch (e) { setErr(e.message); } finally { setBusy(false); }
  }
  return (
    <div className="login">
      <div className="login__art">
        <div className="login__brand">
          <div className="m">DTF</div>
          <div className="t">DTF.uk · Ops</div>
        </div>
        <div className="login__big">
          <h2>Print it.<br/>Press it.<br/><span className="hl">Ship it.</span></h2>
          <p>The backoffice for DTF.uk — orders, dispatch, customers and inbox in one place. Pressed in Milton Keynes, shipped UK-wide.</p>
        </div>
      </div>
      <div className="login__form">
        <form className="login__card" onSubmit={submit}>
          <h1>Sign in</h1>
          <p className="lede">Welcome back. Sign in to the SD Group Backoffice.</p>
          <div className="field">
            <label>Work email</label>
            <input type="email" value={email} onChange={e=>setEmail(e.target.value)} autoComplete="username" />
          </div>
          <div className="field">
            <div className="field__row"><label>Password</label></div>
            <input type="password" value={pw} onChange={e=>setPw(e.target.value)} placeholder="••••••••" autoComplete="current-password" />
          </div>
          {err && <div className="login__err">{err}</div>}
          <button className="btn btn--primary btn--block" type="submit" style={{marginTop:8}} disabled={busy}>
            {busy ? <span className="spin" /> : null}{busy ? "Signing in…" : "Sign in"}
          </button>
          <div className="login__hint">SD Group Backoffice — authorised access only.</div>
        </form>
      </div>
    </div>
  );
}

const TITLES = {
  orders:    { t:"Orders",    s:"Dispatch board · grouped by date, sorted by urgency" },
  designs:   { t:"Designs",   s:"Customer gang sheets · draft, in cart & ordered" },
  transfers: { t:"Transfers", s:"Send Files · customer artwork uploads" },
  order:     { t:"Order",     s:"" },
  drafts:    { t:"Draft orders", s:"Shopify drafts · amend, invoice, then pay-later or mark paid" },
  draft:     { t:"Draft order",  s:"" },
  customers: { t:"Customers", s:"" },
  customer:  { t:"Customer",  s:"" },
  inbox:       { t:"Inbox",       s:"Gmail · threads linked to CRM" },
  shipments:   { t:"Shipments",   s:"Royal Mail Click & Drop · labels, tracking & manifest" },
  browsing:    { t:"Browsing",    s:"Live storefront activity · who's watching what" },
  production:  { t:"Production",   s:"Print & press queues · printers · artwork" },
  outreach:    { t:"Print-shop outreach", s:"Prospects · AI profile read · mockup · send" },
  conversions: { t:"Conversions",  s:"What brings sales · revenue by source & attribution model" },
  analytics:   { t:"Analytics",   s:"Sessions · conversions · customer growth · retention" },
  email:       { t:"Email",         s:"Email performance · sent, opened, clicked, converted" },
  marketing:   { t:"Marketing",     s:"VBOUT · sync customer segments to lists & track campaigns" },
  reviews:     { t:"Google Reviews", s:"Review discount codes · 20% off for customers who leave a Google review" },
  attachments: { t:"Attachments", s:"Files from email threads" },
  stock:       { t:"Stock",        s:"Our blank-garment inventory · hand-editable" },
  pricing:     { t:"Price groups", s:"Per-customer trade pricing · flat £/m rates applied to members' quotes" },
  gangqueue:   { t:"Gang sheets",   s:"Generation queue · status, failures & retry" },
  settings:    { t:"Settings",    s:"Integrations & account" },
};

function Attachments({ go }) {
  const atts = DTF.collectAttachments(DTF.liveThreads);
  return (
    <div className="board">
      <div className="toolbar">
        <span className="kick">{atts.length} attachment{atts.length===1?"":"s"} from loaded email threads</span>
      </div>
      {atts.length
        ? <div className="att-grid">{atts.map((a,i) => <AttachmentTile key={i} att={a} meta={(a.sender||"") + (a.time ? " · " + a.time : "")} />)}</div>
        : <div className="empty"><Icon name="paperclip" /><div className="t">No attachments yet</div><div className="m">Open the Inbox or a customer profile to load email threads with attachments.</div></div>}
    </div>
  );
}

/* ---- hash routing: URL <-> {view,id} so reloads land on the same section ---- */
const IS_LOCAL = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])$/.test(location.hostname);

function parseHash() {
  const rawHash = (location.hash || "").replace(/^#\/?/, "").split("?")[0];
  const parts = rawHash.split("/").filter(Boolean);
  const seg = parts[0];
  let id = parts[1] ? decodeURIComponent(parts[1]) : null;
  // Order numbers are stored with a leading "#". We keep it out of the URL for
  // clean links (#/orders/3653) and restore it here; old %23 links still work.
  const orderId = id != null ? (/^\d+$/.test(id) ? "#" + id : id) : null;
  // Customer ids are Shopify gids — keep only the numeric tail in the URL and
  // rebuild the gid here (mail:/seed ids pass through unchanged).
  const customerId = id != null ? (/^\d+$/.test(id) ? "gid://shopify/Customer/" + id : id) : null;
  switch (seg) {
    case "orders":    return orderId ? { view:"order", id: orderId } : { view:"orders", board:"all" };
    case "drafts":    return id ? { view:"draft", id } : { view:"drafts" };
    case "draft":     return { view:"draft", id };
    case "neworders": return { view:"orders", board:"new" };
    case "dispatch":  return { view:"orders", board:"board" };
    case "order":     return { view:"order", id: orderId };
    case "customers": return customerId ? { view:"customer", id: customerId } : { view:"customers" };
    case "customer":  return { view:"customer", id: customerId };
    case "inbox":       return { view:"inbox", id };
    case "designs":     return { view:"designs" };
    case "transfers":   return { view:"transfers" };
    // Partner shop. List tabs/filters get their own URLs so they're bookmarkable
    // and an order's "back" returns to the exact list it was opened from:
    //   #/shop/<slug>                     → To-do (pending)
    //   #/shop/<slug>/todo/printed|all    → To-do, printed / all filter
    //   #/shop/<slug>/dispatched          → Dispatched
    //   #/shop/<slug>[/…tab]/<orderNo>    → order detail (order no. is the numeric tail)
    case "shop": {
      const slug = parts[1] || null;
      const rest = parts.slice(2);
      let sid = null;
      if (rest.length && /^#?\d+$/.test(rest[rest.length - 1])) sid = String(rest.pop()).replace(/^#/, "");
      let tab = "todo", filter = "pending";
      if (rest[0] === "dispatched") tab = "dispatched";
      else if (rest[0] === "todo") { if (rest[1] === "printed") filter = "printed"; else if (rest[1] === "all") filter = "all"; }
      return { view:"shop", slug, id: sid, tab, filter };
    }
    case "shipments":   return { view:"shipments" };
    case "browsing":  { const p = parts[1]; return { view:"browsing", period: (p === "week" || p === "all" || p === "today") ? p : null }; }
    // Production sub-view + its sub-tab get their own URL so a tab is bookmarkable
    // and shareable: #/production/printing/printed  ·  #/production/pressing/ready_press
    case "production":  {
      const p = parts[1];
      const sub = ["design","rip","printing","pressing"].includes(p) ? p : null;
      const TABS = { printing:["toprint","printing","printed"], pressing:["awaiting_prints","awaiting_garment","ready_press"] };
      const t = parts[2] ? decodeURIComponent(parts[2]) : null;
      const tab = (sub && TABS[sub] && TABS[sub].includes(t)) ? t : null;
      return { view:"production", sub, tab };
    }
    case "outreach":    return { view:"outreach", id };
    case "conversions": return { view:"conversions" };
    case "analytics": return { view:"analytics" };
    case "email":       return { view:"email" };
    case "marketing":   return { view:"marketing" };
    case "reviews":     return { view:"reviews" };
    case "attachments": return { view:"attachments" };
    case "gang-sheets": return { view:"gangqueue" };
    case "stock":       return { view:"stock", sub: parts[1] === "forecast" ? "forecast" : null };
    case "pricing":     return { view:"pricing" };
    case "settings":    return { view:"settings" };
    default:            return { view:"orders", board:"board" };
  }
}
function toHash(route) {
  const e = encodeURIComponent;
  switch (route.view) {
    case "order":     return "#/orders/" + e(String(route.id).replace(/^#/, ""));
    case "drafts":    return "#/drafts";
    case "draft":     return "#/drafts/" + e(route.id);
    case "customer":  return "#/customers/" + e(String(route.id).startsWith("gid://") ? String(route.id).split("/").pop() : route.id);
    case "customers": return "#/customers";
    case "inbox":       return route.id ? "#/inbox/" + e(route.id) : "#/inbox";
    case "designs":     return "#/designs";
    case "transfers":   return "#/transfers";
    case "shop": {
      const nav = [];
      const tab = route.tab === "dispatched" ? "dispatched" : "todo";
      const filter = route.filter || "pending";
      if (tab === "dispatched") nav.push("dispatched");
      else if (filter === "printed") nav.push("todo", "printed");
      else if (filter === "all") nav.push("todo", "all");
      else if (route.id) nav.push("todo"); // pending default is bare; add "todo" only to give a detail a clean back target
      if (route.id) nav.push(String(route.id).replace(/^#/, ""));
      return "#/shop/" + e(route.slug || "") + (nav.length ? "/" + nav.map(e).join("/") : "");
    }
    case "shipments":   return "#/shipments";
    case "browsing":    return "#/browsing" + (route.period ? "/" + e(route.period) : "");
    case "production":  return "#/production" + (route.sub ? "/" + e(route.sub) : "") + (route.sub && route.tab ? "/" + e(route.tab) : "");
    case "outreach":    return route.id ? "#/outreach/" + e(route.id) : "#/outreach";
    case "conversions": return "#/conversions";
    case "analytics":   return "#/analytics";
    case "email":       return "#/email";
    case "marketing":   return "#/marketing";
    case "reviews":     return "#/reviews";
    case "attachments": return "#/attachments";
    case "gangqueue":   return "#/gang-sheets";
    case "stock":       return route.sub === "forecast" ? "#/stock/forecast" : "#/stock";
    case "pricing":     return "#/pricing";
    case "settings":    return "#/settings";
    case "orders":      return route.board === "new" ? "#/neworders" : route.board === "board" ? "#/dispatch" : "#/orders";
    default:            return "#/dispatch";
  }
}

/* ---- Settings: Gmail IMAP (inbox) + SMTP (sending) ---- */
const MAIL_KINDS = {
  imap: {
    title:"Gmail / IMAP — receiving", icon:"mail", base:"/api/settings/imap",
    defHost:"imap.gmail.com", defPort:993, fromName:false, file:"server/.imap.json",
    blurb:"Pull real emails into the Inbox. For Gmail, enable 2-Step Verification and create an App Password (Google Account → Security → App passwords) — use that here, not your normal password.",
    okMsg:"Saved. Open the Inbox to pull your latest emails.",
    testMsg:(d)=>`Connected — ${d.messages} messages in INBOX.`,
  },
  smtp: {
    title:"Gmail / SMTP — sending", icon:"send", base:"/api/settings/smtp",
    defHost:"smtp.gmail.com", defPort:465, fromName:true, file:"server/.smtp.json",
    blurb:"Send replies from the Inbox. Leave the password blank to reuse your IMAP app password (same Gmail login).",
    okMsg:"Saved. Replies from the Inbox will send via this account.",
    testMsg:()=>"SMTP connection verified — ready to send.",
  },
};

function MailCard({ kind }) {
  const K = MAIL_KINDS[kind];
  const [cfg, setCfg] = useState({ host:K.defHost, port:K.defPort, user:"", pass:"", fromName:"" });
  const [hasPassword, setHasPassword] = useState(false);
  const [busy, setBusy] = useState(false);
  const [note, setNote] = useState(null); // {type:"ok"|"err"|"info", msg}

  useEffect(() => {
    fetch(K.base).then(r=>r.json()).then(d => {
      setCfg(c => ({ ...c, host:d.host||K.defHost, port:d.port||K.defPort, user:d.user||"", fromName:d.fromName||"" }));
      setHasPassword(!!d.hasPassword);
    }).catch(()=>{});
  }, []);

  const set = (k,v) => setCfg(c => ({ ...c, [k]: v }));

  async function save(){
    setBusy(true); setNote(null);
    try {
      const r = await fetch(K.base, { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(cfg) });
      const d = await r.json();
      if (!r.ok) throw new Error(d.error || "Save failed");
      setHasPassword(!!d.hasPassword);
      setCfg(c => ({ ...c, pass:"" }));
      setNote({ type:"ok", msg:K.okMsg });
    } catch(e){ setNote({ type:"err", msg:e.message }); }
    setBusy(false);
  }
  async function test(){
    setBusy(true); setNote({ type:"info", msg:"Connecting…" });
    try {
      const r = await fetch(K.base + "/test", { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(cfg) });
      const d = await r.json();
      if (!r.ok) throw new Error(d.error || "Connection failed");
      setNote({ type:"ok", msg:K.testMsg(d) });
    } catch(e){ setNote({ type:"err", msg:e.message }); }
    setBusy(false);
  }

  return (
    <div className="card">
      <div className="card__head">
        <Icon name={K.icon} /><h3>{K.title}</h3>
        {hasPassword && <span className="right badge badge--mint">Connected</span>}
      </div>
      <div className="card__body">
        <p style={{ fontSize:13, color:"var(--txt-2)", margin:"0 0 16px", lineHeight:1.6 }}>{K.blurb}</p>
        {note && (
          <div className={"ai-banner" + (note.type==="info"?" thinking":"")}
               style={ note.type==="err" ? { background:"var(--c-red-w)", borderColor:"#f3c2bd", color:"#b32a1f" } : null }>
            {note.type==="info" ? <span className="spin spin--dark" /> : <Icon name={note.type==="ok"?"check":"x"} />}
            <span>{note.msg}</span>
          </div>
        )}
        {K.fromName && (
          <div className="field"><label>From name</label>
            <input value={cfg.fromName} onChange={e=>set("fromName", e.target.value)} placeholder="DTF.uk" />
          </div>
        )}
        <div className="field"><label>Email address</label>
          <input type="email" value={cfg.user} onChange={e=>set("user", e.target.value)} placeholder="hello@dtf.uk" autoComplete="username" />
        </div>
        <div className="field"><label>App password</label>
          <input type="password" value={cfg.pass} onChange={e=>set("pass", e.target.value)}
            placeholder={hasPassword ? "•••••••••••• (saved — leave blank to keep)" : (kind==="smtp" ? "blank = reuse IMAP password" : "16-character app password")} autoComplete="off" />
        </div>
        <div style={{ display:"flex", gap:12 }}>
          <div className="field" style={{ flex:2 }}><label>{kind.toUpperCase()} host</label>
            <input value={cfg.host} onChange={e=>set("host", e.target.value)} placeholder={K.defHost} />
          </div>
          <div className="field" style={{ flex:1 }}><label>Port</label>
            <input value={cfg.port} onChange={e=>set("port", e.target.value)} placeholder={String(K.defPort)} />
          </div>
        </div>
        <div style={{ display:"flex", gap:8, marginTop:8 }}>
          <button className="btn btn--primary btn--sm" onClick={save} disabled={busy}>{busy ? <span className="spin" /> : null} Save</button>
          <button className="btn btn--ghost btn--sm" onClick={test} disabled={busy}>Test connection</button>
        </div>
        <div className="login__hint" style={{ marginTop:18 }}>
          Stored server-side in <span className="mono">{K.file}</span> (gitignored).
        </div>
      </div>
    </div>
  );
}

function RoyalMailCard() {
  const [data, setData] = useState(null);
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState("");
  useEffect(() => {
    fetch("/api/settings/royalmail").then(r => r.json()).then(setData).catch(() => setData({ error: true }));
  }, []);
  if (!data) return <div className="card"><div className="card__body dim">Loading Royal Mail settings…</div></div>;
  if (data.error) return <div className="card"><div className="card__body dim">Couldn't load Royal Mail settings.</div></div>;

  const upd = (i, patch) => setData(d => ({ ...d, services: d.services.map((s, j) => j === i ? { ...s, ...patch } : s) }));
  const addService = () => setData(d => ({ ...d, services: [...d.services, { id: "custom-" + Date.now(), name: "New service", code: "", format: "smallParcel", enabled: true }] }));
  const removeService = (i) => setData(d => {
    const removed = d.services[i];
    return { ...d, services: d.services.filter((_, j) => j !== i), defaultServiceId: d.defaultServiceId === removed.id ? null : d.defaultServiceId };
  });
  async function save() {
    setSaving(true); setMsg("");
    try {
      const r = await fetch("/api/settings/royalmail", {
        method: "PUT", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ services: data.services, defaultServiceId: data.defaultServiceId }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "Save failed");
      setData(j); setMsg("Saved.");
    } catch (e) { setMsg("Error: " + e.message); }
    finally { setSaving(false); }
  }
  const enabledCount = data.services.filter(s => s.enabled).length;

  return (
    <div className="card">
      <div className="card__head"><Icon name="truck" /><h3>Royal Mail — Click &amp; Drop</h3>
        <span className={"right badge " + (data.oba.clickAndDropConfigured ? "badge--mint" : "badge--ghost")}>
          {data.oba.clickAndDropConfigured ? "API key set" : "No API key"}
        </span>
      </div>
      <div className="card__body">
        <dl className="kv" style={{ marginBottom: 16 }}>
          <dt>OBA email</dt><dd>{data.oba.email || "—"}</dd>
          <dt>Account no.</dt><dd className="mono">{data.oba.accountNumber || "—"}</dd>
          <dt>Posting location</dt><dd className="mono">{data.oba.postingLocation || "—"}</dd>
        </dl>
        <div className="kick" style={{ marginBottom: 8 }}>
          Services you use · {enabledCount} enabled — verify each <b>code</b> against your OBA contract before printing labels.
        </div>
        <table className="rm-svc">
          <thead><tr><th>Use</th><th>Service</th><th>Code</th><th>Package</th><th>Default</th><th></th></tr></thead>
          <tbody>
            {data.services.map((s, i) => (
              <tr key={s.id} className={s.enabled ? "" : "is-off"}>
                <td><input type="checkbox" checked={s.enabled} onChange={e => upd(i, { enabled: e.target.checked })} /></td>
                <td><input className="rm-in rm-in--name" value={s.name} onChange={e => upd(i, { name: e.target.value })} placeholder="Service name" /></td>
                <td><input className="rm-in mono" value={s.code} onChange={e => upd(i, { code: e.target.value })} placeholder="code" /></td>
                <td>
                  <select value={s.format} onChange={e => upd(i, { format: e.target.value })}>
                    {data.packageFormats.map(f => <option key={f} value={f}>{f}</option>)}
                  </select>
                </td>
                <td><input type="radio" name="rm-default" checked={data.defaultServiceId === s.id} disabled={!s.enabled}
                           onChange={() => setData(d => ({ ...d, defaultServiceId: s.id }))} /></td>
                <td><button className="rm-del" title="Remove service" onClick={() => removeService(i)}>×</button></td>
              </tr>
            ))}
          </tbody>
        </table>
        <button className="btn btn--ghost btn--sm" style={{ marginTop: 10 }} onClick={addService}><Icon name="plus" /> Add service</button>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 14 }}>
          <button className="btn btn--primary btn--sm" disabled={saving} onClick={save}>{saving ? "Saving…" : "Save services"}</button>
          {msg && <span className="kick">{msg}</span>}
        </div>
      </div>
    </div>
  );
}

// Label print-agent targets — central override the packing-PC agent picks up on
// its heartbeat. Each doc type points at an OS printer name (Windows: exactly as
// installed) OR a network address: a bare IP like 192.168.1.108 (macOS/Linux
// auto-adds a CUPS queue), an ip:port, or a device URI (socket://…, ipp://…).
function LabelPrinterCard() {
  const [data, setData] = useState(null);
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState("");
  useEffect(() => {
    fetch("/api/settings/printer-agent").then(r => r.json()).then(setData).catch(() => setData({ error: true }));
  }, []);
  if (!data) return <div className="card"><div className="card__body dim">Loading label printer settings…</div></div>;
  if (data.error) return <div className="card"><div className="card__body dim">Couldn't load label printer settings.</div></div>;

  const setP = (k, v) => setData(d => ({ ...d, printers: { ...d.printers, [k]: v } }));
  async function save() {
    setSaving(true); setMsg("");
    try {
      const r = await fetch("/api/settings/printer-agent", {
        method: "PUT", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ enabled: data.enabled, printers: data.printers }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "Save failed");
      setData(j); setMsg("Saved. The agent picks it up within a few seconds.");
    } catch (e) { setMsg("Error: " + e.message); }
    finally { setSaving(false); }
  }
  const ROWS = [
    { key: "label", label: "Shipping labels", hint: "thermal — Royal Mail labels" },
    { key: "packing", label: "Packing slips", hint: "thermal" },
    { key: "summary", label: "Order summaries", hint: "A4 — leave blank to keep the agent's own" },
  ];
  return (
    <div className="card">
      <div className="card__head"><Icon name="print" /><h3>Label printer</h3>
        <span className={"right badge " + (data.enabled ? "badge--mint" : "badge--ghost")}>{data.enabled ? "Override on" : "Using agent config"}</span>
      </div>
      <div className="card__body">
        <div className="kick" style={{ marginBottom: 12 }}>
          Set where the packing-station agent prints. A value is an <b>OS printer name</b> (Windows: exactly as installed)
          or a network address — a bare <b>IP</b> (e.g. <span className="mono">192.168.1.108</span>), <span className="mono">ip:9100</span>,
          or a device URI (<span className="mono">socket://…</span>, <span className="mono">ipp://…</span>). On macOS/Linux the agent
          auto-adds a print queue for an IP/URI. When the override is off, the agent uses its own <span className="mono">agent.config.json</span>.
        </div>
        <label style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 14 }}>
          <input type="checkbox" checked={!!data.enabled} onChange={e => setData(d => ({ ...d, enabled: e.target.checked }))} />
          <span>Use these targets (override the agent's local config)</span>
        </label>
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {ROWS.map(row => (
            <div key={row.key} style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
              <span style={{ minWidth: 130, fontWeight: 600 }}>{row.label}</span>
              <input className="rm-in mono" style={{ minWidth: 220 }} placeholder="printer name or IP…"
                     value={data.printers[row.key] || ""} onChange={e => setP(row.key, e.target.value)} />
              <span className="dim" style={{ fontSize: 12 }}>{row.hint}</span>
            </div>
          ))}
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 16 }}>
          <button className="btn btn--primary btn--sm" disabled={saving} onClick={save}>{saving ? "Saving…" : "Save"}</button>
          {msg && <span className="kick">{msg}</span>}
        </div>
      </div>
    </div>
  );
}

// Company & invoice details — editable at any time; used on the PDF invoice
// (From block + payment/bank details footer). Persisted server-side (.settings.json).
function CompanyCard() {
  const [data, setData] = useState(null);
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState("");
  useEffect(() => { fetch("/api/company").then(r => r.json()).then(setData).catch(() => setData({ error: true })); }, []);
  if (!data) return <div className="card"><div className="card__body dim">Loading company details…</div></div>;
  if (data.error) return <div className="card"><div className="card__body dim">Couldn't load company details.</div></div>;
  const set = (k, v) => setData(d => ({ ...d, [k]: v }));
  const addrText = Array.isArray(data.addr) ? data.addr.join("\n") : (data.addr || "");
  async function save() {
    setSaving(true); setMsg("");
    try {
      const body = { ...data, addr: typeof data.addr === "string" ? data.addr : (data.addr || []).join("\n") };
      const r = await fetch("/api/company", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "Save failed");
      setData(j); setMsg("Saved.");
    } catch (e) { setMsg("Error: " + e.message); }
    finally { setSaving(false); }
  }
  const field = (label, key, ph) => (
    <label className="out-field"><span>{label}</span>
      <input className="rm-in" value={data[key] || ""} onChange={e => set(key, e.target.value)} placeholder={ph || ""} /></label>
  );
  return (
    <div className="card">
      <div className="card__head"><Icon name="doc" /><h3>Company &amp; invoice details</h3></div>
      <div className="card__body" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        <div className="kick">Shown on the PDF invoice — the “From” block and the payment/bank details footer. Changes apply to the next invoice generated.</div>
        {field("Trading name", "name", "DTF.UK")}
        <label className="out-field"><span>From address (one line each)</span>
          <textarea className="out-html" style={{ height: 90 }} value={addrText} onChange={e => set("addr", e.target.value)} /></label>
        {field("Email", "email")}
        {field("Website", "website")}
        {field("Legal company name", "legalName")}
        {field("Bank", "bank")}
        {field("Sort code", "sortCode")}
        {field("Account number", "accountNo")}
        {field("Registered company No.", "companyNo")}
        {field("VAT registration number", "vat")}
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 6 }}>
          <button className="btn btn--primary btn--sm" disabled={saving} onClick={save}>{saving ? "Saving…" : "Save company details"}</button>
          {msg && <span className="kick">{msg}</span>}
        </div>
      </div>
    </div>
  );
}

// Global font library — upload TTF/OTF once; usable in every text/artwork builder.
function FontsCard() {
  const [fonts, setFonts] = useState(null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const inputRef = React.useRef(null);
  const refresh = () => DTF.listFonts().then(setFonts);
  useEffect(() => { refresh(); }, []);
  useEffect(() => {
    (fonts || []).forEach(f => {
      const fam = "dtf-font-" + f.id;
      if ([...document.fonts].some(ff => ff.family === fam)) return;
      new FontFace(fam, "url(" + DTF.fontFileUrl(f.id) + ")").load().then(ff => { document.fonts.add(ff); setFonts(s => s ? s.slice() : s); }).catch(() => {});
    });
  }, [fonts]);
  async function upload(file) {
    if (!file) return;
    if (!/\.(ttf|otf)$/i.test(file.name || "")) { setErr("Only .ttf or .otf fonts are supported"); return; }
    setBusy(true); setErr("");
    try { await DTF.uploadFont(file, (file.name || "").replace(/\.(ttf|otf)$/i, "")); await refresh(); }
    catch (e) { setErr(e.message || "Upload failed"); }
    setBusy(false);
  }
  async function del(id) { if (!confirm("Delete this font? Existing artwork keeps its rendered PNG; only new renders are affected.")) return; setFonts(await DTF.deleteFont(id)); }
  return (
    <div className="card">
      <div className="card__head"><Icon name="spark" /><h3>Fonts</h3>{fonts && fonts.length > 0 && <span className="right kick">{fonts.length}</span>}</div>
      <div className="card__body">
        <div className="dim" style={{ fontSize: 12.5, marginBottom: 10 }}>Upload TTF/OTF fonts once here — they’re available in every text/artwork builder.</div>
        {fonts === null ? <div className="dim" style={{ fontSize: 12.5 }}>Loading…</div>
          : fonts.length === 0 ? <div className="dim" style={{ fontSize: 12.5 }}>No fonts yet.</div>
          : <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {fonts.map(f => (
                <div key={f.id} style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 10px", border: "1px solid var(--line,#dfe4ea)", borderRadius: 8 }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontFamily: '"dtf-font-' + f.id + '", serif', fontSize: 26, lineHeight: 1.15, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{f.name} — Aa Bb 123</div>
                    <div className="mono dim" style={{ fontSize: 10.5 }}>{f.name} · {(f.format || "").toUpperCase()} · {window.fmtBytes ? window.fmtBytes(f.size) : f.size + "B"}</div>
                  </div>
                  <button className="btn btn--ghost btn--sm" onClick={() => del(f.id)}>Delete</button>
                </div>
              ))}
            </div>}
        {err && <div className="form-err" style={{ marginTop: 8 }}>{err}</div>}
        <div style={{ marginTop: 12 }}>
          <button className="btn btn--dark btn--sm" disabled={busy} onClick={() => inputRef.current && inputRef.current.click()}>{busy ? "Uploading…" : "Upload font (.ttf / .otf)"}</button>
          <input ref={inputRef} type="file" accept=".ttf,.otf,font/ttf,font/otf" style={{ display: "none" }} onChange={e => { const f = e.target.files[0]; e.target.value = ""; upload(f); }} />
        </div>
      </div>
    </div>
  );
}

function Settings() {
  return (
    <div className="board" style={{ maxWidth:760, display:"flex", flexDirection:"column", gap:16 }}>
      <PrintersManager />
      <LabelPrinterCard />
      <SummaryListCard />
      <FontsCard />
      <CompanyCard />
      <RoyalMailCard />
      <MailCard kind="imap" />
      <MailCard kind="smtp" />
    </div>
  );
}

// Which order statuses the "Today's Summary List" button (Orders board) includes.
function SummaryListCard() {
  const [picks, setPicks] = useState(null);     // null = loading
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState("");
  useEffect(() => {
    fetch("/api/settings/summary-statuses").then(r => r.json())
      .then(j => setPicks(new Set(Array.isArray(j.statuses) ? j.statuses : [])))
      .catch(() => setPicks(new Set()));
  }, []);
  const opts = (typeof STATUS_OPTIONS !== "undefined" ? STATUS_OPTIONS : []);
  const toggle = (k) => setPicks(prev => { const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k); return n; });
  async function save() {
    setSaving(true); setMsg("");
    try {
      const r = await fetch("/api/settings/summary-statuses", {
        method: "PUT", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ statuses: [...picks] }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || "Save failed");
      setPicks(new Set(j.statuses || [])); setMsg("Saved.");
    } catch (e) { setMsg("Error: " + e.message); }
    finally { setSaving(false); }
  }
  return (
    <div className="card">
      <div className="card__head"><Icon name="doc" /><h3>Today's Summary List</h3>
        {picks && <span className="right badge badge--ghost">{picks.size} status{picks.size === 1 ? "" : "es"}</span>}
      </div>
      <div className="card__body">
        <div className="kick" style={{ marginBottom: 12 }}>
          The <b>Today's Summary List</b> button on the Orders board selects every loaded order in these statuses (you can then print the one-row-per-order summary from the selection toolbar). Choose which statuses to include.
        </div>
        {!picks ? <div className="dim">Loading…</div> : (
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(190px,1fr))", gap: 8 }}>
            {opts.map(k => {
              const meta = (DTF.statusMeta && DTF.statusMeta[k]) || {};
              return (
                <label key={k} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }}>
                  <input type="checkbox" checked={picks.has(k)} onChange={() => toggle(k)} />
                  <span className="status-multi__dot" style={{ background: meta.dot || "#ccc", width: 8, height: 8, borderRadius: "50%", display: "inline-block", flex: "none" }} />
                  <span>{meta.label || k}</span>
                </label>
              );
            })}
          </div>
        )}
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 14 }}>
          <button className="btn btn--primary btn--sm" disabled={saving || !picks} onClick={save}>{saving ? "Saving…" : "Save"}</button>
          {msg && <span className="kick" style={{ color: "var(--mint-ink)" }}>{msg}</span>}
        </div>
      </div>
    </div>
  );
}

function DataScreen({ children }) {
  return (
    <div className="app">
      <div className="main" style={{ gridColumn:"1 / -1" }}>
        <div className="content"><div className="empty">{children}</div></div>
      </div>
    </div>
  );
}

function App() {
  const [authed, setAuthed] = useState(null); // null = checking session, false = show login, true = in
  const [phase, setPhase] = useState(DTF.loaded ? "ready" : "loading"); // loading | ready | error
  const [errMsg, setErrMsg] = useState("");
  const [route, setRoute] = useState(() => parseHash());
  const [threads, setThreads] = useState(() => DTF.threads.map(t => ({ ...t, messages: t.messages.slice() })));

  const [userEmail, setUserEmail] = useState("");
  const [role, setRole] = useState(null);        // "admin" | "partner" (null until /api/me resolves)
  const [myShops, setMyShops] = useState([]);    // shops this user may see (partner) or all shops (admin)
  const [, setAssignTick] = useState(0);         // bump when order→shop assignments load, to re-tag the list
  const [navOpen, setNavOpen] = useState(false);                                  // mobile off-canvas drawer
  const [collapsed, setCollapsed] = useState(() => localStorage.getItem("dtf_nav_collapsed") === "1"); // desktop collapse
  useEffect(() => { localStorage.setItem("dtf_nav_collapsed", collapsed ? "1" : "0"); }, [collapsed]);
  // Burger: on mobile toggles the drawer; on desktop collapses/expands the sidebar.
  const toggleNav = () => {
    if (window.matchMedia("(max-width: 860px)").matches) setNavOpen(o => !o);
    else setCollapsed(c => !c);
  };
  // Check the server session on boot (auth is off in local dev → authed immediately).
  useEffect(() => {
    fetch("/api/me").then(r => r.json())
      .then(j => {
        setAuthed(j.authed || !j.required); setUserEmail(j.email || "");
        setRole(j.role || "admin");
        DTF.role = j.role || "admin";
        // Partners get their shops here; admins load the full list separately
        // (below) — don't clobber that list with the empty set /api/me returns
        // for admins (this effect re-runs on login).
        if (j.role === "partner") { setMyShops(j.shops || []); DTF.myShops = j.shops || []; }
        // Expose the signed-in user's display name for email signatures etc.
        const local = ((j.email || "").split("@")[0] || "");
        DTF.userName = local.replace(/[._-]+/g, " ").replace(/\b\w/g, c => c.toUpperCase()).trim();
        DTF.userEmail = j.email || "";
      })
      .catch(() => { setAuthed(true); setRole("admin"); }); // server unreachable → don't lock out
  }, [authed]);

  // Admins get the full shop list (for the sidebar's Partner-shops section and
  // the preview). Partners already have their shops from /api/me.
  useEffect(() => {
    if (role !== "admin") return;
    fetch("/api/shop/mine").then(r => r.json()).then(j => { if (j && j.shops) { setMyShops(j.shops); DTF.myShops = j.shops; } }).catch(() => {});
    // Order→shop assignments, so the orders list can tag which location an order is handed to.
    fetch("/api/shops").then(r => r.json()).then(j => { if (j && j.orders) { DTF.shopAssignments = j.orders; DTF.shopsById = Object.fromEntries((j.shops || []).map(s => [s.slug, s])); setAssignTick(x => x + 1); } }).catch(() => {});
  }, [role]);

  function runLoad() {
    setPhase("loading"); setErrMsg("");
    DTF.load()
      .then(() => setPhase("ready"))
      .catch(e => { setErrMsg(e.message || String(e)); setPhase("error"); });
  }

  const [reloading, setReloading] = useState(false);
  function reloadAll() {
    setReloading(true);
    DTF.reload()
      .then(() => setThreads(prev => DTF.liveThreads.length ? [...DTF.liveThreads] : prev))
      .catch(() => {})
      .finally(() => setReloading(false));
  }

  useEffect(() => {
    // Partners never load the admin dataset (/api/orders, /api/customers 403 for
    // them) — the shop shell fetches its own scoped data.
    if (authed && role && role !== "partner" && !DTF.loaded) runLoad();
  }, [authed, role]);

  // Live-sync: once data is ready, poll for server-side changes and auto-refresh
  // the current view (throttled). Pauses while the tab is hidden.
  useEffect(() => {
    if (phase !== "ready" || !DTF.live) return;
    DTF.live.start();
    if (DTF.printLive) DTF.printLive.start();
    if (DTF.versionWatch) DTF.versionWatch.start();
    return () => { DTF.live.stop(); if (DTF.printLive) DTF.printLive.stop(); if (DTF.versionWatch) DTF.versionWatch.stop(); };
  }, [phase]);

  // A newer frontend build has been deployed — surface a reload prompt (see
  // DTF.versionWatch). Once shown it stays until the user reloads.
  const [updateReady, setUpdateReady] = useState(false);
  useEffect(() => {
    if (DTF._updateReady) { setUpdateReady(true); return; }
    const onUpd = () => setUpdateReady(true);
    window.addEventListener("dtf:update-available", onUpd);
    return () => window.removeEventListener("dtf:update-available", onUpd);
  }, []);

  // Pull live Gmail/IMAP emails once data is ready, so the Inbox AND the orders
  // grid (per-customer email counts) reflect real threads.
  const [inboxTried, setInboxTried] = useState(false);
  useEffect(() => {
    if (phase !== "ready" || inboxTried) return;
    setInboxTried(true);
    DTF.loadInbox()
      .then(r => { if (r.configured) { DTF.setLiveThreads(r.threads); if (r.threads.length) setThreads(r.threads); } })
      .catch(() => {}); // not configured / unreachable -> keep demo threads
  }, [phase, inboxTried]);

  const mergeLive = () => setThreads(prev => {
    const map = new Map(prev.map(t => [t.id, t]));
    let added = false;
    (DTF.liveThreads || []).forEach(t => { if (!map.has(t.id)) { map.set(t.id, t); added = true; } });
    return added ? [...map.values()] : prev;
  });

  // When opening the Inbox, merge any threads discovered elsewhere (e.g. a mailbox
  // search from an order page) so they can be opened and replied to here.
  useEffect(() => {
    if (route.view !== "inbox" || !DTF.liveThreads || !DTF.liveThreads.length) return;
    mergeLive();
  }, [route.view]);

  // Deep link to a specific thread (#/inbox/mail:<uid>) that isn't loaded yet —
  // resolve it from the mailbox so a cold reload opens that exact conversation.
  useEffect(() => {
    if (phase !== "ready" || route.view !== "inbox" || !route.id) return;
    if ((DTF.liveThreads || []).some(t => t.id === route.id)) return;
    let alive = true;
    DTF.resolveThread(route.id).then(ok => { if (alive && ok) mergeLive(); }).catch(() => {});
    return () => { alive = false; };
  }, [phase, route.view, route.id]);

  // Keep the URL hash and route in sync (back/forward + reload land on the same view).
  // Also canonicalise the URL — rewrite any encoded-gid / "#" form to the clean
  // hash so the address bar always reads e.g. #/customers/123, #/orders/3653.
  useEffect(() => {
    const sync = () => {
      const r = parseHash();
      const clean = toHash(r);
      if (location.hash && location.hash !== clean) history.replaceState(null, "", clean);
      setRoute(r);
    };
    window.addEventListener("hashchange", sync);
    if (!location.hash) location.replace("#/dispatch"); else sync();
    return () => window.removeEventListener("hashchange", sync);
  }, []);

  // Put the most identifying token FIRST in the tab title so it stays visible when
  // the browser truncates narrow tabs — e.g. an order shows "#3924 · …" not the
  // brand name. Falls back to a per-view label.
  useEffect(() => {
    const BRAND = "DTF Ops";
    let t = BRAND;
    switch (route.view) {
      case "order": t = "#" + String(route.id || "").replace(/^#/, "") + " · Order · " + BRAND; break;
      case "customer": { const c = DTF.getCustomer && DTF.getCustomer(route.id); t = (c && c.name ? c.name : "Customer") + " · " + BRAND; break; }
      case "orders": t = (route.board === "new" ? "New orders" : route.board === "board" ? "Dispatch" : "Orders") + " · " + BRAND; break;
      case "drafts": t = "Draft orders · " + BRAND; break;
      case "draft": t = "Draft order · " + BRAND; break;
      case "inbox": t = "Inbox · " + BRAND; break;
      case "outreach": t = "Outreach · " + BRAND; break;
      case "conversions": t = "Conversions · " + BRAND; break;
      case "analytics": t = "Analytics · " + BRAND; break;
      case "email": t = "Email · " + BRAND; break;
      case "marketing": t = "Marketing · " + BRAND; break;
      case "reviews": t = "Google Reviews · " + BRAND; break;
      case "production": t = "Production · " + BRAND; break;
      case "shipments": t = "Shipments · " + BRAND; break;
      case "browsing": t = "Browsing · " + BRAND; break;
      case "customers": t = "Customers · " + BRAND; break;
      case "settings": t = "Settings · " + BRAND; break;
      case "shop": {
        const shops = (DTF && DTF.myShops) || [];
        const sh = shops.find(s => s.slug === route.slug);
        const nm = (sh && sh.name) || "Shop";
        t = (route.id ? "#" + String(route.id).replace(/^#/, "") + " · " : "") + nm + " - Orders - DTF.uk Ops";
        break;
      }
    }
    document.title = t;
  }, [route.view, route.id, route.board, route.slug, phase]);

  const go = (r) => {
    const next = { id:null, ...r };
    const h = toHash(next);
    if (location.hash !== h) location.hash = h; // hashchange listener updates route
    else setRoute(next);
  };

  // Gang-sheet queue polling for the nav badge. MUST sit above the early returns
  // below so the hook order never changes. Badge lights up when a sheet finishes
  // (done/failed) since last viewed; clears when the queue view opens.
  const [gangJobs, setGangJobs] = useState([]);
  const [gangSeen, setGangSeen] = useState(() => Number(localStorage.getItem("dtf_gang_seen") || 0));
  useEffect(() => {
    let alive = true;
    const load = () => { if (DTF.gangSheetQueue) DTF.gangSheetQueue().then(j => { if (alive) setGangJobs(j || []); }).catch(() => {}); };
    load(); const t = setInterval(load, 15000);
    return () => { alive = false; clearInterval(t); };
  }, []);
  useEffect(() => {
    if (route.view === "gangqueue") { const now = Date.now(); localStorage.setItem("dtf_gang_seen", String(now)); setGangSeen(now); }
  }, [route.view, gangJobs.length]);

  if (authed === null || (authed && role === null)) return <div className="app"><div className="main" style={{gridColumn:"1 / -1"}}><div className="content"><div className="empty"><span className="spin spin--dark" /></div></div></div></div>;
  if (!authed) return <Login onLogin={()=>setAuthed(true)} />;

  // Partner users get a locked-down standalone shell — only their shop's orders,
  // no admin nav, no commercial data. Everything is fetched scoped + redacted.
  if (role === "partner") return <ShopApp shops={myShops} route={route} go={go} userEmail={userEmail} />;

  if (phase === "error") return (
    <DataScreen>
      <Icon name="box" />
      <div className="t">Couldn't load Shopify data</div>
      <div className="m">{errMsg}</div>
      <button className="btn btn--primary btn--sm" style={{marginTop:8}} onClick={runLoad}>Retry</button>
    </DataScreen>
  );
  if (phase !== "ready") return <SkeletonScreen />;

  const gangFinishedUnseen = gangJobs.filter(j => (j.status === "done" || j.status === "error") && (j.finishedAt || 0) > gangSeen);

  const counts = {
    gang: gangFinishedUnseen.length || null,
    gangFailed: gangFinishedUnseen.filter(j => j.status === "error").length,
    gangActive: gangJobs.filter(j => j.status === "queued" || j.status === "running").length,
    // Only orders still needing dispatch/collection (matches the dispatch board — excludes untriaged "new").
    orders: DTF.orders.filter(o=>![ "new","closed","delivered","dispatched","collected","refunded","cancelled", ...DTF.PRODUCTION_STAGES, ...(DTF.HOLD_STAGES||[]) ].includes(o.status)).length,
    // Brand-new orders awaiting triage (the "New orders" board).
    newOrders: DTF.orders.filter(o => o.status === "new").length,
    // All jobs currently on the production floor (any queue stage).
    production: DTF.orders.filter(o => DTF.PRODUCTION_STAGES.includes(o.status)).length,
    customers: DTF.customers.length,
    inbox: threads.filter(t=>t.unread && t.status!=="closed").length,
    attachments: DTF.collectAttachments(DTF.liveThreads).length || null,
  };

  const head = TITLES[route.view] || TITLES.orders;
  let title = head.t, sub = head.s;
  const modeTag = DTF.mode === "live" ? "Live Shopify · dtf.uk" : "Seed snapshot · dtf.uk";
  if (route.view === "orders") sub = `${sub} · ${modeTag}`;
  if (route.view === "customers") sub = modeTag;
  if (route.view === "order") title = route.id;
  if (route.view === "customer") { const c = DTF.getCustomer(route.id); if (c) sub = c.email; }

  const isInbox = route.view === "inbox";

  let body;
  switch (route.view) {
    case "orders":    body = <OrdersList go={go} board={route.board} />; break;
    case "order":     body = <OrderDetail id={route.id} go={go} />; break;
    case "drafts":    body = <DraftOrders go={go} />; break;
    case "draft":     body = <DraftOrderDetail id={route.id} go={go} />; break;
    case "customers": body = <CustomersList go={go} />; break;
    case "customer":  body = <CustomerProfile id={route.id} go={go} />; break;
    case "inbox":     body = <Inbox threads={threads} setThreads={setThreads} initialId={route.id} go={go} />; break;
    case "shipments": body = <Shipments go={go} />; break;
    case "browsing":  body = <BrowsingReport go={go} route={route} />; break;
    case "production": body = <Production go={go} sub={route.sub} tab={route.tab} />; break;
    case "outreach": body = <OutreachView go={go} id={route.id} />; break;
    case "conversions": body = <ConversionsReport go={go} />; break;
    case "analytics": body = <AnalyticsDashboard go={go} />; break;
    case "email": body = <EmailReport go={go} />; break;
    case "marketing": body = <MarketingView go={go} />; break;
    case "reviews": body = <ReviewsView go={go} />; break;
    case "attachments": body = <Attachments go={go} />; break;
    case "gangqueue": body = <GangQueue go={go} />; break;
    case "designs":    body = <DesignsView go={go} />; break;
    case "transfers":  body = <TransfersView go={go} />; break;
    case "pricing": body = <PricingView go={go} />; break;
    case "stock":     body = route.sub === "forecast" ? <ForecastView go={go} /> : <StockView go={go} />; break;
    case "settings":  body = <Settings />; break;
    case "shop":      body = <div className="shop-preview-wrap"><ShopView slug={route.slug} name={route.id} go={go} preview shops={myShops} routeTab={route.tab} routeFilter={route.filter} /></div>; break;
    default:          body = <OrdersList go={go} />;
  }

  return (
    <div className={"app" + (collapsed ? " is-collapsed" : "")}>
      <Sidebar route={route} go={go} counts={counts} userEmail={userEmail} shops={myShops} open={navOpen} onClose={()=>setNavOpen(false)} />
      <div className="main">
        {updateReady && (
          <div className="update-bar" role="alert">
            <Icon name="spark" />
            <span>A new version of the app is available.</span>
            <button className="btn btn--sm update-bar__btn" onClick={() => location.reload()}>Reload to update</button>
          </div>
        )}
        <Topbar title={title} sub={sub} route={route} go={go} onReload={reloadAll} reloading={reloading} onMenu={toggleNav} />
        <div className="content" style={isInbox ? { overflow:"hidden" } : null}>
          {DTF.loadError && (
            <div className="warn-banner">
              <Icon name="spark" /> Shopify data unavailable — {DTF.loadError}. Orders &amp; customers won’t show until the Admin token is refreshed. Settings &amp; Inbox still work.
              <button className="linklike" style={{marginLeft:8}} onClick={reloadAll}>Retry</button>
            </div>
          )}
          {body}
        </div>
      </div>
    </div>
  );
}

// App-wide error boundary. A single unguarded throw in any view used to blank
// the whole page (React unmounts the tree on an uncaught render error). This
// catches it, shows a recoverable panel with the error, and offers Reload /
// Go home instead of a white screen. React only recovers render/lifecycle
// errors via boundaries — async handlers still need their own try/catch.
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { error: null }; }
  static getDerivedStateFromError(error) { return { error }; }
  componentDidCatch(error, info) { console.error("[ErrorBoundary]", error, info && info.componentStack); }
  render() {
    if (!this.state.error) return this.props.children;
    const msg = (this.state.error && (this.state.error.message || String(this.state.error))) || "Unknown error";
    return (
      <div style={{ maxWidth: 560, margin: "12vh auto", padding: "0 20px", fontFamily: "inherit" }}>
        <div className="card">
          <div className="card__body">
            <h3 style={{ margin: "0 0 8px" }}>Something broke on this screen</h3>
            <p className="dim" style={{ fontSize: 13, lineHeight: 1.5, marginTop: 0 }}>
              The page hit an error and stopped rendering. Nothing was lost — reload to carry on. If it keeps happening, tell Slava what you were doing.
            </p>
            <pre style={{ background: "#faf0f0", color: "#8a1c1c", border: "1px solid #f0d0d0", borderRadius: 6, padding: "8px 10px", fontSize: 12, whiteSpace: "pre-wrap", wordBreak: "break-word", margin: "10px 0 14px" }}>{msg}</pre>
            <div style={{ display: "flex", gap: 10 }}>
              <button className="btn btn--primary" onClick={() => location.reload()}>Reload page</button>
              <button className="btn btn--ghost" onClick={() => { location.hash = "#/orders"; location.reload(); }}>Go to orders</button>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

ReactDOM.createRoot(document.getElementById("root")).render(<ErrorBoundary><App /></ErrorBoundary>);
