// MostlyQR — dashboard (the app). Auth-gated; lists the account's dynamic codes,
// creates/edits them (repoint a printed code's destination — the dynamic value),
// shows per-code + account analytics (plan-gated), and account/billing settings.
// All data flows through window.MQRApi (real callables, or demo data offline).
const I18N = (typeof window !== 'undefined' && window.ForgeI18n) || { t: (k) => k, has: () => false, locale: 'en', dir: 'ltr', fmt: { number: (n)=>String(n??''), currency:(n)=>String(n??''), percent:(n)=>String(n??''), date:(d)=>String(d??''), time:(d)=>String(d??''), relative:(d)=>String(d??'') } };
const tr = I18N.t, fmt = I18N.fmt;
const { Icon, MQMark, MTMark, Wordmark, QR, Barcode, bareUrl } = window.MQR;
const F = window.ForgeDesignSystem_e40d74;
const { Button, Badge, Field, Input } = F;
const Api = window.MQRApi;

// Client-side mirror of billing.config.js `limits.dynamic_codes` (for the usage
// display only — the real cap is enforced server-side). Keep in sync with it.
const PLAN_LIMITS = { free: 3, lite: 10, pro: 50, team: 250, enterprise: 250 };
const PLAN_LABEL = { free: tr('mqr.dashboard.plan.free'), lite: tr('mqr.dashboard.plan.lite'), pro: tr('mqr.dashboard.plan.pro'), team: tr('mqr.dashboard.plan.team'), enterprise: tr('mqr.dashboard.plan.enterprise') };
const PLAN_ORDER = ["free", "lite", "pro", "team"];
// Per-month price for each paid plan, monthly vs annual (billed yearly). Mirrors
// the Landing pricing cards + billing.config.js amounts.
const PLAN_PRICE = { lite: { mo: 4, yr: 3 }, pro: { mo: 9, yr: 7 }, team: { mo: 29, yr: 24 } };
const ACCENT = "#5b8def"; // blue accent; QR fills are literal SVG colors, not CSS vars

function num(n) { return fmt.number(n || 0); }
function tsAgo(ts) {
  if (!ts) return tr('mqr.dashboard.lastscan.never');
  return fmt.relative(ts);
}
function hostOf(url) { try { return new URL(url).host.replace(/^www\./, ""); } catch { return url; } }

/* ── Topbar — shared AppHeader + AccountMenu (window.ForgeKit) ──────────────
   Pixels/behaviour come from the framework kit; MostlyQR only supplies its brand
   marks, the nav tabs, and the account actions (billing injected as menu items,
   so the kit never imports billing). */
const { AppHeader, AccountMenu, SignupSurvey, UpgradeDialog } = window.ForgeKit || {};
function Topbar({ view, setView, plan, email, onSignOut, onUpgrade }) {
  const tabs = [["codes", tr('mqr.dashboard.tab.codes')], ["analytics", tr('mqr.dashboard.tab.analytics')], ["pages", tr('mqr.dashboard.tab.pages')], ["serialized", tr('mqr.dashboard.tab.serialized')], ["settings", tr('mqr.dashboard.tab.settings')]];
  const items = [
    { id: "settings", label: tr('auth.account.settings'), icon: "sliders", onClick: () => setView("settings") },
    ...(plan !== "team" ? [{ id: "upgrade", label: tr('auth.account.upgrade'), icon: "zap", onClick: () => onUpgrade() }] : []),
    ...(plan !== "free" ? [{ id: "billing", label: tr('auth.account.billing'), icon: "card", onClick: () => Api.billingPortal() }] : []),
  ];
  return (
    <AppHeader
      brand={<a className="forge-appheader__brand" href="index.html"><MQMark s={26} /><Wordmark size={16} /></a>}
      nav={tabs.map(([k, l]) => ({ key: k, label: l, active: view === k, onClick: () => setView(k) }))}
      actions={<>
        <Badge tone="accent">{PLAN_LABEL[plan] || PLAN_LABEL.free}</Badge>
        <AccountMenu email={email} items={items} onSignOut={onSignOut} />
      </>}
    />
  );
}

/* ── Stat row ───────────────────────────────────────────── */
function Stat({ label, value, delta, sub }) {
  return (
    <div className="ap-stat">
      <div className="ap-stat__label">{label}</div>
      <div className="ap-stat__value">{value}</div>
      {delta && <div className={"ap-stat__delta " + (delta.startsWith("+") ? "up" : "flat")}>{delta}</div>}
      {sub && <div className="ap-stat__sub">{sub}</div>}
    </div>
  );
}

/* ── Codes table ────────────────────────────────────────── */
function StatusPill({ status }) {
  const s = status === "frozen" ? "frozen" : status === "disabled" ? "disabled" : "active";
  const label = s === "frozen" ? tr('mqr.dashboard.status.frozen') : s === "disabled" ? tr('mqr.dashboard.status.disabled') : tr('mqr.dashboard.status.active');
  return <span className={"ap-status ap-status--" + s}>{label}</span>;
}

