// Solo staff admin area — Settings: this is the single home for every
// external API/data connection Solo Secure has (Xero accounting,
// QuickBooks Online accounting, the live asset-tracking Google Sheet,
// Victron VRM, and any future integration) — admins should always look
// here first, not on the individual feature pages, to see what's
// connected, its live status, and to trigger a manual sync or link.
// Company-level Xero *contact* linking still lives on the Customer record
// page (see CompanyXeroLink in admin-companies-page.jsx) since that's a
// per-customer mapping, not a connection itself — this page is about the
// connections/credentials. QuickBooks's per-company *customer* linking
// follows the exact same split (see CompanyQuickBooksLink in
// admin-companies-page.jsx, gated to North America region companies
// only). Victron VRM is the one exception to "link stays on the Customer
// page": every customer runs their own separate VRM account (confirmed
// with the customer), so per-company linking is deliberately kept inline
// HERE as a list, not split out to the Customer record page, per
// explicit instruction.
//
// QuickBooks is READ-ONLY (confirmed with the customer, unlike Xero):
// it never pushes orders/quotes, and binding a company to a QuickBooks
// Customer never overwrites that company's local name/address the way
// Xero's bind does — it's purely for pulling customer details and
// invoices on demand. QuickBooks only applies to "North America" region
// companies; UK/EU/Rest of the World continue to use Xero unchanged.
//
// Reached via the sidebar nav in AdminShell (see admin-shell.jsx). Reuses
// ModalError/SectionLabel/EmptyNote/ActionButton/ImportSheetModal/
// adminInputStyle from admin-assets-page.jsx (loaded earlier — see
// index.html).
//
// "Connect to Xero" is a plain <a href> to a GET route (routes/admin-xero.ts
// /xero/connect), not a fetch+redirect — Xero's OAuth authorize step is a
// real top-level browser navigation to login.xero.com, which only works as
// a direct link click, never an XHR. After the admin approves access on
// Xero's consent screen, Xero redirects back to /api/admin/xero/callback,
// which itself redirects into /admin?xero=connected|denied|error|state_mismatch
// — read once on mount below and shown as a dismissible banner.

// One collapsible per external connection category (Xero, QuickBooks,
// Victron, Ajax, Teltonika, EFOY, the asset spreadsheet) -- the customer's
// explicit instruction: the page had grown too busy with every category's
// full card/table always rendered at once, so each one now starts
// collapsed and shows a compact live-status `subtitle` in its own header
// (e.g. "Connected" or "3/5 linked") so an admin can scan status without
// opening anything, then click through to just the one category they need.
// Each section's own useState/useEffect data-fetch above is unaffected --
// this only controls whether that already-loaded content is rendered,
// same as any other conditional render.
function CollapsibleSection({ title, subtitle, subtitleColor, isOpen, onToggle, testId, children }) {
  return (
    <div style={{ maxWidth: 560, marginBottom: 20 }}>
      <button
        type="button" onClick={onToggle} data-testid={testId}
        style={{
          width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between",
          background: "none", border: "none", borderBottom: "1px solid rgba(0,0,0,0.14)",
          padding: "14px 2px", cursor: "pointer", textAlign: "left",
        }}
      >
        <span style={{
          fontFamily: "var(--font-body)", fontSize: 10, letterSpacing: "0.28em",
          textTransform: "uppercase", color: "rgba(0,0,0,0.55)", fontWeight: 500,
        }}>
          {title}
        </span>
        <span style={{ display: "flex", alignItems: "center", gap: 10 }}>
          {subtitle && (
            <span style={{
              fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 600,
              color: subtitleColor || "rgba(0,0,0,0.5)",
            }}>
              {subtitle}
            </span>
          )}
          <span style={{
            display: "inline-block", fontSize: 9, color: "rgba(0,0,0,0.4)",
            transform: isOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s ease",
          }}>
            ▼
          </span>
        </span>
      </button>
      {isOpen && <div style={{ paddingTop: 18 }}>{children}</div>}
    </div>
  );
}

