// Solo staff admin area — asset management: product catalogue + serialized
// unit inventory + assign/move-between-companies with invoice/PO capture.
//
// Reached via the sidebar nav in AdminShell (see admin-shell.jsx). Reuses
// FieldBlock / DarkInput / LiveDot / CornerHairlines from
// reseller-login-page.jsx (loaded earlier — see index.html), plus the
// plain inputStyle/selectStyle/ActionButton-style helpers defined locally
// below (kept local rather than reused from admin-portal-page.jsx since
// that file doesn't export them on window).

function AdminAssetsPage({ onNavigate }) {
  const [state, setState] = useState({ status: "loading", admin: null, products: [], units: [], companies: [] });
  const [busyId, setBusyId] = useState(null);
  const [showAddUnit, setShowAddUnit] = useState(false);
  const [showBulkAdd, setShowBulkAdd] = useState(false);
  const [filterProduct, setFilterProduct] = useState("");
  const [filterStatus, setFilterStatus] = useState("");
  const [filterSerial, setFilterSerial] = useState("");
  const [filterCompany, setFilterCompany] = useState("");
  const [assigningUnit, setAssigningUnit] = useState(null); // unit object or null
  const [historyUnit, setHistoryUnit] = useState(null); // { unit, history } or null
  const [linkingUnit, setLinkingUnit] = useState(null); // unit object or null (Mission Control vendor device link)

  const load = () => {
    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((s) => ({
          ...s, status: "ready", admin: me.admin,
          products: products.products || [], companies: companies.companies || [],
        }));
        return loadUnits();
      })
      .catch(() => onNavigate("admin-login"));
  };

  const loadUnits = () => {
    const params = new URLSearchParams();
    if (filterProduct) params.set("productId", filterProduct);
    if (filterStatus) params.set("status", filterStatus);
    if (filterSerial) params.set("serial", filterSerial);
    if (filterCompany) params.set("companyId", filterCompany);
    return fetch(`/api/admin/units?${params.toString()}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setState((s) => ({ ...s, units: data.units || [] })));
  };

  useEffect(load, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only
  useEffect(() => {
    if (state.status === "ready") loadUnits();
  }, [filterProduct, filterStatus, filterSerial, filterCompany]); // eslint-disable-line react-hooks/exhaustive-deps

  const handleAddUnit = async (form) => {
    setBusyId("add-unit");
    try {
      const res = await fetch("/api/admin/units", {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(form),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) return data.error || "Something went wrong.";
      setShowAddUnit(false);
      await loadUnits();
      return null;
    } finally { setBusyId(null); }
  };

  const handleBulkAdd = async (form) => {
    setBusyId("bulk-add");
    try {
      const res = await fetch("/api/admin/units/bulk", {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(form),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) return { error: data.error || "Something went wrong.", conflicts: data.conflicts };
      setShowBulkAdd(false);
      await loadUnits();
      return { ok: true, count: data.count };
    } finally { setBusyId(null); }
  };

  const handleAssign = async (unitId, form) => {
    setBusyId(`assign-${unitId}`);
    try {
      const res = await fetch(`/api/admin/units/${unitId}/assign`, {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(form),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) return data.error || "Something went wrong.";
      setAssigningUnit(null);
      await loadUnits();
      return null;
    } finally { setBusyId(null); }
  };

  const handleUnassign = async (unitId) => {
    setBusyId(`unassign-${unitId}`);
    try { await fetch(`/api/admin/units/${unitId}/unassign`, { method: "POST", credentials: "same-origin" }); await loadUnits(); }
    finally { setBusyId(null); }
  };

  const openHistory = async (unitId) => {
    const res = await fetch(`/api/admin/units/${unitId}`, { credentials: "same-origin" });
    const data = await res.json().catch(() => null);
    if (data) setHistoryUnit(data);
  };

  // Grant/revoke a unit's System Management access for the reseller
  // portal (admin's own view of the unit is never affected by this).
  const handleToggleAccess = async (unit) => {
    setBusyId(`access-${unit.id}`);
    try {
      await fetch(`/api/admin/units/${unit.id}/management-access`, {
        method: "PATCH", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ enabled: !unit.management_access_enabled }),
      });
      await loadUnits();
    } finally { setBusyId(null); }
  };

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

  // Solo Staff (role "admin") are system-wide read-only on the backend
  // already (requireSuperAdmin) — this just keeps the UI from offering
  // actions that would 403. See UnitRow/AddUnitModal/etc. below for where
  // this is actually threaded through to individual controls.
  const isReadOnly = state.admin && state.admin.role !== "super_admin";

  return (
    <AdminShell admin={state.admin} page="admin-assets" onNavigate={onNavigate}
      subtitle="Staff only" title="Asset management."
      actions={(
        <div style={{ display: "flex", gap: 10 }}>
          <ActionButton readOnly={isReadOnly} onClick={() => setShowAddUnit(true)} testId="open-add-unit">+ Add unit</ActionButton>
          <ActionButton readOnly={isReadOnly} onClick={() => setShowBulkAdd(true)} testId="open-bulk-add">+ Bulk add units</ActionButton>
          {/* The spreadsheet import lives on Settings > Data Connections
              now (single home for every external connection) — this just
              links there rather than duplicating the trigger here. Left
              ungated: navigating to Settings is harmless read-only for
              Jay, and that page's own "Update from spreadsheet" opener
              is already gated (see admin-settings-page.jsx). */}
          <ActionButton onClick={() => onNavigate("admin-settings")} testId="open-import-csv">
            Update from spreadsheet →
          </ActionButton>
        </div>
      )}
    >
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 20 }}>
        <MiniField label="Product">
          <select value={filterProduct} onChange={(e) => setFilterProduct(e.target.value)} style={selectStyle}>
            <option value="">All products</option>
            {state.products.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
          </select>
        </MiniField>
        <MiniField label="Status">
          <select value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)} style={selectStyle}>
            <option value="">All statuses</option>
            <option value="in_stock">In stock</option>
            <option value="assigned">Assigned</option>
            <option value="retired">Retired</option>
          </select>
        </MiniField>
        <MiniField label="Serial search">
          <input value={filterSerial} onChange={(e) => setFilterSerial(e.target.value)} placeholder="e.g. PCT-V1-UK" style={inputStyle} />
        </MiniField>
        <MiniField label="Customer">
          <select value={filterCompany} onChange={(e) => setFilterCompany(e.target.value)} style={selectStyle}>
            <option value="">All customers</option>
            {state.companies.map((co) => <option key={co.id} value={co.id}>{co.name}</option>)}
          </select>
        </MiniField>
      </div>

      <SectionLabel>Units ({state.units.length})</SectionLabel>
      {state.units.length === 0 ? (
        <EmptyNote>No units match these filters.</EmptyNote>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {state.units.map((u) => (
            <UnitRow
              key={u.id} unit={u}
              busy={busyId === `assign-${u.id}` || busyId === `unassign-${u.id}` || busyId === `access-${u.id}`}
              isReadOnly={isReadOnly}
              onAssign={() => setAssigningUnit(u)}
              onUnassign={() => handleUnassign(u.id)}
              onHistory={() => openHistory(u.id)}
              onManage={() => onNavigate("unit-management", { unitId: u.id, viewerType: "admin" })}
              onToggleAccess={() => handleToggleAccess(u)}
              onLinkDevices={() => setLinkingUnit(u)}
              onNavigate={onNavigate}
            />
          ))}
        </div>
      )}

      {showAddUnit && (
        <AddUnitModal
          products={state.products} companies={state.companies}
          busy={busyId === "add-unit"} isReadOnly={isReadOnly}
          onCancel={() => setShowAddUnit(false)}
          onSubmit={handleAddUnit}
        />
      )}

      {showBulkAdd && (
        <BulkAddUnitsModal
          products={state.products} companies={state.companies}
          busy={busyId === "bulk-add"} isReadOnly={isReadOnly}
          onCancel={() => setShowBulkAdd(false)}
          onSubmit={handleBulkAdd}
        />
      )}

      {assigningUnit && (
        <AssignUnitModal
          unit={assigningUnit} companies={state.companies}
          busy={busyId === `assign-${assigningUnit.id}`} isReadOnly={isReadOnly}
          onCancel={() => setAssigningUnit(null)}
          onSubmit={(form) => handleAssign(assigningUnit.id, form)}
        />
      )}

      {historyUnit && (
        <HistoryModal data={historyUnit} onClose={() => setHistoryUnit(null)} />
      )}

      {linkingUnit && (
        <LinkDevicesModal
          unit={linkingUnit} isReadOnly={isReadOnly}
          onCancel={() => setLinkingUnit(null)}
          onChanged={loadUnits}
        />
      )}
    </AdminShell>
  );
}

function UnitRow({ unit, busy, isReadOnly, onAssign, onUnassign, onHistory, onManage, onToggleAccess, onLinkDevices, onNavigate }) {
  const statusColor = unit.status === "assigned" ? "rgba(20,140,60,0.95)"
    : unit.status === "retired" ? "rgba(190,40,40,0.9)"
    : "rgba(0,0,0,0.55)";
  const accessEnabled = !!unit.management_access_enabled;
  const [xeroInvoice, setXeroInvoice] = useState(null); // { found, url } | null (not yet looked up)

  useEffect(() => {
    if (!unit.current_invoice_number || !unit.current_company_id) return;
    let cancelled = false;
    fetch(`/api/admin/xero/invoices/lookup?companyId=${unit.current_company_id}&invoiceNumber=${encodeURIComponent(unit.current_invoice_number)}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : null))
      .then((data) => { if (!cancelled && data) setXeroInvoice(data); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [unit.current_invoice_number, unit.current_company_id]);

  return (
    <div data-testid={`unit-row-${unit.id}`} style={{
      background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)",
      padding: "16px 20px", display: "flex", justifyContent: "space-between",
      alignItems: "center", flexWrap: "wrap", gap: 14,
    }}>
      <div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 14.5, fontWeight: 600, color: "#000" }}>
          {unit.serial_number} <span style={{ color: "rgba(0,0,0,0.5)", fontWeight: 400 }}>· {unit.product_name}</span>
          {unit.version && <span style={{ color: "rgba(0,0,0,0.4)", fontWeight: 400 }}> · {unit.version}</span>}
        </div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)", marginTop: 4 }}>
          <span style={{ color: statusColor, textTransform: "uppercase", letterSpacing: "0.08em", fontSize: 11 }}>{unit.status.replace("_", " ")}</span>
          {unit.current_company_name && <> · {unit.current_company_name}</>}
          {(unit.current_region || unit.current_country) && (
            <> · {[unit.current_country, unit.current_region].filter(Boolean).join(", ")}</>
          )}
          {unit.current_user_name && <> · assigned to {unit.current_user_name} ({unit.current_user_email})</>}
          {" · "}
          <span style={{ color: accessEnabled ? "rgba(0,0,0,0.45)" : "rgba(190,80,30,0.9)" }}>
            System Management {accessEnabled ? "enabled" : "disabled"}
          </span>
        </div>
        {(unit.current_po_number || unit.current_invoice_number || unit.current_po_missing || unit.current_invoice_missing) && (
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)", marginTop: 4 }}>
            {unit.current_po_number ? (
              <>
                {unit.po_position && unit.po_total ? `${unit.po_position} of ${unit.po_total} · ` : ""}
                PO{" "}
                {unit.current_po_id && onNavigate ? (
                  <a
                    onClick={(e) => { e.preventDefault(); onNavigate("admin-purchase-orders", { poId: unit.current_po_id }); }}
                    href="#" style={{ color: "#000", textDecoration: "underline" }}
                  >{unit.current_po_number}</a>
                ) : unit.current_po_number}
              </>
            ) : unit.current_po_missing ? (
              <span style={{ color: "rgba(190,40,40,0.9)" }} title="No PO Number was on file for this unit at import time.">⚠ PO missing</span>
            ) : null}
            {(unit.current_po_number || unit.current_po_missing) && (unit.current_invoice_number || unit.current_invoice_missing) && " · "}
            {unit.current_invoice_number ? (
              <>
                Invoice {unit.current_invoice_number}
                {xeroInvoice?.found && (
                  <> — <a href={xeroInvoice.url} target="_blank" rel="noreferrer" style={{ color: "#000", textDecoration: "underline" }}>View in Xero</a></>
                )}
                {xeroInvoice && !xeroInvoice.found && xeroInvoice.reason === "not_linked" && (
                  <span style={{ color: "rgba(180,110,0,0.9)" }}> (customer not linked to Xero yet)</span>
                )}
              </>
            ) : unit.current_invoice_missing ? (
              <span style={{ color: "rgba(190,40,40,0.9)" }} title="No invoice number was on file for this unit at import time, or it could not be verified against Xero.">⚠ Invoice missing</span>
            ) : null}
            {unit.current_invoice_number && unit.current_invoice_missing && (
              <span style={{ color: "rgba(190,40,40,0.9)" }} title="This invoice number could not be found on the customer's Xero invoices at import time.">
                {" "}⚠ not verified in Xero
              </span>
            )}
          </div>
        )}
      </div>
      <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
        <ActionButton disabled={busy} onClick={onManage} testId={`manage-${unit.id}`}>System Management</ActionButton>
        <ActionButton disabled={busy} readOnly={isReadOnly} onClick={onToggleAccess} testId={`toggle-access-${unit.id}`}>
          {accessEnabled ? "Revoke access" : "Allow access"}
        </ActionButton>
        <ActionButton disabled={busy} onClick={onHistory} testId={`history-${unit.id}`}>History</ActionButton>
        {unit.current_company_id && (
          <ActionButton disabled={busy} readOnly={isReadOnly} onClick={onLinkDevices} testId={`link-devices-${unit.id}`}>
            Link devices
          </ActionButton>
        )}
        {unit.status !== "retired" && (
          <ActionButton disabled={busy} onClick={onAssign} testId={`open-assign-${unit.id}`}>
            {unit.status === "assigned" ? "Move" : "Assign"}
          </ActionButton>
        )}
        {unit.status === "assigned" && (
          <ActionButton disabled={busy} danger readOnly={isReadOnly} onClick={onUnassign} testId={`unassign-btn-${unit.id}`}>Unassign</ActionButton>
        )}
      </div>
    </div>
  );
}