function CodesTable({ links, onAnalytics, onFlow, onDelete }) {
  if (!links.length) {
    return (
      <div className="ap-gate">
        <div className="ap-gate__ic"><Icon name="qr" size={22} /></div>
        <div className="ap-gate__t">{tr('mqr.dashboard.empty.title')}</div>
        <div className="ap-gate__d">{tr('mqr.dashboard.empty.body')}</div>
        <a href="/Builder" style={{ textDecoration: "none" }}><Button variant="primary" iconRight={<Icon name="arrow" size={15} />}>{tr('mqr.dashboard.empty.cta')}</Button></a>
      </div>
    );
  }
  return (
    <table className="ap-codes">
      <thead>
        <tr>
          <th>{tr('mqr.dashboard.col.code')}</th><th>{tr('mqr.dashboard.col.destination')}</th><th>{tr('mqr.dashboard.col.scans')}</th><th>{tr('mqr.dashboard.col.status')}</th><th>{tr('mqr.dashboard.col.lastscan')}</th><th></th>
        </tr>
      </thead>
      <tbody>
        {links.map((l) => (
          <tr key={l.code}>
            <td>
              <div className="ap-code__cell">
                <span className="ap-code__qr">
                  {l.style && l.style.renderMode === "barcode" ? (
                    <Barcode data={bareUrl(l.short_url)} format={l.style.barcodeFormat || "code128"}
                      fg={l.style.fg || "#1d1d1f"} bg={l.style.bg || "#ffffff"} showText={false} width={120} />
                  ) : (
                    <QR data={bareUrl(l.short_url)} size={34} quiet={1} eyeColor={ACCENT}
                      fg="#1d1d1f" bodyShape="rounded" frameShape="rounded" ballShape="rounded" />
                  )}
                </span>
                <span>
                  <div className="ap-code__title">{l.title || tr('mqr.dashboard.untitled')}</div>
                  <div className="ap-code__url">{(l.short_url || "").replace(/^https?:\/\//, "")}</div>
                </span>
              </div>
            </td>
            <td><div className="ap-code__dest">{hostOf(l.destination_url)}</div></td>
            <td><span className="ap-code__scans">{num(l.scan_count)}</span></td>
            <td><StatusPill status={l.status} /></td>
            <td style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>{tsAgo(l.last_scanned_at)}</td>
            <td>
              <div className="ap-code__actions">
                <button className="ap-iconbtn" title={tr('mqr.dashboard.action.analytics')} onClick={() => onAnalytics(l)}><Icon name="chart" size={15} /></button>
                <button className="ap-iconbtn" title={tr('mqr.dashboard.action.edit')} onClick={() => onFlow(l)}><Icon name="pencil" size={15} /></button>
                <button className="ap-iconbtn danger" title={tr('mqr.dashboard.action.delete')} onClick={() => onDelete(l)}><Icon name="x" size={15} /></button>
              </div>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

/* ── Analytics view ─────────────────────────────────────── */
function Bars({ series }) {
  const max = Math.max(1, ...series.map((d) => d.count));
  // A few "nice" integer ticks for the Y axis, max (top) → 0 (bottom).
  const steps = max <= 5 ? max : 4;
  const yTicks = [];
  for (let i = steps; i >= 0; i--) {
    const v = Math.round((max * i) / steps);
    if (!yTicks.length || v !== yTicks[yTicks.length - 1]) yTicks.push(v);
  }
  return (
    <div>
      <div className="ap-chart">
        <div className="ap-chart__y" aria-hidden="true">
          {yTicks.map((t, i) => <span key={i}>{num(t)}</span>)}
        </div>
        <div className="ap-chart__main">
          <div className="ap-bars">
            {series.map((d, i) => (
              <span key={i} className="ap-bar" style={{ height: (6 + (d.count / max) * 128) + "px" }} title={tr('mqr.dashboard.scans.count', { count: d.count })} />
            ))}
          </div>
          <div className="ap-chartcap"><span>{tr('mqr.dashboard.chart.start')}</span><span>{tr('mqr.dashboard.chart.end')}</span></div>
        </div>
      </div>
    </div>
  );
}
function Breakdown({ rows }) {
  const total = Math.max(1, rows.reduce((s, r) => s + r[1], 0));
  return (
    <div className="ap-break">
      {rows.map(([label, v]) => (
        <div className="ap-break__row" key={label}>
          <span className="ap-break__label">{label}</span>
          <span className="ap-break__track"><span className="ap-break__fill" style={{ width: Math.round((v / total) * 100) + "%" }} /></span>
          <span className="ap-break__val">{num(v)}</span>
        </div>
      ))}
    </div>
  );
}
// Which device family each OS rolls up under — so the OS rows nest (indented)
// beneath their device in the "By device & OS" panel for a clear hierarchy.
const OS_FAMILY = {
  ios: "mobile", android: "mobile", ipados: "tablet",
  windows: "desktop", macos: "desktop", "mac os": "desktop", linux: "desktop",
  "chrome os": "desktop", chromeos: "desktop",
};
// Device breakdown with OS nested underneath its family (mobile → iOS/Android,
// desktop → Windows/macOS/Linux). Bars share one denominator (captured scans) so
// a child's bar is directly comparable to its parent's.
function DeviceOSBreakdown({ byDevice, byOS }) {
  const devs = Array.isArray(byDevice) ? byDevice : [];
  const oss = Array.isArray(byOS) ? byOS : [];
  const total = Math.max(1, devs.reduce((s, r) => s + (r[1] || 0), 0));
  const familyOf = (l) => OS_FAMILY[String(l).toLowerCase()] || "other";
  const shown = new Set(devs.map((d) => String(d[0]).toLowerCase()));
  const row = (label, v, child) => (
    <div className={"ap-break__row" + (child ? " ap-break__row--child" : "")} key={(child ? "os:" : "dev:") + label}>
      <span className="ap-break__label">{label}</span>
      <span className="ap-break__track"><span className="ap-break__fill" style={{ width: Math.round((v / total) * 100) + "%" }} /></span>
      <span className="ap-break__val">{num(v)}</span>
    </div>
  );
  // OS whose device family has no row of its own — surface them (indented) at the end.
  const orphans = oss.filter((o) => !shown.has(familyOf(o[0])));
  return (
    <div className="ap-break">
      {devs.flatMap(([devLabel, devCount]) => [
        row(devLabel, devCount, false),
        ...oss.filter((o) => familyOf(o[0]) === String(devLabel).toLowerCase()).map(([os, c]) => row(os, c, true)),
      ])}
      {orphans.map(([os, c]) => row(os, c, true))}
    </div>
  );
}
/* ── Scan map (heat ⇄ live) ───────────────────────────────────────────────────
 * Wraps the @mostly-tiny/geo map kit (window.MostlyGeo). Heatmap = the durable
 * density from getLinkStats (respects the range selector); Live = pins streaming in
 * from the RTDB scan stream (Api.subscribeScanStream), seeded by livePoints. Manual
 * toggle — no auto-cycle (that's the marketing mock, not the working tool). Degrades
 * to a branded placeholder when no MapTiler key is configured. */
const MAP_RANGES = [["24h", "mqr.dashboard.map.range.24h"], ["7d", "mqr.dashboard.map.range.7d"], ["30d", "mqr.dashboard.map.range.30d"], ["all", "mqr.dashboard.map.range.all"]];
function MapAnalytics({ code, analytics, range, onRange }) {
  const elRef = React.useRef(null);
  const ctrlRef = React.useRef(null);
  const [mode, setMode] = React.useState("heat");
  const heatPoints = (analytics && analytics.heatPoints) || [];
  const livePoints = (analytics && analytics.livePoints) || [];
  const mapCfg = React.useMemo(() => (Api.geoMapConfig ? Api.geoMapConfig() : { styleUrl: "", maptilerKey: "", liveAvailable: false }), []);

  // Create the map once (per code), tear down on unmount/code change.
  React.useEffect(() => {
    const MG = window.MostlyGeo;
    if (!MG || !elRef.current) return undefined;
    const ctrl = MG.createMap(elRef.current, {
      styleUrl: mapCfg.styleUrl,       // any MapLibre style (provider-agnostic)
      maptilerKey: mapCfg.maptilerKey, // convenience fallback if a key is set instead
      mode: "heat",
      heatPoints,
      livePoints,
      liveLimit: 100,
      labels: { placeholder: tr("mqr.dashboard.map.placeholder") },
      onReady: (c) => c.fit(heatPoints && heatPoints.length ? heatPoints : livePoints),
    });
    ctrlRef.current = ctrl;
    return () => { try { ctrl.destroy(); } catch (e) {} ctrlRef.current = null; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [code]);

  // Feed fresh heat points when stats/range change.
  React.useEffect(() => { if (ctrlRef.current) ctrlRef.current.setHeatPoints(heatPoints); }, [heatPoints]);
  React.useEffect(() => { if (ctrlRef.current) ctrlRef.current.setLivePoints(livePoints); }, [livePoints]);

  // Mode switch: push to the controller; subscribe to the live stream only in Live mode.
  React.useEffect(() => {
    const ctrl = ctrlRef.current;
    if (ctrl) ctrl.setMode(mode);
    if (mode !== "live") return undefined;
    if (!Api.subscribeScanStream) return undefined;
    const unsub = Api.subscribeScanStream(code, {
      onPoint: (p) => { if (ctrlRef.current) ctrlRef.current.pushLivePoint(p); },
    });
    return () => { try { unsub && unsub(); } catch (e) {} };
  }, [mode, code]);

  return (
    <div className="ap-panel ap-map" style={{ marginBottom: 16 }}>
      <div className="ap-panel__head">
        <h3 className="ap-panel__title">{tr("mqr.dashboard.map.title")}</h3>
        <div className="ap-map__controls">
          {mode === "heat" && (
            <select className="forge-select ap-map__range" value={range} onChange={(e) => onRange(e.target.value)} aria-label={tr("mqr.dashboard.map.rangelabel")}>
              {MAP_RANGES.map(([v, k]) => <option key={v} value={v}>{tr(k)}</option>)}
            </select>
          )}
          <div className="ap-map__toggle" role="tablist">
            <button type="button" role="tab" aria-selected={mode === "heat"} className={"ap-map__tab" + (mode === "heat" ? " is-on" : "")} onClick={() => setMode("heat")}>{tr("mqr.dashboard.map.heatmap")}</button>
            <button type="button" role="tab" aria-selected={mode === "live"} className={"ap-map__tab" + (mode === "live" ? " is-on" : "")} onClick={() => setMode("live")}>{tr("mqr.dashboard.map.live")}</button>
          </div>
        </div>
      </div>
      <div className="ap-map__canvas" ref={elRef} />
      <div className="ap-map__foot">
        {mode === "heat"
          ? <span className="mtgeo-legend"><span>{tr("mqr.dashboard.map.less")}</span><span className="mtgeo-legend__bar" /><span>{tr("mqr.dashboard.map.more")}</span></span>
          : <span className="ap-map__livedot">{tr(mapCfg.liveAvailable || mapCfg.demo ? "mqr.dashboard.map.livenote" : "mqr.dashboard.map.liveseed")}</span>}
        {/* Honesty label: scan locations are IP-derived (~city level), not precise GPS. */}
        <span className="ap-map__approx" title={tr("mqr.dashboard.map.approxtip")}>{tr("mqr.dashboard.map.approxnote")}</span>
      </div>
    </div>
  );
}

function AnalyticsView({ links, selected, setSelected, onUpgrade }) {
  const [stats, setStats] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [range, setRange] = React.useState("all");
  const code = selected || (links[0] && links[0].code);
  React.useEffect(() => {
    if (!code) { setLoading(false); return; }
    setLoading(true);
    Api.getLinkStats(code, range).then((s) => { setStats(s); setLoading(false); }).catch(() => setLoading(false));
  }, [code, range]);

  if (!code) return <div className="ap-panel"><div className="ap-gate"><div className="ap-gate__t">{tr('mqr.dashboard.analytics.empty')}</div></div></div>;

  return (
    <div>
      <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 16, flexWrap: "wrap" }}>
        <span style={{ fontSize: 13, color: "var(--fg-muted)" }}>{tr('mqr.dashboard.analytics.showing')}</span>
        <select className="forge-select" value={code} onChange={(e) => setSelected(e.target.value)}
          style={{ minWidth: 220 }}>
          {links.map((l) => <option key={l.code} value={l.code}>{l.title || l.code} — {l.code}</option>)}
        </select>
      </div>

      {loading ? (
        <div className="ap-panel"><div className="ap-panel__body" style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div></div>
      ) : stats && stats.upgradeRequired ? (
        <div className="ap-panel"><div className="ap-gate">
          <div className="ap-gate__ic"><Icon name="chart" size={22} /></div>
          <div className="ap-gate__t">{tr('mqr.dashboard.analytics.gate.title', { count: stats.scan_count, capped: stats.capped ? '+' : '' })}</div>
          <div className="ap-gate__d">{tr('mqr.dashboard.analytics.gate.body')}</div>
          <Button variant="primary" iconRight={<Icon name="arrow" size={15} />} onClick={() => onUpgrade && onUpgrade()}>{tr('mqr.dashboard.analytics.gate.cta')}</Button>
        </div></div>
      ) : stats ? (
        <>
          <div className="ap-stats" style={{ gridTemplateColumns: "repeat(3,1fr)" }}>
            <Stat label={tr('mqr.dashboard.stat.totalscans')} value={num(stats.scan_count)} delta="+12.4%" sub={tr('mqr.dashboard.stat.vsprev')} />
            <Stat label={tr('mqr.dashboard.stat.topcountry')} value={(stats.analytics.byCountry[0] || ["—"])[0]} sub={tr('mqr.dashboard.scans.count', { count: (stats.analytics.byCountry[0] || [0, 0])[1] })} />
            <Stat label={tr('mqr.dashboard.stat.mobileshare')} value={fmt.percent((stats.analytics.byDevice.find((d) => d[0] === "mobile") || [0, 0])[1] / Math.max(1, stats.analytics.total))} sub={tr('mqr.dashboard.stat.ofallscans')} />
          </div>
          <MapAnalytics code={code} analytics={stats.analytics} range={range} onRange={setRange} />
          <div className="ap-panel" style={{ marginBottom: 16 }}>
            <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.panel.scansovertime')}</h3>
              {stats.analytics.truncated && <span style={{ fontSize: 11.5, color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.panel.recentwindow')}</span>}</div>
            <div className="ap-panel__body"><Bars series={stats.analytics.byDay} /></div>
          </div>
          <div className="ap-grid2">
            <div className="ap-panel">
              <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.panel.bycountry')}</h3></div>
              <div className="ap-panel__body"><Breakdown rows={stats.analytics.byCountry} /></div>
            </div>
            <div className="ap-panel">
              <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.panel.bydevice')}</h3></div>
              <div className="ap-panel__body">
                <DeviceOSBreakdown byDevice={stats.analytics.byDevice} byOS={stats.analytics.byOS} />
              </div>
            </div>
          </div>
          {/* Sub-country geo: region + city (from IP). Shown only when populated —
              region/city need the edge geo headers (Cloudflare), so older/un-proxied
              traffic has none and these panels would be empty. */}
          {((stats.analytics.byRegion && stats.analytics.byRegion.length) || (stats.analytics.byCity && stats.analytics.byCity.length)) ? (
            <div className="ap-grid2">
              <div className="ap-panel">
                <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.panel.byregion')}</h3></div>
                <div className="ap-panel__body"><Breakdown rows={stats.analytics.byRegion} /></div>
              </div>
              <div className="ap-panel">
                <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.panel.bycity')}</h3></div>
                <div className="ap-panel__body"><Breakdown rows={stats.analytics.byCity} /></div>
              </div>
            </div>
          ) : null}
          {/* Geo zones: only when geo_radius rules are in play (labelled scans). The
              "blocked" tally surfaces restrict-to-a-zone denials. */}
          {(stats.analytics.byZone && stats.analytics.byZone.length) || stats.analytics.blocked ? (
            <div className="ap-panel">
              <div className="ap-panel__head">
                <h3 className="ap-panel__title">{tr('mqr.dashboard.panel.byzone')}</h3>
                {stats.analytics.blocked ? <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>{tr('mqr.dashboard.panel.blocked', { count: stats.analytics.blocked })}</span> : null}
              </div>
              <div className="ap-panel__body"><Breakdown rows={stats.analytics.byZone} /></div>
            </div>
          ) : null}
        </>
      ) : null}
    </div>
  );
}

/* ── Settings view ──────────────────────────────────────── */
/* ── API keys (F008) ────────────────────────────────────────
   Pro+ feature. Lists the account's keys (masked) and mints new ones — the full key is shown
   exactly ONCE on creation (we only ever store a hash), so it's surfaced in a copyable box. */
const API_PLANS = new Set(["pro", "team", "enterprise"]);
function ApiKeysPanel({ plan, onUpgrade }) {
  const showcase = !Api.ready;
  const entitled = API_PLANS.has(plan) || showcase;
  const [keys, setKeys] = React.useState(null);
  const [creating, setCreating] = React.useState(false);
  const [name, setName] = React.useState("");
  const [fresh, setFresh] = React.useState(null); // the just-minted full key (shown once)
  const [busy, setBusy] = React.useState(false);
  const [copied, setCopied] = React.useState(false);

  const load = React.useCallback(() => { Api.listApiKeys().then((k) => setKeys(k || [])).catch(() => setKeys([])); }, []);
  React.useEffect(() => { if (entitled) load(); }, [entitled, load]);

  if (!entitled) {
    return (
      <div className="ap-panel">
        <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.api.title')}</h3></div>
        <div className="ap-panel__body"><div className="ap-gate">
          <div className="ap-gate__ic"><Icon name="lock" size={22} /></div>
          <div className="ap-gate__t">{tr('mqr.dashboard.api.gate.title')}</div>
          <div className="ap-gate__d">{tr('mqr.dashboard.api.gate.body')}</div>
          <Button variant="primary" iconRight={<Icon name="arrow" size={15} />} onClick={() => onUpgrade && onUpgrade()}>{tr('mqr.dashboard.api.gate.cta')}</Button>
        </div></div>
      </div>
    );
  }

  const create = async () => {
    setBusy(true);
    try { const r = await Api.createApiKey(name.trim() || undefined); setFresh(r); setName(""); setCreating(false); load(); }
    catch (e) { window.alert(String(e.message || e)); } finally { setBusy(false); }
  };
  const revoke = async (k) => { setBusy(true); try { await Api.revokeApiKey(k.key_id); load(); } finally { setBusy(false); } };
  const copyFresh = () => { try { navigator.clipboard.writeText(fresh.key); setCopied(true); window.setTimeout(() => setCopied(false), 2000); } catch (e) {} };

  return (
    <div className="ap-panel">
      <div className="ap-panel__head">
        <h3 className="ap-panel__title">{tr('mqr.dashboard.api.title')}</h3>
        <a href="/api/v1/openapi.json" target="_blank" rel="noopener" style={{ fontSize: 13, fontWeight: 600, color: "var(--accent)", textDecoration: "none" }}>{tr('mqr.dashboard.api.docs')}</a>
      </div>
      <div className="ap-panel__body">
        <p className="ap-stat__sub" style={{ marginTop: 0, marginBottom: 14 }}>{tr('mqr.dashboard.api.sub')}</p>

        {fresh && (
          <div className="ap-demo" style={{ flexDirection: "column", alignItems: "stretch", gap: 8, marginBottom: 14 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 600 }}><Icon name="check" size={15} sw={2.2} />{tr('mqr.dashboard.api.created')}</div>
            <code className="forge-mono" style={{ wordBreak: "break-all", padding: "8px 10px", background: "var(--surface-2, rgba(0,0,0,0.04))", borderRadius: 8 }}>{fresh.key}</code>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
              <Button variant="secondary" onClick={copyFresh}><Icon name="copy" size={14} /> {copied ? tr('mqr.dashboard.api.copied') : tr('mqr.dashboard.api.copy')}</Button>
              <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>{tr('mqr.dashboard.api.once')}</span>
              <button className="ap-iconbtn" style={{ marginLeft: "auto" }} onClick={() => setFresh(null)}><Icon name="x" size={15} /></button>
            </div>
          </div>
        )}

        {keys === null ? <div style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div>
          : keys.length === 0 ? <div className="ap-stat__sub" style={{ marginBottom: 12 }}>{tr('mqr.dashboard.api.none')}</div>
            : (
              <table className="ap-codes" style={{ marginBottom: 12 }}>
                <thead><tr><th>{tr('mqr.dashboard.api.name')}</th><th>{tr('mqr.dashboard.api.key')}</th><th>{tr('mqr.dashboard.api.lastused')}</th><th></th></tr></thead>
                <tbody>
                  {keys.map((k) => (
                    <tr key={k.key_id}>
                      <td>{k.name || "—"}</td>
                      <td><code className="forge-mono">mtq_{k.mode || "live"}_{String(k.key_id).slice(0, 4)}…{k.last4}</code></td>
                      <td style={{ color: "var(--fg-muted)" }}>{k.last_used_at ? tr('mqr.dashboard.api.used') : tr('mqr.dashboard.api.neverused')}</td>
                      <td style={{ textAlign: "right" }}><button className="ap-iconbtn danger" title={tr('mqr.dashboard.api.revoke')} disabled={busy} onClick={() => revoke(k)}><Icon name="x" size={15} /></button></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}

        {creating ? (
          <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
            <div style={{ flex: 1 }}><Field label={tr('mqr.dashboard.api.name')}><Input value={name} placeholder={tr('mqr.dashboard.api.name.placeholder')} onChange={(e) => setName(e.target.value)} /></Field></div>
            <Button variant="primary" disabled={busy} onClick={create}>{busy ? tr('mqr.dashboard.saving') : tr('mqr.dashboard.api.generate')}</Button>
            <Button variant="secondary" onClick={() => setCreating(false)}>{tr('mqr.dashboard.drawer.cancel')}</Button>
          </div>
        ) : (
          <Button variant="primary" iconLeft={<Icon name="plus" size={15} sw={2.2} />} onClick={() => setCreating(true)}>{tr('mqr.dashboard.api.new')}</Button>
        )}
      </div>
    </div>
  );
}

/* ── Custom domains / white-label (F017) ────────────────────
   Team+ feature. Register a brand host, prove control via a DNS TXT record, then point it (CNAME)
   at our infra. TLS provisioning for a verified host is an operator step (flagged in the UI). */
const DOMAIN_PLANS = new Set(["team", "enterprise"]);
function CustomDomainsPanel({ plan, onUpgrade }) {
  const showcase = !Api.ready;
  const entitled = DOMAIN_PLANS.has(plan) || showcase;
  const [domains, setDomains] = React.useState(null);
  const [adding, setAdding] = React.useState(false);
  const [host, setHost] = React.useState("");
  const [inst, setInst] = React.useState(null); // verification instructions for a just-added host
  const [busy, setBusy] = React.useState(false);

  const load = React.useCallback(() => { Api.listCustomDomains().then((d) => setDomains(d || [])).catch(() => setDomains([])); }, []);
  React.useEffect(() => { if (entitled) load(); }, [entitled, load]);

  if (!entitled) {
    return (
      <div className="ap-panel">
        <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.domains.title')}</h3></div>
        <div className="ap-panel__body"><div className="ap-gate">
          <div className="ap-gate__ic"><Icon name="globe" size={22} /></div>
          <div className="ap-gate__t">{tr('mqr.dashboard.domains.gate.title')}</div>
          <div className="ap-gate__d">{tr('mqr.dashboard.domains.gate.body')}</div>
          <Button variant="primary" iconRight={<Icon name="arrow" size={15} />} onClick={() => onUpgrade && onUpgrade()}>{tr('mqr.dashboard.domains.gate.cta')}</Button>
        </div></div>
      </div>
    );
  }

  const add = async () => {
    const h = host.trim().toLowerCase();
    if (!h) return;
    setBusy(true);
    try { const r = await Api.addCustomDomain(h); setInst(r); setHost(""); setAdding(false); load(); }
    catch (e) { window.alert(String((e && e.message) || e)); } finally { setBusy(false); }
  };
  const verify = async (d) => {
    setBusy(true);
    try { const r = await Api.verifyCustomDomain(d.host); if (!r.verified) window.alert(tr('mqr.dashboard.domains.notyet')); load(); }
    finally { setBusy(false); }
  };
  const remove = async (d) => { if (!window.confirm(tr('mqr.dashboard.domains.confirmremove', { host: d.host }))) return; setBusy(true); try { await Api.removeCustomDomain(d.host); if (inst && inst.host === d.host) setInst(null); load(); } finally { setBusy(false); } };

  return (
    <div className="ap-panel">
      <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.domains.title')}</h3></div>
      <div className="ap-panel__body">
        <p className="ap-stat__sub" style={{ marginTop: 0, marginBottom: 14 }}>{tr('mqr.dashboard.domains.sub')}</p>

        {inst && (
          <div className="ap-demo" style={{ flexDirection: "column", alignItems: "stretch", gap: 8, marginBottom: 14 }}>
            <div style={{ fontWeight: 600 }}>{tr('mqr.dashboard.domains.dns', { host: inst.host })}</div>
            <code className="forge-mono" style={{ display: "block", padding: 8, background: "var(--surface-2, rgba(0,0,0,0.04))", borderRadius: 8, fontSize: 12, whiteSpace: "pre-wrap", wordBreak: "break-all" }}>{`${inst.txt.type}  ${inst.txt.name}\n     ${inst.txt.value}\n\n${inst.cname.type}  ${inst.cname.name}\n     ${inst.cname.value}`}</code>
            <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>{tr('mqr.dashboard.domains.dnshint')}</span>
          </div>
        )}

        {domains === null ? <div style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div>
          : domains.length === 0 ? <div className="ap-stat__sub" style={{ marginBottom: 12 }}>{tr('mqr.dashboard.domains.none')}</div>
            : (
              <table className="ap-codes" style={{ marginBottom: 12 }}>
                <thead><tr><th>{tr('mqr.dashboard.domains.host')}</th><th>{tr('mqr.dashboard.domains.status')}</th><th></th></tr></thead>
                <tbody>
                  {domains.map((d) => (
                    <tr key={d.host}>
                      <td><code className="forge-mono">{d.host}</code></td>
                      <td><span className={"ap-status ap-status--" + (d.status === "verified" ? "active" : "frozen")}>{d.status === "verified" ? tr('mqr.dashboard.domains.verified') : tr('mqr.dashboard.domains.pending')}</span></td>
                      <td style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                        {d.status !== "verified" && <Button variant="secondary" disabled={busy} onClick={() => verify(d)}>{tr('mqr.dashboard.domains.verify')}</Button>}
                        <button className="ap-iconbtn danger" style={{ marginLeft: 8 }} title={tr('mqr.dashboard.domains.remove')} disabled={busy} onClick={() => remove(d)}><Icon name="x" size={15} /></button>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}

        {adding ? (
          <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
            <div style={{ flex: 1 }}><Field label={tr('mqr.dashboard.domains.host')}><Input value={host} placeholder="qr.yourbrand.com" onChange={(e) => setHost(e.target.value)} /></Field></div>
            <Button variant="primary" disabled={busy} onClick={add}>{busy ? tr('mqr.dashboard.saving') : tr('mqr.dashboard.domains.add')}</Button>
            <Button variant="secondary" onClick={() => setAdding(false)}>{tr('mqr.dashboard.drawer.cancel')}</Button>
          </div>
        ) : (
          <Button variant="primary" iconLeft={<Icon name="plus" size={15} sw={2.2} />} onClick={() => setAdding(true)}>{tr('mqr.dashboard.domains.new')}</Button>
        )}
      </div>
    </div>
  );
}

/* ── Team workspace: members, invites, roles (F009) ─────────
   Team+ feature. Owner/admin can invite (email + role), change roles (owner only), and remove
   members. A member's requests resolve to this shared workspace server-side. */
const TEAM_PLANS = new Set(["team", "enterprise"]);
const TEAM_ROLES = ["admin", "member", "viewer"];
function TeamPanel({ plan, onUpgrade }) {
  const showcase = !Api.ready;
  const entitled = TEAM_PLANS.has(plan) || showcase;
  const [data, setData] = React.useState(null);
  const [inviting, setInviting] = React.useState(false);
  const [email, setEmail] = React.useState("");
  const [role, setRole] = React.useState("member");
  const [busy, setBusy] = React.useState(false);

  const load = React.useCallback(() => { Api.listMembers().then((d) => setData(d)).catch(() => setData({ members: [], invites: [], my_role: "member" })); }, []);
  React.useEffect(() => { if (entitled) load(); }, [entitled, load]);

  if (!entitled) {
    return (
      <div className="ap-panel">
        <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.team.title')}</h3></div>
        <div className="ap-panel__body"><div className="ap-gate">
          <div className="ap-gate__ic"><Icon name="users" size={22} /></div>
          <div className="ap-gate__t">{tr('mqr.dashboard.team.gate.title')}</div>
          <div className="ap-gate__d">{tr('mqr.dashboard.team.gate.body')}</div>
          <Button variant="primary" iconRight={<Icon name="arrow" size={15} />} onClick={() => onUpgrade && onUpgrade()}>{tr('mqr.dashboard.team.gate.cta')}</Button>
        </div></div>
      </div>
    );
  }

  const canManage = data && (data.my_role === "owner" || data.my_role === "admin");
  const isOwner = data && data.my_role === "owner";
  const invite = async () => {
    if (!email.trim()) return;
    setBusy(true);
    try { await Api.inviteMember(email.trim().toLowerCase(), role); setEmail(""); setInviting(false); load(); }
    catch (e) { window.alert(String((e && e.message) || e)); } finally { setBusy(false); }
  };
  const setMemberRole = async (m, r) => { setBusy(true); try { await Api.changeRole(m.uid, r); load(); } catch (e) { window.alert(String((e && e.message) || e)); } finally { setBusy(false); } };
  const remove = async (m) => { if (!window.confirm(tr('mqr.dashboard.team.confirmremove', { email: m.email || m.uid }))) return; setBusy(true); try { await Api.removeMember(m.uid); load(); } finally { setBusy(false); } };
  const revoke = async (i) => { setBusy(true); try { await Api.revokeInvite(i.token); load(); } finally { setBusy(false); } };

  return (
    <div className="ap-panel">
      <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.team.title')}</h3></div>
      <div className="ap-panel__body">
        <p className="ap-stat__sub" style={{ marginTop: 0, marginBottom: 14 }}>{tr('mqr.dashboard.team.sub')}</p>
        {data === null ? <div style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div> : (
          <>
            <table className="ap-codes" style={{ marginBottom: 12 }}>
              <thead><tr><th>{tr('mqr.dashboard.team.member')}</th><th>{tr('mqr.dashboard.team.role')}</th><th></th></tr></thead>
              <tbody>
                {data.members.map((m) => (
                  <tr key={m.uid}>
                    <td>{m.email || m.uid}{m.is_you ? ` (${tr('mqr.dashboard.team.you')})` : ""}</td>
                    <td>
                      {isOwner && m.role !== "owner" ? (
                        <select className="forge-select" value={m.role} disabled={busy} onChange={(e) => setMemberRole(m, e.target.value)}>
                          {TEAM_ROLES.map((r) => <option key={r} value={r}>{tr('mqr.dashboard.team.role.' + r)}</option>)}
                        </select>
                      ) : <span className="ap-status ap-status--active">{tr('mqr.dashboard.team.role.' + m.role)}</span>}
                    </td>
                    <td style={{ textAlign: "right" }}>
                      {canManage && m.role !== "owner" && <button className="ap-iconbtn danger" title={tr('mqr.dashboard.team.remove')} disabled={busy} onClick={() => remove(m)}><Icon name="x" size={15} /></button>}
                    </td>
                  </tr>
                ))}
                {data.invites.map((i) => (
                  <tr key={i.token} style={{ opacity: 0.7 }}>
                    <td>{i.email} <span style={{ fontSize: 11, color: "var(--fg-muted)" }}>· {tr('mqr.dashboard.team.invited')}</span></td>
                    <td><span className="ap-status ap-status--frozen">{tr('mqr.dashboard.team.role.' + i.role)}</span></td>
                    <td style={{ textAlign: "right" }}>{canManage && <button className="ap-iconbtn danger" title={tr('mqr.dashboard.team.revoke')} disabled={busy} onClick={() => revoke(i)}><Icon name="x" size={15} /></button>}</td>
                  </tr>
                ))}
              </tbody>
            </table>

            {canManage && (inviting ? (
              <div style={{ display: "flex", gap: 8, alignItems: "flex-end", flexWrap: "wrap" }}>
                <div style={{ flex: 1, minWidth: 180 }}><Field label={tr('mqr.dashboard.team.email')}><Input value={email} placeholder="teammate@yourbrand.com" onChange={(e) => setEmail(e.target.value)} /></Field></div>
                <Field label={tr('mqr.dashboard.team.role')}><select className="forge-select" value={role} onChange={(e) => setRole(e.target.value)}>{TEAM_ROLES.map((r) => <option key={r} value={r}>{tr('mqr.dashboard.team.role.' + r)}</option>)}</select></Field>
                <Button variant="primary" disabled={busy} onClick={invite}>{busy ? tr('mqr.dashboard.saving') : tr('mqr.dashboard.team.send')}</Button>
                <Button variant="secondary" onClick={() => setInviting(false)}>{tr('mqr.dashboard.drawer.cancel')}</Button>
              </div>
            ) : (
              <Button variant="primary" iconLeft={<Icon name="plus" size={15} sw={2.2} />} onClick={() => setInviting(true)}>{tr('mqr.dashboard.team.invite')}</Button>
            ))}
          </>
        )}
      </div>
    </div>
  );
}

/* ── Audit log (F018) ───────────────────────────────────────
   Team+ workspace security trail (owner/admin). SSO/SAML, SCIM and 2FA are Identity-Platform infra
   (flagged for the operator); this is the buildable compliance surface. */
function AuditLogPanel({ plan }) {
  const showcase = !Api.ready;
  const entitled = TEAM_PLANS.has(plan) || showcase;
  const [events, setEvents] = React.useState(null);
  React.useEffect(() => { if (entitled) Api.listAuditLog().then((e) => setEvents(e || [])).catch(() => setEvents([])); }, [entitled]);
  if (!entitled) return null;
  const label = (a) => tr('mqr.dashboard.audit.action.' + a) || a;
  const when = (at) => { try { return new Date((at && at.seconds ? at.seconds * 1000 : Date.parse(at)) || Date.now()).toLocaleString(); } catch (e) { return ""; } };
  return (
    <div className="ap-panel">
      <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.audit.title')}</h3></div>
      <div className="ap-panel__body">
        <p className="ap-stat__sub" style={{ marginTop: 0, marginBottom: 12 }}>{tr('mqr.dashboard.audit.sub')}</p>
        {events === null ? <div style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div>
          : events.length === 0 ? <div className="ap-stat__sub">{tr('mqr.dashboard.audit.none')}</div>
            : (
              <table className="ap-codes">
                <thead><tr><th>{tr('mqr.dashboard.audit.event')}</th><th>{tr('mqr.dashboard.audit.actor')}</th><th>{tr('mqr.dashboard.audit.when')}</th></tr></thead>
                <tbody>
                  {events.map((e) => (
                    <tr key={e.id}>
                      <td>{label(e.action)} {e.detail && (e.detail.email || e.detail.role) ? <span style={{ fontSize: 11, color: "var(--fg-muted)" }}>· {[e.detail.email, e.detail.role].filter(Boolean).join(" · ")}</span> : ""}</td>
                      <td style={{ color: "var(--fg-muted)" }}>{e.actor || "—"}</td>
                      <td style={{ color: "var(--fg-muted)", whiteSpace: "nowrap" }}>{when(e.at)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
      </div>
    </div>
  );
}

function SettingsView({ plan, email, onUpgrade }) {
  const limit = PLAN_LIMITS[plan] || 3;
  return (
    <>
    <div className="ap-grid2">
      <div className="ap-panel">
        <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.settings.billing.title')}</h3></div>
        <div className="ap-panel__body">
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, marginBottom: 18 }}>
            <div>
              <div style={{ fontFamily: "var(--font-display)", fontSize: 18, fontWeight: 600 }}>{tr('mqr.dashboard.settings.planname', { plan: PLAN_LABEL[plan] })}</div>
              <div style={{ fontSize: 13, color: "var(--fg-muted)", marginTop: 2 }}>{tr('mqr.dashboard.settings.upto', { count: limit })}</div>
            </div>
            <div style={{ display: "flex", gap: 10, flexShrink: 0 }}>
              {plan !== "free" && <Button variant="secondary" onClick={() => Api.billingPortal()}>{tr('mqr.dashboard.settings.managebilling')}</Button>}
              {plan !== "team" && <Button variant="primary" onClick={() => onUpgrade()}>{tr('auth.account.upgrade')}</Button>}
            </div>
          </div>
          <div className="ap-stat__sub" style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <Icon name="infinity" size={15} sw={2.2} style={{ color: "var(--accent)" }} />
            {tr('mqr.dashboard.settings.cancelnote')}
          </div>
        </div>
      </div>
      <div className="ap-panel">
        <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.settings.account.title')}</h3></div>
        <div className="ap-panel__body" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <Field label={tr('mqr.dashboard.settings.email')}><Input value={email} disabled /></Field>
          <div style={{ display: "flex", gap: 10 }}>
            <Button variant="secondary" onClick={() => Api.signOut().then(() => window.location.assign("index.html"))}>{tr('mqr.dashboard.signout')}</Button>
          </div>
        </div>
      </div>
    </div>
    <div style={{ marginTop: 16 }}><TeamPanel plan={plan} onUpgrade={onUpgrade} /></div>
    <div style={{ marginTop: 16 }}><AuditLogPanel plan={plan} /></div>
    <div style={{ marginTop: 16 }}><ApiKeysPanel plan={plan} onUpgrade={onUpgrade} /></div>
    <div style={{ marginTop: 16 }}><CustomDomainsPanel plan={plan} onUpgrade={onUpgrade} /></div>
    </>
  );
}

/* ── Serialized codes (F003 — enterprise) ───────────────────
   Mint, manage & bulk-export large populations of unique codes — one per physical UNIT
   or per BATCH — for product & batch traceability, with per-unit/per-batch analytics and
   anti-counterfeit clone detection. Gated to the Enterprise plan; the showcase (?demo=1)
   always renders so the capability is browsable without a backend. */
function SerializedAlerts({ alerts }) {
  if (!alerts || !alerts.length) {
    return <div className="ap-stat__sub" style={{ display: "flex", alignItems: "center", gap: 8 }}>
      <Icon name="shield" size={15} sw={2} style={{ color: "var(--accent)" }} />{tr('mqr.dashboard.serialized.alerts.none')}</div>;
  }
  return (
    <table className="ap-codes">
      <thead><tr>
        <th>{tr('mqr.dashboard.serialized.alerts.serial')}</th>
        <th>{tr('mqr.dashboard.serialized.alerts.reason')}</th>
        <th>{tr('mqr.dashboard.serialized.alerts.countries')}</th>
        <th style={{ textAlign: "right" }}>{tr('mqr.dashboard.serialized.alerts.scans')}</th>
      </tr></thead>
      <tbody>
        {alerts.map((a) => (
          <tr key={a.serial}>
            <td><code className="forge-mono">{a.serial}</code></td>
            <td><span style={{ display: "inline-flex", alignItems: "center", gap: 6, color: "var(--bad, #e8413a)" }}><Icon name="shield" size={14} sw={2} />{a.reason}</span></td>
            <td style={{ color: "var(--fg-muted)" }}>{(a.countries || []).join(" · ")}</td>
            <td style={{ textAlign: "right" }}>{num(a.scan_count)}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function CampaignCard({ c, onMint, onExport, onRepoint, onToggle, onAlerts, onSetAttributes, alerts, busy, refreshKey }) {
  const [dest, setDest] = React.useState(c.destination_url || "");
  // The list payload is a summary (no batches) — fetch the full campaign for its batch rows + passport.
  const [batches, setBatches] = React.useState(c.batches || null);
  const [attrs, setAttrs] = React.useState(c.attributes || []);
  React.useEffect(() => {
    let live = true;
    Api.getSerializedCampaign(c.campaign_id).then((full) => { if (live && full) { setBatches(full.batches || []); setAttrs(full.attributes || []); } }).catch(() => {});
    return () => { live = false; };
  }, [c.campaign_id, refreshKey]);
  const setAttr = (i, k, v) => setAttrs((a) => { const x = a.slice(); x[i] = { ...x[i], [k]: v }; return x; });
  const addAttrRow = () => setAttrs((a) => [...a, { label: "", value: "" }]);
  const delAttrRow = (i) => setAttrs((a) => a.filter((_, j) => j !== i));
  const saveAttrs = () => onSetAttributes(c, attrs.map((a) => ({ label: (a.label || "").trim(), value: (a.value || "").trim() })).filter((a) => a.label && a.value));
  c = Object.assign({}, c, batches ? { batches } : {});
  return (
    <div className="ap-panel" style={{ marginBottom: 16 }}>
      <div className="ap-panel__head">
        <h3 className="ap-panel__title" style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <Icon name="layers" size={17} />{c.name || c.campaign_id}
          <StatusPill status={c.status} />
          {c.clone_alert_count > 0 && (
            <span className="ap-status ap-status--disabled" style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
              <Icon name="shield" size={12} sw={2.2} />{tr('mqr.dashboard.serialized.flagged', { count: c.clone_alert_count })}
            </span>
          )}
        </h3>
        <button className="ap-iconbtn" title={c.status === "disabled" ? tr('mqr.dashboard.serialized.enable') : tr('mqr.dashboard.serialized.disable')}
          onClick={() => onToggle(c)}><Icon name={c.status === "disabled" ? "check" : "lock"} size={15} /></button>
      </div>
      <div className="ap-panel__body">
        <div className="ap-stats" style={{ gridTemplateColumns: "repeat(3,1fr)", marginBottom: 14 }}>
          <Stat label={tr('mqr.dashboard.serialized.stat.units')} value={num(c.unit_count)} sub={tr('mqr.dashboard.serialized.stat.minted')} />
          <Stat label={tr('mqr.dashboard.serialized.stat.scans')} value={num(c.scan_count)} sub={tr('mqr.dashboard.serialized.stat.perunit')} />
          <Stat label={tr('mqr.dashboard.serialized.stat.alerts')} value={num(c.clone_alert_count || 0)} sub={tr('mqr.dashboard.serialized.stat.clones')} />
        </div>

        {/* Instant repoint — every printed unit follows, no reprint. */}
        <Field label={tr('mqr.dashboard.serialized.repoint.label')} hint={tr('mqr.dashboard.serialized.repoint.hint')}>
          <div style={{ display: "flex", gap: 8 }}>
            <Input value={dest} onChange={(e) => setDest(e.target.value)} placeholder="https://brand.example/verify" />
            <Button variant="secondary" disabled={busy || dest.trim() === (c.destination_url || "")} onClick={() => onRepoint(c, dest.trim())}>
              <Icon name="refresh" size={14} /> {tr('mqr.dashboard.serialized.repoint.cta')}
            </Button>
          </div>
        </Field>

        {/* Product passport (F034) — brand attributes shown on the verify page. Edit anytime, no reprint. */}
        <Field label="Product passport" hint="Shown to anyone who verifies a unit — e.g. Model, Release year, Material. Updates every unit instantly.">
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {attrs.map((a, i) => (
              <div key={i} style={{ display: "flex", gap: 8, alignItems: "center" }}>
                <Input value={a.label || ""} placeholder="Label (e.g. Model)" onChange={(e) => setAttr(i, "label", e.target.value)} />
                <Input value={a.value || ""} placeholder="Value (e.g. Air Max 90)" onChange={(e) => setAttr(i, "value", e.target.value)} />
                <button className="ap-iconbtn" type="button" onClick={() => delAttrRow(i)} aria-label="Remove field"><Icon name="x" size={15} /></button>
              </div>
            ))}
            <div style={{ display: "flex", gap: 10 }}>
              <Button variant="secondary" iconLeft={<Icon name="plus" size={14} sw={2.2} />} onClick={addAttrRow}>Add field</Button>
              <Button variant="primary" disabled={busy} onClick={saveAttrs}>{busy ? tr('mqr.dashboard.saving') : "Save passport"}</Button>
            </div>
          </div>
        </Field>

        {c.batches && c.batches.length > 0 && (
          <div style={{ marginTop: 14 }}>
            <div className="ap-stat__label" style={{ marginBottom: 6 }}>{tr('mqr.dashboard.serialized.batches')}</div>
            <table className="ap-codes">
              <thead><tr><th>{tr('mqr.dashboard.serialized.batch')}</th><th style={{ textAlign: "right" }}>{tr('mqr.dashboard.serialized.stat.units')}</th><th style={{ textAlign: "right" }}>{tr('mqr.dashboard.serialized.stat.scans')}</th><th style={{ textAlign: "right" }}></th></tr></thead>
              <tbody>
                {c.batches.map((b) => (
                  <tr key={b.batch_id}>
                    <td>{b.label || b.batch_id}</td>
                    <td style={{ textAlign: "right" }}>{num(b.unit_count)}</td>
                    <td style={{ textAlign: "right" }}>{num(b.scan_count)}</td>
                    <td style={{ textAlign: "right" }}>
                      <button className="ap-iconbtn" title={tr('mqr.dashboard.serialized.export')} disabled={busy} onClick={() => onExport(c, b.batch_id)}><Icon name="download" size={15} /></button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}

        <div style={{ display: "flex", gap: 10, marginTop: 16, flexWrap: "wrap" }}>
          <Button variant="primary" disabled={busy} iconLeft={<Icon name="plus" size={15} sw={2.2} />} onClick={() => onMint(c)}>{tr('mqr.dashboard.serialized.mint.cta')}</Button>
          <Button variant="secondary" disabled={busy} iconLeft={<Icon name="download" size={15} />} onClick={() => onExport(c, null)}>{tr('mqr.dashboard.serialized.exportall')}</Button>
          <Button variant="secondary" disabled={busy} iconLeft={<Icon name="shield" size={15} />} onClick={() => onAlerts(c)}>{tr('mqr.dashboard.serialized.viewalerts')}</Button>
        </div>

        {alerts && alerts.campaign_id === c.campaign_id && (
          <div style={{ marginTop: 16 }}>
            <div className="ap-stat__label" style={{ marginBottom: 6 }}>{tr('mqr.dashboard.serialized.alerts.title')}</div>
            <SerializedAlerts alerts={alerts.rows} />
          </div>
        )}
      </div>
    </div>
  );
}

function SerializedView({ plan, onUpgrade }) {
  const showcase = !Api.ready; // demo mode → always render the showcase (plan derives to "pro")
  const isEnterprise = plan === "enterprise" || showcase;
  const [campaigns, setCampaigns] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [creating, setCreating] = React.useState(false);
  const [form, setForm] = React.useState({ name: "", destination_url: "", attributes: [] });
  const [mint, setMint] = React.useState(null); // { campaign, count, batch }
  const [alerts, setAlerts] = React.useState(null); // { campaign_id, rows }
  const [toast, setToast] = React.useState(null);
  const [refreshKey, setRefreshKey] = React.useState(0);

  const load = React.useCallback(() => {
    Api.listSerializedCampaigns().then((cs) => { setCampaigns(cs || []); setRefreshKey((k) => k + 1); }).catch(() => setCampaigns([]));
  }, []);
  React.useEffect(() => { if (isEnterprise) load(); }, [isEnterprise, load]);

  if (!isEnterprise) {
    return (
      <div className="ap-panel"><div className="ap-gate">
        <div className="ap-gate__ic"><Icon name="layers" size={22} /></div>
        <div className="ap-gate__t">{tr('mqr.dashboard.serialized.gate.title')}</div>
        <div className="ap-gate__d">{tr('mqr.dashboard.serialized.gate.body')}</div>
        <a href="mailto:hi@mostlyqr.com?subject=MostlyQR%20Enterprise%20%E2%80%94%20serialized%20codes" style={{ textDecoration: "none" }}>
          <Button variant="primary" iconRight={<Icon name="arrow" size={15} />}>{tr('mqr.dashboard.serialized.gate.cta')}</Button>
        </a>
      </div></div>
    );
  }

  const flash = (msg) => { setToast(msg); window.setTimeout(() => setToast(null), 4000); };

  // Digital-passport attribute rows on the create form (Product/Model/Release year…).
  const addAttr = () => setForm((f) => ({ ...f, attributes: [...(f.attributes || []), { label: "", value: "" }] }));
  const updateAttr = (i, k, v) => setForm((f) => { const a = (f.attributes || []).slice(); a[i] = { ...a[i], [k]: v }; return { ...f, attributes: a }; });
  const removeAttr = (i) => setForm((f) => ({ ...f, attributes: (f.attributes || []).filter((_, j) => j !== i) }));

  const doCreate = async () => {
    if (!form.destination_url.trim()) return;
    setBusy(true);
    const attributes = (form.attributes || []).map((a) => ({ label: (a.label || "").trim(), value: (a.value || "").trim() })).filter((a) => a.label && a.value);
    try { await Api.createSerializedCampaign({ name: form.name.trim() || null, destination_url: form.destination_url.trim(), attributes }); setForm({ name: "", destination_url: "", attributes: [] }); setCreating(false); load(); }
    catch (e) { flash(String(e.message || e)); } finally { setBusy(false); }
  };
  const doMint = async () => {
    if (!mint) return;
    const count = Math.max(1, parseInt(mint.count, 10) || 0);
    setBusy(true);
    try { const r = await Api.mintSerializedCodes({ campaign_id: mint.campaign.campaign_id, count, batch_id: mint.batch.trim() || undefined }); flash(tr('mqr.dashboard.serialized.mint.queued', { count: num(r.total || count) })); setMint(null); load(); }
    catch (e) { flash(String(e.message || e)); } finally { setBusy(false); }
  };
  const doExport = async (c, batchId) => {
    setBusy(true);
    try { const r = await Api.exportSerializedCodes({ campaign_id: c.campaign_id, batch_id: batchId || undefined }); if (r && r.url) { window.open(r.url, "_blank"); flash(tr('mqr.dashboard.serialized.export.ready', { count: num(r.count) })); } }
    catch (e) { flash(String(e.message || e)); } finally { setBusy(false); }
  };
  const doRepoint = async (c, dest) => {
    setBusy(true);
    try { await Api.repointSerializedCampaign({ campaign_id: c.campaign_id, destination_url: dest }); flash(tr('mqr.dashboard.serialized.repoint.done')); load(); }
    catch (e) { flash(String(e.message || e)); } finally { setBusy(false); }
  };
  const doSetAttributes = async (c, attributes) => {
    setBusy(true);
    try { await Api.repointSerializedCampaign({ campaign_id: c.campaign_id, attributes }); flash("Product passport updated — live on every unit."); load(); }
    catch (e) { flash(String(e.message || e)); } finally { setBusy(false); }
  };
  const doToggle = async (c) => {
    setBusy(true);
    const next = c.status === "disabled" ? "active" : "disabled";
    try { await Api.setSerializedCampaignStatus({ campaign_id: c.campaign_id, status: next }); load(); }
    catch (e) { flash(String(e.message || e)); } finally { setBusy(false); }
  };
  const doAlerts = async (c) => {
    if (alerts && alerts.campaign_id === c.campaign_id) { setAlerts(null); return; }
    try { const rows = await Api.listSerializedAlerts(c.campaign_id); setAlerts({ campaign_id: c.campaign_id, rows }); }
    catch (e) { flash(String(e.message || e)); }
  };

  return (
    <div>
      {toast && <div className="ap-demo" style={{ marginBottom: 14 }}><Icon name="check" size={15} sw={2.2} />{toast}</div>}

      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16, gap: 12, flexWrap: "wrap" }}>
        <p className="ap-sub" style={{ margin: 0 }}>{tr('mqr.dashboard.serialized.sub')}</p>
        <Button variant="primary" iconLeft={<Icon name="plus" size={15} sw={2.2} />} onClick={() => setCreating((v) => !v)}>{tr('mqr.dashboard.serialized.newcampaign')}</Button>
      </div>

      {creating && (
        <div className="ap-panel" style={{ marginBottom: 16 }}>
          <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.serialized.create.title')}</h3></div>
          <div className="ap-panel__body" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
            <Field label={tr('mqr.dashboard.serialized.create.name')}><Input value={form.name} placeholder={tr('mqr.dashboard.serialized.create.name.placeholder')} onChange={(e) => setForm({ ...form, name: e.target.value })} /></Field>
            <Field label={tr('mqr.dashboard.serialized.create.dest')} hint={tr('mqr.dashboard.serialized.create.dest.hint')}><Input value={form.destination_url} placeholder="https://brand.example/verify" onChange={(e) => setForm({ ...form, destination_url: e.target.value })} /></Field>
            {/* Digital product passport — brand-authored fields shown on the verify page. Editable
               later (no reprint), and overridable per batch at mint time. */}
            <Field label="Product passport (shown on scan)" hint="Optional details a shopper sees when they verify — e.g. Model, Release year, Material. Edit anytime; no reprint.">
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {(form.attributes || []).map((a, i) => (
                  <div key={i} style={{ display: "flex", gap: 8, alignItems: "center" }}>
                    <Input value={a.label} placeholder="Label (e.g. Model)" onChange={(e) => updateAttr(i, "label", e.target.value)} />
                    <Input value={a.value} placeholder="Value (e.g. Air Max 90)" onChange={(e) => updateAttr(i, "value", e.target.value)} />
                    <button className="ap-iconbtn" type="button" onClick={() => removeAttr(i)} aria-label="Remove field"><Icon name="x" size={15} /></button>
                  </div>
                ))}
                <div><Button variant="secondary" iconLeft={<Icon name="plus" size={14} sw={2.2} />} onClick={addAttr}>Add field</Button></div>
              </div>
            </Field>
            <div style={{ display: "flex", gap: 10 }}>
              <Button variant="secondary" onClick={() => setCreating(false)}>{tr('mqr.dashboard.drawer.cancel')}</Button>
              <Button variant="primary" disabled={busy || !form.destination_url.trim()} onClick={doCreate}>{busy ? tr('mqr.dashboard.saving') : tr('mqr.dashboard.serialized.create.cta')}</Button>
            </div>
          </div>
        </div>
      )}

      {mint && (
        <div className="ap-panel" style={{ marginBottom: 16 }}>
          <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.serialized.mint.title', { name: mint.campaign.name || mint.campaign.campaign_id })}</h3>
            <button className="ap-iconbtn" onClick={() => setMint(null)}><Icon name="x" size={16} /></button></div>
          <div className="ap-panel__body" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
            <Field label={tr('mqr.dashboard.serialized.mint.count')} hint={tr('mqr.dashboard.serialized.mint.count.hint')}><Input type="number" value={mint.count} onChange={(e) => setMint({ ...mint, count: e.target.value })} /></Field>
            <Field label={tr('mqr.dashboard.serialized.mint.batch')} hint={tr('mqr.dashboard.serialized.mint.batch.hint')}><Input value={mint.batch} placeholder="lot-AW25-03" onChange={(e) => setMint({ ...mint, batch: e.target.value })} /></Field>
            <div style={{ display: "flex", gap: 10 }}>
              <Button variant="secondary" onClick={() => setMint(null)}>{tr('mqr.dashboard.drawer.cancel')}</Button>
              <Button variant="primary" disabled={busy} onClick={doMint}>{busy ? tr('mqr.dashboard.saving') : tr('mqr.dashboard.serialized.mint.go')}</Button>
            </div>
          </div>
        </div>
      )}

      {campaigns === null ? (
        <div className="ap-panel"><div className="ap-panel__body" style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div></div>
      ) : campaigns.length === 0 ? (
        <div className="ap-panel"><div className="ap-gate">
          <div className="ap-gate__ic"><Icon name="layers" size={22} /></div>
          <div className="ap-gate__t">{tr('mqr.dashboard.serialized.empty.title')}</div>
          <div className="ap-gate__d">{tr('mqr.dashboard.serialized.empty.body')}</div>
        </div></div>
      ) : (
        campaigns.map((c) => (
          <CampaignCard key={c.campaign_id} c={c} busy={busy} alerts={alerts} refreshKey={refreshKey}
            onMint={(camp) => setMint({ campaign: camp, count: 1000, batch: "" })}
            onExport={doExport} onRepoint={doRepoint} onToggle={doToggle} onAlerts={doAlerts} onSetAttributes={doSetAttributes} />
        ))
      )}
    </div>
  );
}

/* ── Create / edit drawer ───────────────────────────────── */
function CodeDrawer({ mode, link, onClose, onSaved, onUpgrade }) {
  const [title, setTitle] = React.useState(link ? link.title || "" : "");
  const [dest, setDest] = React.useState(link ? link.destination_url || "" : "");
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const previewUrl = link ? link.short_url : (Api.baseUrl + "/______");
  // A frozen code keeps redirecting but is read-only until the owner re-subscribes
  // (ADR 0007). Editing is gated behind an upgrade rather than a save.
  const frozen = mode === "edit" && link && link.status === "frozen";

  const save = async () => {
    if (!dest.trim()) { setErr(tr('mqr.dashboard.drawer.error.dest')); return; }
    setBusy(true); setErr(null);
    try {
      if (mode === "edit") await Api.updateLink({ code: link.code, destination_url: dest.trim(), title: title.trim() || null });
      else await Api.createLink({ destination_url: dest.trim(), title: title.trim() || null });
      onSaved();
    } catch (e) { setErr(String(e.message || e)); setBusy(false); }
  };

  return (
    <>
      <div className="ap-drawer__scrim" onClick={onClose} />
      <aside className="ap-drawer">
        <div className="ap-drawer__head">
          <span className="ap-drawer__title">{mode === "edit" ? tr('mqr.dashboard.drawer.title.edit') : tr('mqr.dashboard.drawer.title.new')}</span>
          <button className="ap-iconbtn" onClick={onClose}><Icon name="x" size={16} /></button>
        </div>
        <div className="ap-drawer__body">
          <div className="ap-drawer__preview">
            {link && link.style && link.style.renderMode === "barcode" ? (
              <Barcode data={bareUrl(previewUrl)} format={link.style.barcodeFormat || "code128"}
                fg={(link.style && link.style.fg) || "#1d1d1f"} bg={(link.style && link.style.bg) || "#ffffff"} showText width={240} />
            ) : (
              <QR data={bareUrl(previewUrl)} size={150} quiet={2} fg="#1d1d1f" eyeColor={ACCENT}
                bodyShape="rounded" frameShape="rounded" ballShape="rounded" />
            )}
          </div>
          {frozen && (
            <div className="ap-frozen-note">
              <Icon name="infinity" size={15} sw={2.2} />
              {tr('mqr.dashboard.drawer.frozen.note')}
            </div>
          )}
          {mode === "edit" && !frozen && (
            <div style={{ fontSize: 12.5, color: "var(--fg-muted)", display: "flex", alignItems: "center", gap: 8 }}>
              <Icon name="infinity" size={15} sw={2.2} style={{ color: "var(--accent)" }} />
              {tr('mqr.dashboard.drawer.editnote')}
            </div>
          )}
          <Field label={tr('mqr.dashboard.drawer.label')} hint={tr('mqr.dashboard.drawer.label.hint')}>
            <Input placeholder={tr('mqr.dashboard.drawer.label.placeholder')} value={title} onChange={(e) => setTitle(e.target.value)} disabled={frozen} />
          </Field>
          <Field label={tr('mqr.dashboard.drawer.dest')} hint={tr('mqr.dashboard.drawer.dest.hint')}
            error={err || undefined}>
            <Input placeholder="https://your-site.com/page" value={dest} onChange={(e) => setDest(e.target.value)} disabled={frozen} />
          </Field>
          {mode === "edit" && (
            <Field label={tr('mqr.dashboard.drawer.shortlink')}><Input value={link.short_url} disabled /></Field>
          )}
        </div>
        <div className="ap-drawer__foot">
          <Button variant="secondary" block onClick={onClose}>{tr('mqr.dashboard.drawer.cancel')}</Button>
          {frozen ? (
            <Button variant="primary" block iconRight={<Icon name="arrow" size={15} />} onClick={() => { onClose(); if (onUpgrade) onUpgrade(); }}>
              {tr('mqr.dashboard.drawer.frozen.cta')}
            </Button>
          ) : (
            <Button variant="primary" block onClick={save} disabled={busy}>
              {busy ? tr('mqr.dashboard.saving') : mode === "edit" ? tr('mqr.dashboard.drawer.save') : tr('mqr.dashboard.drawer.create')}
            </Button>
          )}
        </div>
      </aside>
    </>
  );
}

/* ── App ────────────────────────────────────────────────── */
/* ── Folders + bulk CSV import (F010) ───────────────────────── */
function FoldersBar({ folders, active, setActive, onNew, onDelete }) {
  if (!folders) return null;
  return (
    <div className="ap-folders">
      <button className={"ap-chip" + (active === null ? " on" : "")} onClick={() => setActive(null)}>{tr('mqr.dashboard.folders.all')}</button>
      {folders.map((f) => (
        <span key={f.folder_id} className={"ap-chip" + (active === f.folder_id ? " on" : "")}>
          <button className="ap-chip__lbl" onClick={() => setActive(f.folder_id)}><Icon name="grid" size={12} /> {f.name}</button>
          {active === f.folder_id && <button className="ap-chip__x" title={tr('mqr.dashboard.folders.delete')} onClick={() => onDelete(f)}><Icon name="x" size={11} /></button>}
        </span>
      ))}
      <button className="ap-chip ap-chip--add" onClick={onNew}><Icon name="plus" size={12} sw={2.2} /> {tr('mqr.dashboard.folders.new')}</button>
    </div>
  );
}

function BulkImportDialog({ folders, onClose, onDone }) {
  const [csv, setCsv] = React.useState("");
  const [folderId, setFolderId] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [result, setResult] = React.useState(null);
  const onFile = (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => setCsv(String(r.result || "")); r.readAsText(f); };
  const run = async () => {
    setBusy(true);
    try { const r = await Api.bulkImportLinks(csv, folderId || undefined); setResult(r); if (r && r.total) onDone(); }
    catch (e) { setResult({ error: String((e && e.message) || e) }); }
    finally { setBusy(false); }
  };
  return (
    <>
      <div className="ap-drawer__scrim" onClick={onClose} />
      <aside className="ap-drawer">
        <div className="ap-drawer__head"><span className="ap-drawer__title">{tr('mqr.dashboard.bulk.title')}</span><button className="ap-iconbtn" onClick={onClose}><Icon name="x" size={16} /></button></div>
        <div className="ap-drawer__body">
          <div style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>{tr('mqr.dashboard.bulk.help')}</div>
          <code className="forge-mono" style={{ display: "block", padding: 8, background: "var(--surface-2, rgba(0,0,0,0.04))", borderRadius: 8, fontSize: 12, whiteSpace: "pre" }}>{"destination_url,title\nhttps://acme.com/a,Flyer A\nhttps://acme.com/b,Flyer B"}</code>
          <Field label={tr('mqr.dashboard.bulk.file')}><input type="file" accept=".csv,text/csv" onChange={onFile} /></Field>
          <Field label={tr('mqr.dashboard.bulk.paste')}><textarea className="forge-input" rows={6} value={csv} onChange={(e) => setCsv(e.target.value)} placeholder={"destination_url,title\nhttps://…"} /></Field>
          {folders && folders.length > 0 && (
            <Field label={tr('mqr.dashboard.bulk.folder')}>
              <select className="forge-select" value={folderId} onChange={(e) => setFolderId(e.target.value)}>
                <option value="">{tr('mqr.dashboard.bulk.nofolder')}</option>
                {folders.map((f) => <option key={f.folder_id} value={f.folder_id}>{f.name}</option>)}
              </select>
            </Field>
          )}
          {result && (result.error
            ? <div className="ap-gate__d" style={{ color: "var(--bad, #e8413a)" }}>{result.error}</div>
            : <div className="ap-demo"><Icon name="check" size={15} sw={2.2} />{tr('mqr.dashboard.bulk.queued', { count: num(result.total) })}{result.errors && result.errors.length ? ` · ${tr('mqr.dashboard.bulk.skipped', { count: result.errors.length })}` : ""}</div>)}
        </div>
        <div className="ap-drawer__foot">
          <Button variant="secondary" block onClick={onClose}>{tr('mqr.dashboard.drawer.cancel')}</Button>
          <Button variant="primary" block disabled={busy || !csv.trim()} onClick={run}>{busy ? tr('mqr.dashboard.saving') : tr('mqr.dashboard.bulk.import')}</Button>
        </div>
      </aside>
    </>
  );
}

/* ── Hosted micro-pages (F015) ──────────────────────────────
   Digital business cards · linkpages · lead-capture forms, served at /p/<slug>. A focused
   builder: pick a type, fill the essentials, publish — the page gets a public URL. */
const PAGE_TYPES = [
  { key: "card", icon: "user", labelKey: "mqr.dashboard.pages.type.card" },
  { key: "linkpage", icon: "link", labelKey: "mqr.dashboard.pages.type.linkpage" },
  { key: "form", icon: "file", labelKey: "mqr.dashboard.pages.type.form" },
];
function PageCreate({ onClose, onCreated }) {
  const [type, setType] = React.useState(null);
  const [title, setTitle] = React.useState("");
  const [a, setA] = React.useState(""); // card: name · linkpage/form: heading
  const [rows, setRows] = React.useState([{ x: "", y: "" }]); // linkpage links / form fields
  const [busy, setBusy] = React.useState(false);
  const setRow = (i, k, v) => setRows((rs) => rs.map((r, j) => (j === i ? { ...r, [k]: v } : r)));
  const addRow = () => setRows((rs) => [...rs, { x: "", y: "" }]);

  const submit = async () => {
    setBusy(true);
    try {
      const payload = { type, title: title.trim() || a.trim() || type };
      if (type === "card") payload.name = a.trim() || title.trim();
      if (type === "linkpage") { payload.heading = a.trim() || title.trim(); payload.links = rows.filter((r) => r.x && r.y).map((r) => ({ label: r.x, url: /^https?:\/\//.test(r.y) ? r.y : "https://" + r.y })); }
      if (type === "form") { payload.heading = a.trim() || title.trim(); payload.fields = rows.filter((r) => r.x).map((r) => ({ name: r.x.toLowerCase().replace(/[^a-z0-9]+/g, "_"), label: r.x, type: r.y || "text" })); if (!payload.fields.length) payload.fields = [{ name: "email", label: "Email", type: "email" }]; }
      await Api.createPage(payload);
      onCreated();
    } catch (e) { window.alert(String((e && e.message) || e)); } finally { setBusy(false); }
  };

  return (
    <>
      <div className="ap-drawer__scrim" onClick={onClose} />
      <aside className="ap-drawer">
        <div className="ap-drawer__head"><span className="ap-drawer__title">{tr('mqr.dashboard.pages.new')}</span><button className="ap-iconbtn" onClick={onClose}><Icon name="x" size={16} /></button></div>
        <div className="ap-drawer__body">
          {!type ? (
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              {PAGE_TYPES.map((t) => (
                <button key={t.key} className="ap-chip" style={{ justifyContent: "flex-start", padding: "12px 14px" }} onClick={() => setType(t.key)}>
                  <Icon name={t.icon} size={16} /> {tr(t.labelKey)}
                </button>
              ))}
            </div>
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
              <Field label={tr('mqr.dashboard.pages.title')}><Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder={tr('mqr.dashboard.pages.title.ph')} /></Field>
              {type === "card" && <Field label={tr('mqr.dashboard.pages.card.name')}><Input value={a} onChange={(e) => setA(e.target.value)} placeholder="Ada Vega" /></Field>}
              {type !== "card" && <Field label={tr('mqr.dashboard.pages.heading')}><Input value={a} onChange={(e) => setA(e.target.value)} /></Field>}
              {(type === "linkpage" || type === "form") && (
                <div>
                  <div className="ap-stat__label" style={{ marginBottom: 6 }}>{type === "linkpage" ? tr('mqr.dashboard.pages.links') : tr('mqr.dashboard.pages.fields')}</div>
                  {rows.map((r, i) => (
                    <div key={i} style={{ display: "flex", gap: 8, marginBottom: 6 }}>
                      <Input value={r.x} onChange={(e) => setRow(i, "x", e.target.value)} placeholder={type === "linkpage" ? tr('mqr.dashboard.pages.link.label') : tr('mqr.dashboard.pages.field.label')} />
                      {type === "linkpage"
                        ? <Input value={r.y} onChange={(e) => setRow(i, "y", e.target.value)} placeholder="https://…" />
                        : <select className="forge-select" value={r.y || "text"} onChange={(e) => setRow(i, "y", e.target.value)}><option value="text">Text</option><option value="email">Email</option><option value="tel">Phone</option><option value="textarea">Long text</option></select>}
                    </div>
                  ))}
                  <Button variant="secondary" onClick={addRow}><Icon name="plus" size={13} /> {tr('mqr.dashboard.pages.addrow')}</Button>
                </div>
              )}
            </div>
          )}
        </div>
        <div className="ap-drawer__foot">
          <Button variant="secondary" block onClick={onClose}>{tr('mqr.dashboard.drawer.cancel')}</Button>
          {type && <Button variant="primary" block disabled={busy} onClick={submit}>{busy ? tr('mqr.dashboard.saving') : tr('mqr.dashboard.pages.publish')}</Button>}
        </div>
      </aside>
    </>
  );
}
function PagesView() {
  const [pages, setPages] = React.useState(null);
  const [creating, setCreating] = React.useState(false);
  const load = React.useCallback(() => { Api.listPages().then((p) => setPages(p || [])).catch(() => setPages([])); }, []);
  React.useEffect(() => { load(); }, [load]);
  const del = (p) => { if (window.confirm(tr('mqr.dashboard.pages.confirmdelete', { title: p.title }))) Api.deletePage(p.slug).then(load); };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16, gap: 12, flexWrap: "wrap" }}>
        <p className="ap-sub" style={{ margin: 0 }}>{tr('mqr.dashboard.pages.sub')}</p>
        <Button variant="primary" iconLeft={<Icon name="plus" size={15} sw={2.2} />} onClick={() => setCreating(true)}>{tr('mqr.dashboard.pages.new')}</Button>
      </div>
      {pages === null ? <div className="ap-panel"><div className="ap-panel__body" style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div></div>
        : pages.length === 0 ? (
          <div className="ap-panel"><div className="ap-gate">
            <div className="ap-gate__ic"><Icon name="file" size={22} /></div>
            <div className="ap-gate__t">{tr('mqr.dashboard.pages.empty.title')}</div>
            <div className="ap-gate__d">{tr('mqr.dashboard.pages.empty.body')}</div>
          </div></div>
        ) : (
          <div className="ap-panel"><div className="ap-panel__body" style={{ padding: "8px 4px" }}>
            <table className="ap-codes">
              <thead><tr><th>{tr('mqr.dashboard.pages.col.title')}</th><th>{tr('mqr.dashboard.pages.col.type')}</th><th>{tr('mqr.dashboard.pages.col.link')}</th><th></th></tr></thead>
              <tbody>
                {pages.map((p) => (
                  <tr key={p.slug}>
                    <td>{p.title}{p.type === "form" && p.submission_count ? <span style={{ fontSize: 11, color: "var(--fg-muted)" }}> · {tr('mqr.dashboard.pages.subs', { count: p.submission_count })}</span> : ""}</td>
                    <td><span className="ap-status ap-status--active">{tr('mqr.dashboard.pages.type.' + p.type)}</span></td>
                    <td><a href={p.public_url} target="_blank" rel="noopener" className="forge-mono" style={{ color: "var(--accent)", textDecoration: "none" }}>/p/{p.slug}</a></td>
                    <td style={{ textAlign: "right" }}><button className="ap-iconbtn danger" title={tr('mqr.dashboard.pages.delete')} onClick={() => del(p)}><Icon name="x" size={15} /></button></td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div></div>
        )}
      {creating && <PageCreate onClose={() => setCreating(false)} onCreated={() => { setCreating(false); load(); }} />}
    </div>
  );
}

function Dashboard() {
  const [ready, setReady] = React.useState(false);
  const [email, setEmail] = React.useState("");
  const [view, setView] = React.useState("codes");
  const [links, setLinks] = React.useState([]);
  const [acct, setAcct] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [drawer, setDrawer] = React.useState(null);
  const [selected, setSelected] = React.useState(null);
  const [showSurvey, setShowSurvey] = React.useState(false);
  const [upgradeOpen, setUpgradeOpen] = React.useState(false);
  const [folders, setFolders] = React.useState([]);
  const [activeFolder, setActiveFolder] = React.useState(null);
  const [importing, setImporting] = React.useState(false);

  const load = React.useCallback(() => {
    setLoading(true);
    Promise.all([Api.listLinks(), Api.getAccountStats(), Api.listFolders()])
      .then(([ls, a, f]) => { setLinks(ls); setAcct(a); setFolders(f || []); setLoading(false); })
      .catch(() => setLoading(false));
  }, []);

  React.useEffect(() => {
    const unsub = Api.onAuth((user) => {
      if (Api.ready && !user) { window.location.assign("SignIn.html"); return; }
      setEmail((user && user.email) || "demo@mostlyqr.com");
      setReady(true);
      // First load after sign-up → show the "how did you hear about us?" survey once.
      if (user) setShowSurvey(Api.signupSurveyPending());
      load();
    });
    return unsub;
  }, [load]);

  // Accept a team invite (F009): ?invite=<token> → join the workspace, strip the param, reload.
  React.useEffect(() => {
    if (!ready) return;
    let token = null;
    try { token = new URLSearchParams(window.location.search).get("invite"); } catch (e) {}
    if (!token) return;
    Api.acceptInvite(token).then(() => {
      try { const u = new URL(window.location.href); u.searchParams.delete("invite"); window.history.replaceState({}, "", u); } catch (e) {}
      window.alert(tr('mqr.dashboard.team.accepted'));
      load();
    }).catch((e) => { window.alert(tr('mqr.dashboard.team.acceptfail') + " " + ((e && e.message) || "")); });
  }, [ready, load]);

  // Live plan from the account rollup (getAccountStats → currentPlan). Fall back to a link's
  // denormalized account_plan only until acct loads, then "free". (Previously this read the link's
  // stale account_plan first, so a post-creation plan change showed the wrong plan + gated tabs.)
  const plan = (acct && acct.plan) || (links[0] && links[0].account_plan) || "free";

  const onDelete = (l) => { Api.deleteLink(l.code).then(load); };
  const onAnalytics = (l) => { setSelected(l.code); setView("analytics"); };
  const onNewFolder = () => { const name = window.prompt(tr('mqr.dashboard.folders.prompt')); if (name && name.trim()) Api.createFolder(name.trim()).then(load); };
  const onDeleteFolder = (f) => { if (window.confirm(tr('mqr.dashboard.folders.confirmdelete', { name: f.name }))) Api.deleteFolder(f.folder_id).then(() => { setActiveFolder(null); load(); }); };
  const visibleLinks = activeFolder ? links.filter((l) => l.folder_id === activeFolder) : links;
  // Use the CLEAN url (no .html): the /Builder.html → /Builder cleanUrls 301 drops
  // the query string, which would lose ?code= and open the wrong code.
  const onFlow = (l) => { window.location.assign("/Builder?code=" + encodeURIComponent(l.code) + "#flow"); };
  const openUpgrade = () => setUpgradeOpen(true);

  if (!ready) return <div className="ap-root"><div className="ap-wrap" style={{ color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div></div>;

  const limit = PLAN_LIMITS[plan] || 3;
  const used = links.length;

  return (
    <div className="ap-root">
      <Topbar view={view} setView={setView} plan={plan} email={email} onUpgrade={openUpgrade}
        onSignOut={() => Api.signOut().then(() => window.location.assign("index.html"))} />
      <div className="ap-wrap">
        {!Api.ready && (
          <div className="ap-demo"><Icon name="infinity" size={15} sw={2.2} />
            {tr('mqr.dashboard.demo.1')} <code className="forge-mono">mqr/config.js</code> {tr('mqr.dashboard.demo.2')}</div>
        )}

        <div className="ap-head">
          <div>
            <h1 className="ap-title">{view === "codes" ? tr('mqr.dashboard.title.codes') : view === "analytics" ? tr('mqr.dashboard.title.analytics') : view === "pages" ? tr('mqr.dashboard.title.pages') : view === "serialized" ? tr('mqr.dashboard.title.serialized') : tr('mqr.dashboard.title.settings')}</h1>
            {view === "codes" && <p className="ap-sub">{tr('mqr.dashboard.codes.sub', { used: used, limit: limit })}</p>}
          </div>
          {view === "codes" && (
            <div style={{ display: "flex", gap: 10 }}>
              <Button variant="secondary" iconLeft={<Icon name="file" size={15} />} onClick={() => setImporting(true)}>{tr('mqr.dashboard.bulk.cta')}</Button>
              <Button variant="primary" iconLeft={<Icon name="plus" size={15} sw={2.2} />}
                onClick={() => setDrawer({ mode: "create" })}>{tr('mqr.dashboard.newcode')}</Button>
            </div>
          )}
        </div>

        {view === "codes" && (
          <>
            <div className="ap-stats">
              <Stat label={tr('mqr.dashboard.stat.totalscans')} value={num(acct && acct.total_scans)} delta="+8.1%" sub={tr('mqr.dashboard.stat.allcodes')} />
              <Stat label={tr('mqr.dashboard.stat.activecodes')} value={num(used)} sub={tr('mqr.dashboard.stat.lefton', { count: Math.max(0, limit - used), plan: PLAN_LABEL[plan] || PLAN_LABEL.free })} />
              <Stat label={tr('mqr.dashboard.stat.topperformer')} value={(acct && acct.top_links[0] ? (acct.top_links[0].title || acct.top_links[0].code) : "—")} sub={acct && acct.top_links[0] ? tr('mqr.dashboard.scans.count', { count: acct.top_links[0].scan_count }) : ""} />
              <Stat label={tr('mqr.dashboard.stat.neverexpired')} value="100%" sub={tr('mqr.dashboard.stat.stilllive')} />
            </div>
            {(folders.length > 0 || true) && (
              <FoldersBar folders={folders} active={activeFolder} setActive={setActiveFolder} onNew={onNewFolder} onDelete={onDeleteFolder} />
            )}
            <div className="ap-panel">
              <div className="ap-panel__head"><h3 className="ap-panel__title">{tr('mqr.dashboard.panel.dynamiccodes')}</h3>
                <a href="/Builder" style={{ textDecoration: "none", fontSize: 13, fontWeight: 600, color: "var(--accent)" }}>{tr('mqr.dashboard.openbuilder')}</a>
              </div>
              <div className="ap-panel__body" style={{ padding: "8px 4px 8px" }}>
                {loading ? <div style={{ padding: 24, color: "var(--fg-subtle)" }}>{tr('mqr.dashboard.loading')}</div>
                  : <CodesTable links={visibleLinks} onAnalytics={onAnalytics} onFlow={onFlow} onDelete={onDelete} />}
              </div>
            </div>
          </>
        )}

        {view === "analytics" && <AnalyticsView links={links} selected={selected} setSelected={setSelected} onUpgrade={openUpgrade} />}
        {view === "pages" && <PagesView />}
        {view === "serialized" && <SerializedView plan={plan} onUpgrade={openUpgrade} />}
        {view === "settings" && <SettingsView plan={plan} email={email} onUpgrade={openUpgrade} />}
      </div>

      {drawer && (
        <CodeDrawer mode={drawer.mode} link={drawer.link} onUpgrade={openUpgrade}
          onClose={() => setDrawer(null)}
          onSaved={() => { setDrawer(null); load(); }} />
      )}

      {importing && (
        <BulkImportDialog folders={folders} onClose={() => setImporting(false)} onDone={() => load()} />
      )}

      {upgradeOpen && UpgradeDialog && (
        <UpgradeDialog
          plans={window.MQR.plansAbove(tr, plan)}
          currentPlan={plan}
          defaultAnnual={false}
          title={tr('mqr.upgrade.title')}
          labels={window.MQR.upgradeLabels(tr)}
          onSelect={(key, annual) => Api.checkout(key, annual)}
          onClose={() => setUpgradeOpen(false)} />
      )}

      {showSurvey && SignupSurvey && (
        <SignupSurvey
          channels={(Api.SIGNUP_SURVEY_CHANNELS || []).map((v) => ({ value: v, label: tr('auth.survey.channel.' + v) }))}
          onSubmit={(answers) => Api.submitSignupSurvey(answers).then(() => setShowSurvey(false))}
          onSkip={() => { Api.dismissSignupSurvey(); setShowSurvey(false); }}
        />
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("mqr-app")).render(<Dashboard />);