function AdminSettingsPage({ onNavigate }) {
  const [state, setState] = useState({ status: "loading", admin: null, products: [], companies: [] });
  // Which Data Connections categories are expanded -- all start collapsed
  // (see CollapsibleSection above), keyed by category so any combination
  // can be open at once (not an accordion -- an admin may want Ajax and
  // Victron open side by side while cross-checking a hub/installation pair).
  const [openSections, setOpenSections] = useState({});
  const toggleSection = (key) => setOpenSections((s) => ({ ...s, [key]: !s[key] }));
  const [xero, setXero] = useState({ status: "loading", connected: false, tenantName: null, connectedAt: null });
  const [disconnecting, setDisconnecting] = useState(false);
  const [disconnectError, setDisconnectError] = useState("");
  const [quickbooks, setQuickbooks] = useState({ status: "loading", connected: false, companyName: null, connectedAt: null });
  const [qbDisconnecting, setQbDisconnecting] = useState(false);
  const [qbDisconnectError, setQbDisconnectError] = useState("");
  const [callbackNotice, setCallbackNotice] = useState(null); // { kind: "connected"|"denied"|"error"|"state_mismatch", provider: "xero"|"quickbooks", message? }
  const [sheetImport, setSheetImport] = useState({ status: "loading", lastRun: null });
  const [showSheetImport, setShowSheetImport] = useState(false);
  const [victron, setVictron] = useState({ status: "loading", connections: [] });
  const [ajax, setAjax] = useState({ status: "loading", connections: [] });
  const [teltonika, setTeltonika] = useState({ status: "loading", connections: [] });
  const [efoy, setEfoy] = useState({ status: "loading", connections: [] });
  const [contactDefaults, setContactDefaults] = useState({ status: "loading", roles: [], regions: [], defaults: [] });

  const loadXeroStatus = () => {
    setXero((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/xero/status", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setXero({ status: "ready", connected: !!data.connected, tenantName: data.tenantName || null, connectedAt: data.connectedAt || null }))
      .catch(() => setXero((s) => ({ ...s, status: "error" })));
  };

  // QuickBooks Online (North America accounting — read-only pull of
  // customer details + invoices, no order-push; see routes/admin-quickbooks.ts).
  const loadQuickbooksStatus = () => {
    setQuickbooks((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/quickbooks/status", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setQuickbooks({ status: "ready", connected: !!data.connected, companyName: data.companyName || null, connectedAt: data.connectedAt || null }))
      .catch(() => setQuickbooks((s) => ({ ...s, status: "error" })));
  };

  const loadSheetImportStatus = () => {
    setSheetImport((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/assets/import/last-run", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setSheetImport({ status: "ready", lastRun: data.lastRun || null }))
      .catch(() => setSheetImport((s) => ({ ...s, status: "error" })));
  };

  const loadVictronConnections = () => {
    setVictron((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/victron/connections", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setVictron({ status: "ready", connections: data.connections || [] }))
      .catch(() => setVictron((s) => ({ ...s, status: "error" })));
  };

  // Ajax Systems: same per-company shape as Victron/Teltonika (one row
  // per customer company, each with its own separate Ajax PRO account) --
  // see routes/admin-ajax.ts's GET /ajax/connections.
  const loadAjaxConnections = () => {
    setAjax((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/ajax/connections", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setAjax({ status: "ready", connections: data.connections || [] }))
      .catch(() => setAjax((s) => ({ ...s, status: "error" })));
  };

  // Teltonika RMS: same per-company shape as Victron (one row per
  // customer company, joined against whatever link exists) -- see
  // routes/admin-mission-control.ts's GET /teltonika/connections.
  const loadTeltonikaConnections = () => {
    setTeltonika((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/teltonika/connections", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setTeltonika({ status: "ready", connections: data.connections || [] }))
      .catch(() => setTeltonika((s) => ({ ...s, status: "error" })));
  };

  // EFOY Cloud: same per-company shape as Victron/Ajax/Teltonika (one
  // row per customer company, joined against whatever link exists) --
  // see routes/admin-efoy.ts's GET /efoy/connections. Option A per the
  // customer's explicit instruction: "i think its A but check the
  // documentation for efoy cloud".
  const loadEfoyConnections = () => {
    setEfoy((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/efoy/connections", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setEfoy({ status: "ready", connections: data.connections || [] }))
      .catch(() => setEfoy((s) => ({ ...s, status: "error" })));
  };

  // Regional Contact Defaults -- Account Manager + Technical Contact
  // fixed cards (name/email/phone), set by Solo staff only, resolved
  // for each customer via lib/customerContacts.ts (override -> regional
  // default -> global default). Lives here on Settings per explicit
  // customer instruction, not a new nav item.
  const loadContactDefaults = () => {
    setContactDefaults((s) => ({ ...s, status: s.status === "ready" ? "ready" : "loading" }));
    return fetch("/api/admin/contact-defaults", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setContactDefaults({
        status: "ready", roles: data.roles || [], regions: data.regions || [], defaults: data.defaults || [],
      }))
      .catch(() => setContactDefaults((s) => ({ ...s, status: "error" })));
  };

  useEffect(() => {
    Promise.all([
      fetch("/api/admin/me", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
      fetch("/api/admin/products", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
      fetch("/api/admin/companies", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
    ])
      .then(([me, products, companies]) => setState({
        status: "ready", admin: me.admin,
        products: products.products || [], companies: companies.companies || [],
      }))
      .catch(() => onNavigate("admin-login"));
    loadXeroStatus();
    loadQuickbooksStatus();
    loadSheetImportStatus();
    loadVictronConnections();
    loadAjaxConnections();
    loadTeltonikaConnections();
    loadEfoyConnections();
    loadContactDefaults();

    // Read + immediately strip the one-shot ?xero=... or ?quickbooks=...
    // flag left by each provider's OAuth callback redirect so a page
    // refresh doesn't re-show the banner. Only one of the two can be
    // present at a time (a single OAuth round-trip only ever touches one
    // provider), so checking xero first then falling back to quickbooks
    // is safe.
    const params = new URLSearchParams(window.location.search);
    const xeroFlag = params.get("xero");
    const quickbooksFlag = params.get("quickbooks");
    if (xeroFlag) {
      setCallbackNotice({ kind: xeroFlag, provider: "xero", message: params.get("xero_message") || "" });
      params.delete("xero");
      params.delete("xero_message");
    } else if (quickbooksFlag) {
      setCallbackNotice({ kind: quickbooksFlag, provider: "quickbooks", message: params.get("quickbooks_message") || "" });
      params.delete("quickbooks");
      params.delete("quickbooks_message");
    }
    if (xeroFlag || quickbooksFlag) {
      const cleanUrl = window.location.pathname + (params.toString() ? `?${params.toString()}` : "");
      window.history.replaceState(window.history.state, "", cleanUrl);
    }
  }, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  const disconnect = async () => {
    if (!window.confirm("Disconnect Xero? Orders won't push quotes and invoice data won't load in the reseller portal until it's reconnected.")) return;
    setDisconnecting(true);
    setDisconnectError("");
    try {
      const res = await fetch("/api/admin/xero/disconnect", { method: "POST", credentials: "same-origin" });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        setDisconnectError(d.error || "Couldn't disconnect. Please try again.");
        setDisconnecting(false);
        return;
      }
      await loadXeroStatus();
    } catch {
      setDisconnectError("Couldn't reach the server. Check your connection and try again.");
    }
    setDisconnecting(false);
  };

  const disconnectQuickbooks = async () => {
    if (!window.confirm("Disconnect QuickBooks? North America customer details and invoice data won't load until it's reconnected.")) return;
    setQbDisconnecting(true);
    setQbDisconnectError("");
    try {
      const res = await fetch("/api/admin/quickbooks/disconnect", { method: "POST", credentials: "same-origin" });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        setQbDisconnectError(d.error || "Couldn't disconnect. Please try again.");
        setQbDisconnecting(false);
        return;
      }
      await loadQuickbooksStatus();
    } catch {
      setQbDisconnectError("Couldn't reach the server. Check your connection and try again.");
    }
    setQbDisconnecting(false);
  };

  if (state.status === "loading") {
    return <section style={{ background: "#fff", minHeight: "calc(100vh - 88px)" }} />;
  }

  const isReadOnly = state.admin && state.admin.role !== "super_admin";

  return (
    <AdminShell admin={state.admin} page="admin-settings" onNavigate={onNavigate}
      subtitle="Staff only" title="Settings.">

      {callbackNotice && (
        <CallbackBanner notice={callbackNotice} onDismiss={() => setCallbackNotice(null)} />
      )}

      <AdminAccountSection admin={state.admin} />

      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 26 }}>
        <strong>Data Connections</strong> — every external system Solo Secure reads from or writes
        to lives here: Xero accounting, the live asset-tracking spreadsheet, and anything added
        later. Each category below is collapsed by default — click one to open it, check
        connection status, or run a manual sync/link.
      </div>

      <CollapsibleSection
        title="Asset spreadsheet"
        subtitle={sheetImport.status === "ready" ? (sheetImport.lastRun ? "Connected" : "Never synced") : undefined}
        subtitleColor={sheetImport.lastRun ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.5)"}
        isOpen={!!openSections.sheet} onToggle={() => toggleSection("sheet")}
        testId="settings-section-sheet"
      >
        {sheetImport.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {sheetImport.status === "error" && <EmptyNote>Couldn't load the spreadsheet sync status.</EmptyNote>}

        {sheetImport.status === "ready" && (
          <div style={{
            border: "1px solid rgba(0,0,0,0.14)", background: "rgba(0,0,0,0.02)",
            padding: "20px 22px", marginBottom: 20, maxWidth: 560,
          }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14 }}>
              <span style={{
                display: "inline-block", width: 8, height: 8, borderRadius: "50%",
                background: sheetImport.lastRun ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
              }} />
              <span style={{
                fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
                letterSpacing: "0.06em", textTransform: "uppercase",
                color: sheetImport.lastRun ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.55)",
              }}>
                {sheetImport.lastRun ? "Connected" : "Never synced"}
              </span>
            </div>

            {sheetImport.lastRun ? (
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.75)", marginBottom: 18, lineHeight: 1.6 }}>
                Last synced {sheetImport.lastRun.created_at} by <strong>{sheetImport.lastRun.run_by_name || "an admin"}</strong>:{" "}
                {sheetImport.lastRun.imported_rows} new,{" "}
                {sheetImport.lastRun.skipped_conflict > 0 && <>{sheetImport.lastRun.skipped_conflict} already tracked (left untouched), </>}
                {sheetImport.lastRun.skipped_reserved} reserved skipped.
                <br />
                Pulls live from the master asset tracking spreadsheet and adds brand-new units, purchase
                orders, and invoice references to the Assets page — existing units are never overwritten.
              </div>
            ) : (
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.6)", marginBottom: 18, lineHeight: 1.6 }}>
                Not synced yet. Run the import to pull units, purchase orders, and invoice references
                from the live asset tracking spreadsheet.
              </div>
            )}

            <ActionButton readOnly={isReadOnly} onClick={() => setShowSheetImport(true)} testId="settings-open-sheet-import">
              Update from spreadsheet
            </ActionButton>
          </div>
        )}
      </CollapsibleSection>

      <CollapsibleSection
        title="Xero"
        subtitle={xero.status === "ready" ? (xero.connected ? "Connected" : "Not connected") : undefined}
        subtitleColor={xero.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.5)"}
        isOpen={!!openSections.xero} onToggle={() => toggleSection("xero")}
        testId="settings-section-xero"
      >
        {xero.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {xero.status === "error" && <EmptyNote>Couldn't load the Xero connection status.</EmptyNote>}
        {disconnectError && <ModalError>{disconnectError}</ModalError>}

        {xero.status === "ready" && (
          <div style={{
            border: "1px solid rgba(0,0,0,0.14)", background: "rgba(0,0,0,0.02)",
            padding: "20px 22px", marginBottom: 20, maxWidth: 560,
          }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: xero.connected ? 14 : 4 }}>
              <span style={{
                display: "inline-block", width: 8, height: 8, borderRadius: "50%",
                background: xero.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
              }} />
              <span style={{
                fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
                letterSpacing: "0.06em", textTransform: "uppercase",
                color: xero.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.55)",
              }}>
                {xero.connected ? "Connected" : "Not connected"}
              </span>
            </div>

            {xero.connected ? (
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.75)", marginBottom: 18, lineHeight: 1.6 }}>
                Connected to <strong>{xero.tenantName || "your Xero organisation"}</strong>.
                {xero.connectedAt && <> Since {xero.connectedAt}.</>}
                <br />
                Order quotes push to Xero automatically once a customer is linked to a Xero contact
                (see the Xero link section on each Customer record), and invoice data reads live from Xero.
              </div>
            ) : (
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.6)", marginBottom: 18, lineHeight: 1.6 }}>
                Not connected yet. Order quotes won't push to Xero and reseller invoice data won't load
                until an admin connects this Xero organisation.
              </div>
            )}

            {xero.connected ? (
              <ActionButton danger disabled={disconnecting} readOnly={isReadOnly} onClick={disconnect} testId="xero-disconnect">
                {disconnecting ? "Disconnecting…" : "Disconnect"}
              </ActionButton>
            ) : isReadOnly ? (
              <span
                data-testid="xero-connect"
                title="Master admins only — you have read-only access"
                style={{
                  display: "inline-block", background: "rgba(0,0,0,0.15)", color: "rgba(0,0,0,0.5)",
                  border: "1px solid rgba(0,0,0,0.15)", padding: "10px 18px", cursor: "not-allowed",
                  fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                  letterSpacing: "0.14em", textTransform: "uppercase",
                }}
              >
                Connect to Xero
              </span>
            ) : (
              <a
                href="/api/admin/xero/connect"
                data-testid="xero-connect"
                style={{
                  display: "inline-block", background: "#000", color: "#fff",
                  border: "1px solid #000", padding: "10px 18px", textDecoration: "none",
                  fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                  letterSpacing: "0.14em", textTransform: "uppercase",
                }}
              >
                Connect to Xero
              </a>
            )}
          </div>
        )}

        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6 }}>
          Connecting takes you to Xero's own sign-in and consent screen. Solo Secure only ever
          connects to <strong>one</strong> Xero organisation at a time — connecting again while
          already connected replaces the existing connection.
        </div>
      </CollapsibleSection>

      <CollapsibleSection
        title="QuickBooks (North America)"
        subtitle={quickbooks.status === "ready" ? (quickbooks.connected ? "Connected" : "Not connected") : undefined}
        subtitleColor={quickbooks.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.5)"}
        isOpen={!!openSections.quickbooks} onToggle={() => toggleSection("quickbooks")}
        testId="settings-section-quickbooks"
      >
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
          North America customers use QuickBooks Online instead of Xero. This connection is{" "}
          <strong>read-only</strong> — Solo Secure Technologies USA Inc's QuickBooks company is
          never pushed to; it's only used to pull customer details and invoices for North America
          companies once each one is linked to a QuickBooks customer.
        </div>

        {quickbooks.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {quickbooks.status === "error" && <EmptyNote>Couldn't load the QuickBooks connection status.</EmptyNote>}
        {qbDisconnectError && <ModalError>{qbDisconnectError}</ModalError>}

        {quickbooks.status === "ready" && (
          <div style={{
            border: "1px solid rgba(0,0,0,0.14)", background: "rgba(0,0,0,0.02)",
            padding: "20px 22px", marginBottom: 20, maxWidth: 560,
          }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: quickbooks.connected ? 14 : 4 }}>
              <span style={{
                display: "inline-block", width: 8, height: 8, borderRadius: "50%",
                background: quickbooks.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
              }} />
              <span style={{
                fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
                letterSpacing: "0.06em", textTransform: "uppercase",
                color: quickbooks.connected ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.55)",
              }}>
                {quickbooks.connected ? "Connected" : "Not connected"}
              </span>
            </div>

            {quickbooks.connected ? (
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.75)", marginBottom: 18, lineHeight: 1.6 }}>
                Connected to <strong>{quickbooks.companyName || "your QuickBooks company"}</strong>.
                {quickbooks.connectedAt && <> Since {quickbooks.connectedAt}.</>}
                <br />
                Customer details and invoice data read live from QuickBooks once a North America
                customer is linked (see the QuickBooks link section on each Customer record).
              </div>
            ) : (
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, color: "rgba(0,0,0,0.6)", marginBottom: 18, lineHeight: 1.6 }}>
                Not connected yet. North America customer details and invoice data won't load
                until an admin connects the QuickBooks company for Solo Secure Technologies USA
                Inc.
              </div>
            )}

            {quickbooks.connected ? (
              <ActionButton danger disabled={qbDisconnecting} readOnly={isReadOnly} onClick={disconnectQuickbooks} testId="quickbooks-disconnect">
                {qbDisconnecting ? "Disconnecting…" : "Disconnect"}
              </ActionButton>
            ) : isReadOnly ? (
              <span
                data-testid="quickbooks-connect"
                title="Master admins only — you have read-only access"
                style={{
                  display: "inline-block", background: "rgba(0,0,0,0.15)", color: "rgba(0,0,0,0.5)",
                  border: "1px solid rgba(0,0,0,0.15)", padding: "10px 18px", cursor: "not-allowed",
                  fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                  letterSpacing: "0.14em", textTransform: "uppercase",
                }}
              >
                Connect to QuickBooks
              </span>
            ) : (
              <a
                href="/api/admin/quickbooks/connect"
                data-testid="quickbooks-connect"
                style={{
                  display: "inline-block", background: "#000", color: "#fff",
                  border: "1px solid #000", padding: "10px 18px", textDecoration: "none",
                  fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
                  letterSpacing: "0.14em", textTransform: "uppercase",
                }}
              >
                Connect to QuickBooks
              </a>
            )}
          </div>
        )}

        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6 }}>
          Connecting takes you to Intuit's own sign-in and consent screen. Solo Secure only ever
          connects to <strong>one</strong> QuickBooks company at a time — connecting again while
          already connected replaces the existing connection.
        </div>
      </CollapsibleSection>

      <CollapsibleSection
        title="Victron VRM"
        subtitle={victron.status === "ready" ? `${victron.connections.filter((c) => !!c.vrm_installation_id).length}/${victron.connections.length} linked` : undefined}
        isOpen={!!openSections.victron} onToggle={() => toggleSection("victron")}
        testId="settings-section-victron"
      >
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
          Every customer runs their own separate VRM account, so each one is linked with its own
          access token below — there's no single shared Victron connection like there is for Xero.
        </div>

        {victron.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {victron.status === "error" && <EmptyNote>Couldn't load Victron VRM connections.</EmptyNote>}

        {victron.status === "ready" && (
          <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
            {victron.connections.length === 0 && (
              <div style={{ padding: "20px 22px" }}><EmptyNote>No customers yet.</EmptyNote></div>
            )}
            {victron.connections.map((conn, idx) => (
              <VictronCompanyRow
                key={conn.company_id} connection={conn}
                isLast={idx === victron.connections.length - 1}
                isReadOnly={isReadOnly}
                onChanged={loadVictronConnections}
              />
            ))}
          </div>
        )}
      </CollapsibleSection>

      <CollapsibleSection
        title="Ajax Systems"
        subtitle={ajax.status === "ready" ? `${ajax.connections.filter((c) => !!c.company_id_ajax).length}/${ajax.connections.length} linked` : undefined}
        isOpen={!!openSections.ajax} onToggle={() => toggleSection("ajax")}
        testId="settings-section-ajax"
      >
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
          Every customer runs their own separate Ajax PRO account, so each one is linked with its
          own API key, Ajax Company ID, and Company Token below, same pattern as Victron VRM and
          Teltonika RMS.
        </div>

        {ajax.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {ajax.status === "error" && <EmptyNote>Couldn't load Ajax Systems connections.</EmptyNote>}

        {ajax.status === "ready" && (
          <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
            {ajax.connections.length === 0 && (
              <div style={{ padding: "20px 22px" }}><EmptyNote>No customers yet.</EmptyNote></div>
            )}
            {ajax.connections.map((conn, idx) => (
              <AjaxCompanyRow
                key={conn.company_id} connection={conn}
                isLast={idx === ajax.connections.length - 1}
                isReadOnly={isReadOnly}
                onChanged={loadAjaxConnections}
              />
            ))}
          </div>
        )}

        <AjaxDeviceBackfill isReadOnly={isReadOnly} />
      </CollapsibleSection>

      <CollapsibleSection
        title="Teltonika RMS"
        subtitle={teltonika.status === "ready" ? `${teltonika.connections.filter((c) => !!c.rms_company_name).length}/${teltonika.connections.length} linked` : undefined}
        isOpen={!!openSections.teltonika} onToggle={() => toggleSection("teltonika")}
        testId="settings-section-teltonika"
      >
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
          Every customer runs their own separate Teltonika RMS account, so each one is linked with
          its own access token below, same pattern as Victron VRM.
        </div>

        {teltonika.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {teltonika.status === "error" && <EmptyNote>Couldn't load Teltonika RMS connections.</EmptyNote>}

        {teltonika.status === "ready" && (
          <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
            {teltonika.connections.length === 0 && (
              <div style={{ padding: "20px 22px" }}><EmptyNote>No customers yet.</EmptyNote></div>
            )}
            {teltonika.connections.map((conn, idx) => (
              <TeltonikaCompanyRow
                key={conn.company_id} connection={conn}
                isLast={idx === teltonika.connections.length - 1}
                isReadOnly={isReadOnly}
                onChanged={loadTeltonikaConnections}
              />
            ))}
          </div>
        )}
      </CollapsibleSection>

      <CollapsibleSection
        title="EFOY Cloud"
        subtitle={efoy.status === "ready" ? `${efoy.connections.filter((c) => c.device_count != null).length}/${efoy.connections.length} linked` : undefined}
        isOpen={!!openSections.efoy} onToggle={() => toggleSection("efoy")}
        testId="settings-section-efoy"
      >
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
          Every customer runs their own separate EFOY Cloud account, so each one is linked with its
          own personal access token below, same pattern as Victron VRM and Teltonika RMS.
        </div>

        {efoy.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {efoy.status === "error" && <EmptyNote>Couldn't load EFOY Cloud connections.</EmptyNote>}

        {efoy.status === "ready" && (
          <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
            {efoy.connections.length === 0 && (
              <div style={{ padding: "20px 22px" }}><EmptyNote>No customers yet.</EmptyNote></div>
            )}
            {efoy.connections.map((conn, idx) => (
              <EfoyCompanyRow
                key={conn.company_id} connection={conn}
                isLast={idx === efoy.connections.length - 1}
                isReadOnly={isReadOnly}
                onChanged={loadEfoyConnections}
              />
            ))}
          </div>
        )}

        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6 }}>
          Once a company is connected to Victron, Ajax, Teltonika, or EFOY Cloud above, use{" "}
          <strong>Link devices</strong> on each unit's row on the{" "}
          <a onClick={(e) => { e.preventDefault(); onNavigate("admin-assets"); }} href="#" style={{ color: "#000", textDecoration: "underline" }}>
            Assets
          </a>{" "}
          page to sync that company's devices and match one to this specific asset.
        </div>
      </CollapsibleSection>

      <CollapsibleSection
        title="Regional Contact Defaults"
        subtitle={contactDefaults.status === "ready" ? `${contactDefaults.defaults.length} override${contactDefaults.defaults.length === 1 ? "" : "s"} set` : undefined}
        isOpen={!!openSections.contactDefaults} onToggle={() => toggleSection("contactDefaults")}
        testId="settings-section-contact-defaults"
      >
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
          Every customer sees a fixed <strong>Account Manager</strong> and{" "}
          <strong>Technical Contact</strong> card on their dashboard. Set here by Solo staff only —
          customers never edit this. Each role falls back in this order: a customer-specific
          override (set on that Customer record) → a regional default for that role → the
          worldwide default. Leave a field's name blank to clear it and fall back to the next
          level.
        </div>

        {contactDefaults.status === "loading" && <EmptyNote>Loading…</EmptyNote>}
        {contactDefaults.status === "error" && <EmptyNote>Couldn't load contact defaults.</EmptyNote>}

        {contactDefaults.status === "ready" && contactDefaults.roles.map((role) => (
          <ContactRoleCard
            key={role} role={role}
            regions={contactDefaults.regions}
            defaults={contactDefaults.defaults.filter((d) => d.role === role)}
            isReadOnly={isReadOnly}
            onChanged={loadContactDefaults}
          />
        ))}

        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6 }}>
          A customer-specific override (taking precedence over everything above) can be set on that
          customer's own record from the{" "}
          <a onClick={(e) => { e.preventDefault(); onNavigate("admin-companies"); }} href="#" style={{ color: "#000", textDecoration: "underline" }}>
            Customers
          </a>{" "}
          page.
        </div>
      </CollapsibleSection>

      {showSheetImport && (
        <ImportSheetModal
          products={state.products} companies={state.companies}
          isReadOnly={isReadOnly}
          onCancel={() => setShowSheetImport(false)}
          onDone={() => { setShowSheetImport(false); loadSheetImportStatus(); }}
          onNavigate={onNavigate}
        />
      )}
    </AdminShell>
  );
}

// Small helper text shown under an Ajax credential input, explaining
// exactly what to paste there and where it comes from -- Ajax sends two
// different grant emails ("Company API" vs "Enterprise API") that share
// some field names/values, which is what caused the field-mixup bug this
// hint set is meant to prevent from recurring.
function FieldHint({ children }) {
  return (
    <div style={{
      fontFamily: "var(--font-body)", fontSize: 10.5, color: "rgba(0,0,0,0.45)",
      marginTop: 4, marginBottom: 4, lineHeight: 1.4,
    }}>{children}</div>
  );
}

// One row per company in the Ajax Systems list — same shape as
// VictronCompanyRow/TeltonikaCompanyRow (per-customer credentials, no
// OAuth), except Ajax needs three fields (API key + Ajax Company ID +
// Company Token) instead of one token. See routes/admin-ajax.ts's
// ajax-link/ajax-sync endpoints. Hub-level linking to a specific asset
// happens from the Assets page's "Link devices" picker (see
// admin-assets-page.jsx), once a company shows Connected here.
function AjaxCompanyRow({ connection, isLast, isReadOnly, onChanged }) {
  const isLinked = !!connection.company_id_ajax;
  const [editing, setEditing] = useState(false);
  // Ajax's Company API needs all three of these together (see lib/ajax.ts's
  // header comment): X-Api-Key is the FULL integration key issued in the
  // Ajax PRO dashboard -- NOT the same as the short "Integration ID" Ajax
  // shows in its UI (that's only the trailing segment after the last "/" in
  // the full key; e.g. Integration ID "aj9iCjtJ" vs. the full X-Api-Key
  // "p6VJH63ujs5P29BdoGviaRF/aj9iCjtJ"). Sending only the short Integration
  // ID as X-Api-Key doesn't 401 cleanly -- Ajax's API returns a 500, which is
  // why this field is labeled to match Ajax's own header name exactly rather
  // than "Integration ID" (confirmed live against the Ajax Company API).
  // X-Company-Token is the long-lived per-company token, and the Company ID
  // is embedded in every request URL. Labeled with Ajax's own field names
  // (customer's explicit instruction) rather than generic "API key" wording.
  const [apiKey, setApiKey] = useState("");
  const [companyIdAjax, setCompanyIdAjax] = useState("");
  const [companyToken, setCompanyToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  // Test-before-you-save: hits /ajax-test (validates live against Ajax,
  // never stores anything) so an admin can confirm the three fields are
  // correct before committing Confirm -- same "test button + connected
  // status" pattern as the RTSP "Test connection" button elsewhere.
  const [testing, setTesting] = useState(false);
  const [testResult, setTestResult] = useState(null); // { ok: true, hubCount } | { ok: false, error }

  const hasAllFields = apiKey.trim() && companyIdAjax.trim() && companyToken.trim();

  const test = async () => {
    if (!hasAllFields) {
      setTestResult({ ok: false, error: "AJAX Company ID, X-Api-Key, and X-Company-Token are all required." });
      return;
    }
    setTesting(true);
    setTestResult(null);
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/ajax-test`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ apiKey: apiKey.trim(), companyIdAjax: companyIdAjax.trim(), companyToken: companyToken.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setTestResult({ ok: false, error: data.error || "Couldn't connect with those credentials." });
      } else {
        setTestResult({ ok: true, hubCount: data.hubCount });
      }
    } catch {
      setTestResult({ ok: false, error: "Couldn't reach the server. Check your connection and try again." });
    }
    setTesting(false);
  };

  const link = async () => {
    if (!hasAllFields) {
      setError("AJAX Company ID, X-Api-Key, and X-Company-Token are all required.");
      return;
    }
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/ajax-link`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ apiKey: apiKey.trim(), companyIdAjax: companyIdAjax.trim(), companyToken: companyToken.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't link this connection. Please try again.");
        setBusy(false);
        return;
      }
      setApiKey(""); setCompanyIdAjax(""); setCompanyToken(""); setTestResult(null);
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const unlink = async () => {
    if (!window.confirm(`Unlink ${connection.company_name} from Ajax Systems?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/companies/${connection.company_id}/ajax-link`, { method: "DELETE", credentials: "same-origin" });
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{
      padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: isLinked ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {connection.company_name}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {isLinked
                ? <>Ajax Company ID <strong>{connection.company_id_ajax}</strong>{connection.linked_by_name && <> by {connection.linked_by_name}</>} · {connection.hub_count} hub{connection.hub_count === 1 ? "" : "s"}</>
                : "Not linked"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          {isLinked ? (
            <ActionButton disabled={busy} readOnly={isReadOnly} danger onClick={unlink} testId={`ajax-unlink-${connection.company_id}`}>
              Unlink
            </ActionButton>
          ) : (
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => setEditing((v) => !v)} testId={`ajax-link-toggle-${connection.company_id}`}>
              Link
            </ActionButton>
          )}
        </div>
      </div>

      {editing && !isLinked && (
        <div style={{ marginTop: 12 }}>
          <MiniField label="AJAX Company ID">
            <input
              value={companyIdAjax} onChange={(e) => { setCompanyIdAjax(e.target.value); setTestResult(null); }}
              style={adminInputStyle}
              data-testid={`ajax-company-id-input-${connection.company_id}`}
            />
            <FieldHint>
              From the "Ajax Company API" access email — labeled "AJAX Company ID"
              (a short alphanumeric code). This is different from the "Integration ID".
            </FieldHint>
          </MiniField>
          <MiniField label="X-Api-Key">
            <input
              value={apiKey} onChange={(e) => { setApiKey(e.target.value); setTestResult(null); }}
              style={adminInputStyle}
              data-testid={`ajax-api-key-input-${connection.company_id}`}
            />
            <FieldHint>
              The full key from the Ajax PRO dashboard (Integrations → Company API)
              — a long code that ENDS with your Integration ID, in the form
              &lt;prefix&gt;/&lt;IntegrationID&gt;. Do NOT paste just the short
              "Integration ID" suffix on its own — that will fail.
            </FieldHint>
          </MiniField>
          <MiniField label="X-Company-Token">
            <input
              value={companyToken} onChange={(e) => { setCompanyToken(e.target.value); setTestResult(null); }}
              style={adminInputStyle}
              data-testid={`ajax-company-token-input-${connection.company_id}`}
            />
            <FieldHint>
              From the same "Ajax Company API" access email — labeled
              "X-Company-Token".
            </FieldHint>
          </MiniField>
          <div style={{
            fontFamily: "var(--font-body)", fontSize: 10.5, color: "rgba(0,0,0,0.5)",
            marginTop: -4, marginBottom: 10, lineHeight: 1.4, fontStyle: "italic",
          }}>
            These three values come from Ajax's "Company API" grant email only.
            Ignore any email titled "Ajax Enterprise API" (AWS access keys, events
            queue, etc.) — that's a separate product Solo doesn't use.
          </div>

          <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
            <button
              type="button" onClick={test} disabled={testing || !hasAllFields}
              data-testid={`ajax-test-${connection.company_id}`}
              style={{
                background: "none", border: "1px solid rgba(0,0,0,0.35)", color: "#1A1712",
                cursor: (testing || !hasAllFields) ? "default" : "pointer",
                opacity: (testing || !hasAllFields) ? 0.5 : 1,
                padding: "9px 16px", fontFamily: "var(--font-body)", fontSize: 11,
                fontWeight: 600, letterSpacing: "0.1em", textTransform: "uppercase",
              }}
            >{testing ? "Testing…" : "Test"}</button>

            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={link} testId={`ajax-confirm-link-${connection.company_id}`}>
              {busy ? "Linking…" : "Confirm"}
            </ActionButton>

            {testResult && (
              <span
                data-testid={`ajax-test-result-${connection.company_id}`}
                style={{
                  display: "inline-flex", alignItems: "center", gap: 6,
                  fontFamily: "var(--font-body)", fontSize: 12, fontWeight: 600,
                  color: testResult.ok ? "rgba(20,140,60,0.95)" : "#a30000",
                }}
              >
                <span style={{
                  display: "inline-block", width: 7, height: 7, borderRadius: "50%",
                  background: testResult.ok ? "rgba(20,140,60,0.95)" : "#a30000",
                }} />
                {testResult.ok
                  ? `Connected — ${testResult.hubCount} hub${testResult.hubCount === 1 ? "" : "s"} found`
                  : testResult.error}
              </span>
            )}
          </div>
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

// One-off catch-up action: pulls device lists for every ALREADY-linked
// Ajax hub across the whole system in one click. Needed because the
// auto-link feature (Assets page's Link devices modal) linked 123 hubs
// to assets before this device-sync feature existed, so those hubs
// never got their afterLink device pull -- this is the one place an
// admin can backfill them without opening every single asset's Link
// devices modal by hand. Going forward, newly-linked hubs (manual link
// or auto-link) already get their devices synced automatically the
// moment they're linked, so this button should rarely be needed again.
// Backend: routes/admin-ajax.ts's POST /ajax-hubs/backfill-devices,
// paginated via offset/limit -- driven page-by-page here rather than
// looping server-side so a large fleet can't blow one request's
// Workers subrequest ceiling.
function AjaxDeviceBackfill({ isReadOnly }) {
  const [state, setState] = useState({ status: "idle" }); // idle | running | done | error
  const [progress, setProgress] = useState({ processed: 0, total: 0, hubsSynced: 0, devicesSynced: 0, errors: [] });

  const run = async () => {
    setState({ status: "running" });
    setProgress({ processed: 0, total: 0, hubsSynced: 0, devicesSynced: 0, errors: [] });
    let offset = 0;
    let totalHubsSynced = 0;
    let totalDevicesSynced = 0;
    let allErrors = [];
    try {
      while (true) {
        const res = await fetch("/api/admin/ajax-hubs/backfill-devices", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          credentials: "same-origin",
          body: JSON.stringify({ offset, limit: 15 }),
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) { setState({ status: "error" }); return; }
        totalHubsSynced += data.hubsSynced || 0;
        totalDevicesSynced += data.devicesSynced || 0;
        allErrors = allErrors.concat(data.errors || []);
        setProgress({ processed: data.processed, total: data.total, hubsSynced: totalHubsSynced, devicesSynced: totalDevicesSynced, errors: allErrors });
        if (data.done || !data.nextOffset) break;
        offset = data.nextOffset;
      }
      setState({ status: "done" });
    } catch {
      setState({ status: "error" });
    }
  };

  return (
    <div style={{ marginTop: 4, marginBottom: 20, paddingTop: 16, borderTop: "1px solid rgba(0,0,0,0.1)", maxWidth: 560 }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, marginBottom: 8 }}>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, fontWeight: 600, color: "rgba(0,0,0,0.7)" }}>
          Backfill devices for already-linked hubs
        </div>
        <ActionButton disabled={state.status === "running"} readOnly={isReadOnly} onClick={run} testId="ajax-backfill-devices">
          {state.status === "running" ? "Syncing…" : "Run backfill"}
        </ActionButton>
      </div>
      <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.4)", lineHeight: 1.5 }}>
        One-off catch-up for hubs linked before device syncing existed — pulls each linked hub's
        real connected-device list from Ajax. Newly linked hubs sync automatically already.
      </div>
      {state.status === "running" && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.6)", marginTop: 8 }}>
          Processed {progress.processed} of {progress.total || "…"} linked hubs — {progress.devicesSynced} devices synced so far.
        </div>
      )}
      {state.status === "done" && (
        <div style={{
          fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.7)",
          marginTop: 8, padding: "8px 10px", background: "rgba(20,140,60,0.06)",
          border: "1px solid rgba(20,140,60,0.2)", lineHeight: 1.6,
        }}>
          Done: synced {progress.devicesSynced} devices across {progress.hubsSynced} hubs
          {progress.errors.length > 0 && <> — {progress.errors.length} hub(s) failed.</>}
        </div>
      )}
      {state.status === "error" && <div style={{ marginTop: 8 }}><ModalError>Couldn't run the backfill. Please try again.</ModalError></div>}
    </div>
  );
}

// One row per company in the Victron VRM list. Each customer has their
// own separate VRM account, so linking is just "paste this customer's
// token, we validate it against VRM and store it" -- no OAuth, no
// shared connection. Kept entirely inline on Settings per the
// customer's explicit instruction (unlike Xero, which also has a
// per-company UI on the Customer record page).
function VictronCompanyRow({ connection, isLast, isReadOnly, onChanged }) {
  const isLinked = !!connection.vrm_installation_id;
  const [editing, setEditing] = useState(false);
  const [token, setToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const link = async () => {
    if (!token.trim()) { setError("Paste this customer's VRM access token first."); return; }
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/victron-link`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ apiToken: token.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't link this token. Please try again.");
        setBusy(false);
        return;
      }
      setToken("");
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const unlink = async () => {
    if (!window.confirm(`Unlink ${connection.company_name} from Victron VRM?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/companies/${connection.company_id}/victron-link`, { method: "DELETE", credentials: "same-origin" });
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{
      padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: isLinked ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {connection.company_name}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {isLinked
                ? <>Connected (via API){connection.linked_by_name && <> by {connection.linked_by_name}</>}</>
                : "Not linked"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          {isLinked ? (
            <ActionButton disabled={busy} readOnly={isReadOnly} danger onClick={unlink} testId={`victron-unlink-${connection.company_id}`}>
              Unlink
            </ActionButton>
          ) : (
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => setEditing((v) => !v)} testId={`victron-link-toggle-${connection.company_id}`}>
              Link
            </ActionButton>
          )}
        </div>
      </div>

      {editing && !isLinked && (
        <div style={{ marginTop: 12, display: "flex", gap: 8 }}>
          <input
            value={token} onChange={(e) => setToken(e.target.value)}
            placeholder="Paste this customer's VRM access token"
            style={{ ...adminInputStyle, flex: 1 }}
            data-testid={`victron-token-input-${connection.company_id}`}
          />
          <ActionButton disabled={busy} readOnly={isReadOnly} onClick={link} testId={`victron-confirm-link-${connection.company_id}`}>
            {busy ? "Linking…" : "Confirm"}
          </ActionButton>
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

// One row per company in the Teltonika RMS list — same shape as
// VictronCompanyRow (per-customer access token, no OAuth). See
// routes/admin-mission-control.ts's teltonika-link/teltonika-sync
// endpoints. Device-level linking to a specific asset happens from the
// Assets page's "Link devices" picker (see admin-assets-page.jsx),
// once a company shows Connected here.
function TeltonikaCompanyRow({ connection, isLast, isReadOnly, onChanged }) {
  const isLinked = !!connection.rms_company_name;
  const [editing, setEditing] = useState(false);
  const [token, setToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const link = async () => {
    if (!token.trim()) { setError("Paste this customer's RMS access token first."); return; }
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/teltonika-link`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ accessToken: token.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't link this token. Please try again.");
        setBusy(false);
        return;
      }
      setToken("");
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const unlink = async () => {
    if (!window.confirm(`Unlink ${connection.company_name} from Teltonika RMS?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/companies/${connection.company_id}/teltonika-link`, { method: "DELETE", credentials: "same-origin" });
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{
      padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: isLinked ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {connection.company_name}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {isLinked
                ? <>Linked to <strong>{connection.rms_company_name}</strong>{connection.linked_by_name && <> by {connection.linked_by_name}</>} · {connection.device_count} device{connection.device_count === 1 ? "" : "s"}</>
                : "Not linked"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          {isLinked ? (
            <ActionButton disabled={busy} readOnly={isReadOnly} danger onClick={unlink} testId={`teltonika-unlink-${connection.company_id}`}>
              Unlink
            </ActionButton>
          ) : (
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => setEditing((v) => !v)} testId={`teltonika-link-toggle-${connection.company_id}`}>
              Link
            </ActionButton>
          )}
        </div>
      </div>

      {editing && !isLinked && (
        <div style={{ marginTop: 12, display: "flex", gap: 8 }}>
          <input
            value={token} onChange={(e) => setToken(e.target.value)}
            placeholder="Paste this customer's RMS access token"
            style={{ ...adminInputStyle, flex: 1 }}
            data-testid={`teltonika-token-input-${connection.company_id}`}
          />
          <ActionButton disabled={busy} readOnly={isReadOnly} onClick={link} testId={`teltonika-confirm-link-${connection.company_id}`}>
            {busy ? "Linking…" : "Confirm"}
          </ActionButton>
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

// One row per company in the EFOY Cloud list — same shape as
// VictronCompanyRow/TeltonikaCompanyRow (per-customer personal access
// token, no OAuth). See routes/admin-efoy.ts's efoy-link/efoy-sync
// endpoints. Device-level linking to a specific asset happens from the
// Assets page's "Link devices" picker (see admin-assets-page.jsx),
// once a company shows Connected here.
function EfoyCompanyRow({ connection, isLast, isReadOnly, onChanged }) {
  const isLinked = connection.device_count != null;
  const [editing, setEditing] = useState(false);
  const [token, setToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const link = async () => {
    if (!token.trim()) { setError("Paste this customer's EFOY Cloud personal access token first."); return; }
    setBusy(true);
    setError("");
    try {
      const res = await fetch(`/api/admin/companies/${connection.company_id}/efoy-link`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ apiToken: token.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't link this token. Please try again.");
        setBusy(false);
        return;
      }
      setToken("");
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  const unlink = async () => {
    if (!window.confirm(`Unlink ${connection.company_name} from EFOY Cloud?`)) return;
    setBusy(true);
    try {
      await fetch(`/api/admin/companies/${connection.company_id}/efoy-link`, { method: "DELETE", credentials: "same-origin" });
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{
      padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)",
    }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: isLinked ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {connection.company_name}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {isLinked
                ? <>Connected{connection.linked_by_name && <> by {connection.linked_by_name}</>} · {connection.synced_device_count} device{connection.synced_device_count === 1 ? "" : "s"} synced</>
                : "Not linked"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          {isLinked ? (
            <ActionButton disabled={busy} readOnly={isReadOnly} danger onClick={unlink} testId={`efoy-unlink-${connection.company_id}`}>
              Unlink
            </ActionButton>
          ) : (
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => setEditing((v) => !v)} testId={`efoy-link-toggle-${connection.company_id}`}>
              Link
            </ActionButton>
          )}
        </div>
      </div>

      {editing && !isLinked && (
        <div style={{ marginTop: 12, display: "flex", gap: 8 }}>
          <input
            value={token} onChange={(e) => setToken(e.target.value)}
            placeholder="Paste this customer's EFOY Cloud personal access token"
            style={{ ...adminInputStyle, flex: 1 }}
            data-testid={`efoy-token-input-${connection.company_id}`}
          />
          <ActionButton disabled={busy} readOnly={isReadOnly} onClick={link} testId={`efoy-confirm-link-${connection.company_id}`}>
            {busy ? "Linking…" : "Confirm"}
          </ActionButton>
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

const CONTACT_ROLE_LABELS = { account_manager: "Account Manager", technical_contact: "Technical Contact" };

// One card per contact role (Account Manager / Technical Contact),
// listing the worldwide default plus a row for every region that has
// (or could have) its own override. Each row edits in place via
// PUT /api/admin/contact-defaults -- see lib/customerContacts.ts's
// upsertContactDefault for the "blank name clears it" semantics this
// mirrors exactly.
function ContactRoleCard({ role, regions, defaults, isReadOnly, onChanged }) {
  const global = defaults.find((d) => d.region === null) || null;
  const byRegion = new Map(defaults.filter((d) => d.region !== null).map((d) => [d.region, d]));

  return (
    <div style={{ border: "1px solid rgba(0,0,0,0.14)", maxWidth: 560, marginBottom: 20 }}>
      <div style={{
        padding: "14px 22px", borderBottom: "1px solid rgba(0,0,0,0.08)",
        fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
        letterSpacing: "0.06em", textTransform: "uppercase", color: "rgba(0,0,0,0.7)",
      }}>
        {CONTACT_ROLE_LABELS[role] || role}
      </div>

      <ContactDefaultRow
        role={role} region={null} label="Worldwide default" row={global}
        isReadOnly={isReadOnly} onChanged={onChanged} isLast={false}
      />

      {regions.map((region, idx) => (
        <ContactDefaultRow
          key={region} role={role} region={region} label={region} row={byRegion.get(region) || null}
          isReadOnly={isReadOnly} onChanged={onChanged} isLast={idx === regions.length - 1}
        />
      ))}
    </div>
  );
}

// One row -- either the worldwide default (region=null) or one named
// region -- with an inline edit form for name/email/phone. Clearing the
// name (leaving it blank) on Save deletes the row server-side and this
// level falls back to the next one down the resolution chain.
function ContactDefaultRow({ role, region, label, row, isReadOnly, onChanged, isLast }) {
  const [editing, setEditing] = useState(false);
  const [name, setName] = useState(row?.name || "");
  const [email, setEmail] = useState(row?.email || "");
  const [phone, setPhone] = useState(row?.phone || "");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const startEditing = () => {
    setName(row?.name || ""); setEmail(row?.email || ""); setPhone(row?.phone || "");
    setError("");
    setEditing(true);
  };

  const save = async () => {
    setBusy(true);
    setError("");
    try {
      const res = await fetch("/api/admin/contact-defaults", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify({ role, region, name: name.trim(), email: email.trim() || null, phone: phone.trim() || null }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Couldn't save. Please try again.");
        setBusy(false);
        return;
      }
      setEditing(false);
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setBusy(false);
  };

  return (
    <div style={{ padding: "16px 22px", borderBottom: isLast ? "none" : "1px solid rgba(0,0,0,0.08)" }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <span style={{
            display: "inline-block", width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
            background: row ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
          }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "rgba(0,0,0,0.85)" }}>
              {label}
            </div>
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
              {row
                ? <>{row.name}{row.email && <> · {row.email}</>}{row.phone && <> · {row.phone}</>}</>
                : "Not set — falls back to the next level"}
            </div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          <ActionButton disabled={busy} readOnly={isReadOnly} onClick={() => (editing ? setEditing(false) : startEditing())}>
            {editing ? "Cancel" : row ? "Edit" : "Set"}
          </ActionButton>
        </div>
      </div>

      {editing && (
        <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 8 }}>
          <input
            value={name} onChange={(e) => setName(e.target.value)}
            placeholder="Name" style={adminInputStyle}
          />
          <input
            value={email} onChange={(e) => setEmail(e.target.value)}
            placeholder="Email" style={adminInputStyle}
          />
          <input
            value={phone} onChange={(e) => setPhone(e.target.value)}
            placeholder="Phone" style={adminInputStyle}
          />
          <div>
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={save}>
              {busy ? "Saving…" : "Save"}
            </ActionButton>
          </div>
          {row && (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 11, color: "rgba(0,0,0,0.4)" }}>
              Leave the name blank and Save to clear this and fall back to the next level.
            </div>
          )}
        </div>
      )}
      {error && <div style={{ marginTop: 8 }}><ModalError>{error}</ModalError></div>}
    </div>
  );
}

function CallbackBanner({ notice, onDismiss }) {
  // provider defaults to "xero" so any old/leftover ?xero=... callback
  // (from a page that hasn't reloaded this bundle yet) still renders the
  // same text it always has -- see admin-quickbooks.ts's /quickbooks/callback
  // for the ?quickbooks=... equivalent flow.
  const providerLabel = notice.provider === "quickbooks" ? "QuickBooks" : "Xero";
  const variants = {
    connected: { bg: "rgba(20,140,60,0.08)", border: "rgba(20,140,60,0.35)", color: "rgba(15,100,45,0.95)", text: `${providerLabel} connected successfully.` },
    denied: { bg: "rgba(180,110,0,0.08)", border: "rgba(180,110,0,0.35)", color: "rgba(140,85,0,0.95)", text: `${providerLabel} connection was cancelled — access wasn't granted.` },
    state_mismatch: { bg: "rgba(190,40,40,0.08)", border: "rgba(190,40,40,0.35)", color: "#8a1f1f", text: `That ${providerLabel} connection attempt expired or was invalid. Please try again.` },
    error: { bg: "rgba(190,40,40,0.08)", border: "rgba(190,40,40,0.35)", color: "#8a1f1f", text: notice.message || `Couldn't connect to ${providerLabel}. Please try again.` },
  };
  const v = variants[notice.kind] || variants.error;
  return (
    <div style={{
      background: v.bg, border: `1px solid ${v.border}`, color: v.color,
      padding: "12px 14px", marginBottom: 24, display: "flex",
      justifyContent: "space-between", alignItems: "center", gap: 12,
      fontFamily: "var(--font-body)", fontSize: 13, lineHeight: 1.5,
    }}>
      <span>{v.text}</span>
      <button type="button" onClick={onDismiss} style={{
        background: "none", border: "none", color: "inherit", cursor: "pointer",
        fontFamily: "var(--font-body)", fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase",
      }}>Dismiss</button>
    </div>
  );
}

// "Your account" -- brand new section (no personal-account block existed
// here before; unlike reseller-settings-page.jsx, which already had a
// ChangePasswordForm to extend). Two-factor is OPT-IN for Solo staff
// (explicit customer instruction, see migrations/0060_two_factor_auth.sql's
// header) -- unlike the reseller panel, this one supports a full disable,
// via POST /api/admin/2fa/disable.
function AdminAccountSection({ admin }) {
  const [state, setState] = useState({ phase: "loading", enrolled: false, remainingRecoveryCodes: 0 });
  const [pendingToken, setPendingToken] = useState("");
  const [otpauthUri, setOtpauthUri] = useState("");
  const [recoveryCodes, setRecoveryCodes] = useState(null);
  const [error, setError] = useState("");
  const [disabling, setDisabling] = useState(false);

  const loadStatus = () => {
    fetch("/api/admin/2fa/status", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : { enrolled: false, remainingRecoveryCodes: 0 }))
      .then((d) => setState({ phase: "ready", enrolled: !!d.enrolled, remainingRecoveryCodes: d.remainingRecoveryCodes || 0 }))
      .catch(() => setState({ phase: "ready", enrolled: false, remainingRecoveryCodes: 0 }));
  };

  useEffect(() => { loadStatus(); }, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  const startEnroll = async () => {
    setError("");
    try {
      const res = await fetch("/api/admin/2fa/enroll", { method: "POST", credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong. Please try again."); return; }
      setPendingToken(data.pendingToken);
      setOtpauthUri(data.otpauthUri);
      setState((s) => ({ ...s, phase: "enrolling" }));
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
  };

  const disable = async () => {
    if (!window.confirm("Turn off two-factor authentication on your account? You'll only need your password to sign in.")) return;
    setDisabling(true);
    setError("");
    try {
      const res = await fetch("/api/admin/2fa/disable", { method: "POST", credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong. Please try again."); setDisabling(false); return; }
      loadStatus();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setDisabling(false);
  };

  return (
    <div style={{ marginBottom: 32 }}>
      <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", maxWidth: 560, lineHeight: 1.6, marginBottom: 16 }}>
        <strong>Your account</strong> — {admin ? admin.name : ""} ({admin ? admin.email : ""}). Two-factor authentication is optional for staff — enable it here for extra protection on your own login.
      </div>

      {state.phase === "loading" ? null : state.phase === "enrolling" ? (
        <div style={{ maxWidth: 480 }}>
          <TwoFactorEnrollCard
            confirmUrl="/api/admin/2fa/enroll/confirm"
            pendingToken={pendingToken}
            otpauthUri={otpauthUri}
            accountLabel={admin ? admin.email : ""}
            intro="Scan this QR code with an authenticator app (Google Authenticator, Authy, 1Password, etc.), then enter the 6-digit code it shows."
            onSuccess={(codes) => { setRecoveryCodes(codes); setState((s) => ({ ...s, phase: "done" })); }}
          />
        </div>
      ) : state.phase === "done" ? (
        <div style={{ maxWidth: 480 }}>
          <RecoveryCodesCard
            codes={recoveryCodes}
            onContinue={() => { setRecoveryCodes(null); loadStatus(); }}
          />
        </div>
      ) : (
        <div style={{
          background: "rgba(0,0,0,0.02)", border: "1px solid rgba(0,0,0,0.14)",
          padding: "20px 22px", maxWidth: 480,
        }}>
          {error && (
            <div style={{
              background: "rgba(190,40,40,0.08)", border: "1px solid rgba(190,40,40,0.35)",
              color: "#8a1f1f", padding: "12px 14px", marginBottom: 16,
              fontFamily: "var(--font-body)", fontSize: 13, lineHeight: 1.5,
            }}>{error}</div>
          )}
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
            <span style={{
              display: "inline-block", width: 8, height: 8, borderRadius: "50%",
              background: state.enrolled ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.3)",
            }} />
            <span style={{
              fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600,
              letterSpacing: "0.06em", textTransform: "uppercase",
              color: state.enrolled ? "rgba(20,140,60,0.95)" : "rgba(0,0,0,0.55)",
            }}>
              Two-factor authentication {state.enrolled ? "enabled" : "off"}
            </span>
          </div>

          {state.enrolled ? (
            <>
              <p style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.6)", margin: "0 0 16px", lineHeight: 1.5 }}>
                {state.remainingRecoveryCodes} unused recovery code{state.remainingRecoveryCodes === 1 ? "" : "s"} remaining.
              </p>
              <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
                <ActionButton onClick={startEnroll} testId="admin-2fa-reset">Reset (new device)</ActionButton>
                <ActionButton danger disabled={disabling} onClick={disable} testId="admin-2fa-disable">
                  {disabling ? "Turning off…" : "Turn off"}
                </ActionButton>
              </div>
            </>
          ) : (
            <>
              <p style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.6)", margin: "0 0 16px", lineHeight: 1.5 }}>
                Add an authenticator-app code on top of your password.
              </p>
              <ActionButton onClick={startEnroll} testId="admin-2fa-enable">Set up two-factor authentication</ActionButton>
            </>
          )}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { AdminSettingsPage });