// `lockCompanyId` (optional): when set, the company picker is replaced
// with a fixed, non-editable label showing that company's name — used by
// admin-companies-page.jsx's per-customer asset register, where a new
// unit added from that screen should always land on that customer.
function AddUnitModal({ products, companies, busy, isReadOnly, onCancel, onSubmit, lockCompanyId }) {
  const [productId, setProductId] = useState(products[0]?.id || "");
  const [serialNumber, setSerialNumber] = useState("");
  const [companyId, setCompanyId] = useState(lockCompanyId ? String(lockCompanyId) : "");
  const [invoiceNumber, setInvoiceNumber] = useState("");
  const [poNumber, setPoNumber] = useState("");
  const [error, setError] = useState("");

  const canSubmit = productId && serialNumber.trim() && !busy;
  const lockedCompanyName = lockCompanyId ? (companies.find((c) => c.id === lockCompanyId)?.name || "This customer") : null;

  const submit = async () => {
    if (!canSubmit) return;
    setError("");
    const err = await onSubmit({
      productId: Number(productId),
      serialNumber: serialNumber.trim(),
      companyId: companyId ? Number(companyId) : null,
      invoiceNumber: invoiceNumber.trim() || null,
      poNumber: poNumber.trim() || null,
    });
    if (err) setError(err);
  };

  return (
    <ModalShell title="Add a new unit" onCancel={onCancel}>
      {error && <ModalError>{error}</ModalError>}
      <MiniField label="Product">
        <select value={productId} onChange={(e) => setProductId(e.target.value)} style={selectStyle} data-testid="add-unit-product-select">
          {products.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
        </select>
      </MiniField>
      <MiniField label="Serial number">
        <input value={serialNumber} onChange={(e) => setSerialNumber(e.target.value)} placeholder="e.g. PCT-V1-UK-100001" style={inputStyle} data-testid="add-unit-serial" />
      </MiniField>
      {lockCompanyId ? (
        <MiniField label="Customer">
          <div style={{ ...inputStyle, opacity: 0.75 }}>{lockedCompanyName}</div>
        </MiniField>
      ) : (
        <MiniField label="Assign to company now (optional)">
          <select value={companyId} onChange={(e) => setCompanyId(e.target.value)} style={selectStyle}>
            <option value="">Leave in stock, unassigned</option>
            {companies.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
        </MiniField>
      )}
      {companyId && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <MiniField label="Invoice number">
            <input value={invoiceNumber} onChange={(e) => setInvoiceNumber(e.target.value)} placeholder="INV-2026-0001" style={inputStyle} />
          </MiniField>
          <MiniField label="PO number">
            <input value={poNumber} onChange={(e) => setPoNumber(e.target.value)} placeholder="PO-0001" style={inputStyle} />
          </MiniField>
        </div>
      )}
      <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
        <ActionButton disabled={!canSubmit} readOnly={isReadOnly} onClick={submit} testId="submit-add-unit">
          {busy ? "Adding…" : "Add unit"}
        </ActionButton>
        <ActionButton disabled={busy} danger onClick={onCancel}>Cancel</ActionButton>
      </div>
    </ModalShell>
  );
}

// Bulk-create a contiguous run of serialized units in one go — e.g.
// selling 25+ units to one customer at once. Serial numbers are built as
// `${prefix}${paddedNumber}${suffix}` for every whole number from `start`
// to `end` inclusive. Prefix/suffix/pad width are all optional, so a
// plain numeric range (e.g. 1099 to 1266, as sold) works with none of
// them set, or a prefixed convention (e.g. PCT-V1-UK-100099..100266)
// works by setting a prefix and pad width. Mirrors AddUnitModal's
// "assign to company now" section so a freshly-created batch can be
// handed straight to one customer, with one invoice/PO covering the lot.
function BulkAddUnitsModal({ products, companies, busy, isReadOnly, onCancel, onSubmit }) {
  const [productId, setProductId] = useState(products[0]?.id || "");
  const [prefix, setPrefix] = useState("");
  const [suffix, setSuffix] = useState("");
  const [start, setStart] = useState("");
  const [end, setEnd] = useState("");
  const [padWidth, setPadWidth] = useState("");
  const [companyId, setCompanyId] = useState("");
  const [invoiceNumber, setInvoiceNumber] = useState("");
  const [poNumber, setPoNumber] = useState("");
  const [error, setError] = useState("");
  const [conflicts, setConflicts] = useState(null);
  const [result, setResult] = useState(null);

  const startNum = start === "" ? null : Number(start);
  const endNum = end === "" ? null : Number(end);
  const validRange =
    startNum !== null && endNum !== null &&
    Number.isInteger(startNum) && Number.isInteger(endNum) && endNum >= startNum;
  const count = validRange ? endNum - startNum + 1 : 0;
  const pad = padWidth ? Number(padWidth) : 0;

  const previewSerial = (n) => `${prefix}${pad ? String(n).padStart(pad, "0") : String(n)}${suffix}`;

  const canSubmit = productId && validRange && count > 0 && count <= 500 && !busy;

  const submit = async () => {
    if (!canSubmit) return;
    setError(""); setConflicts(null);
    const res = await onSubmit({
      productId: Number(productId),
      prefix: prefix.trim() || null,
      suffix: suffix.trim() || null,
      start: startNum,
      end: endNum,
      padWidth: pad || null,
      companyId: companyId ? Number(companyId) : null,
      invoiceNumber: invoiceNumber.trim() || null,
      poNumber: poNumber.trim() || null,
    });
    if (res?.error) { setError(res.error); if (res.conflicts) setConflicts(res.conflicts); return; }
    setResult(res);
  };

  if (result?.ok) {
    return (
      <ModalShell title="Units created" onCancel={onCancel}>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 14, color: "#000", marginBottom: 18, lineHeight: 1.6 }}>
          Created <strong>{result.count}</strong> unit{result.count === 1 ? "" : "s"}
          {companyId ? " and assigned them to the selected company." : ", left in stock."}
        </div>
        <ActionButton onClick={onCancel} testId="bulk-add-done">Done</ActionButton>
      </ModalShell>
    );
  }

  return (
    <ModalShell title="Bulk add units" onCancel={onCancel}>
      {error && (
        <ModalError>
          {error}
          {conflicts && conflicts.length > 0 && (
            <div style={{ marginTop: 8, fontSize: 12, opacity: 0.85 }}>
              {conflicts.slice(0, 10).join(", ")}{conflicts.length > 10 ? `, +${conflicts.length - 10} more` : ""}
            </div>
          )}
        </ModalError>
      )}
      <MiniField label="Product">
        <select value={productId} onChange={(e) => setProductId(e.target.value)} style={selectStyle} data-testid="bulk-add-product-select">
          {products.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
        </select>
      </MiniField>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
        <MiniField label="Start number">
          <input value={start} onChange={(e) => setStart(e.target.value)} placeholder="e.g. 1099" style={inputStyle} data-testid="bulk-add-start" />
        </MiniField>
        <MiniField label="End number">
          <input value={end} onChange={(e) => setEnd(e.target.value)} placeholder="e.g. 1266" style={inputStyle} data-testid="bulk-add-end" />
        </MiniField>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
        <MiniField label="Prefix (optional)">
          <input value={prefix} onChange={(e) => setPrefix(e.target.value)} placeholder="e.g. PCT-V1-UK-" style={inputStyle} data-testid="bulk-add-prefix" />
        </MiniField>
        <MiniField label="Suffix (optional)">
          <input value={suffix} onChange={(e) => setSuffix(e.target.value)} placeholder="" style={inputStyle} />
        </MiniField>
        <MiniField label="Zero-pad width (optional)">
          <input value={padWidth} onChange={(e) => setPadWidth(e.target.value)} placeholder="e.g. 6" style={inputStyle} />
        </MiniField>
      </div>

      {start !== "" && end !== "" && !validRange && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(180,110,0,0.95)", marginBottom: 14 }}>
          End must be a whole number greater than or equal to start.
        </div>
      )}
      {validRange && count > 500 && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(180,110,0,0.95)", marginBottom: 14 }}>
          That range is {count} units — bulk add is limited to 500 at a time. Split it into smaller batches.
        </div>
      )}
      {validRange && count > 0 && count <= 500 && (
        <div data-testid="bulk-add-preview" style={{
          fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.65)",
          background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)",
          padding: "10px 12px", marginBottom: 14, lineHeight: 1.5,
        }}>
          This will create <strong style={{ color: "#000" }}>{count}</strong> unit{count === 1 ? "" : "s"}:
          {" "}{previewSerial(startNum)}{count > 1 ? ` through ${previewSerial(endNum)}` : ""}
        </div>
      )}

      <MiniField label="Assign the whole batch to a company now (optional)">
        <select value={companyId} onChange={(e) => setCompanyId(e.target.value)} style={selectStyle} data-testid="bulk-add-company-select">
          <option value="">Leave in stock, unassigned</option>
          {companies.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
        </select>
      </MiniField>
      {companyId && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <MiniField label="Invoice number">
            <input value={invoiceNumber} onChange={(e) => setInvoiceNumber(e.target.value)} placeholder="INV-2026-0001" style={inputStyle} />
          </MiniField>
          <MiniField label="PO number">
            <input value={poNumber} onChange={(e) => setPoNumber(e.target.value)} placeholder="PO-0001" style={inputStyle} />
          </MiniField>
        </div>
      )}

      <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
        <ActionButton disabled={!canSubmit} readOnly={isReadOnly} onClick={submit} testId="submit-bulk-add">
          {busy ? "Creating…" : `Create ${count || ""} unit${count === 1 ? "" : "s"}`}
        </ActionButton>
        <ActionButton disabled={busy} danger onClick={onCancel}>Cancel</ActionButton>
      </div>
    </ModalShell>
  );
}

// `lockCompanyId` (optional): pins the target company (shown as a fixed
// label, not a picker) — used when moving/assigning from a single
// customer's asset register (admin-companies-page.jsx), where "move to a
// different company" doesn't make sense as an action on that screen; use
// the main Assets page for cross-company moves instead.
function AssignUnitModal({ unit, companies, busy, isReadOnly, onCancel, onSubmit, lockCompanyId }) {
  const [companyId, setCompanyId] = useState(lockCompanyId ? String(lockCompanyId) : (unit.current_company_id || ""));
  const [userId, setUserId] = useState(unit.current_user_id || "");
  const [invoiceNumber, setInvoiceNumber] = useState("");
  const [poNumber, setPoNumber] = useState("");
  const [notes, setNotes] = useState("");
  const [error, setError] = useState("");
  const [companyUsers, setCompanyUsers] = useState([]);

  useEffect(() => {
    if (!companyId) { setCompanyUsers([]); return; }
    fetch(`/api/admin/companies/${companyId}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : null))
      .then((data) => setCompanyUsers(data?.users?.filter((u) => u.status === "active") || []))
      .catch(() => setCompanyUsers([]));
  }, [companyId]);

  const canSubmit = companyId && !busy;
  const lockedCompanyName = lockCompanyId ? (companies.find((c) => c.id === lockCompanyId)?.name || "This customer") : null;

  const submit = async () => {
    if (!canSubmit) return;
    setError("");
    const err = await onSubmit({
      companyId: Number(companyId),
      userId: userId ? Number(userId) : null,
      invoiceNumber: invoiceNumber.trim() || null,
      poNumber: poNumber.trim() || null,
      notes: notes.trim() || null,
    });
    if (err) setError(err);
  };

  return (
    <ModalShell title={`${unit.status === "assigned" ? "Move" : "Assign"} ${unit.serial_number}`} onCancel={onCancel}>
      {error && <ModalError>{error}</ModalError>}
      {lockCompanyId ? (
        <MiniField label="Customer">
          <div style={{ ...inputStyle, opacity: 0.75 }}>{lockedCompanyName}</div>
        </MiniField>
      ) : (
        <MiniField label="Company">
          <select value={companyId} onChange={(e) => { setCompanyId(e.target.value); setUserId(""); }} style={selectStyle} data-testid="assign-company-select">
            <option value="">Choose a company…</option>
            {companies.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
        </MiniField>
      )}
      <MiniField label="Assign to a specific user (optional — leave blank for company-wide)">
        <select value={userId} onChange={(e) => setUserId(e.target.value)} style={selectStyle} disabled={!companyId}>
          <option value="">Company-wide (no specific user)</option>
          {companyUsers.map((u) => <option key={u.id} value={u.id}>{u.name} ({u.email})</option>)}
        </select>
      </MiniField>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
        <MiniField label="Invoice number">
          <input value={invoiceNumber} onChange={(e) => setInvoiceNumber(e.target.value)} placeholder="INV-2026-0001" style={inputStyle} data-testid="assign-invoice-input" />
        </MiniField>
        <MiniField label="PO number">
          <input value={poNumber} onChange={(e) => setPoNumber(e.target.value)} placeholder="PO-0001" style={inputStyle} data-testid="assign-po-input" />
        </MiniField>
      </div>
      <MiniField label="Notes (optional)">
        <input value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="e.g. Replacement for faulty unit" style={inputStyle} />
      </MiniField>
      <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
        <ActionButton disabled={!canSubmit} readOnly={isReadOnly} onClick={submit} testId="submit-assign-unit">
          {busy ? "Saving…" : "Confirm"}
        </ActionButton>
        <ActionButton disabled={busy} danger onClick={onCancel}>Cancel</ActionButton>
      </div>
    </ModalShell>
  );
}

function HistoryModal({ data, onClose }) {
  const { unit, history } = data;
  return (
    <ModalShell title={`History — ${unit.serial_number}`} onCancel={onClose} wide>
      <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)", marginBottom: 16 }}>
        {unit.product_name} · currently {unit.status.replace("_", " ")}
        {unit.current_company_name && <> · {unit.current_company_name}</>}
        {(unit.current_region || unit.current_country) && (
          <> · {[unit.current_country, unit.current_region].filter(Boolean).join(", ")}</>
        )}
      </div>
      {history.length === 0 ? (
        <EmptyNote>No assignment history yet.</EmptyNote>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 10, maxHeight: 360, overflowY: "auto" }}>
          {history.map((h) => (
            <div key={h.id} style={{
              background: h.ended_at ? "rgba(0,0,0,0.02)" : "rgba(20,140,60,0.06)",
              border: `1px solid ${h.ended_at ? "rgba(0,0,0,0.1)" : "rgba(20,140,60,0.3)"}`,
              padding: "12px 14px",
            }}>
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "#000" }}>
                {h.company_name}{h.user_name && <> → {h.user_name}</>}
                {!h.ended_at && <span style={{ color: "rgba(20,140,60,0.95)", fontSize: 10.5, textTransform: "uppercase", marginLeft: 8 }}>Current</span>}
              </div>
              <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.55)", marginTop: 4 }}>
                {(h.company_country || h.company_region) && (
                  <>{[h.company_country, h.company_region].filter(Boolean).join(", ")} · </>
                )}
                {h.invoice_number && <>Invoice {h.invoice_number} · </>}
                {h.po_number && <>PO {h.po_number} · </>}
                {h.started_at}{h.ended_at ? ` → ${h.ended_at}` : " → present"}
              </div>
              {h.notes && <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.4)", marginTop: 4 }}>{h.notes}</div>}
            </div>
          ))}
        </div>
      )}
      <div style={{ marginTop: 18 }}>
        <ActionButton onClick={onClose}>Close</ActionButton>
      </div>
    </ModalShell>
  );
}

// ---------------------------------------------------------------------------
// Mission Control device linking — "sync, then link" for all three
// vendors (Victron VRM, Ajax Systems, Teltonika RMS), per unit. The
// company-level connection (paste-a-token) lives on Settings > Data
// Connections (see admin-settings-page.jsx); this modal is the second
// step: pull that company's raw device/installation list from the
// vendor and match one specific item to this specific asset_unit.
// Backend: routes/admin-mission-control.ts.
//
// All three vendors are scoped to this unit's current company — each
// customer has their own separate Victron VRM, Ajax Systems, and
// Teltonika RMS account (confirmed by the customer for all three), so
// every sync/list call below is company-filtered the same way.
function LinkDevicesModal({ unit, isReadOnly, onCancel, onChanged }) {
  return (
    <ModalShell title={`Link devices — ${unit.serial_number}`} onCancel={onCancel} wide>
      <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)", marginBottom: 20 }}>
        {unit.product_name} · {unit.current_company_name}. Sync each vendor to pull its latest
        device list, then link the one that matches this physical unit. A company must already
        be connected on <a onClick={(e) => { e.preventDefault(); }} style={{ color: "rgba(0,0,0,0.55)" }}>Settings → Data Connections</a> before anything appears below.
      </div>

      <VendorLinkSection
        vendorLabel="Victron VRM" testIdPrefix="victron"
        unit={unit} isReadOnly={isReadOnly} onChanged={onChanged}
        syncUrl={`/api/admin/companies/${unit.current_company_id}/victron-sync`}
        syncBody={null}
        listUrl={`/api/admin/companies/${unit.current_company_id}/victron-installations`}
        listKey="installations"
        linkUrlFor={(item) => `/api/admin/victron-installations/${item.id}/link`}
        getName={(item) => item.installation_name || item.gateway_identifier || `Installation ${item.vrm_installation_id}`}
        getSubtext={(item) => item.gateway_identifier || null}
        getSyncedAt={(item) => item.last_synced_at}
        afterLink={(item) =>
          // Immediately pull that installation's live sub-device diagnostics
          // (Shunt / Mppt 1 / Mppt 2 / Mains Charger) so Mission Control's
          // asset detail screen has real data the moment it's linked.
          fetch(`/api/admin/victron-installations/${item.id}/sync-devices`, { method: "POST", credentials: "same-origin" }).catch(() => {})
        }
      />

      <VendorLinkSection
        vendorLabel="Ajax Systems" testIdPrefix="ajax"
        unit={unit} isReadOnly={isReadOnly} onChanged={onChanged}
        syncUrl={`/api/admin/companies/${unit.current_company_id}/ajax-sync`}
        syncBody={null}
        listUrl={`/api/admin/companies/${unit.current_company_id}/ajax-hubs`}
        listKey="hubs"
        linkUrlFor={(item) => `/api/admin/ajax-hubs/${item.id}/link`}
        getName={(item) => item.name || `Hub ${item.ajax_hub_id}`}
        getSubtext={(item) => item.model || null}
        getSyncedAt={(item) => item.last_synced_at}
      />

      <VendorLinkSection
        vendorLabel="Teltonika RMS" testIdPrefix="teltonika"
        unit={unit} isReadOnly={isReadOnly} onChanged={onChanged}
        syncUrl={`/api/admin/companies/${unit.current_company_id}/teltonika-sync`}
        syncBody={null}
        listUrl={`/api/admin/companies/${unit.current_company_id}/teltonika-devices`}
        listKey="devices"
        linkUrlFor={(item) => `/api/admin/teltonika-devices/${item.id}/link`}
        getName={(item) => item.name || item.serial || item.mac || `Device ${item.rms_device_id}`}
        getSubtext={(item) => [item.serial, item.mac].filter(Boolean).join(" · ") || null}
        getSyncedAt={(item) => item.last_synced_at}
      />

      <div style={{ marginTop: 4 }}>
        <ActionButton onClick={onCancel}>Close</ActionButton>
      </div>
    </ModalShell>
  );
}

// One vendor's sync-then-link panel, shared by all three vendors above —
// the API shapes differ slightly (different field names per vendor) but
// the sync/list/link/unlink flow is identical (all three are company-
// scoped), so this is the one generic implementation.
function VendorLinkSection({
  vendorLabel, testIdPrefix, unit, isReadOnly, onChanged,
  syncUrl, listUrl, listKey, linkUrlFor, getName, getSubtext, getSyncedAt,
  afterLink, note,
}) {
  const [items, setItems] = useState(null); // null = loading, [] = loaded empty
  const [error, setError] = useState("");
  const [syncing, setSyncing] = useState(false);
  const [linkingId, setLinkingId] = useState(null);

  const load = () => {
    fetch(listUrl, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setItems(data[listKey] || []))
      .catch(() => setItems([]));
  };

  useEffect(load, [listUrl]); // eslint-disable-line react-hooks/exhaustive-deps

  const sync = async () => {
    setSyncing(true);
    setError("");
    try {
      const res = await fetch(syncUrl, { method: "POST", credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Couldn't sync. Please try again."); setSyncing(false); return; }
      load();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setSyncing(false);
  };

  const link = async (item, assetUnitId) => {
    setLinkingId(item.id);
    setError("");
    try {
      const res = await fetch(linkUrlFor(item), {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ assetUnitId }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Couldn't link. Please try again."); setLinkingId(null); return; }
      if (assetUnitId != null && afterLink) await afterLink(item);
      load();
      await onChanged();
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
    }
    setLinkingId(null);
  };

  return (
    <div style={{ marginBottom: 24 }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }}>
        <SectionLabel>{vendorLabel}</SectionLabel>
        <ActionButton disabled={syncing} readOnly={isReadOnly} onClick={sync} testId={`${testIdPrefix}-sync-devices`}>
          {syncing ? "Syncing…" : "Sync now"}
        </ActionButton>
      </div>
      {note && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.4)", marginBottom: 8, lineHeight: 1.5 }}>
          {note}
        </div>
      )}
      {error && <div style={{ marginBottom: 8 }}><ModalError>{error}</ModalError></div>}

      {items === null ? (
        <EmptyNote>Loading…</EmptyNote>
      ) : items.length === 0 ? (
        <EmptyNote>Nothing synced yet — connect this company on Settings, then Sync now.</EmptyNote>
      ) : (
        <div style={{ border: "1px solid rgba(0,0,0,0.14)" }}>
          {items.map((item, idx) => {
            const linkedToThis = item.asset_unit_id === unit.id;
            const linkedElsewhere = item.asset_unit_id && !linkedToThis;
            return (
              <div key={item.id} style={{
                padding: "10px 14px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10,
                borderBottom: idx === items.length - 1 ? "none" : "1px solid rgba(0,0,0,0.08)",
                background: linkedToThis ? "rgba(20,140,60,0.05)" : "transparent",
              }}>
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontFamily: "var(--font-body)", fontSize: 13, fontWeight: 600, color: "rgba(0,0,0,0.85)" }}>
                    {getName(item)}
                  </div>
                  <div style={{ fontFamily: "var(--font-body)", fontSize: 11, color: "rgba(0,0,0,0.5)" }}>
                    {getSubtext(item) && <>{getSubtext(item)} · </>}
                    {linkedToThis
                      ? "Linked to this unit"
                      : linkedElsewhere
                        ? <>Linked to <strong>{item.linked_serial_number}</strong></>
                        : "Not linked"}
                    {getSyncedAt(item) && <> · synced {getSyncedAt(item)}</>}
                  </div>
                </div>
                {linkedToThis ? (
                  <ActionButton disabled={linkingId === item.id} readOnly={isReadOnly} danger onClick={() => link(item, null)} testId={`${testIdPrefix}-unlink-${item.id}`}>
                    Unlink
                  </ActionButton>
                ) : (
                  <ActionButton disabled={linkingId === item.id || linkedElsewhere} readOnly={isReadOnly} onClick={() => link(item, unit.id)} testId={`${testIdPrefix}-link-${item.id}`}>
                    {linkingId === item.id ? "Linking…" : "Link"}
                  </ActionButton>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// Import from spreadsheet — CSV upload of the master asset tracking sheet
// (Asset / Version / Business Unit / PO Qty / PO Number / Asset Invoice
// No columns). Direct Google Sheets "Anyone with the link" access was
// explicitly ruled out by the customer as a security risk, so this is a
// manual CSV upload for now (a Google Service Account for genuine hourly
// polling is a planned fast-follow, once set up).
//
// Two-step, stateless flow, matching the backend in
// routes/admin-assets-import.ts. Additive-only: a serial already on file
// is never touched by an import, no matter what the sheet currently says
// for it — this is for bringing in new stock, not correcting old records.
//   1. "analyze" — parses the CSV and returns a full preview with nothing
//      written yet: reserved rows excluded, already-tracked serials set
//      aside untouched, PO/invoice numbers normalized, and everything
//      that needs a decision for the remaining BRAND-NEW rows (unmatched
//      Business Unit names, unmatched product-serial prefixes) surfaced
//      for review.
//   2. "commit" — re-sends the same CSV text plus the admin's resolutions
//      (link-to-existing-customer / create-new-customer for each
//      unmatched Business Unit, pick-a-product for each unmatched
//      prefix). Every resolution is remembered permanently server-side,
//      so future imports of the same Business Unit name or serial prefix
//      auto-resolve without asking again. Rows whose serial already
//      exists are skipped outright — never updated.
const IMPORT_TIERS = [
  { value: "gold", label: "Gold" },
  { value: "platinum", label: "Platinum" },
  { value: "regional_reseller", label: "Regional Reseller" },
];

function ImportSheetModal({ products, companies, isReadOnly, onCancel, onDone, onNavigate }) {
  const [step, setStep] = useState("upload"); // upload | fetching | analyzing | review | committing | done
  const [csvText, setCsvText] = useState("");
  const [fileName, setFileName] = useState("");
  const [fetchedAt, setFetchedAt] = useState(null);
  const [analysis, setAnalysis] = useState(null);
  const [error, setError] = useState("");
  const [result, setResult] = useState(null);

  // Resolution state, built up as the admin works through the review step.
  const [businessUnitLinks, setBusinessUnitLinks] = useState({}); // raw -> { companyId } | { xeroContact: {...} }
  const [productLinks, setProductLinks] = useState({}); // prefix -> productId

  const handleFile = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setFileName(file.name);
    setFetchedAt(null);
    const reader = new FileReader();
    reader.onload = () => setCsvText(String(reader.result || ""));
    reader.readAsText(file);
  };

  const runFetchSheet = async () => {
    setStep("fetching"); setError("");
    try {
      const res = await fetch("/api/admin/assets/import/fetch-sheet", { method: "POST", credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Could not pull the live spreadsheet."); setStep("upload"); return; }
      setCsvText(data.csvText);
      setFileName("");
      setFetchedAt(data.fetchedAt);
      setStep("upload");
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStep("upload");
    }
  };

  const runAnalyze = async () => {
    if (!csvText.trim()) { setError("Choose a CSV file first."); return; }
    setStep("analyzing"); setError("");
    try {
      const res = await fetch("/api/admin/assets/import/analyze", {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ csvText }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Could not read that file."); setStep("upload"); return; }
      setAnalysis(data);
      // Pre-fill any single-product-prefix case to the only matching product by name prefix, as a convenience —
      // still requires the admin to confirm by leaving/selecting it before commit is enabled.
      setStep("review");
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStep("upload");
    }
  };

  const allBusinessUnitsResolved = (analysis?.unmatchedBusinessUnits || []).every(
    (bu) => businessUnitLinks[bu.raw]?.companyId || businessUnitLinks[bu.raw]?.xeroContact?.xeroContactId
  );
  const allProductsResolved = (analysis?.unmatchedProductPrefixes || []).every((p) => productLinks[p.prefix]);
  const canCommit = allBusinessUnitsResolved && allProductsResolved;

  const runCommit = async () => {
    setStep("committing"); setError("");
    try {
      const res = await fetch("/api/admin/assets/import/commit", {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ csvText, resolutions: { businessUnitLinks, productLinks } }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Import failed."); setStep("review"); return; }
      setResult(data);
      setStep("done");
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStep("review");
    }
  };

  if (step === "done") {
    return (
      <ModalShell title="Import complete" onCancel={onDone} wide>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 14, color: "#000", lineHeight: 1.7, marginBottom: 18 }}>
          <strong>{result.imported}</strong> new unit{result.imported === 1 ? "" : "s"} created.
          <br />{result.skippedReserved} reserved/blank row{result.skippedReserved === 1 ? "" : "s"} skipped.
          {result.skippedExisting > 0 && <><br />{result.skippedExisting} row{result.skippedExisting === 1 ? "" : "s"} already tracked — left untouched.</>}
          {(result.poMissingCount > 0 || result.invoiceMissingCount > 0) && (
            <>
              <br /><span style={{ color: "rgba(190,40,40,0.9)" }}>
                {result.poMissingCount > 0 && <>{result.poMissingCount} unit{result.poMissingCount === 1 ? "" : "s"} imported with no PO Number</>}
                {result.poMissingCount > 0 && result.invoiceMissingCount > 0 && ", "}
                {result.invoiceMissingCount > 0 && <>{result.invoiceMissingCount} unit{result.invoiceMissingCount === 1 ? "" : "s"} with no verified invoice</>}
                {" "}— flagged for manual review on each unit's record.
              </span>
            </>
          )}
          {result.stillUnresolved?.length > 0 && (
            <>
              <br /><span style={{ color: "rgba(190,40,40,0.9)" }}>{result.stillUnresolved.length} row{result.stillUnresolved.length === 1 ? "" : "s"} skipped — still unresolved:</span>
              <div style={{ fontSize: 12, marginTop: 6, maxHeight: 140, overflowY: "auto" }}>
                {result.stillUnresolved.slice(0, 30).map((r, i) => <div key={i}>{r.serial}: {r.reason}</div>)}
              </div>
            </>
          )}
        </div>
        <ActionButton onClick={onDone} testId="import-done">Done</ActionButton>
      </ModalShell>
    );
  }

  if (step === "upload" || step === "fetching" || step === "analyzing") {
    const busy = step === "fetching" || step === "analyzing";
    return (
      <ModalShell title="Import from spreadsheet" onCancel={onCancel}>
        {error && <ModalError>{error}</ModalError>}
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.65)", lineHeight: 1.6, marginBottom: 16 }}>
          Pull the current data straight from the live spreadsheet, or upload a CSV export manually
          (columns: Asset, Version, Business Unit, PO Qty, PO Number, Asset Invoice No).
          Nothing is saved until you review and confirm on the next screen.
        </div>
        <div style={{ marginBottom: 18 }}>
          <ActionButton disabled={busy} onClick={runFetchSheet} testId="import-fetch-sheet">
            {step === "fetching" ? "Pulling latest data…" : "Update from live spreadsheet"}
          </ActionButton>
          {fetchedAt && (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,120,60,0.85)", marginTop: 8 }}>
              Pulled the current live spreadsheet at {new Date(fetchedAt).toLocaleString()}.
            </div>
          )}
        </div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", marginBottom: 10 }}>— or upload a file manually —</div>
        <MiniField label="CSV file">
          <input type="file" accept=".csv,text/csv" onChange={handleFile} disabled={busy} data-testid="import-file-input" />
        </MiniField>
        {fileName && <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)", marginBottom: 14 }}>Selected: {fileName}</div>}
        <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
          <ActionButton disabled={!csvText.trim() || busy} onClick={runAnalyze} testId="import-analyze">
            {step === "analyzing" ? "Reading…" : "Analyze"}
          </ActionButton>
          <ActionButton disabled={busy} danger onClick={onCancel}>Cancel</ActionButton>
        </div>
      </ModalShell>
    );
  }

  // step === "review" or "committing"
  const s = analysis.summary;
  return (
    <ModalShell title="Review import" onCancel={onCancel} wide>
      {error && <ModalError>{error}</ModalError>}
      <div style={{
        fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.65)",
        background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)", padding: "10px 12px", marginBottom: 18,
      }}>
        {s.totalRows} rows read · {s.skippedReserved} reserved/blank (always excluded) · {s.newUnits} new (will be imported) · {s.existingUnits} already tracked (left untouched)
        {(s.poMissingPreview > 0 || s.invoiceMissingPreview > 0) && (
          <><br /><span style={{ color: "rgba(190,40,40,0.9)" }}>
            ~{s.poMissingPreview} new unit{s.poMissingPreview === 1 ? "" : "s"} with no PO Number, ~{s.invoiceMissingPreview} with no verified invoice —
            these will still be imported, just flagged for review (estimate; the final count is confirmed after commit).
          </span></>
        )}
      </div>

      {analysis.unmatchedBusinessUnits.length > 0 && (
        <>
          <SectionLabel>Link customers ({analysis.unmatchedBusinessUnits.length})</SectionLabel>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.55)", marginBottom: 12 }}>
            These "Business Unit" names from the sheet don't match any existing customer. Link each to an existing
            customer or create a new one — this is remembered, so future imports won't ask again.
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 22 }}>
            {analysis.unmatchedBusinessUnits.map((bu) => (
              <BusinessUnitLinkRow
                key={bu.raw} bu={bu} companies={companies} isReadOnly={isReadOnly}
                value={businessUnitLinks[bu.raw]}
                onChange={(val) => setBusinessUnitLinks((s) => ({ ...s, [bu.raw]: val }))}
              />
            ))}
          </div>
        </>
      )}

      {analysis.unmatchedProductPrefixes.length > 0 && (
        <>
          <SectionLabel>Link products ({analysis.unmatchedProductPrefixes.length})</SectionLabel>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.55)", marginBottom: 12 }}>
            These serial-number prefixes don't match any known product yet.
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 22 }}>
            {analysis.unmatchedProductPrefixes.map((p) => (
              <div key={p.prefix} style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
                <div style={{ fontFamily: "var(--font-body)", fontSize: 13, fontWeight: 600, minWidth: 90 }}>{p.prefix}-*</div>
                <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)" }}>({p.count} unit{p.count === 1 ? "" : "s"})</div>
                <select value={productLinks[p.prefix] || ""} onChange={(e) => setProductLinks((s) => ({ ...s, [p.prefix]: Number(e.target.value) }))} style={{ ...selectStyle, width: 220 }}>
                  <option value="">Choose a product…</option>
                  {products.map((prod) => <option key={prod.id} value={prod.id}>{prod.name}</option>)}
                </select>
              </div>
            ))}
          </div>
        </>
      )}

      {(analysis.poNeedsReview.length > 0 || analysis.invoiceNeedsReview.length > 0) && (
        <>
          <SectionLabel>Flagged for manual review</SectionLabel>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(180,110,0,0.95)", marginBottom: 12, lineHeight: 1.6 }}>
            These will still be imported, but their PO/invoice number couldn't be safely auto-corrected — check them
            manually afterwards on the unit's record.
            <div style={{ marginTop: 6, maxHeight: 100, overflowY: "auto", color: "rgba(0,0,0,0.6)" }}>
              {analysis.poNeedsReview.map((r, i) => <div key={`po-${i}`}>{r.serial}: PO "{r.poNumber}" — ambiguous, no prefix</div>)}
              {analysis.invoiceNeedsReview.map((r, i) => <div key={`inv-${i}`}>{r.serial}: Invoice "{r.invoiceNumber}" — unrecognized format</div>)}
            </div>
          </div>
        </>
      )}

      <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
        <ActionButton disabled={!canCommit || step === "committing"} readOnly={isReadOnly} onClick={runCommit} testId="import-commit">
          {step === "committing" ? "Importing…" : "Confirm import"}
        </ActionButton>
        <ActionButton disabled={step === "committing"} danger onClick={onCancel}>Cancel</ActionButton>
      </div>
      {!canCommit && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(180,110,0,0.95)", marginTop: 10 }}>
          Resolve every item above before confirming.
        </div>
      )}
    </ModalShell>
  );
}

// ---------------------------------------------------------------------------
// XeroContactPicker — shared "pick a Xero Contact" widget. Loads early
// (this file) so it's available to admin-companies-page.jsx's Add
// customer modal and admin-portal-page.jsx's pending-approval flow, both
// of which load after this file (see index.html) and now need the exact
// same pick UX. Exported on window (see bottom of this block).
//
// Loads the FULL Xero contact list once on mount (GET
// /api/admin/xero/contacts/list — see listXeroContacts in lib/xero.ts)
// and lets the admin instantly filter it client-side as they type, rather
// than the old flow of typing a query and waiting on a live per-keystroke
// Xero API round-trip. This is what "instead of searching, give me a list
// to choose from" (reported as slow) refers to.
//
// Deliberately dumb/controlled: it only lists/filters and reports the
// admin's pick via onSelect({ contactId, name, linkedCompany }) — every
// caller decides what to actually DO with that pick (create a company,
// bind an existing one, etc.), since that differs per call site. It does
// NOT check Xero connection status itself — callers show their own
// "not connected" warning (via GET /api/admin/xero/status) before even
// rendering this, since the right recovery action reads differently in
// each context.
function XeroContactPicker({ defaultQuery, onSelect, disabledContactId, disabledNote }) {
  const [filter, setFilter] = useState(defaultQuery || "");
  const [state, setState] = useState("loading"); // loading | done | error
  const [contacts, setContacts] = useState([]);
  const [error, setError] = useState("");

  const loadList = async () => {
    setState("loading"); setError("");
    try {
      const res = await fetch("/api/admin/xero/contacts/list", { credentials: "same-origin" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Couldn't load Xero contacts."); setState("error"); return; }
      setContacts(data.contacts || []);
      setState("done");
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setState("error");
    }
  };

  // NOTE: must wrap in a synchronous arrow, not pass loadList directly —
  // loadList is `async` and always returns a Promise, which React tries
  // to call as an effect cleanup function ("destroy is not a function"),
  // crashing this component (and everything above it, with no error
  // boundary) the moment Xero isn't connected and the fetch 500s. Every
  // other load() in this codebase avoids this by using .then() chains
  // instead of async/await — this is the one exception.
  useEffect(() => { loadList(); }, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  const needle = filter.trim().toLowerCase();
  const visible = needle
    ? contacts.filter((c) => c.name.toLowerCase().includes(needle))
    : contacts;

  return (
    <div>
      <div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
        <input
          value={filter} onChange={(e) => setFilter(e.target.value)}
          placeholder="Filter the list by name…" style={{ ...inputStyle, flex: 1 }}
          data-testid="xero-picker-filter"
        />
      </div>
      {state === "loading" && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)" }}>
          Loading Xero contacts…
        </div>
      )}
      {state === "error" && (
        <div style={{ marginBottom: 8 }}>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "#a30000", marginBottom: 6 }}>{error}</div>
          <ActionButton onClick={loadList} testId="xero-picker-retry">Retry</ActionButton>
        </div>
      )}
      {state === "done" && visible.length === 0 && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)" }}>
          {contacts.length === 0 ? "No Xero contacts found." : "No matches — try a different name."}
        </div>
      )}
      {state === "done" && visible.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 6, maxHeight: 220, overflowY: "auto" }}>
          {visible.map((contact) => {
            const isDisabled = disabledContactId && contact.contactId === disabledContactId;
            const isTaken = !!contact.linkedCompany && !isDisabled;
            return (
              <div key={contact.contactId} style={{
                display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10,
                background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.12)", padding: "8px 10px",
              }}>
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontFamily: "var(--font-body)", fontSize: 13, fontWeight: 600, color: "#000" }}>{contact.name}</div>
                  <div style={{ fontFamily: "var(--font-body)", fontSize: 11.5, color: "rgba(0,0,0,0.5)" }}>
                    {contact.emailAddress || "no email on file"}
                    {isTaken && <> · already linked to <strong>{contact.linkedCompany.name}</strong></>}
                    {isDisabled && disabledNote && <> · {disabledNote}</>}
                  </div>
                </div>
                <ActionButton
                  disabled={isTaken || isDisabled}
                  onClick={() => onSelect(contact)}
                  testId={`xero-picker-select-${contact.contactId}`}
                >
                  {isTaken ? "Already linked" : "Select"}
                </ActionButton>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}
if (typeof window !== "undefined") window.XeroContactPicker = XeroContactPicker;

// Small inline warning + "Connect to Xero" shown wherever a flow now
// requires Xero and the org isn't connected yet — lets the admin connect
// without leaving the modal/flow they were in (the actual OAuth hop is a
// real <a href> top-level navigation, same reasoning as Settings' own
// "Connect to Xero" link — see admin-settings-page.jsx). `onRecheck` lets
// the caller re-poll /xero/status after the admin comes back from Xero.
// isReadOnly: Solo Staff (role "admin") can't complete the Xero OAuth
// connect flow (backend requires super_admin on /xero/connect), so the
// link is swapped for a disabled-look span with an explanatory note
// rather than a clickable <a> — it's a top-level navigation, not a
// fetch(), so it can't just take a `disabled` attribute like a button.
function XeroNotConnectedWarning({ onRecheck, isReadOnly }) {
  return (
    <div style={{ background: "rgba(180,110,0,0.06)", border: "1px solid rgba(180,110,0,0.3)", padding: "12px 14px", marginBottom: 10 }}>
      <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(140,85,0,0.95)", marginBottom: 10, lineHeight: 1.5 }}>
        Xero isn't connected yet — connect it to search and link customers to Xero contacts.
        {isReadOnly && " Only master admins can connect Xero."}
      </div>
      <div style={{ display: "flex", gap: 10 }}>
        {isReadOnly ? (
          <span
            data-testid="xero-notconnected-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" target="_blank" rel="noopener noreferrer"
            data-testid="xero-notconnected-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>
        )}
        <ActionButton onClick={onRecheck} testId="xero-notconnected-recheck">
          I've connected — refresh
        </ActionButton>
      </div>
    </div>
  );
}
if (typeof window !== "undefined") window.XeroNotConnectedWarning = XeroNotConnectedWarning;

function BusinessUnitLinkRow({ bu, companies, value, onChange, isReadOnly }) {
  const [mode, setMode] = useState(value?.xeroContact ? "xero" : "existing");
  const [companyId, setCompanyId] = useState(value?.companyId || "");
  const [xeroContact, setXeroContact] = useState(value?.xeroContact || null); // { xeroContactId, name, tier, region, country }
  const [tier, setTier] = useState(value?.xeroContact?.tier || "gold");
  const [region, setRegion] = useState(value?.xeroContact?.region || bu.suggestion?.region || "UK");
  const [country, setCountry] = useState(value?.xeroContact?.country || bu.suggestion?.country || window.GEO_DATA.countriesForRegion(bu.suggestion?.region || "UK")[0] || "");
  const [xeroStatus, setXeroStatus] = useState({ status: "loading", connected: false });

  // Existing-but-unlinked company inline "link to Xero too" (Q4).
  const [linkingExisting, setLinkingExisting] = useState(false);
  const [existingLinkStatus, setExistingLinkStatus] = useState(null); // { linked, xeroContactId } once fetched
  const [existingLinkBusy, setExistingLinkBusy] = useState(false);
  const [existingLinkNote, setExistingLinkNote] = useState("");

  const countryOptions = window.GEO_DATA.countriesForRegion(region);

  const loadXeroStatus = () => {
    fetch("/api/admin/xero/status", { credentials: "same-origin" })
      .then((r) => r.json())
      .then((data) => setXeroStatus({ status: "ready", connected: !!data.connected }))
      .catch(() => setXeroStatus({ status: "error", connected: false }));
  };
  useEffect(loadXeroStatus, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  useEffect(() => {
    if (mode === "existing") {
      onChange(companyId ? { companyId: Number(companyId) } : null);
    } else if (mode === "xero") {
      onChange(xeroContact ? { xeroContact: { xeroContactId: xeroContact.contactId, tier, region, country } } : null);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mode, companyId, xeroContact, tier, region, country]);

  // When "Link to existing customer" is chosen, check whether that
  // company already has a Xero link, so the "offer to link" prompt (Q4)
  // only shows up when it's actually missing.
  useEffect(() => {
    setExistingLinkStatus(null);
    setLinkingExisting(false);
    setExistingLinkNote("");
    if (mode !== "existing" || !companyId) return;
    fetch(`/api/admin/companies/${companyId}/xero-link`, { credentials: "same-origin" })
      .then((r) => r.json())
      .then((data) => setExistingLinkStatus(data))
      .catch(() => setExistingLinkStatus(null));
  }, [mode, companyId]);

  const linkExistingToXero = async (contact) => {
    if (isReadOnly) return;
    setExistingLinkBusy(true);
    try {
      const res = await fetch(`/api/admin/companies/${companyId}/xero-link`, {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ xeroContactId: contact.contactId }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setExistingLinkNote(data.error || "Couldn't link to Xero."); return; }
      setExistingLinkStatus({ linked: true, xeroContactId: contact.contactId });
      setExistingLinkNote(`Linked to "${contact.name}" on Xero.`);
      setLinkingExisting(false);
    } catch {
      setExistingLinkNote("Couldn't reach the server. Check your connection and try again.");
    } finally {
      setExistingLinkBusy(false);
    }
  };

  return (
    <div style={{ background: "rgba(0,0,0,0.02)", border: "1px solid rgba(0,0,0,0.12)", padding: "12px 14px" }}>
      <div style={{ fontFamily: "var(--font-body)", fontSize: 13, fontWeight: 600, color: "#000", marginBottom: 8 }}>
        "{bu.raw}" <span style={{ color: "rgba(0,0,0,0.5)", fontWeight: 400 }}>({bu.count} unit{bu.count === 1 ? "" : "s"})</span>
      </div>
      <div style={{ display: "flex", gap: 16, marginBottom: 10 }}>
        <label style={{ fontFamily: "var(--font-body)", fontSize: 12, cursor: "pointer" }}>
          <input type="radio" checked={mode === "existing"} onChange={() => setMode("existing")} /> Link to existing customer
        </label>
        <label style={{ fontFamily: "var(--font-body)", fontSize: 12, cursor: "pointer" }}>
          <input type="radio" checked={mode === "xero"} onChange={() => setMode("xero")} /> Search Xero
        </label>
      </div>

      {mode === "existing" && (
        <div>
          <select value={companyId} onChange={(e) => setCompanyId(e.target.value)} style={{ ...selectStyle, width: 280 }}>
            <option value="">Choose a customer…</option>
            {companies.map((co) => <option key={co.id} value={co.id}>{co.name}</option>)}
          </select>
          {companyId && existingLinkStatus && !existingLinkStatus.linked && !linkingExisting && (
            <div style={{ marginTop: 8, fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(140,85,0,0.95)" }}>
              🔗 Not linked to Xero yet.{" "}
              {isReadOnly ? (
                <span title="Master admins only — you have read-only access" style={{ opacity: 0.6 }}>Choose from list &amp; link now</span>
              ) : (
                <button type="button" onClick={() => setLinkingExisting(true)} style={{ background: "none", border: "none", padding: 0, color: "#000", textDecoration: "underline", cursor: "pointer", fontFamily: "inherit", fontSize: "inherit" }}>
                  Choose from list &amp; link now
                </button>
              )}
            </div>
          )}
          {companyId && existingLinkStatus?.linked && (
            <div style={{ marginTop: 8, fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(20,140,60,0.95)" }}>✓ Already linked to Xero.</div>
          )}
          {linkingExisting && (
            <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid rgba(0,0,0,0.1)" }}>
              {xeroStatus.status === "ready" && !xeroStatus.connected ? (
                <XeroNotConnectedWarning onRecheck={loadXeroStatus} isReadOnly={isReadOnly} />
              ) : (
                <XeroContactPicker onSelect={linkExistingToXero} />
              )}
              {existingLinkBusy && <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.55)" }}>Linking…</div>}
            </div>
          )}
          {existingLinkNote && (
            <div style={{ marginTop: 8, fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.6)" }}>{existingLinkNote}</div>
          )}
        </div>
      )}

      {mode === "xero" && (
        <div>
          {xeroStatus.status === "ready" && !xeroStatus.connected ? (
            <XeroNotConnectedWarning onRecheck={loadXeroStatus} isReadOnly={isReadOnly} />
          ) : xeroContact ? (
            <div>
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13, marginBottom: 8 }}>
                Will create <strong>{xeroContact.name}</strong>, linked to this Xero contact.{" "}
                <button type="button" onClick={() => setXeroContact(null)} style={{ background: "none", border: "none", padding: 0, color: "#000", textDecoration: "underline", cursor: "pointer", fontFamily: "inherit", fontSize: 12 }}>
                  Change
                </button>
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
                <select value={tier} onChange={(e) => setTier(e.target.value)} style={selectStyle}>
                  {IMPORT_TIERS.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
                </select>
                <select value={region} onChange={(e) => { setRegion(e.target.value); setCountry(window.GEO_DATA.countriesForRegion(e.target.value)[0] || ""); }} style={selectStyle}>
                  {window.GEO_DATA.REGIONS.map((r) => <option key={r} value={r}>{r}</option>)}
                </select>
                <select value={country} onChange={(e) => setCountry(e.target.value)} style={selectStyle}>
                  {countryOptions.map((c) => <option key={c} value={c}>{c}</option>)}
                </select>
              </div>
            </div>
          ) : (
            <XeroContactPicker defaultQuery={bu.raw} onSelect={(contact) => setXeroContact({ contactId: contact.contactId, name: contact.name })} />
          )}
        </div>
      )}
    </div>
  );
}

function ModalShell({ title, children, onCancel, wide }) {
  return (
    <div style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)",
      display: "flex", alignItems: "center", justifyContent: "center",
      padding: 24, zIndex: 50,
    }} onClick={(e) => { if (e.target === e.currentTarget) onCancel(); }}>
      <div style={{
        background: "#fff", border: "1px solid rgba(0,0,0,0.18)",
        boxShadow: "0 12px 40px rgba(0,0,0,0.25)",
        padding: 28, width: "100%", maxWidth: wide ? 560 : 440,
        maxHeight: "85vh", overflowY: "auto",
      }}>
        <h2 style={{
          fontFamily: "var(--font-display)", fontWeight: 700, fontSize: 20,
          textTransform: "uppercase", color: "#000", margin: "0 0 20px",
        }}>{title}</h2>
        {children}
      </div>
    </div>
  );
}

function ModalError({ children }) {
  return (
    <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,
    }}>{children}</div>
  );
}

function SectionLabel({ children }) {
  return (
    <div style={{
      fontFamily: "var(--font-body)", fontSize: 10,
      letterSpacing: "0.28em", textTransform: "uppercase",
      color: "rgba(0,0,0,0.55)", fontWeight: 500, marginBottom: 16,
    }}>{children}</div>
  );
}

function EmptyNote({ children }) {
  return (
    <div style={{
      fontFamily: "var(--font-body)", fontSize: 14,
      color: "rgba(0,0,0,0.45)", marginBottom: 40,
    }}>{children}</div>
  );
}

function MiniField({ label, children }) {
  return (
    <label style={{ display: "block", marginBottom: 14 }}>
      <div style={{
        fontFamily: "var(--font-body)", fontSize: 9.5,
        letterSpacing: "0.16em", textTransform: "uppercase",
        color: "rgba(0,0,0,0.5)", marginBottom: 6,
      }}>{label}</div>
      {children}
    </label>
  );
}

// readOnly: pass true when the signed-in admin is role "admin" (Solo Staff)
// rather than "super_admin" — forces the button disabled regardless of the
// caller's own `disabled` logic, and swaps in an explanatory tooltip. The
// backend already rejects these calls for non-super-admins (requireSuperAdmin
// middleware) — this is UX polish so read-only viewers don't hit a dead-end
// 403 after filling in a form, not a security boundary itself.
function ActionButton({ children, danger, disabled, readOnly, onClick, testId }) {
  const isDisabled = disabled || readOnly;
  return (
    <button
      type="button" onClick={readOnly ? undefined : onClick} disabled={isDisabled} data-testid={testId}
      title={readOnly ? "Master admins only — you have read-only access" : undefined}
      style={{
        background: danger ? "transparent" : "#000",
        color: danger ? "#a30000" : "#fff",
        border: `1px solid ${danger ? "rgba(190,40,40,0.55)" : "#000"}`,
        padding: "10px 18px", cursor: isDisabled ? "not-allowed" : "pointer",
        opacity: isDisabled ? 0.5 : 1,
        fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
        letterSpacing: "0.14em", textTransform: "uppercase",
      }}>
      {children}
    </button>
  );
}

const navBtnStyle = {
  background: "none", border: "1px solid rgba(0,0,0,0.35)",
  color: "rgba(0,0,0,0.8)", cursor: "pointer", padding: "12px 20px",
  fontFamily: "var(--font-body)", fontSize: 12, fontWeight: 500,
  letterSpacing: "0.18em", textTransform: "uppercase",
};

const inputStyle = {
  width: "100%", boxSizing: "border-box",
  background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.25)",
  color: "#000", fontFamily: "var(--font-body)", fontSize: 13.5, padding: "9px 10px",
  outline: "none",
};
const selectStyle = { ...inputStyle, appearance: "auto" };

// Exported for reuse by admin-companies-page.jsx's per-customer asset
// register section, so that page doesn't need to duplicate the unit
// row/modal markup — same components, same behaviour, either place.
// ImportSheetModal is also exported so admin-settings-page.jsx's "Data
// Connections" > Asset Spreadsheet card can launch the exact same import
// wizard directly from Settings, not just from the Assets page toolbar.
Object.assign(window, {
  AdminAssetsPage,
  UnitRow, AddUnitModal, BulkAddUnitsModal, AssignUnitModal, HistoryModal, ImportSheetModal,
  MiniField, ActionButton, ModalShell, ModalError, SectionLabel, EmptyNote,
  adminInputStyle: inputStyle, adminSelectStyle: selectStyle, adminNavBtnStyle: navBtnStyle,
});
