/* FLOWLINE — shell: auth gating, app chrome, admin */

const TWEAK_DEFAULTS = {
  dark: false,
  dotStyle: "filled",
  pulse: true,
  quietGreens: false,
};

/* ======  PASSWORD RULES  ======
   The rules the server actually enforces, served by GET /api/auth/me and
   cached here so every form that sets a password (sign-up, reset, invitation,
   change password) states the same requirements up front instead of only
   surfacing them as an error after submitting. The literals below are only the
   fallback for the moment before /me resolves; boot() overwrites them. */
var PASSWORD_POLICY = {
  minLength: 12,
  maxLength: 200,
  requirements: [
    "At least 12 characters — a short phrase works well",
    "No more than 200 characters",
    "Not a password found in known data breaches",
    "Doesn't reuse your email address",
  ],
};

function PasswordRequirements() {
  var reqs = PASSWORD_POLICY.requirements || [];
  if (!reqs.length) return null;
  return (
    <div className="pw-reqs" data-testid="password-requirements">
      <div className="pw-reqs-title">Password requirements</div>
      <ul>
        {reqs.map(function (r) {
          return <li key={r}>{r}</li>;
        })}
      </ul>
    </div>
  );
}

/* ======  AUTH SCREENS  ====== */
function AuthScreen({
  devLoginEnabled,
  initialReset,
  initialInvite,
  onAuthed,
}) {
  const [mode, setMode] = useState(
    initialInvite ? "invite" : initialReset ? "reset" : "login",
  );
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");
  const [token, setToken] = useState(initialInvite || initialReset || "");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const [ok, setOk] = useState("");
  // Whether this deployment offers Google sign-in, and whether it still
  // accepts passwords. Both come from the server: the dashboard must not
  // guess, or a password form would linger after AUTH_MODE=google.
  const [sso, setSso] = useState({
    googleEnabled: false,
    passwordEnabled: true,
  });
  // Second sign-in step. The password step returns twoFactorRequired instead
  // of a session; the pending state itself lives in an httpOnly cookie, so all
  // this screen has to remember is that it should now ask for a code.
  const [code, setCode] = useState("");
  const [useRecovery, setUseRecovery] = useState(false);

  const reset = () => {
    setErr("");
    setOk("");
  };

  const backToPassword = () => {
    reset();
    setCode("");
    setUseRecovery(false);
    setPassword("");
    setMode("login");
  };

  const submit = async (e) => {
    e.preventDefault();
    reset();
    setBusy(true);
    try {
      if (mode === "login") {
        const r = await API.login(email.trim(), password);
        if (r && r.twoFactorRequired) {
          // Password accepted, but no session yet.
          setPassword("");
          setCode("");
          setUseRecovery(false);
          setMode("twofactor");
        } else {
          onAuthed();
        }
      } else if (mode === "twofactor") {
        const entry = code.trim();
        if (useRecovery) await API.loginRecoveryCode(entry);
        else await API.loginTwoFactor(entry);
        onAuthed();
      } else if (mode === "signup") {
        await API.signup(email.trim(), password, name.trim());
        onAuthed();
      } else if (mode === "forgot") {
        const r = await API.requestReset(email.trim());
        setOk(
          r.message ||
            "If that email is registered, a reset code is on its way.",
        );
      } else if (mode === "reset") {
        await API.resetPassword(token.trim(), password);
        setOk("Password updated. You can sign in now.");
        setMode("login");
      } else if (mode === "invite") {
        await API.acceptInvite(token.trim(), password);
        setOk("Account created. You can sign in now.");
        setMode("login");
      }
    } catch (e2) {
      setErr(e2.message || "Something went wrong.");
      // The pending sign-in expired (or was invalidated) — the password step
      // has to be redone, so don't leave the user typing codes into nothing.
      if (e2.data && e2.data.restart) {
        setCode("");
        setUseRecovery(false);
        setMode("login");
      }
    } finally {
      setBusy(false);
    }
  };

  const devLogin = async () => {
    reset();
    setBusy(true);
    try {
      await API.devLogin();
      onAuthed();
    } catch (e2) {
      setErr(e2.message || "Dev login failed.");
      setBusy(false);
    }
  };

  const titles = {
    login: "Sign in",
    twofactor: useRecovery ? "Use a recovery code" : "Two-step verification",
    signup: "Create account",
    forgot: "Reset your password",
    reset: "Set a new password",
    invite: "Accept your invitation",
  };
  const descs = {
    login: "Sign in to the Flowline exception command center.",
    twofactor: useRecovery
      ? "Enter one of the recovery codes you saved when you set up two-step verification. Each code works once."
      : "Enter the 6-digit code from your authenticator app to finish signing in.",
    signup: "New accounts need admin approval before they can view data.",
    forgot: "Enter your email and we'll send a one-time reset code.",
    reset: "Paste the reset code from your email and choose a new password.",
    invite:
      "Paste the invitation code from your email and choose a password to activate your account.",
  };

  // Probe once on mount. A failure here must never block sign-in, so the
  // defaults stay as they are and the password form is shown regardless.
  useEffect(() => {
    let cancelled = false;
    API.ssoConfig()
      .then((cfg) => {
        if (cancelled || !cfg) return;
        // Read each flag defensively rather than adopting the response
        // wholesale. Hiding the password form is the destructive direction, so
        // it happens only when the server EXPLICITLY says passwordEnabled is
        // false; any unexpected shape leaves sign-in exactly as it was. A
        // truthy-but-empty response previously blanked the form entirely.
        setSso({
          googleEnabled: cfg.googleEnabled === true,
          passwordEnabled: cfg.passwordEnabled !== false,
        });
      })
      .catch(() => {});
    return () => {
      cancelled = true;
    };
  }, []);

  return (
    <div className="auth-wrap">
      <div className="auth-card">
        <div className="auth-brand">
          <span className="brand-mark" />
          <span className="brand-name">FLOWLINE</span>
        </div>
        <div className="auth-title">{titles[mode]}</div>
        <div className="auth-desc">{descs[mode]}</div>

        {err && <div className="auth-msg err">{err}</div>}
        {ok && <div className="auth-msg ok">{ok}</div>}

        {mode === "login" && sso.googleEnabled && (
          <div className="sso-block">
            <a
              className="btn sso-btn"
              href="/api/auth/google"
              data-testid="google-signin"
            >
              Sign in with Google
            </a>
            {sso.passwordEnabled && (
              <div className="sso-divider">
                <span>or</span>
              </div>
            )}
          </div>
        )}

        {(mode !== "login" || sso.passwordEnabled) && (
          <form onSubmit={submit}>
            {mode === "signup" && (
              <div className="field">
                <label>Name</label>
                <input
                  className="input"
                  value={name}
                  onChange={(e) => setName(e.target.value)}
                  placeholder="Your name"
                  autoComplete="name"
                />
              </div>
            )}
            {(mode === "login" || mode === "signup" || mode === "forgot") && (
              <div className="field">
                <label>Email</label>
                <input
                  className="input"
                  type="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  placeholder="you@company.com"
                  autoComplete="email"
                  required
                />
              </div>
            )}
            {(mode === "reset" || mode === "invite") && (
              <div className="field">
                <label>
                  {mode === "invite" ? "Invitation code" : "Reset code"}
                </label>
                <input
                  className="input"
                  value={token}
                  onChange={(e) => setToken(e.target.value)}
                  placeholder="Paste your code"
                  required
                />
              </div>
            )}
            {mode === "twofactor" && (
              <div className="field">
                <label>
                  {useRecovery ? "Recovery code" : "Authentication code"}
                </label>
                <input
                  className="input"
                  value={code}
                  onChange={(e) => setCode(e.target.value)}
                  placeholder={useRecovery ? "XXXXX-XXXXX" : "123456"}
                  inputMode={useRecovery ? "text" : "numeric"}
                  autoComplete="one-time-code"
                  data-testid={useRecovery ? "recovery-code" : "totp-code"}
                  autoFocus
                  required
                />
              </div>
            )}
            {(mode === "login" ||
              mode === "signup" ||
              mode === "reset" ||
              mode === "invite") && (
              <div className="field">
                <label>
                  {mode === "reset" || mode === "invite"
                    ? "New password"
                    : "Password"}
                </label>
                <input
                  className="input"
                  type="password"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  placeholder="••••••••"
                  autoComplete={
                    mode === "login" ? "current-password" : "new-password"
                  }
                  required
                />
              </div>
            )}
            {/* Sign-in reads an existing password, so the rules only belong on
              the screens that SET one. */}
            {(mode === "signup" || mode === "reset" || mode === "invite") && (
              <PasswordRequirements />
            )}

            <button
              className="btn primary auth-btn"
              type="submit"
              disabled={busy}
            >
              {busy ? "Working…" : titles[mode]}
            </button>
          </form>
        )}

        {devLoginEnabled && mode === "login" && (
          <>
            <div className="auth-divider">dev only</div>
            <button className="btn auth-btn" onClick={devLogin} disabled={busy}>
              One-click dev login
            </button>
          </>
        )}

        <div className="auth-alt">
          {mode === "twofactor" && (
            <>
              <button
                className="auth-link"
                onClick={() => {
                  reset();
                  setCode("");
                  setUseRecovery(!useRecovery);
                }}
              >
                {useRecovery
                  ? "Use my authenticator app"
                  : "Use a recovery code"}
              </button>
              <span style={{ margin: "0 8px", color: "var(--border-strong)" }}>
                ·
              </span>
              <button className="auth-link" onClick={backToPassword}>
                Start over
              </button>
            </>
          )}
          {mode === "login" && (
            <>
              <button
                className="auth-link"
                onClick={() => {
                  reset();
                  setMode("forgot");
                }}
              >
                Forgot password?
              </button>
              <span style={{ margin: "0 8px", color: "var(--border-strong)" }}>
                ·
              </span>
              <button
                className="auth-link"
                onClick={() => {
                  reset();
                  setMode("signup");
                }}
              >
                Create account
              </button>
            </>
          )}
          {mode === "signup" && (
            <button
              className="auth-link"
              onClick={() => {
                reset();
                setMode("login");
              }}
            >
              Already have an account? Sign in
            </button>
          )}
          {(mode === "forgot" || mode === "reset") && (
            <>
              {mode === "forgot" && (
                <button
                  className="auth-link"
                  onClick={() => {
                    reset();
                    setMode("reset");
                  }}
                >
                  I have a code
                </button>
              )}
              <span style={{ margin: "0 8px", color: "var(--border-strong)" }}>
                ·
              </span>
              <button
                className="auth-link"
                onClick={() => {
                  reset();
                  setMode("login");
                }}
              >
                Back to sign in
              </button>
            </>
          )}
          {mode === "invite" && (
            <button
              className="auth-link"
              onClick={() => {
                reset();
                setMode("login");
              }}
            >
              Back to sign in
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

function PendingScreen({ user, onSignOut }) {
  return (
    <div className="auth-wrap">
      <div className="auth-card">
        <div className="auth-brand">
          <span className="brand-mark" />
          <span className="brand-name">FLOWLINE</span>
        </div>
        <div className="auth-title">Awaiting approval</div>
        <div className="auth-desc">
          You're signed in as <b>{user.email}</b>, but your account hasn't been
          approved yet. An admin needs to grant you a permission level before
          you can see exception data.
        </div>
        <button className="btn auth-btn" onClick={onSignOut}>
          Sign out
        </button>
      </div>
    </div>
  );
}

/* ======  ADMIN (Team) PANEL  ====== */
/* Permission levels: admin (full org + admin panel), manager (their assigned
   shops' subtrees — any number of shops), employee (own card only). hr /
   billing / ar stay exception-type-scoped. Job title (service writer, lube
   tech, …) is display-only card info, decoupled from these levels. */
const ROLE_OPTIONS = ["admin", "manager", "employee", "hr", "billing", "ar"];
const ROLE_LABELS = {
  admin: "Admin",
  manager: "Manager",
  employee: "Employee",
  hr: "HR",
  billing: "Billing",
  ar: "AR",
};
const SCOPE_FOR_ROLE = { manager: "shop", employee: "employee" };
// Roles that can receive alert emails when opted in. Manager roles get
// escalation alerts (server: agent/managerAlerts.ts); HR gets labor
// escalations from HR-flagged shops (server: agent/hrEscalations.ts). Only
// these roles show the opt-in toggle.
const ALERT_ELIGIBLE_ROLES = {
  admin: true,
  manager: true,
  hr: true,
};
const EMAIL_OK = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((v || "").trim());

/* Small removable chip showing one picked shop. */
function ShopChip({ id, busy, onRemove }) {
  const FL = window.FL;
  var node = FL.unitById[id];
  var name = node ? node.name : id;
  return (
    <span
      style={{
        display: "inline-flex",
        alignItems: "center",
        gap: 4,
        padding: "2px 8px",
        border: "1px solid var(--line, #c9ced6)",
        borderRadius: 999,
        fontSize: 12,
        whiteSpace: "nowrap",
      }}
    >
      {name}
      <button
        type="button"
        disabled={busy}
        aria-label={"Remove " + name}
        onClick={() => onRemove(id)}
        style={{
          border: "none",
          background: "none",
          cursor: "pointer",
          padding: 0,
          fontSize: 13,
          lineHeight: 1,
          color: "inherit",
        }}
      >
        ×
      </button>
    </span>
  );
}

/* Chips + add-select picker for one-or-more shops (multi-shop watchers). */
function MultiShopPicker({ scopes, busy, onChange }) {
  const FL = window.FL;
  var remaining = FL.SHOPS.filter(function (o) {
    return scopes.indexOf(o.id) === -1;
  });
  return (
    <>
      {scopes.map(function (id) {
        return (
          <ShopChip
            key={id}
            id={id}
            busy={busy}
            onRemove={function (rid) {
              onChange(
                scopes.filter(function (s) {
                  return s !== rid;
                }),
              );
            }}
          />
        );
      })}
      {remaining.length > 0 && (
        <select
          className="select"
          value=""
          disabled={busy}
          onChange={(e) => {
            var v = e.target.value;
            if (v && scopes.indexOf(v) === -1) onChange(scopes.concat([v]));
          }}
        >
          <option value="" disabled>
            {scopes.length ? "Add shop…" : "Shop…"}
          </option>
          {remaining.map((o) => (
            <option key={o.id} value={o.id}>
              {o.name}
            </option>
          ))}
        </select>
      )}
    </>
  );
}

/* Inline role + scope assigner used for both approval and role change.
   Shop managers may watch one OR several shops: picked shops render as
   removable chips and the select adds more. onApply(role, scopeEntityId,
   scopeEntityIds) — scopeEntityId is the first watched id (kept for legacy
   single-scope readers), scopeEntityIds the full list (null for single-scope
   roles like tech). */
function RoleAssigner({ initialRole, busy, label, onApply }) {
  const FL = window.FL;
  const [role, setRole] = useState(initialRole || "");
  const [scope, setScope] = useState("");
  const [scopes, setScopes] = useState([]);
  const needsScope = SCOPE_FOR_ROLE[role];
  const multiScope = needsScope === "shop";
  const scopeOptions = needsScope === "employee" ? FL.PEOPLE : [];
  const ready =
    role && (!needsScope || (multiScope ? scopes.length > 0 : scope));

  return (
    <div
      style={{
        display: "inline-flex",
        gap: 6,
        flexWrap: "wrap",
        justifyContent: "flex-end",
        alignItems: "center",
      }}
    >
      <select
        className="select"
        value={role}
        disabled={busy}
        onChange={(e) => {
          setRole(e.target.value);
          setScope("");
          setScopes([]);
        }}
      >
        <option value="" disabled>
          {label}
        </option>
        {ROLE_OPTIONS.map((r) => (
          <option key={r} value={r}>
            {ROLE_LABELS[r]}
          </option>
        ))}
      </select>
      {needsScope && multiScope && (
        <MultiShopPicker scopes={scopes} busy={busy} onChange={setScopes} />
      )}
      {needsScope && !multiScope && (
        <select
          className="select"
          value={scope}
          disabled={busy}
          onChange={(e) => setScope(e.target.value)}
        >
          <option value="" disabled>
            Person…
          </option>
          {scopeOptions.map((o) => (
            <option key={o.id} value={o.id}>
              {o.name}
            </option>
          ))}
        </select>
      )}
      <button
        className="btn primary sm"
        disabled={busy || !ready}
        onClick={() =>
          onApply(
            role,
            !needsScope ? null : multiScope ? scopes[0] : scope,
            multiScope && needsScope ? scopes : null,
          )
        }
      >
        Apply
      </button>
    </div>
  );
}

/* Searchable org-chart person picker: type-to-filter input + select. Value is
   an employee entity id ("" = no link). Shows who already holds each card
   and warns when the selected card already has an active login or pending
   invite, so owners never accidentally double-assign. */
function PersonPicker({ value, disabled, onChange, placeholder }) {
  const FL = window.FL;
  const [query, setQuery] = useState("");
  const q = query.trim().toLowerCase();
  const people = FL.PEOPLE.slice().sort(function (a, b) {
    return a.name.localeCompare(b.name);
  });
  const filtered = q
    ? people.filter(function (p) {
        return p.name.toLowerCase().indexOf(q) !== -1;
      })
    : people;
  const selected = value ? FL.peopleById[value] : null;
  // Keep the current selection visible even when the filter excludes it.
  const options =
    selected &&
    !filtered.some(function (p) {
      return p.id === selected.id;
    })
      ? [selected].concat(filtered)
      : filtered;

  var logins = FL.loginByEntity || {};
  var selectedLogin = value ? logins[value] : null;

  return (
    <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
      <input
        className="input"
        style={{ flex: "1 1 90px", minWidth: 0 }}
        value={query}
        disabled={disabled}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search people…"
      />
      <select
        className="select"
        style={{ flex: "2 1 140px", minWidth: 0 }}
        value={value || ""}
        disabled={disabled}
        onChange={(e) => onChange(e.target.value)}
      >
        <option value="">{placeholder || "No link"}</option>
        {options.map(function (p) {
          var login = logins[p.id];
          var suffix = login
            ? login.status === "active"
              ? " [active: " + login.email + "]"
              : login.status === "invited"
                ? " [invite pending: " + login.email + "]"
                : " [" + login.status + ": " + login.email + "]"
            : "";
          return (
            <option key={p.id} value={p.id}>
              {p.name + suffix}
            </option>
          );
        })}
      </select>
      {selectedLogin && (
        <div
          style={{
            flex: "1 0 100%",
            fontSize: 11,
            marginTop: 2,
            padding: "4px 8px",
            borderRadius: 6,
            background:
              selectedLogin.status === "active"
                ? "rgba(200,140,0,0.10)"
                : "var(--surface-2)",
            color:
              selectedLogin.status === "active"
                ? "var(--amber-ink, #9a6b00)"
                : "var(--ink-2)",
          }}
        >
          {selectedLogin.status === "active"
            ? "⚠ This card is already linked to an active account: " +
              selectedLogin.email
            : selectedLogin.status === "invited"
              ? "A pending invite for " +
                selectedLogin.email +
                " already claims this card."
              : "Card is held by " +
                selectedLogin.email +
                " (" +
                selectedLogin.status +
                ")"}
        </div>
      )}
    </div>
  );
}

/* Create-account form — owner invites a person by email: the server creates
   an invitation and emails a one-time code/link to the invitee, and the
   account materializes only when they accept it. Nothing secret is shown
   here — the invite code goes to the invitee's inbox, never to the admin.
   Until accepted, the invite is visible in the Invitations list below.
   Accepts optional initialLinkId/initialName so an org-chart "Invite" button
   can pre-populate the form for a specific employee card. */
function CreateUserForm({ onCreated, onCancel, initialLinkId, initialName }) {
  const FL = window.FL;
  const [email, setEmail] = useState("");
  const [name, setName] = useState(initialName || "");
  const [role, setRole] = useState("");
  const [scope, setScope] = useState("");
  const [scopes, setScopes] = useState([]);
  const [linkId, setLinkId] = useState(initialLinkId || "");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const needsScope = SCOPE_FOR_ROLE[role];
  const multiScope = needsScope === "shop";
  const scopeOptions = needsScope === "employee" ? FL.PEOPLE : [];
  const ready =
    EMAIL_OK(email) &&
    role &&
    (!needsScope || (multiScope ? scopes.length > 0 : scope));

  const submit = async (e) => {
    e.preventDefault();
    setErr("");
    setBusy(true);
    try {
      const payload = {
        email: email.trim(),
        name: name.trim() || undefined,
        role,
      };
      if (needsScope) {
        if (multiScope) {
          payload.scopeEntityId = scopes[0];
          payload.scopeEntityIds = scopes;
        } else {
          payload.scopeEntityId = scope;
        }
      }
      if (linkId) payload.linkedEntityId = linkId;
      let r;
      try {
        r = await API.adminCreateUser(payload);
      } catch (e1) {
        if (
          e1.status === 409 &&
          e1.data &&
          e1.data.code === "LINKED_ENTITY_TAKEN" &&
          window.confirm(
            e1.message + ". Link this account to the same card anyway?",
          )
        ) {
          payload.confirmSharedLink = true;
          r = await API.adminCreateUser(payload);
        } else {
          throw e1;
        }
      }
      onCreated(r);
    } catch (e2) {
      setErr(e2.message || "Failed to create account.");
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="card" style={{ padding: 16, marginBottom: 14 }}>
      <div style={{ fontWeight: 700, marginBottom: 10 }}>New account</div>
      {err && <div className="auth-msg err">{err}</div>}
      <form onSubmit={submit}>
        <div
          style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}
        >
          <div className="field">
            <label>Email</label>
            <input
              className="input"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="person@company.com"
              required
            />
          </div>
          <div className="field">
            <label>Name (optional)</label>
            <input
              className="input"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="Their name"
            />
          </div>
          <div className="field">
            <label>Role</label>
            <select
              className="select"
              value={role}
              onChange={(e) => {
                setRole(e.target.value);
                setScope("");
                setScopes([]);
              }}
            >
              <option value="" disabled>
                Select role…
              </option>
              {ROLE_OPTIONS.map((r) => (
                <option key={r} value={r}>
                  {ROLE_LABELS[r]}
                </option>
              ))}
            </select>
          </div>
          {needsScope && multiScope && (
            <div className="field">
              <label>Shops (one or more)</label>
              <div
                style={{
                  display: "flex",
                  gap: 6,
                  flexWrap: "wrap",
                  alignItems: "center",
                }}
              >
                <MultiShopPicker
                  scopes={scopes}
                  busy={busy}
                  onChange={setScopes}
                />
              </div>
            </div>
          )}
          {needsScope && !multiScope && (
            <div className="field">
              <label>Person</label>
              <select
                className="select"
                value={scope}
                onChange={(e) => setScope(e.target.value)}
              >
                <option value="" disabled>
                  Select person…
                </option>
                {scopeOptions.map((o) => (
                  <option key={o.id} value={o.id}>
                    {o.name}
                  </option>
                ))}
              </select>
            </div>
          )}
          <div className="field" style={{ gridColumn: "1 / -1" }}>
            <label>Link to org chart card (optional)</label>
            <PersonPicker
              value={linkId}
              disabled={busy}
              onChange={(id) => {
                setLinkId(id);
                // Boss-assigned watcher defaults: picking a card that carries
                // watchesShopIds prefills a shop-watcher invite (role + shop
                // set + name) — all still editable before sending.
                const card = id ? FL.peopleById[id] : null;
                const watched =
                  card && card.watchesShopIds && card.watchesShopIds.length
                    ? card.watchesShopIds
                    : null;
                if (watched) {
                  setRole("manager");
                  setScopes(watched.slice());
                  setScope("");
                  if (!name.trim()) setName(card.name || "");
                }
              }}
              placeholder="No link"
            />
          </div>
        </div>
        <div style={{ display: "flex", gap: 8, marginTop: 6 }}>
          <button
            className="btn primary sm"
            type="submit"
            disabled={busy || !ready}
          >
            {busy ? "Creating…" : "Create account"}
          </button>
          <button
            className="btn sm"
            type="button"
            disabled={busy}
            onClick={onCancel}
          >
            Cancel
          </button>
        </div>
      </form>
    </div>
  );
}

/* Linked-card table cell: shows the linked person's name with edit/remove
   controls; expands into a PersonPicker while editing. */
/* Display-only job title on the linked org-chart card (e.g. "Service
   writer"). Editable inline; fully decoupled from the permission level —
   changing it never affects what the user can see or do. */
function JobTitleEditor({ entityId, busy }) {
  const FL = window.FL;
  const person = FL.peopleById[entityId];
  const [editing, setEditing] = useState(false);
  const [val, setVal] = useState("");
  const [saving, setSaving] = useState(false);
  const [, force] = useState(0);
  if (!person) return null;
  const title = person.role || "";

  const save = async () => {
    setSaving(true);
    try {
      const r = await API.adminSetJobTitle(entityId, val.trim());
      // Update the local caches so the new title shows without a full reload.
      const t = r && r.entity ? r.entity.jobTitle : val.trim() || null;
      person.role = t || "Team member";
      if (FL.nodeById[entityId]) FL.nodeById[entityId].role = t;
      setEditing(false);
      force((n) => n + 1);
    } catch (e) {
      window.alert(e.message || "Failed to update job title.");
    } finally {
      setSaving(false);
    }
  };

  if (!editing) {
    return (
      <div style={{ fontSize: 11, color: "var(--ink-3)", marginTop: 2 }}>
        Job title: {title || "—"}{" "}
        <button
          className="btn ghost sm"
          disabled={busy}
          style={{ padding: "0 4px", fontSize: 11 }}
          onClick={() => {
            setVal(title === "Team member" ? "" : title);
            setEditing(true);
          }}
        >
          Edit
        </button>
      </div>
    );
  }
  return (
    <div
      style={{
        display: "inline-flex",
        gap: 4,
        alignItems: "center",
        marginTop: 2,
      }}
    >
      <input
        className="input"
        style={{ fontSize: 11, padding: "2px 6px", width: 140 }}
        value={val}
        disabled={saving}
        placeholder="e.g. Service writer"
        onChange={(e) => setVal(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            e.preventDefault();
            save();
          }
        }}
      />
      <button className="btn primary sm" disabled={saving} onClick={save}>
        Save
      </button>
      <button
        className="btn ghost sm"
        disabled={saving}
        onClick={() => setEditing(false)}
      >
        Cancel
      </button>
    </div>
  );
}

function LinkedCardCell({ user, busy, onApply }) {
  const FL = window.FL;
  const [editing, setEditing] = useState(false);
  const [pick, setPick] = useState(user.linkedEntityId || "");
  const linked = user.linkedEntityId
    ? FL.peopleById[user.linkedEntityId]
    : null;

  if (!editing) {
    return (
      <div
        style={{
          display: "inline-flex",
          gap: 6,
          alignItems: "center",
          flexWrap: "wrap",
        }}
      >
        {linked ? (
          <span>{linked.name}</span>
        ) : user.linkedEntityId ? (
          <span
            style={{ color: "var(--ink-3)", fontStyle: "italic" }}
            title={"Linked card " + user.linkedEntityId + " no longer exists"}
          >
            Card no longer exists
          </span>
        ) : (
          <span style={{ color: "var(--ink-3)" }}>—</span>
        )}
        <button
          className="btn ghost sm"
          disabled={busy}
          onClick={() => {
            setPick(user.linkedEntityId || "");
            setEditing(true);
          }}
        >
          {user.linkedEntityId ? "Change" : "Link"}
        </button>
        {user.linkedEntityId && (
          <button
            className="btn ghost sm"
            disabled={busy}
            onClick={() => onApply(null)}
          >
            Remove
          </button>
        )}
      </div>
    );
  }

  return (
    <div
      style={{
        display: "flex",
        gap: 6,
        alignItems: "center",
        flexWrap: "wrap",
        minWidth: 220,
      }}
    >
      <div style={{ flex: "1 1 200px" }}>
        <PersonPicker
          value={pick}
          disabled={busy}
          onChange={setPick}
          placeholder="No link"
        />
      </div>
      <button
        className="btn primary sm"
        disabled={busy}
        onClick={() => {
          setEditing(false);
          onApply(pick || null);
        }}
      >
        Save
      </button>
      <button
        className="btn sm"
        disabled={busy}
        onClick={() => setEditing(false)}
      >
        Cancel
      </button>
    </div>
  );
}

function ResetCodeBanner({ info, onDismiss }) {
  if (!info) return null;
  return (
    <div
      className="auth-msg ok"
      style={{
        display: "flex",
        alignItems: "center",
        gap: 10,
        flexWrap: "wrap",
      }}
    >
      <span>
        Reset code for <b>{info.email}</b>:
      </span>
      <code
        style={{
          background: "var(--surface-2)",
          padding: "2px 8px",
          borderRadius: 6,
          fontSize: 13,
          userSelect: "all",
        }}
      >
        {info.code}
      </code>
      <button
        className="btn sm"
        onClick={() => {
          try {
            navigator.clipboard.writeText(info.code);
          } catch (_e) {}
        }}
      >
        Copy
      </button>
      <button
        className="btn ghost sm"
        style={{ marginLeft: "auto" }}
        onClick={onDismiss}
      >
        Dismiss
      </button>
    </div>
  );
}

/* Current watched-scope names for a user row (shops for managers, the
   employee card for techs). Renders nothing when the account has no scope. */
function ScopeNames({ user }) {
  const FL = window.FL;
  var ids = FL.watchedIds(user);
  if (!ids.length) return null;
  var names = ids.map(function (id) {
    var node = FL.unitById[id] || FL.peopleById[id];
    return node ? node.name : id;
  });
  return (
    <div style={{ color: "var(--ink-3)", fontSize: 11, marginTop: 4 }}>
      {names.join(", ")}
    </div>
  );
}

/* Relative expiry copy for an invitation row: "expires in 6 days" /
   "expired 3 hours ago". Plain string math — this file is compiled in the
   browser by babel-standalone (no optional chaining / nullish coalescing). */
function inviteExpiryText(iso) {
  var t = new Date(iso).getTime();
  if (isNaN(t)) return "";
  var diff = t - Date.now();
  var abs = Math.abs(diff);
  var n, unit;
  if (abs >= 86400000) {
    n = Math.round(abs / 86400000);
    unit = "day";
  } else if (abs >= 3600000) {
    n = Math.round(abs / 3600000);
    unit = "hour";
  } else {
    n = Math.max(1, Math.round(abs / 60000));
    unit = "minute";
  }
  var span = n + " " + unit + (n === 1 ? "" : "s");
  return diff >= 0 ? "expires in " + span : "expired " + span + " ago";
}

/* Pending / recently-expired invitations. An invited person is visible here
   from the moment the owner creates the account until they accept the invite
   (or it's revoked, or the expired-grace window passes). Status is truthful:
   "Email failed" means the last send attempt could not hand the message to
   the mail provider — Re-send issues a fresh code with a fresh expiry. The
   invite code itself is never shown to the admin. */
function InvitationsCard({ invites, busyId, onResend, onRevoke }) {
  const FL = window.FL;
  if (!invites.length) return null;
  return (
    <div className="card" style={{ padding: 4, marginBottom: 14 }}>
      <div style={{ fontWeight: 700, padding: "10px 12px 2px" }}>
        Invitations
      </div>
      <div
        style={{ color: "var(--ink-3)", fontSize: 12, padding: "0 12px 6px" }}
      >
        Invited by email, not signed up yet — each becomes a user account when
        the invitation is accepted.
      </div>
      <table className="admin-table" data-testid="invitations-table">
        <thead>
          <tr>
            <th>Invitee</th>
            <th>Status</th>
            <th>Role</th>
            <th>Linked card</th>
            <th>Expiry</th>
            <th style={{ textAlign: "right" }}>Actions</th>
          </tr>
        </thead>
        <tbody>
          {invites.map((inv) => {
            const busy = busyId === "inv:" + inv.email;
            const linked = inv.linkedEntityId
              ? FL.peopleById[inv.linkedEntityId]
              : null;
            const expired = inv.status === "expired";
            const emailFailed = inv.emailStatus === "failed";
            return (
              <tr key={inv.email}>
                <td>
                  <div style={{ fontWeight: 650 }}>{inv.name || inv.email}</div>
                  <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
                    {inv.email}
                  </div>
                </td>
                <td>
                  <div
                    style={{
                      display: "inline-flex",
                      gap: 4,
                      flexWrap: "wrap",
                    }}
                  >
                    <Tag tone={expired ? "red" : "yellow"}>
                      {expired ? "Expired" : "Invited"}
                    </Tag>
                    {emailFailed && <Tag tone="red">Email failed</Tag>}
                  </div>
                </td>
                <td>
                  <span style={{ color: "var(--ink-2)" }}>
                    {inv.role ? ROLE_LABELS[inv.role] || inv.role : "—"}
                  </span>
                </td>
                <td>
                  {linked ? (
                    <span>{linked.name}</span>
                  ) : inv.linkedEntityId ? (
                    <span
                      style={{ color: "var(--ink-3)", fontStyle: "italic" }}
                      title={
                        "Linked card " +
                        inv.linkedEntityId +
                        " no longer exists"
                      }
                    >
                      Card no longer exists
                    </span>
                  ) : (
                    <span style={{ color: "var(--ink-3)" }}>—</span>
                  )}
                </td>
                <td>
                  <span style={{ color: "var(--ink-2)", fontSize: 12 }}>
                    {inviteExpiryText(inv.expiresAt)}
                  </span>
                </td>
                <td style={{ textAlign: "right" }}>
                  <div
                    style={{
                      display: "inline-flex",
                      gap: 6,
                      flexWrap: "wrap",
                      justifyContent: "flex-end",
                    }}
                  >
                    <button
                      className="btn sm"
                      disabled={busy}
                      onClick={() => onResend(inv)}
                    >
                      Re-send
                    </button>
                    <button
                      className="btn danger sm"
                      disabled={busy}
                      onClick={() => onRevoke(inv)}
                    >
                      Revoke
                    </button>
                  </div>
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

function UsersTab({
  onToast,
  currentUserId,
  pendingInviteCard,
  onClearPendingInviteCard,
}) {
  const [users, setUsers] = useState(null);
  const [invites, setInvites] = useState([]);
  const [loadFailed, setLoadFailed] = useState(false);
  const [busyId, setBusyId] = useState(null);
  const [creating, setCreating] = useState(false);
  const [resetInfo, setResetInfo] = useState(null);
  // Pre-fill data when opened via org-chart "Invite" button (prop-driven so
  // the value is available on first render — a window event would arrive
  // before this component mounts and be lost).
  const [inviteFromCard, setInviteFromCard] = useState(
    pendingInviteCard || null,
  );

  const load = async () => {
    setLoadFailed(false);
    try {
      const r = await API.adminUsers();
      setUsers(r.users || r || []);
      setInvites((r && r.invitations) || []);
    } catch (e) {
      setLoadFailed(true);
      if (onToast) onToast(e.message || "Failed to load users.", "danger");
    }
  };
  useEffect(() => {
    load();
    // If a pending invite card was passed in, open the create form immediately.
    if (pendingInviteCard) {
      setCreating(true);
    }
  }, []);

  // When the parent pushes a new pendingInviteCard (e.g. user clicks Invite
  // on a second card without leaving Admin), absorb it and open the form.
  useEffect(
    function () {
      if (pendingInviteCard) {
        setInviteFromCard(pendingInviteCard);
        setCreating(true);
        setResetInfo(null);
      }
    },
    [pendingInviteCard],
  );

  const act = async (fn, id, successMsg) => {
    setBusyId(id);
    try {
      await fn();
      await load();
      if (successMsg && onToast) onToast(successMsg);
    } catch (e) {
      if (onToast) onToast(e.message || "Action failed.", "danger");
    } finally {
      setBusyId(null);
    }
  };

  const resetPw = async (u) => {
    setBusyId(u.id);
    try {
      const r = await API.adminResetPassword(u.id);
      setResetInfo({ email: u.email, code: r.resetCode });
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to issue reset code.", "danger");
    } finally {
      setBusyId(null);
    }
  };

  /* Re-send replaces the invitation with a fresh code + expiry and emails it
     again; the toast is truthful about whether that email actually went out. */
  const resendInvite = async (inv) => {
    setBusyId("inv:" + inv.email);
    try {
      const r = await API.adminResendInvite(inv.email);
      await load();
      if (onToast) {
        if (r && r.emailDelivered === false) {
          onToast(
            "Invitation renewed for " +
              inv.email +
              ", but the email failed to send — try Re-send again or check the email setup in Settings.",
            "danger",
          );
        } else {
          onToast("Invitation re-sent to " + inv.email);
        }
      }
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to re-send invitation.", "danger");
    } finally {
      setBusyId(null);
    }
  };

  const revokeInvite = async (inv) => {
    if (
      !window.confirm(
        "Revoke the invitation for " +
          inv.email +
          "? The emailed invitation code will stop working.",
      )
    )
      return;
    setBusyId("inv:" + inv.email);
    try {
      await API.adminRevokeInvite(inv.email);
      await load();
      if (onToast) onToast("Invitation revoked");
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to revoke invitation.", "danger");
    } finally {
      setBusyId(null);
    }
  };

  return (
    <div>
      <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
        {!creating && (
          <button
            className="btn primary sm"
            onClick={() => {
              setCreating(true);
              setInviteFromCard(null);
              setResetInfo(null);
            }}
          >
            + New account
          </button>
        )}
        <button
          className="btn sm"
          style={{ marginLeft: "auto" }}
          onClick={load}
        >
          Refresh
        </button>
      </div>

      {creating && (
        <CreateUserForm
          onCancel={() => {
            setCreating(false);
            setInviteFromCard(null);
            if (onClearPendingInviteCard) onClearPendingInviteCard();
          }}
          onCreated={(r) => {
            setCreating(false);
            setInviteFromCard(null);
            if (onClearPendingInviteCard) onClearPendingInviteCard();
            load();
            if (onToast) {
              var invEmail = r && r.invitation ? r.invitation.email : "";
              var who = invEmail ? " to " + invEmail : " to the user";
              if (r && r.emailDelivered === false) {
                onToast(
                  "Invitation created, but the email failed to send — use Re-send in the invitations list below.",
                  "danger",
                );
              } else {
                onToast(
                  "Invitation email sent" +
                    who +
                    " — they'll appear under Invitations until they accept.",
                );
              }
            }
          }}
          initialLinkId={inviteFromCard ? inviteFromCard.entityId : ""}
          initialName={inviteFromCard ? inviteFromCard.name : ""}
        />
      )}

      <ResetCodeBanner info={resetInfo} onDismiss={() => setResetInfo(null)} />
      <InvitationsCard
        invites={invites}
        busyId={busyId}
        onResend={resendInvite}
        onRevoke={revokeInvite}
      />
      {!users && !loadFailed && (
        <div className="empty is-loading">Loading…</div>
      )}

      {users && (
        <div className="card" style={{ padding: 4 }}>
          <table className="admin-table">
            <thead>
              <tr>
                <th>User</th>
                <th>Status</th>
                <th>Role</th>
                <th>Linked card</th>
                <th style={{ textAlign: "right" }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {users.map((u) => {
                const busy = busyId === u.id;
                return (
                  <tr key={u.id}>
                    <td>
                      <div style={{ fontWeight: 650 }}>{u.name || u.email}</div>
                      <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
                        {u.email}
                      </div>
                    </td>
                    <td>
                      <Tag
                        tone={
                          u.status === "active"
                            ? "green"
                            : u.status === "pending"
                              ? "yellow"
                              : "red"
                        }
                      >
                        {u.status}
                      </Tag>
                    </td>
                    <td>
                      {u.status === "active" ? (
                        <>
                          <RoleAssigner
                            key={u.role || ""}
                            initialRole={u.role || ""}
                            busy={busy}
                            label="Set role…"
                            onApply={(role, scope, scopes) =>
                              act(
                                () => API.adminRole(u.id, role, scope, scopes),
                                u.id,
                                "Role updated",
                              )
                            }
                          />
                          <ScopeNames user={u} />
                        </>
                      ) : (
                        <span style={{ color: "var(--ink-3)" }}>
                          {u.role ? ROLE_LABELS[u.role] || u.role : "—"}
                        </span>
                      )}
                    </td>
                    <td>
                      <LinkedCardCell
                        user={u}
                        busy={busy}
                        onApply={(entityId) =>
                          act(
                            async () => {
                              try {
                                await API.adminSetLink(u.id, entityId);
                              } catch (e) {
                                if (
                                  e.status === 409 &&
                                  e.data &&
                                  e.data.code === "LINKED_ENTITY_TAKEN"
                                ) {
                                  const msg =
                                    e.message +
                                    ". Link this account to the same card anyway?";
                                  if (window.confirm(msg)) {
                                    await API.adminSetLink(
                                      u.id,
                                      entityId,
                                      true,
                                    );
                                    return;
                                  }
                                }
                                throw e;
                              }
                            },
                            u.id,
                            entityId
                              ? "Org chart link updated"
                              : "Link removed",
                          )
                        }
                      />
                      {u.linkedEntityId && (
                        <JobTitleEditor
                          entityId={u.linkedEntityId}
                          busy={busy}
                        />
                      )}
                    </td>
                    <td style={{ textAlign: "right" }}>
                      <div
                        style={{
                          display: "inline-flex",
                          gap: 6,
                          flexWrap: "wrap",
                          justifyContent: "flex-end",
                        }}
                      >
                        {u.status === "pending" && (
                          <RoleAssigner
                            busy={busy}
                            label="Approve as…"
                            onApply={(role, scope, scopes) =>
                              act(
                                () =>
                                  API.adminApprove(u.id, role, scope, scopes),
                                u.id,
                                "User approved",
                              )
                            }
                          />
                        )}
                        {u.status === "active" &&
                          ALERT_ELIGIBLE_ROLES[u.role] && (
                            <button
                              className={
                                "btn sm" +
                                (u.emailAlertsEnabled ? " primary" : "")
                              }
                              disabled={busy}
                              title="Manager-alert emails (sent only when MANAGER_ALERT_MODE=live)"
                              onClick={() => {
                                var nextEnabled = !u.emailAlertsEnabled;
                                act(
                                  () =>
                                    API.adminSetUserNotifications(
                                      u.id,
                                      nextEnabled,
                                    ).then(function (r) {
                                      // Sync EmailNotificationsCard roster
                                      window.dispatchEvent(
                                        new CustomEvent("fl:userOptInChanged", {
                                          detail: {
                                            id: u.id,
                                            emailAlertsEnabled: nextEnabled,
                                          },
                                        }),
                                      );
                                      return r;
                                    }),
                                  u.id,
                                  nextEnabled
                                    ? "Email alerts on"
                                    : "Email alerts off",
                                );
                              }}
                            >
                              {u.emailAlertsEnabled
                                ? "Email alerts: on"
                                : "Email alerts: off"}
                            </button>
                          )}
                        {u.status === "active" && (
                          <button
                            className="btn sm"
                            disabled={busy}
                            onClick={() => resetPw(u)}
                          >
                            Reset password
                          </button>
                        )}
                        {/* Lockout recovery: clears the user's second factor so
                            they can sign in with their password and re-enrol.
                            Only offered for accounts that actually have one. */}
                        {u.twoFactorEnabled && u.id !== currentUserId && (
                          <button
                            className="btn sm"
                            data-testid={"reset-2fa-" + u.id}
                            disabled={busy}
                            title="Clear this user's two-step verification so they can sign in and set it up again"
                            onClick={() =>
                              act(
                                () => API.adminResetTwoFactor(u.id),
                                u.id,
                                "Two-step verification cleared",
                              )
                            }
                          >
                            Reset 2FA
                          </button>
                        )}
                        {u.status === "active" && (
                          <button
                            className="btn danger sm"
                            disabled={busy}
                            onClick={() =>
                              act(
                                () => API.adminDisable(u.id),
                                u.id,
                                "User disabled",
                              )
                            }
                          >
                            Disable
                          </button>
                        )}
                        {u.status === "disabled" && (
                          <button
                            className="btn sm"
                            disabled={busy}
                            onClick={() =>
                              act(
                                () => API.adminEnable(u.id),
                                u.id,
                                "User re-enabled",
                              )
                            }
                          >
                            Enable
                          </button>
                        )}
                      </div>
                    </td>
                  </tr>
                );
              })}
              {users.length === 0 && (
                <tr>
                  <td colSpan={5}>
                    <div className="empty">No users yet.</div>
                  </td>
                </tr>
              )}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

function ThresholdField({ label, hint, value, onChange, disabled }) {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        gap: 12,
        marginBottom: 14,
      }}
    >
      <div style={{ flex: 1 }}>
        <div style={{ fontWeight: 600 }}>{label}</div>
        <div style={{ color: "var(--ink-3)", fontSize: 11 }}>{hint}</div>
      </div>
      <input
        className="input"
        type="number"
        min="0"
        step="0.25"
        value={value}
        disabled={disabled}
        onChange={(e) => onChange(e.target.value)}
        style={{ width: 96, textAlign: "right" }}
      />
      <span style={{ color: "var(--ink-3)", fontSize: 12, width: 38 }}>
        hours
      </span>
    </div>
  );
}

function CoachingField({ label, hint, value, onChange, disabled, min, max }) {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        gap: 12,
        marginBottom: 14,
      }}
    >
      <div style={{ flex: 1 }}>
        <div style={{ fontWeight: 600 }}>{label}</div>
        <div style={{ color: "var(--ink-3)", fontSize: 11 }}>{hint}</div>
      </div>
      <input
        className="input"
        type="number"
        min={min}
        max={max}
        step="1"
        value={value}
        disabled={disabled}
        onChange={(e) => onChange(e.target.value)}
        style={{ width: 80, textAlign: "right" }}
      />
    </div>
  );
}

function SafetyField({ label, hint, value, onChange, disabled }) {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        gap: 12,
        marginBottom: 14,
      }}
    >
      <div style={{ flex: 1 }}>
        <div style={{ fontWeight: 600 }}>{label}</div>
        <div style={{ color: "var(--ink-3)", fontSize: 11 }}>{hint}</div>
      </div>
      <input
        className="input"
        type="number"
        min="0"
        step="1"
        value={value}
        disabled={disabled}
        onChange={(e) => onChange(e.target.value)}
        style={{ width: 96, textAlign: "right" }}
      />
      <span style={{ color: "var(--ink-3)", fontSize: 12, width: 38 }}>
        pts
      </span>
    </div>
  );
}

/* Coaching rule IDs + labels for the per-rule override inputs. */
var COACHING_RULE_FIELDS = [
  { id: "missed_clock_out", label: "Missed clock-out" },
  { id: "no_break_taken", label: "No break taken" },
  { id: "break_period_short", label: "Break cut short" },
  { id: "ro_hours_over_authorized", label: "Hours worked past authorization" },
  { id: "tech_low_efficiency", label: "Low efficiency for the day" },
  { id: "invoice_exception", label: "Invoice exception" },
  { id: "idle_truck_no_repair_order", label: "Idle truck, no repair order" },
  {
    id: "samsara_fullbay_cross_reference",
    label: "Samsara / Fullbay mismatch",
  },
  { id: "repair_order_never_opened", label: "Repair order never opened" },
  { id: "time_discrepancy", label: "Time discrepancy" },
  { id: "samsara_safety_event", label: "Samsara safety event" },
];

function ThresholdsTab({ onToast }) {
  const [data, setData] = useState(null);
  const [toOrange, setToOrange] = useState("");
  const [toRed, setToRed] = useState("");
  const [ownerStep, setOwnerStep] = useState("");
  const [coachWindow, setCoachWindow] = useState("30");
  const [coachMinOcc, setCoachMinOcc] = useState("");
  const [coachRuleOverrides, setCoachRuleOverrides] = useState({});
  const [wCritical, setWCritical] = useState("");
  const [wHigh, setWHigh] = useState("");
  const [wMedium, setWMedium] = useState("");
  const [wLow, setWLow] = useState("");
  const [bandOrange, setBandOrange] = useState("");
  const [bandRed, setBandRed] = useState("");
  const [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState(false);

  const hoursOf = (ms) => String(Math.round((ms / 3600000) * 100) / 100);

  const apply = (d) => {
    setToOrange(hoursOf(d.escalation.toOrangeMs));
    setToRed(hoursOf(d.escalation.toRedMs));
    setOwnerStep(hoursOf(d.escalation.ownerStepMs));
    if (d.coaching) {
      // Snap to nearest of the three accepted API values (7 / 30 / 90 days).
      var wd = d.coaching.windowDays;
      setCoachWindow(wd <= 18 ? "7" : wd <= 60 ? "30" : "90");
      setCoachMinOcc(String(d.coaching.minOccurrences));
      setCoachRuleOverrides(d.coaching.ruleOverrides || {});
    }
    if (d.safety) {
      setWCritical(String(d.safety.weights.critical));
      setWHigh(String(d.safety.weights.high));
      setWMedium(String(d.safety.weights.medium));
      setWLow(String(d.safety.weights.low));
      setBandOrange(String(d.safety.bands.orange));
      setBandRed(String(d.safety.bands.red));
    }
  };

  const load = async () => {
    setSaved(false);
    try {
      const r = await API.adminSettings();
      setData(r);
      apply(r);
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to load thresholds.", "danger");
    }
  };
  useEffect(() => {
    load();
  }, []);

  const num = (s) => {
    const n = Number(s);
    return isFinite(n) ? n : NaN;
  };

  const save = async () => {
    setSaved(false);
    const o = num(toOrange),
      r = num(toRed),
      w = num(ownerStep);
    const cw = num(coachWindow),
      cm = num(coachMinOcc);
    if (!(o > 0) || !(r > 0) || !(w >= 0)) {
      if (onToast)
        onToast("Enter positive hours (owner step may be 0).", "danger");
      return;
    }
    if (r <= o) {
      if (onToast)
        onToast(
          "\u201CTurns red\u201D must be longer than \u201Cturns orange\u201D.",
          "danger",
        );
      return;
    }
    const bounds = data && data.coachingBounds;
    const cmMin = bounds ? bounds.minOccurrences.min : 1,
      cmMax = bounds ? bounds.minOccurrences.max : 20;
    if (![7, 30, 90].includes(cw)) {
      if (onToast)
        onToast("Coaching window must be 7, 30, or 90 days.", "danger");
      return;
    }
    if (!Number.isInteger(cm) || cm < cmMin || cm > cmMax) {
      if (onToast)
        onToast(
          "Min occurrences must be a whole number between " +
            cmMin +
            " and " +
            cmMax +
            ".",
          "danger",
        );
      return;
    }
    // Validate per-rule overrides: each value must be an integer in bounds.
    for (var ruleId of Object.keys(coachRuleOverrides)) {
      var v = coachRuleOverrides[ruleId];
      if (!Number.isInteger(v) || v < cmMin || v > cmMax) {
        if (onToast)
          onToast(
            "Per-rule overrides must be whole numbers between " +
              cmMin +
              " and " +
              cmMax +
              ".",
            "danger",
          );
        return;
      }
    }
    const sc = num(wCritical),
      sh = num(wHigh),
      sm = num(wMedium),
      sl = num(wLow);
    const bo = num(bandOrange),
      br = num(bandRed);
    const safetyVals = [sc, sh, sm, sl, bo, br];
    const safetyValid = safetyVals.every((v) => isFinite(v) && v >= 0);
    if (!safetyValid) {
      if (onToast)
        onToast(
          "Safety weights and score bands must be zero or positive numbers.",
          "danger",
        );
      return;
    }
    if (bo > br) {
      if (onToast)
        onToast(
          "The orange score band must not be higher than the red band.",
          "danger",
        );
      return;
    }
    setSaving(true);
    try {
      const res = await API.adminUpdateSettings({
        toOrangeMs: Math.round(o * 3600000),
        toRedMs: Math.round(r * 3600000),
        ownerStepMs: Math.round(w * 3600000),
        coachingWindowDays: cw,
        coachingMinOccurrences: cm,
        coachingRuleOverrides: coachRuleOverrides,
        safety: {
          weights: { critical: sc, high: sh, medium: sm, low: sl },
          bands: { orange: bo, red: br },
        },
      });
      const next = {
        escalation: res.escalation,
        isDefault: res.isDefault,
        updatedAt: res.updatedAt,
        coaching: res.coaching || (data ? data.coaching : null),
        safety: res.safety || (data ? data.safety : null),
        safetyIsDefault: res.safetyIsDefault,
        defaults: data ? data.defaults : null,
        bounds: data ? data.bounds : null,
        coachingBounds: data ? data.coachingBounds : null,
        safetyDefaults: data ? data.safetyDefaults : null,
        safetyBounds: data ? data.safetyBounds : null,
      };
      setData(next);
      apply(next);
      setSaved(true);
      if (onToast) onToast("Threshold updated");
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to save thresholds.", "danger");
    } finally {
      setSaving(false);
    }
  };

  const bounds = data && data.coachingBounds;

  return (
    <div>
      <div
        style={{
          display: "flex",
          alignItems: "flex-start",
          marginBottom: 12,
          gap: 12,
        }}
      >
        <div className="view-desc" style={{ margin: 0 }}>
          How long an unresolved exception sits before it escalates, and when
          the coaching engine surfaces repeat offenders. Change them here — no
          code edits needed.
        </div>
        <button
          className="btn sm"
          style={{ marginLeft: "auto", flex: "0 0 auto" }}
          onClick={load}
        >
          Reset
        </button>
      </div>
      {!data && <div className="empty is-loading">Loading…</div>}
      {data && (
        <>
          <div
            className="card"
            style={{ padding: 16, maxWidth: 540, marginBottom: 14 }}
          >
            <div style={{ fontWeight: 700, marginBottom: 10 }}>
              Escalation thresholds
            </div>
            {data.isDefault && (
              <div className="view-desc" style={{ marginTop: 0 }}>
                Using default thresholds — not yet customized for this
                organization.
              </div>
            )}
            <ThresholdField
              label="Turns orange after"
              hint="Yellow → orange (early warning)"
              value={toOrange}
              onChange={setToOrange}
              disabled={saving}
            />
            <ThresholdField
              label="Turns red after"
              hint="Orange → red (critical) — must be longer than orange"
              value={toRed}
              onChange={setToRed}
              disabled={saving}
            />
            <ThresholdField
              label="Escalate up the org every"
              hint="How often ownership climbs the org tree — 0 disables it"
              value={ownerStep}
              onChange={setOwnerStep}
              disabled={saving}
            />
          </div>
          <div
            className="card"
            style={{ padding: 16, maxWidth: 540, marginBottom: 14 }}
          >
            <div style={{ fontWeight: 700, marginBottom: 10 }}>
              Coaching thresholds
            </div>
            <div
              className="view-desc"
              style={{ marginTop: 0, marginBottom: 12 }}
            >
              How the coaching engine decides when a repeat-exception pattern is
              worth surfacing to managers.
            </div>
            <div
              style={{
                display: "flex",
                alignItems: "center",
                gap: 12,
                marginBottom: 14,
              }}
            >
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: 600 }}>Coaching window</div>
                <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
                  Look-back period for the occurrence count
                </div>
              </div>
              <select
                className="select"
                value={coachWindow}
                disabled={saving}
                onChange={(e) => setCoachWindow(e.target.value)}
                style={{ height: 34, fontSize: 13, padding: "0 8px" }}
              >
                <option value="7">7 days</option>
                <option value="30">30 days</option>
                <option value="90">90 days</option>
              </select>
            </div>
            <CoachingField
              label="Min occurrences"
              hint={
                "Resolved exceptions in the window before a pattern is flagged — " +
                (bounds
                  ? bounds.minOccurrences.min + "–" + bounds.minOccurrences.max
                  : "1–20")
              }
              value={coachMinOcc}
              onChange={setCoachMinOcc}
              disabled={saving}
              min={bounds ? bounds.minOccurrences.min : 1}
              max={bounds ? bounds.minOccurrences.max : 20}
            />
          </div>
          <div
            className="card"
            style={{ padding: 16, maxWidth: 540, marginBottom: 14 }}
          >
            <div style={{ fontWeight: 700, marginBottom: 6 }}>
              Per-rule coaching overrides
            </div>
            <div
              className="view-desc"
              style={{ marginTop: 0, marginBottom: 12 }}
            >
              Override the minimum occurrences for a specific rule. Leave blank
              to use the global minimum above.
            </div>
            {COACHING_RULE_FIELDS.map(function (rule) {
              var val =
                coachRuleOverrides[rule.id] !== undefined
                  ? String(coachRuleOverrides[rule.id])
                  : "";
              return (
                <div
                  key={rule.id}
                  style={{
                    display: "flex",
                    alignItems: "center",
                    gap: 12,
                    marginBottom: 10,
                  }}
                >
                  <div style={{ flex: 1 }}>
                    <div style={{ fontWeight: 600, fontSize: 13 }}>
                      {rule.label}
                    </div>
                  </div>
                  <input
                    className="input"
                    type="number"
                    min="1"
                    max="20"
                    step="1"
                    placeholder="—"
                    value={val}
                    disabled={saving}
                    onChange={function (e) {
                      var v = e.target.value.trim();
                      var next = Object.assign({}, coachRuleOverrides);
                      if (!v) {
                        delete next[rule.id];
                      } else {
                        next[rule.id] = Number(v);
                      }
                      setCoachRuleOverrides(next);
                    }}
                    style={{ width: 80, textAlign: "right" }}
                  />
                </div>
              );
            })}
          </div>
          <div
            className="card"
            style={{ padding: 16, maxWidth: 540, marginBottom: 14 }}
          >
            <div style={{ fontWeight: 700, marginBottom: 10 }}>
              Safety severity weighting
            </div>
            <div
              className="view-desc"
              style={{ marginTop: 0, marginBottom: 12 }}
            >
              How Samsara safety incidents score toward a truck's severity. Each
              incident adds its tier's points; the total decides whether the
              truck shows orange or red. A critical incident is always red.
            </div>
            {data.safetyIsDefault && (
              <div className="view-desc" style={{ marginTop: 0 }}>
                Using default weighting — not yet customized for this
                organization.
              </div>
            )}
            <SafetyField
              label="Critical incident"
              hint="Crash, rollover, drowsy driving"
              value={wCritical}
              onChange={setWCritical}
              disabled={saving}
            />
            <SafetyField
              label="High-risk incident"
              hint="Harsh brake, speeding, seat belt, distraction"
              value={wHigh}
              onChange={setWHigh}
              disabled={saving}
            />
            <SafetyField
              label="Medium-risk incident"
              hint="Rolling stop, lane departure, harsh turn"
              value={wMedium}
              onChange={setWMedium}
              disabled={saving}
            />
            <SafetyField
              label="Low-risk incident"
              hint="Harsh acceleration, camera obstructed"
              value={wLow}
              onChange={setWLow}
              disabled={saving}
            />
            <SafetyField
              label="Turns orange at"
              hint="Total score at which a truck shows orange"
              value={bandOrange}
              onChange={setBandOrange}
              disabled={saving}
            />
            <SafetyField
              label="Turns red at"
              hint="Total score at which a truck shows red — at least the orange score"
              value={bandRed}
              onChange={setBandRed}
              disabled={saving}
            />
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            <button className="btn primary sm" disabled={saving} onClick={save}>
              {saving ? "Saving…" : "Save settings"}
            </button>
            {saved && (
              <span style={{ color: "var(--green-ink)", fontSize: 12 }}>
                Saved — applied immediately.
              </span>
            )}
            {!saved && data.updatedAt && (
              <span style={{ color: "var(--ink-3)", fontSize: 12 }}>
                Last changed {new Date(data.updatedAt).toLocaleString()}
              </span>
            )}
          </div>
        </>
      )}
    </div>
  );
}

const ACTION_LABELS = {
  "auth.login": "Login",
  "auth.logout": "Logout",
  "auth.logout_all": "Signed out of all devices",
  "auth.signup": "Sign-up",
  "auth.dev_login": "Dev login",
  "auth.change_password": "Password changed",
  "auth.request_reset": "Reset requested",
  "auth.reset_password": "Password reset",
  "auth.2fa.challenge": "2FA code requested",
  "auth.2fa.verify": "2FA code checked",
  "auth.2fa.recovery_used": "Recovery code used",
  "auth.2fa.enrol": "2FA set up",
  "auth.2fa.disable": "2FA turned off",
  "admin.user.create": "Account created",
  "admin.user.approve": "Account approved",
  "admin.user.role": "Role changed",
  "admin.user.disable": "Account disabled",
  "admin.user.enable": "Account enabled",
  "admin.user.reset_password": "Reset code issued",
  "admin.user.reset_2fa": "2FA cleared by admin",
  "admin.settings.update": "Escalation thresholds changed",
};
const PAGE_SIZE = 25;

function AuditTab() {
  const [page, setPage] = useState(null);
  const [actions, setActions] = useState([]);
  const [err, setErr] = useState("");
  const [loading, setLoading] = useState(false);
  const [filters, setFilters] = useState({
    action: "",
    actorEmail: "",
    outcome: "",
    offset: 0,
  });

  const load = async (f) => {
    setLoading(true);
    setErr("");
    try {
      const r = await API.adminAudit({ ...f, limit: PAGE_SIZE });
      setPage(r);
    } catch (e) {
      setErr(e.message || "Failed to load audit log.");
    } finally {
      setLoading(false);
    }
  };
  useEffect(() => {
    load(filters);
    API.adminAuditActions()
      .then((r) => setActions(r.actions || []))
      .catch(() => {});
  }, []);

  const apply = (patch) => {
    const next = { ...filters, ...patch, offset: 0 };
    setFilters(next);
    load(next);
  };
  const go = (delta) => {
    const next = { ...filters, offset: Math.max(0, filters.offset + delta) };
    setFilters(next);
    load(next);
  };

  const total = page ? page.total : 0;
  const from = total === 0 ? 0 : filters.offset + 1;
  const to = page ? Math.min(filters.offset + PAGE_SIZE, total) : 0;

  return (
    <div>
      <div
        style={{
          display: "flex",
          gap: 8,
          flexWrap: "wrap",
          marginBottom: 12,
          alignItems: "center",
        }}
      >
        <select
          className="select"
          value={filters.action}
          onChange={(e) => apply({ action: e.target.value })}
        >
          <option value="">All actions</option>
          {actions.map((a) => (
            <option key={a} value={a}>
              {ACTION_LABELS[a] || a}
            </option>
          ))}
        </select>
        <select
          className="select"
          value={filters.outcome}
          onChange={(e) => apply({ outcome: e.target.value })}
        >
          <option value="">Any outcome</option>
          <option value="success">Success</option>
          <option value="failure">Failure</option>
        </select>
        <input
          className="input"
          style={{ maxWidth: 220 }}
          placeholder="Filter by email…"
          value={filters.actorEmail}
          onChange={(e) =>
            setFilters({ ...filters, actorEmail: e.target.value })
          }
          onKeyDown={(e) => {
            if (e.key === "Enter") apply({ actorEmail: filters.actorEmail });
          }}
        />
        <button
          className="btn sm"
          onClick={() => apply({ actorEmail: filters.actorEmail })}
        >
          Apply
        </button>
        <button
          className="btn sm"
          style={{ marginLeft: "auto" }}
          onClick={() => load(filters)}
        >
          Refresh
        </button>
      </div>

      {err && <div className="auth-msg err">{err}</div>}
      {!page && !err && <div className="empty is-loading">Loading…</div>}

      {page && (
        <>
          <div className="card" style={{ padding: 4 }}>
            <table className="admin-table">
              <thead>
                <tr>
                  <th>When</th>
                  <th>Action</th>
                  <th>Actor</th>
                  <th>Target</th>
                  <th>Details</th>
                </tr>
              </thead>
              <tbody>
                {page.logs.map((l) => (
                  <tr key={l.id}>
                    <td
                      style={{
                        whiteSpace: "nowrap",
                        fontSize: 11,
                        color: "var(--ink-3)",
                      }}
                    >
                      {new Date(l.createdAt).toLocaleString()}
                    </td>
                    <td>
                      <div style={{ fontWeight: 600 }}>
                        {ACTION_LABELS[l.action] || l.action}
                      </div>
                      {l.outcome === "failure" && <Tag tone="red">failure</Tag>}
                    </td>
                    <td>
                      <div>
                        {l.actorEmail || (
                          <span style={{ color: "var(--ink-3)" }}>—</span>
                        )}
                      </div>
                      {l.actorRole && (
                        <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
                          {ROLE_LABELS[l.actorRole] || l.actorRole}
                        </div>
                      )}
                    </td>
                    <td>
                      {l.targetLabel || (
                        <span style={{ color: "var(--ink-3)" }}>—</span>
                      )}
                    </td>
                    <td
                      style={{
                        fontSize: 11,
                        color: "var(--ink-3)",
                        maxWidth: 260,
                      }}
                    >
                      {l.metadata ? (
                        <code style={{ wordBreak: "break-word" }}>
                          {JSON.stringify(l.metadata)}
                        </code>
                      ) : null}
                      {l.ip ? <div>IP {l.ip}</div> : null}
                    </td>
                  </tr>
                ))}
                {page.logs.length === 0 && (
                  <tr>
                    <td colSpan={5}>
                      <div className="empty">No audit events match.</div>
                    </td>
                  </tr>
                )}
              </tbody>
            </table>
          </div>
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 10,
              marginTop: 10,
            }}
          >
            <span style={{ color: "var(--ink-3)", fontSize: 12 }}>
              {from}–{to} of {total}
            </span>
            <div style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
              <button
                className="btn sm"
                disabled={loading || filters.offset === 0}
                onClick={() => go(-PAGE_SIZE)}
              >
                Previous
              </button>
              <button
                className="btn sm"
                disabled={loading || to >= total}
                onClick={() => go(PAGE_SIZE)}
              >
                Next
              </button>
            </div>
          </div>
        </>
      )}
    </div>
  );
}

function StatCard({ label, value, tone }) {
  return (
    <div className="card" style={{ padding: 14 }}>
      <div
        style={{
          color: "var(--ink-3)",
          fontSize: 11,
          textTransform: "uppercase",
          letterSpacing: 0.5,
        }}
      >
        {label}
      </div>
      <div
        style={{
          fontSize: 24,
          fontWeight: 750,
          marginTop: 4,
          color: tone || "inherit",
        }}
      >
        {value}
      </div>
    </div>
  );
}

function fmtUptime(s) {
  if (s == null) return "—";
  const d = Math.floor(s / 86400),
    h = Math.floor((s % 86400) / 3600),
    m = Math.floor((s % 3600) / 60);
  if (d) return `${d}d ${h}h ${m}m`;
  if (h) return `${h}h ${m}m`;
  return `${m}m`;
}

function SystemTab() {
  const [sys, setSys] = useState(null);
  const [err, setErr] = useState("");
  const load = async () => {
    setErr("");
    try {
      setSys(await API.adminSystem());
    } catch (e) {
      setErr(e.message || "Failed to load system status.");
    }
  };
  useEffect(() => {
    load();
  }, []);

  return (
    <div>
      <div style={{ display: "flex", marginBottom: 12 }}>
        <button
          className="btn sm"
          style={{ marginLeft: "auto" }}
          onClick={load}
        >
          Refresh
        </button>
      </div>
      {err && <div className="auth-msg err">{err}</div>}
      {!sys && !err && <div className="empty is-loading">Loading…</div>}
      {sys && (
        <>
          <div style={{ marginBottom: 14 }}>
            <Tag tone={sys.status === "ok" ? "green" : "red"}>
              {sys.status === "ok" ? "All systems operational" : "Degraded"}
            </Tag>
            {!sys.database.ok && (
              <span style={{ color: "var(--red-ink)", marginLeft: 8 }}>
                Database: {sys.database.error}
              </span>
            )}
          </div>

          <div
            style={{
              display: "grid",
              gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
              gap: 12,
              marginBottom: 18,
            }}
          >
            <StatCard
              label="Active users"
              value={sys.stats ? sys.stats.users.active : "—"}
              tone="var(--green-ink)"
            />
            <StatCard
              label="Pending"
              value={sys.stats ? sys.stats.users.pending : "—"}
              tone="var(--yellow-ink)"
            />
            <StatCard
              label="Disabled"
              value={sys.stats ? sys.stats.users.disabled : "—"}
            />
            <StatCard
              label="Active sessions"
              value={sys.stats ? sys.stats.sessions.active : "—"}
            />
            <StatCard
              label="Audit events"
              value={sys.stats ? sys.stats.audit.total : "—"}
            />
            <StatCard
              label="Events (24h)"
              value={sys.stats ? sys.stats.audit.last24h : "—"}
            />
          </div>

          <div className="card" style={{ padding: 4 }}>
            <table className="admin-table">
              <tbody>
                <tr>
                  <td style={{ fontWeight: 600 }}>Status</td>
                  <td>{sys.status}</td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Environment</td>
                  <td>{sys.environment}</td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Version</td>
                  <td>{sys.version}</td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Node</td>
                  <td>{sys.node}</td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Uptime</td>
                  <td>{fmtUptime(sys.uptimeSeconds)}</td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Started</td>
                  <td>{new Date(sys.startedAt).toLocaleString()}</td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Memory</td>
                  <td>
                    {sys.memory.heapUsedMb} MB heap · {sys.memory.rssMb} MB RSS
                  </td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Database</td>
                  <td>
                    {sys.database.ok
                      ? "Connected"
                      : "Error: " + sys.database.error}
                  </td>
                </tr>
                <tr>
                  <td style={{ fontWeight: 600 }}>Dev login</td>
                  <td>{sys.devLoginEnabled ? "Enabled" : "Disabled"}</td>
                </tr>
                {sys.security && (
                  <tr>
                    <td style={{ fontWeight: 600 }}>Browser protections</td>
                    <td>
                      <div>
                        CSRF origin check:{" "}
                        {sys.security.csrfOriginEnforced ? "On" : "Off"} ·
                        Secure cookies:{" "}
                        {sys.security.secureCookies ? "On" : "Off"} · HSTS:{" "}
                        {sys.security.hsts ? "On" : "Off"} · Framing denied:{" "}
                        {sys.security.frameguard ? "On" : "Off"}
                      </div>
                      {(sys.security.warnings || []).map((w, i) => (
                        <div
                          key={i}
                          style={{ color: "var(--red-ink)", marginTop: 4 }}
                        >
                          {w}
                        </div>
                      ))}
                    </td>
                  </tr>
                )}
                <tr>
                  <td style={{ fontWeight: 600 }}>Server time</td>
                  <td>{new Date(sys.now).toLocaleString()}</td>
                </tr>
              </tbody>
            </table>
          </div>
        </>
      )}
    </div>
  );
}

function UnmappedUsersCard({ onReload, onToast }) {
  const [data, setData] = useState(null); // { live, refreshedAt, unmapped, unmappedUsers, unmappedAlert, threshold, mappableEntities, savedLinks }
  const [picks, setPicks] = useState({}); // userId -> selected entityId (unmapped rows)
  const [edits, setEdits] = useState({}); // userId -> selected entityId (saved-link re-point)
  const [savingId, setSavingId] = useState(null);
  const [busyId, setBusyId] = useState(null); // saved-link row currently updating/removing
  const [msg, setMsg] = useState("");
  const load = async () => {
    try {
      const d = await API.adminTimetracker();
      setData(d);
      setEdits({});
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to load Timetracker status.", "danger");
    }
  };
  useEffect(() => {
    load();
  }, []);

  const linkUser = async (userId) => {
    const entityId = picks[userId];
    if (!entityId) return;
    setSavingId(userId);
    setMsg("");
    try {
      const r = await API.adminMapTimetracker(userId, entityId);
      setPicks((p) => {
        const n = { ...p };
        delete n[userId];
        return n;
      });
      setMsg(
        r && r.status && r.status.live
          ? "Linked. The user has been re-attributed."
          : "Saved. It takes effect on the next Timetracker refresh.",
      );
      await load();
      if (onReload) await onReload();
      if (onToast) onToast("Staff link updated — exceptions re-routed");
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to link user.", "danger");
    } finally {
      setSavingId(null);
    }
  };

  const repointUser = async (userId) => {
    const entityId = edits[userId];
    if (!entityId) return;
    setBusyId(userId);
    setMsg("");
    try {
      const r = await API.adminMapTimetracker(userId, entityId);
      setMsg(
        r && r.status && r.status.live
          ? "Link updated. The user has been re-attributed."
          : "Link updated. It takes effect on the next Timetracker refresh.",
      );
      await load();
      if (onReload) await onReload();
      if (onToast) onToast("Staff link updated — exceptions re-routed");
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to update link.", "danger");
    } finally {
      setBusyId(null);
    }
  };

  const unlinkUser = async (userId) => {
    setBusyId(userId);
    setMsg("");
    try {
      const r = await API.adminUnmapTimetracker(userId);
      setMsg(
        r && r.status && r.status.live
          ? "Link removed. The user reverted to automatic attribution."
          : "Link removed. It takes effect on the next Timetracker refresh.",
      );
      await load();
      if (onReload) await onReload();
      if (onToast) onToast("Staff link removed — exceptions re-routed");
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to remove link.", "danger");
    } finally {
      setBusyId(null);
    }
  };
  if (!data) return null;

  const count = data.unmapped || 0;
  const users = data.unmappedUsers || [];
  const entities = data.mappableEntities || [];
  const saved = data.savedLinks || [];
  const tone = data.unmappedAlert ? "red" : count > 0 ? "yellow" : "green";
  const badgeLabel = !data.live
    ? "Source not live"
    : count === 0
      ? "All staff mapped"
      : count + " unmapped " + (count === 1 ? "user" : "users");

  return (
    <div className="card" style={{ padding: 14, marginBottom: 16 }}>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 10,
          flexWrap: "wrap",
        }}
      >
        <span style={{ fontWeight: 650 }}>Timetracker attribution</span>
        <Tag tone={tone}>{badgeLabel}</Tag>
        <button
          className="btn sm"
          style={{ marginLeft: "auto" }}
          onClick={load}
        >
          Refresh
        </button>
      </div>
      <div className="view-desc" style={{ marginTop: 8 }}>
        {!data.live
          ? "The live Timetracker source isn't configured, so no users are being attributed yet."
          : data.refreshedAt
            ? "Live users that couldn't be matched to the org chart, from the last refresh " +
              new Date(data.refreshedAt).toLocaleString() +
              ". " +
              "Their time isn't routing exceptions until linked."
            : "No refresh has completed yet."}
      </div>
      {msg && (
        <div className="auth-msg ok" style={{ marginTop: 10 }}>
          {msg}
        </div>
      )}
      {users.length > 0 && (
        <table className="admin-table" style={{ marginTop: 10 }}>
          <thead>
            <tr>
              <th>User ID</th>
              <th>Employee name</th>
              <th>Link to org chart</th>
            </tr>
          </thead>
          <tbody>
            {users.map((u) => (
              <tr key={u.userId}>
                <td style={{ fontVariantNumeric: "tabular-nums" }}>
                  {u.userId}
                </td>
                <td>
                  {u.employeeName || (
                    <span style={{ color: "var(--ink-3)" }}>—</span>
                  )}
                </td>
                <td>
                  <div
                    style={{ display: "flex", gap: 8, alignItems: "center" }}
                  >
                    <select
                      className="input sm"
                      value={picks[u.userId] || ""}
                      onChange={(e) =>
                        setPicks((p) =>
                          Object.assign({}, p, { [u.userId]: e.target.value }),
                        )
                      }
                      disabled={savingId === u.userId}
                    >
                      <option value="">Select employee…</option>
                      {entities.map((ent) => (
                        <option key={ent.id} value={ent.id}>
                          {ent.name}
                        </option>
                      ))}
                    </select>
                    <button
                      className="btn sm"
                      onClick={() => linkUser(u.userId)}
                      disabled={!picks[u.userId] || savingId === u.userId}
                    >
                      {savingId === u.userId ? "Linking…" : "Link"}
                    </button>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      {data.live && count > 0 && (
        <div
          className="view-desc"
          style={{
            marginTop: 10,
            fontSize: 11,
            color: "var(--ink-3)",
            fontStyle: "italic",
          }}
        >
          Linking a user persists the mapping for this workspace and takes
          effect on the next Timetracker refresh — no env config or restart
          needed.
        </div>
      )}
      {saved.length > 0 && (
        <div style={{ marginTop: 16 }}>
          <div style={{ fontWeight: 600, fontSize: 13 }}>
            Saved links{" "}
            <span style={{ color: "var(--ink-3)", fontWeight: 400 }}>
              ({saved.length})
            </span>
          </div>
          <div className="view-desc" style={{ marginTop: 4 }}>
            Links you've set for this workspace, and who set each one. Re-point
            a link if it went to the wrong person, or remove it to revert that
            user to automatic attribution.
          </div>
          <table className="admin-table" style={{ marginTop: 10 }}>
            <thead>
              <tr>
                <th>User ID</th>
                <th>Linked to</th>
                <th>Set by</th>
                <th>When</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {saved.map((m) => {
                const sel =
                  edits[m.userId] !== undefined ? edits[m.userId] : m.entityId;
                const changed = sel !== m.entityId;
                const rowBusy = busyId === m.userId;
                return (
                  <tr key={m.userId}>
                    <td style={{ fontVariantNumeric: "tabular-nums" }}>
                      {m.userId}
                    </td>
                    <td>
                      <select
                        className="input sm"
                        value={sel}
                        onChange={(e) =>
                          setEdits((p) =>
                            Object.assign({}, p, {
                              [m.userId]: e.target.value,
                            }),
                          )
                        }
                        disabled={rowBusy}
                      >
                        {!m.employeeName && (
                          <option value={m.entityId}>
                            {m.entityId} (unknown employee)
                          </option>
                        )}
                        {entities.map((ent) => (
                          <option key={ent.id} value={ent.id}>
                            {ent.name}
                          </option>
                        ))}
                      </select>
                    </td>
                    <td>
                      {m.updatedByName || (
                        <span style={{ color: "var(--ink-3)" }}>—</span>
                      )}
                    </td>
                    <td style={{ whiteSpace: "nowrap" }}>
                      {m.updatedAt
                        ? new Date(m.updatedAt).toLocaleString()
                        : "—"}
                    </td>
                    <td>
                      <div
                        style={{
                          display: "flex",
                          gap: 8,
                          alignItems: "center",
                        }}
                      >
                        <button
                          className="btn sm"
                          onClick={() => repointUser(m.userId)}
                          disabled={!changed || rowBusy}
                        >
                          {rowBusy ? "Saving…" : "Update"}
                        </button>
                        <button
                          className="btn sm danger"
                          onClick={() => unlinkUser(m.userId)}
                          disabled={rowBusy}
                        >
                          {rowBusy ? "Removing…" : "Remove"}
                        </button>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

/**
 * Samsara safety events whose recorded driver couldn't be matched to the org
 * chart (nicknames/initials the name-match can't resolve). These produce no
 * exception and reach no supervisor until linked — this card surfaces them so
 * none silently drop, and lets an owner link the Samsara driver to an employee
 * (DB-backed, no env edit). Sibling of UnmappedUsersCard for the driver side.
 */
function UnattributedDriversCard({ onReload, onToast }) {
  const [data, setData] = useState(null); // { drivers, mappableEntities, savedLinks }
  const [picks, setPicks] = useState({}); // driverId -> selected entityId (unattributed rows)
  const [driverEdits, setDriverEdits] = useState({}); // driverId -> selected entityId (saved-link re-point)
  const [savingId, setSavingId] = useState(null);
  const [busyId, setBusyId] = useState(null);
  const [msg, setMsg] = useState("");
  const load = async () => {
    try {
      const d = await API.adminSamsaraUnattributedDrivers();
      setData(d);
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to load Samsara driver status.", "danger");
    }
  };
  useEffect(() => {
    load();
  }, []);

  const linkDriver = async (driverId) => {
    const entityId = picks[driverId];
    if (!entityId) return;
    setSavingId(driverId);
    setMsg("");
    try {
      await API.adminMapSamsaraDriver(driverId, entityId);
      setPicks((p) => {
        const n = { ...p };
        delete n[driverId];
        return n;
      });
      setMsg("Linked. The driver's safety events have been re-attributed.");
      await load();
      if (onReload) await onReload();
      if (onToast) onToast("Driver link saved — safety events re-routed");
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to link driver.", "danger");
    } finally {
      setSavingId(null);
    }
  };

  const repointDriver = async (driverId) => {
    const entityId = driverEdits[driverId];
    if (!entityId) return;
    setBusyId(driverId);
    setMsg("");
    try {
      await API.adminMapSamsaraDriver(driverId, entityId);
      setDriverEdits((e) => {
        const n = { ...e };
        delete n[driverId];
        return n;
      });
      setMsg(
        "Link updated. The driver's safety events have been re-attributed.",
      );
      await load();
      if (onReload) await onReload();
      if (onToast) onToast("Driver link updated — safety events re-routed");
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to update link.", "danger");
    } finally {
      setBusyId(null);
    }
  };

  const unlinkDriver = async (driverId) => {
    setBusyId(driverId);
    setMsg("");
    try {
      await API.adminUnmapSamsaraDriver(driverId);
      setMsg("Link removed. The driver reverted to automatic attribution.");
      await load();
      if (onReload) await onReload();
      if (onToast) onToast("Driver link removed — safety events re-routed");
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to remove link.", "danger");
    } finally {
      setBusyId(null);
    }
  };
  if (!data) return null;

  const drivers = data.drivers || [];
  const entities = data.mappableEntities || [];
  const saved = data.savedLinks || [];
  const count = drivers.length;
  const tone = count > 0 ? "yellow" : "green";
  const badgeLabel =
    count === 0
      ? "All drivers attributed"
      : count + " unattributed " + (count === 1 ? "driver" : "drivers");

  return (
    <div className="card" style={{ padding: 14, marginBottom: 16 }}>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 10,
          flexWrap: "wrap",
        }}
      >
        <span style={{ fontWeight: 650 }}>Samsara driver attribution</span>
        <Tag tone={tone}>{badgeLabel}</Tag>
        <button
          className="btn sm"
          style={{ marginLeft: "auto" }}
          onClick={load}
        >
          Refresh
        </button>
      </div>
      <div className="view-desc" style={{ marginTop: 8 }}>
        {count === 0
          ? "Every recent safety event's driver resolved to an employee — nothing is being dropped."
          : "Safety events whose Samsara driver couldn't be matched to the org chart. These raise no exception and reach no supervisor until the driver is linked."}
      </div>
      {msg && (
        <div className="auth-msg ok" style={{ marginTop: 10 }}>
          {msg}
        </div>
      )}
      {drivers.length > 0 && (
        <table className="admin-table" style={{ marginTop: 10 }}>
          <thead>
            <tr>
              <th>Driver (per Samsara)</th>
              <th>Vehicle</th>
              <th>Behaviours</th>
              <th>Events</th>
              <th>Link to org chart</th>
            </tr>
          </thead>
          <tbody>
            {drivers.map((d, i) => {
              const linkable = !!d.driverId;
              return (
                <tr key={d.driverId || "none-" + i}>
                  <td>
                    {d.driverName || (
                      <span style={{ color: "var(--ink-3)" }}>
                        not recorded by Samsara
                      </span>
                    )}
                  </td>
                  <td>
                    {d.vehicleName || (
                      <span style={{ color: "var(--ink-3)" }}>—</span>
                    )}
                  </td>
                  <td style={{ fontSize: 12 }}>
                    {(d.behaviorLabels || []).join(", ") || "—"}
                  </td>
                  <td style={{ fontVariantNumeric: "tabular-nums" }}>
                    {d.eventCount}
                  </td>
                  <td>
                    {linkable ? (
                      <div
                        style={{
                          display: "flex",
                          gap: 8,
                          alignItems: "center",
                        }}
                      >
                        <select
                          className="input sm"
                          value={picks[d.driverId] || ""}
                          onChange={(e) =>
                            setPicks((p) =>
                              Object.assign({}, p, {
                                [d.driverId]: e.target.value,
                              }),
                            )
                          }
                          disabled={savingId === d.driverId}
                        >
                          <option value="">Select employee…</option>
                          {entities.map((ent) => (
                            <option key={ent.id} value={ent.id}>
                              {ent.name}
                            </option>
                          ))}
                        </select>
                        <button
                          className="btn sm"
                          onClick={() => linkDriver(d.driverId)}
                          disabled={
                            !picks[d.driverId] || savingId === d.driverId
                          }
                        >
                          {savingId === d.driverId ? "Linking…" : "Link"}
                        </button>
                      </div>
                    ) : (
                      <span style={{ color: "var(--ink-3)", fontSize: 12 }}>
                        No driver id — check Samsara for who drove
                      </span>
                    )}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      )}
      {count > 0 && (
        <div
          className="view-desc"
          style={{
            marginTop: 10,
            fontSize: 11,
            color: "var(--ink-3)",
            fontStyle: "italic",
          }}
        >
          Linking a driver persists the mapping for this workspace and
          re-attributes their stored safety events immediately — no env config
          or restart needed.
        </div>
      )}
      {saved.length > 0 && (
        <div style={{ marginTop: 16 }}>
          <div style={{ fontWeight: 600, fontSize: 13 }}>
            Saved links{" "}
            <span style={{ color: "var(--ink-3)", fontWeight: 400 }}>
              ({saved.length})
            </span>
          </div>
          <table className="admin-table" style={{ marginTop: 10 }}>
            <thead>
              <tr>
                <th>Driver ID</th>
                <th>Linked to</th>
                <th>Re-link to</th>
                <th>Set by</th>
                <th>When</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {saved.map((m) => {
                const rowBusy = busyId === m.driverId;
                const editVal = driverEdits[m.driverId] || "";
                const changed = editVal && editVal !== m.entityId;
                return (
                  <tr key={m.driverId}>
                    <td style={{ fontVariantNumeric: "tabular-nums" }}>
                      {m.driverId}
                    </td>
                    <td>
                      {m.employeeName || (
                        <span style={{ color: "var(--ink-3)" }}>
                          {m.entityId} (unknown employee)
                        </span>
                      )}
                    </td>
                    <td>
                      <PersonPicker
                        value={editVal}
                        disabled={rowBusy}
                        onChange={(v) =>
                          setDriverEdits((prev) => ({
                            ...prev,
                            [m.driverId]: v,
                          }))
                        }
                        placeholder="Pick a person…"
                      />
                    </td>
                    <td>
                      {m.updatedByName || (
                        <span style={{ color: "var(--ink-3)" }}>—</span>
                      )}
                    </td>
                    <td style={{ whiteSpace: "nowrap" }}>
                      {m.updatedAt
                        ? new Date(m.updatedAt).toLocaleString()
                        : "—"}
                    </td>
                    <td>
                      <div
                        style={{
                          display: "flex",
                          gap: 8,
                          alignItems: "center",
                        }}
                      >
                        <button
                          className="btn sm"
                          onClick={() => repointDriver(m.driverId)}
                          disabled={!changed || rowBusy}
                        >
                          {rowBusy ? "Saving…" : "Update"}
                        </button>
                        <button
                          className="btn sm danger"
                          onClick={() => unlinkDriver(m.driverId)}
                          disabled={rowBusy}
                        >
                          {rowBusy ? "Removing…" : "Remove"}
                        </button>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

/* Samsara vehicles that have events in the DB but no matching org-entity.
   Any exception they would raise is silently suppressed — this card surfaces
   them so operators can add them to the VEHICLE_ASSIGNMENTS map before real
   alerts are lost. Read-only; the fix is a server-side code/config change. */
function UnattributedVehiclesCard({ onToast }) {
  var [vehicles, setVehicles] = useState(null);
  var load = async function () {
    try {
      var r = await API.adminOrphanedVehicles();
      setVehicles(r.vehicles || []);
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to load unattributed vehicles.", "danger");
    }
  };
  useEffect(function () {
    load();
  }, []);

  var count = vehicles ? vehicles.length : 0;
  return (
    <div className="card" style={{ padding: 16, marginBottom: 14 }}>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 8,
          marginBottom: 8,
        }}
      >
        <div style={{ fontWeight: 700 }}>Unattributed vehicles</div>
        {vehicles && count > 0 && (
          <span className="tag red">{count} unmatched</span>
        )}
        {vehicles && count === 0 && (
          <span className="tag green">All matched</span>
        )}
        <button
          className="btn sm"
          style={{ marginLeft: "auto" }}
          onClick={load}
        >
          Refresh
        </button>
      </div>
      <div style={{ color: "var(--ink-3)", fontSize: 12, marginBottom: 8 }}>
        Samsara vehicles with recorded events but no matching org-entity. Every
        exception they would raise is suppressed until the vehicle is added to
        the VEHICLE_ASSIGNMENTS map in orgData.ts.
      </div>
      {!vehicles && <div className="empty is-loading">Loading…</div>}
      {vehicles && count === 0 && (
        <div style={{ color: "var(--ink-3)", fontSize: 13 }}>
          Every active vehicle is attributed — no suppressed events.
        </div>
      )}
      {vehicles && count > 0 && (
        <table className="admin-table">
          <thead>
            <tr>
              <th>Vehicle name</th>
              <th style={{ textAlign: "right" }}>Events</th>
              <th>Last seen</th>
            </tr>
          </thead>
          <tbody>
            {vehicles.map(function (v) {
              return (
                <tr key={v.vehicleName}>
                  <td style={{ fontWeight: 600 }}>{v.vehicleName}</td>
                  <td style={{ textAlign: "right" }}>{v.eventCount}</td>
                  <td style={{ color: "var(--ink-3)", fontSize: 12 }}>
                    {new Date(v.latestOccurredAt).toLocaleString()}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      )}
    </div>
  );
}

/* Fullbay scraper pull freshness — when each report last landed and whether
   the last attempt succeeded. Shown only when Fullbay runs live (the pull
   exists only in live mode). A stale/failing pull renders in warning colors so
   it cannot be confused with a quiet shop. */
function FullbayPullCard() {
  const [pull, setPull] = useState(null);
  const [loaded, setLoaded] = useState(false);
  useEffect(function () {
    let alive = true;
    API.sourceHealth()
      .then(function (r) {
        if (!alive) return;
        const fb = ((r && r.sources) || []).filter(function (s) {
          return s.source === "fullbay";
        })[0];
        setPull(fb && fb.pull ? fb.pull : null);
        setLoaded(true);
      })
      .catch(function () {
        if (alive) setLoaded(true);
      });
    return function () {
      alive = false;
    };
  }, []);

  if (!loaded || !pull) return null;

  const rows = [
    { label: "WIP Details", r: pull.wip },
    { label: "Employee Statistics", r: pull.employeeStats },
  ];
  const failing = pull.consecutiveFailures > 0;

  const statusBadge = function (r) {
    let text, color, bg;
    if (r.stale) {
      text = "OVERDUE";
      color = "var(--amber-ink, #9a6b00)";
      bg = "rgba(200,140,0,0.14)";
    } else if (r.lastStatus === null) {
      text = "NEVER PULLED";
      color = "var(--ink-3)";
      bg = "rgba(120,120,120,0.10)";
    } else if (r.lastStatus === "ok") {
      text = "OK";
      color = "var(--green-ink)";
      bg = "rgba(34,160,90,0.12)";
    } else {
      text = String(r.lastStatus).toUpperCase();
      color = "var(--red-ink, #b3261e)";
      bg = "rgba(200,50,40,0.12)";
    }
    return (
      <span
        style={{
          display: "inline-block",
          textTransform: "uppercase",
          fontSize: 11,
          fontWeight: 700,
          letterSpacing: 0.4,
          padding: "2px 8px",
          borderRadius: 999,
          color: color,
          background: bg,
        }}
      >
        {text}
      </span>
    );
  };

  return (
    <div className="card" style={{ padding: 12, marginBottom: 12 }}>
      <div
        style={{
          display: "flex",
          alignItems: "baseline",
          gap: 8,
          marginBottom: 6,
        }}
      >
        <div style={{ fontWeight: 650 }}>Fullbay pull status</div>
        <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
          {pull.scheduled
            ? "Scheduled pull is running"
            : "Scheduled pull is NOT running"}
        </div>
        {failing && (
          <span
            style={{
              marginLeft: "auto",
              fontSize: 12,
              fontWeight: 700,
              color: "var(--amber-ink, #9a6b00)",
            }}
          >
            ⚠ {pull.consecutiveFailures} failed attempt
            {pull.consecutiveFailures === 1 ? "" : "s"} in a row
          </span>
        )}
      </div>
      <table className="admin-table">
        <thead>
          <tr>
            <th>Report</th>
            <th>Last landed</th>
            <th style={{ textAlign: "right" }}>Last attempt</th>
          </tr>
        </thead>
        <tbody>
          {rows.map(function (row) {
            return (
              <tr key={row.label}>
                <td style={{ fontWeight: 600 }}>{row.label}</td>
                <td
                  style={
                    row.r.stale
                      ? { color: "var(--amber-ink, #9a6b00)", fontWeight: 650 }
                      : undefined
                  }
                >
                  {relTime(row.r.lastLandedAt)}
                  {row.r.stale ? " — overdue" : ""}
                </td>
                <td style={{ textAlign: "right" }}>
                  <span
                    style={{
                      color: "var(--ink-3)",
                      fontSize: 11,
                      marginRight: 8,
                    }}
                  >
                    {relTime(row.r.lastAttemptAt)}
                  </span>
                  {statusBadge(row.r)}
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
      <div
        className="view-desc"
        style={{ marginTop: 8, fontSize: 11, color: "var(--ink-3)" }}
      >
        These are the scraper&apos;s own pulls from Fullbay — separate from the
        mapping refresh above. If pulls stop, the board keeps showing the last
        snapshot even though nothing new is coming in.
      </div>
    </div>
  );
}

function SourcesTab({ onReload, onToast }) {
  const [sources, setSources] = useState(null);
  const load = async () => {
    try {
      const r = await API.adminSources();
      setSources(r.sources || []);
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to load data sources.", "danger");
    }
  };
  useEffect(() => {
    load();
  }, []);

  const modeStyle = (mode) => {
    if (mode === "live")
      return { color: "var(--green-ink)", bg: "rgba(34,160,90,0.12)" };
    if (mode === "mock")
      return { color: "var(--amber-ink, #9a6b00)", bg: "rgba(200,140,0,0.12)" };
    return { color: "var(--ink-3)", bg: "rgba(120,120,120,0.10)" };
  };
  const enabled = sources ? sources.filter((s) => s.mode !== "off") : [];
  const liveCount = sources
    ? sources.filter((s) => s.mode === "live").length
    : 0;
  const mockCount = sources
    ? sources.filter((s) => s.mode === "mock").length
    : 0;

  return (
    <div>
      <UnmappedUsersCard onReload={onReload} onToast={onToast} />
      <UnattributedDriversCard onReload={onReload} onToast={onToast} />
      <UnattributedVehiclesCard onToast={onToast} />
      <FullbayPullCard />
      <div
        style={{
          display: "flex",
          alignItems: "flex-start",
          marginBottom: 12,
          gap: 12,
        }}
      >
        <div className="view-desc" style={{ margin: 0 }}>
          The systems Flowline can ingest from, and the data mode each runs in.
          Only sources that are not off feed the pipeline — every off source is
          excluded from the ingest.
        </div>
        <button
          className="btn sm"
          style={{ marginLeft: "auto", flex: "0 0 auto" }}
          onClick={load}
        >
          Refresh
        </button>
      </div>
      {!sources && <div className="empty is-loading">Loading…</div>}
      {sources && (
        <div className="card" style={{ padding: 4 }}>
          <table className="admin-table">
            <thead>
              <tr>
                <th>Source</th>
                <th>Delivery</th>
                <th style={{ textAlign: "right" }}>Mode</th>
              </tr>
            </thead>
            <tbody>
              {sources.map((s) => {
                const st = modeStyle(s.mode);
                return (
                  <tr key={s.id}>
                    <td>
                      <div style={{ fontWeight: 650 }}>{s.label}</div>
                      <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
                        {s.id}
                      </div>
                    </td>
                    <td style={{ textTransform: "capitalize" }}>
                      {s.delivery}
                    </td>
                    <td style={{ textAlign: "right" }}>
                      <span
                        title="Configured on the server (read-only)"
                        style={{
                          display: "inline-block",
                          textTransform: "uppercase",
                          fontSize: 11,
                          fontWeight: 700,
                          letterSpacing: 0.4,
                          padding: "2px 8px",
                          borderRadius: 999,
                          color: st.color,
                          background: st.bg,
                        }}
                      >
                        {s.mode}
                      </span>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
      {sources && (
        <div className="view-desc" style={{ marginTop: 12 }}>
          {enabled.length === 0
            ? "Every source is off — nothing is feeding the pipeline."
            : `${enabled.length} of ${sources.length} sources are feeding the pipeline (${liveCount} live, ${mockCount} mock). The polling scheduler, startup seed, and webhook endpoint all reject every off source.`}
        </div>
      )}
      <div
        className="view-desc"
        style={{
          marginTop: 8,
          fontSize: 11,
          color: "var(--ink-3)",
          fontStyle: "italic",
        }}
      >
        Note: modes are configured on the server (read-only here). A mock source
        runs off bundled dummy data; a live source pulls from the real
        integration.
      </div>
    </div>
  );
}

function ITSuiteTab() {
  const [data, setData] = useState(null); // { available, reason, files, run }
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);

  const load = async () => {
    try {
      const r = await API.adminItSuite();
      setData(r);
      setErr("");
    } catch (e) {
      setErr(e.message || "Failed to load the IT suite.");
    }
  };
  useEffect(() => {
    load();
  }, []);

  const run = data && data.run ? data.run : null;
  const running = !!(run && run.status === "running");

  // Poll while a run is in flight; clean up the interval when it settles.
  useEffect(() => {
    if (!running) return;
    const id = setInterval(load, 2000);
    return () => clearInterval(id);
  }, [running]);

  const start = async () => {
    setBusy(true);
    setErr("");
    try {
      const r = await API.adminItSuiteRun();
      setData((d) => Object.assign({}, d || {}, { run: r.run }));
    } catch (e) {
      setErr(e.message || "Could not start the run.");
    } finally {
      setBusy(false);
    }
  };

  const available = !data || data.available !== false;
  const files = (data && data.files) || [];
  const tests = (run && run.tests) || [];
  const totals = (run && run.totals) || {
    total: 0,
    passed: 0,
    failed: 0,
    skipped: 0,
  };
  const failures = tests.filter((t) => !t.passed && !t.skipped);
  const testsForFile = (f) => tests.filter((t) => t.file === f);

  const fmtMs = (ms) =>
    typeof ms === "number"
      ? ms >= 1000
        ? (ms / 1000).toFixed(1) + "s"
        : Math.round(ms) + "ms"
      : "—";
  const fmtTime = (iso) => {
    if (!iso) return "—";
    try {
      return new Date(iso).toLocaleTimeString();
    } catch (e) {
      return iso;
    }
  };

  const statusPill = () => {
    if (!run || run.status === "idle")
      return <span className="tag neutral">Not run yet</span>;
    if (run.status === "running")
      return <span className="tag yellow">Running…</span>;
    if (run.status === "error") return <span className="tag red">Error</span>;
    return totals.failed > 0 ? (
      <span className="tag red">{totals.failed} failing</span>
    ) : (
      <span className="tag green">All passing</span>
    );
  };

  return (
    <div>
      <div
        style={{
          display: "flex",
          alignItems: "flex-start",
          marginBottom: 12,
          gap: 12,
        }}
      >
        <div className="view-desc" style={{ margin: 0 }}>
          The integration ("IT") suite boots the real app against a throwaway
          database to exercise ingest, exception rules, auth, and tenant
          isolation end-to-end. Run it on demand to confirm the backend is
          healthy — a full run takes roughly half a minute.
        </div>
        <div
          style={{
            marginLeft: "auto",
            display: "flex",
            gap: 8,
            flex: "0 0 auto",
          }}
        >
          <button className="btn sm" onClick={load} disabled={running}>
            Refresh
          </button>
          <button
            className="btn sm primary"
            onClick={start}
            disabled={busy || running || !available}
          >
            {running ? "Running…" : busy ? "Starting…" : "Run suite"}
          </button>
        </div>
      </div>

      {err && <div className="auth-msg err">{err}</div>}
      {data && !available && (
        <div
          className="auth-msg"
          style={{ background: "var(--surface-3)", color: "var(--ink-2)" }}
        >
          {data.reason ||
            "Running the suite is unavailable in this environment."}
        </div>
      )}
      {!data && !err && <div className="empty is-loading">Loading…</div>}

      {data && (
        <div className="card" style={{ padding: 14, marginBottom: 12 }}>
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              flexWrap: "wrap",
            }}
          >
            {statusPill()}
            <span className="tag neutral">{totals.total} tests</span>
            {totals.passed > 0 && (
              <span className="tag green">{totals.passed} passed</span>
            )}
            {totals.failed > 0 && (
              <span className="tag red">{totals.failed} failed</span>
            )}
            {totals.skipped > 0 && (
              <span className="tag yellow">{totals.skipped} skipped</span>
            )}
            <span
              style={{
                marginLeft: "auto",
                color: "var(--ink-3)",
                fontSize: 11,
              }}
            >
              {run && run.status !== "idle"
                ? (run.startedAt ? "Started " + fmtTime(run.startedAt) : "") +
                  (run.durationMs != null
                    ? " · took " + fmtMs(run.durationMs)
                    : running
                      ? " · running…"
                      : "")
                : "Never run in this session"}
            </span>
          </div>
        </div>
      )}

      {failures.length > 0 && (
        <div
          className="card"
          style={{ padding: 8, marginBottom: 12, borderColor: "var(--red)" }}
        >
          <div
            className="block-h"
            style={{ margin: "4px 6px 8px", color: "var(--red-ink)" }}
          >
            {failures.length} failing
          </div>
          {failures.map((t, i) => (
            <details key={i} style={{ padding: "4px 6px" }}>
              <summary
                style={{
                  cursor: "pointer",
                  color: "var(--red-ink)",
                  fontWeight: 600,
                }}
              >
                {t.name}
              </summary>
              <div
                style={{
                  color: "var(--ink-3)",
                  fontSize: 11,
                  margin: "2px 0 6px",
                }}
              >
                {t.file}
              </div>
              <div
                className="mono"
                style={{
                  fontSize: 11.5,
                  whiteSpace: "pre-wrap",
                  color: "var(--ink-2)",
                }}
              >
                {t.error || "Failed"}
              </div>
            </details>
          ))}
        </div>
      )}

      {data && (
        <div className="card" style={{ padding: 4 }}>
          <table className="admin-table">
            <thead>
              <tr>
                <th>Test file</th>
                <th style={{ textAlign: "right" }}>Tests</th>
                <th style={{ textAlign: "right" }}>Passed</th>
                <th style={{ textAlign: "right" }}>Failed</th>
                <th style={{ textAlign: "right" }}>Time</th>
              </tr>
            </thead>
            <tbody>
              {files.map((f) => {
                const ft = testsForFile(f.file);
                const fp = ft.filter((t) => t.passed && !t.skipped).length;
                const ff = ft.filter((t) => !t.passed && !t.skipped).length;
                const fdur = ft.reduce((a, t) => a + (t.durationMs || 0), 0);
                const hasRun = ft.length > 0;
                return (
                  <tr key={f.id}>
                    <td>
                      <div style={{ fontWeight: 650 }}>{f.name}</div>
                      <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
                        {f.file}
                      </div>
                    </td>
                    <td className="tnum" style={{ textAlign: "right" }}>
                      {hasRun ? ft.length : "—"}
                    </td>
                    <td
                      className="tnum"
                      style={{ textAlign: "right", color: "var(--green-ink)" }}
                    >
                      {hasRun ? fp : "—"}
                    </td>
                    <td
                      className="tnum"
                      style={{
                        textAlign: "right",
                        color: ff > 0 ? "var(--red-ink)" : "var(--ink-3)",
                      }}
                    >
                      {hasRun ? ff : "—"}
                    </td>
                    <td
                      className="tnum"
                      style={{ textAlign: "right", color: "var(--ink-3)" }}
                    >
                      {hasRun ? fmtMs(fdur) : "—"}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

/* Admin > Branches: edit the Samsara branch-code -> shop map behind the
   "By shop / branch" lens. The saved map replaces the built-in defaults, so
   removing a row genuinely removes that mapping. Display lens only —
   supervision, routing and escalation never read it. */
function BranchMappingTab({ onToast }) {
  const [data, setData] = useState(null);
  const [rows, setRows] = useState([]);
  const [saving, setSaving] = useState(false);

  const toRows = (map) => {
    const list = Object.keys(map).map((c) => ({ code: c, shopId: map[c] }));
    list.sort((a, b) => a.code.localeCompare(b.code));
    return list;
  };

  const load = async () => {
    try {
      const r = await API.adminBranchMapping();
      setData(r);
      setRows(toRows(r.map));
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to load branch mappings.", "danger");
    }
  };
  useEffect(() => {
    load();
  }, []);

  const patchRow = (i, field, value) =>
    setRows(
      rows.map((r, j) => {
        if (j !== i) return r;
        const next = { code: r.code, shopId: r.shopId };
        next[field] = value;
        return next;
      }),
    );
  const removeRow = (i) => setRows(rows.filter((r, j) => j !== i));
  const addRow = () =>
    setRows(
      rows.concat([
        {
          code: "",
          shopId: data && data.shops.length > 0 ? data.shops[0].id : "",
        },
      ]),
    );

  const applyResult = (res) => {
    setData({
      map: res.map,
      isDefault: res.isDefault,
      updatedAt: res.updatedAt,
      defaults: data ? data.defaults : {},
      shops: data ? data.shops : [],
    });
    setRows(toRows(res.map));
  };

  const save = async () => {
    const map = {};
    for (let i = 0; i < rows.length; i++) {
      const code = (rows[i].code || "").trim().toUpperCase();
      if (!code) {
        if (onToast) onToast("Every row needs a branch code.", "danger");
        return;
      }
      if (map[code] !== undefined) {
        if (onToast) onToast("Duplicate code: " + code, "danger");
        return;
      }
      if (!rows[i].shopId) {
        if (onToast) onToast("Pick a shop for " + code + ".", "danger");
        return;
      }
      map[code] = rows[i].shopId;
    }
    setSaving(true);
    try {
      const res = await API.adminUpdateBranchMapping({ map });
      applyResult(res);
      if (onToast) onToast("Branch mappings saved.");
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to save branch mappings.", "danger");
    } finally {
      setSaving(false);
    }
  };

  const reset = async () => {
    setSaving(true);
    try {
      const res = await API.adminUpdateBranchMapping({ reset: true });
      applyResult(res);
      if (onToast) onToast("Branch mappings reset to defaults.");
    } catch (e) {
      if (onToast)
        onToast(e.message || "Failed to reset branch mappings.", "danger");
    } finally {
      setSaving(false);
    }
  };

  return (
    <div>
      <div
        style={{
          display: "flex",
          alignItems: "flex-start",
          marginBottom: 12,
          gap: 12,
        }}
      >
        <div className="view-desc" style={{ margin: 0 }}>
          Which shop each Samsara branch code (truck tag or cost center) belongs
          to. Powers the By shop / branch view and truck branch labels only —
          supervision, routing and escalation are unchanged.
        </div>
        <button
          className="btn sm"
          style={{ marginLeft: "auto", flex: "0 0 auto" }}
          onClick={load}
          disabled={saving}
        >
          Reload
        </button>
      </div>
      {!data && <div className="empty is-loading">Loading…</div>}
      {data && (
        <div className="card" style={{ padding: 16, maxWidth: 640 }}>
          <div style={{ fontWeight: 700, marginBottom: 10 }}>
            Branch codes ({rows.length})
          </div>
          {data.isDefault && (
            <div
              className="view-desc"
              style={{ marginTop: 0, marginBottom: 10 }}
            >
              Using the built-in defaults — not yet customized for this
              organization.
            </div>
          )}
          {rows.map((r, i) => (
            <div
              key={i}
              style={{
                display: "flex",
                alignItems: "center",
                gap: 8,
                marginBottom: 8,
              }}
            >
              <input
                className="input"
                value={r.code}
                disabled={saving}
                placeholder="CODE"
                onChange={(e) => patchRow(i, "code", e.target.value)}
                style={{ width: 110, textTransform: "uppercase" }}
              />
              <span style={{ color: "var(--ink-3)" }}>→</span>
              <select
                className="select"
                value={r.shopId}
                disabled={saving}
                onChange={(e) => patchRow(i, "shopId", e.target.value)}
                style={{ flex: 1 }}
              >
                {data.shops.map((s) => (
                  <option key={s.id} value={s.id}>
                    {s.name}
                  </option>
                ))}
              </select>
              <button
                className="btn ghost sm"
                disabled={saving}
                onClick={() => removeRow(i)}
                title="Remove this mapping"
              >
                ✕
              </button>
            </div>
          ))}
          {rows.length === 0 && (
            <div className="view-desc">
              No mappings — every truck code will show as its own raw branch.
            </div>
          )}
          <div
            style={{ display: "flex", gap: 8, marginTop: 12, flexWrap: "wrap" }}
          >
            <button className="btn sm" onClick={addRow} disabled={saving}>
              + Add mapping
            </button>
            <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
              <button
                className="btn ghost sm"
                onClick={reset}
                disabled={saving}
              >
                Reset to defaults
              </button>
              <button
                className="btn primary sm"
                onClick={save}
                disabled={saving}
              >
                {saving ? "Saving…" : "Save mappings"}
              </button>
            </div>
          </div>
          <div className="view-desc" style={{ marginTop: 10 }}>
            Trucks pick up changes on their next Samsara pull (about 15
            minutes); the org tree and people groupings refresh right away.
          </div>
        </div>
      )}
    </div>
  );
}

/* Topbar notification bell — the signed-in user's in-app notifications
   (HR escalations and manager alerts). Self-contained: polls its own endpoint on the same
   30s cadence as the FL data cycle, independent of it. */
function NotificationsBell({ onOpenExc }) {
  var itemsS = useState([]);
  var setItems = itemsS[1];
  var items = itemsS[0];
  var openS = useState(false);
  var setOpen = openS[1];
  var open = openS[0];
  var busyS = useState(false);
  var setBusy = busyS[1];
  var busy = busyS[0];
  // id of a clicked row whose exception no longer exists — shows an inline
  // "gone" hint instead of opening the drawer.
  var goneS = useState(null);
  var setGoneId = goneS[1];
  var goneId = goneS[0];
  // Outcome summary for the "gone" row: null = loading, { outcome } = found,
  // { missing: true } = truly unknown id (degrade to the plain gone note).
  var outcomeS = useState(null);
  var setOutcome = outcomeS[1];
  var outcome = outcomeS[0];

  var load = function () {
    API.notifications()
      .then(function (r) {
        setItems(r.notifications || []);
      })
      .catch(function () {
        /* topbar accessory — stay quiet on transient failures */
      });
  };
  useEffect(function () {
    load();
    var t = setInterval(load, 30000);
    return function () {
      clearInterval(t);
    };
  }, []);

  var unread = items.filter(function (n) {
    return !n.readAt;
  }).length;

  var markAll = function () {
    if (busy) return;
    setBusy(true);
    API.notificationsRead({ all: true })
      .then(function () {
        load();
      })
      .catch(function () {})
      .then(function () {
        setBusy(false);
      });
  };

  // Clicking a row with an exceptionId marks it read and jumps to that
  // exception's detail drawer. If the exception is gone from the current
  // board (resolved + pruned, or out of the viewer's scope), degrade to an
  // inline note on the row instead of opening an empty drawer.
  var clickItem = function (n) {
    if (!n.exceptionId) return;
    if (!n.readAt) {
      API.notificationsRead({ ids: [n.id] })
        .then(function () {
          load();
        })
        .catch(function () {});
    }
    var FL = window.FL;
    var exists = FL && FL.excById && FL.excById(n.exceptionId);
    if (exists && onOpenExc) {
      setOpen(false);
      setGoneId(null);
      setOutcome(null);
      onOpenExc(n.exceptionId);
    } else {
      setGoneId(n.id);
      setOutcome(null);
      // The exception is off the board, but its resolution record usually
      // survives — fetch it so the row can say what happened instead of
      // dead-ending. A 404 (or any failure) degrades to the plain gone note.
      API.exceptionOutcome(n.exceptionId)
        .then(function (r) {
          setOutcome(
            r && r.outcome ? { outcome: r.outcome } : { missing: true },
          );
        })
        .catch(function () {
          setOutcome({ missing: true });
        });
    }
  };

  // One-line human summary of an outcome record.
  var outcomeText = function (o) {
    var verb =
      o.status === "resolved"
        ? "Resolved"
        : o.status === "snoozed"
          ? "Snoozed"
          : o.status === "acknowledged"
            ? "Acknowledged"
            : null;
    if (!verb) return null;
    var s = verb;
    if (o.actorName) s += " by " + o.actorName;
    if (o.at) s += " on " + new Date(o.at).toLocaleString();
    return s;
  };

  return (
    <div style={{ position: "relative" }}>
      <button
        className="btn ghost sm"
        title="Notifications"
        onClick={function () {
          setOpen(!open);
        }}
        style={{ position: "relative", paddingLeft: 8, paddingRight: 8 }}
      >
        <svg
          width="15"
          height="15"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
          style={{ display: "block" }}
        >
          <path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
          <path d="M13.7 21a2 2 0 0 1-3.4 0" />
        </svg>
        {unread > 0 && (
          <span
            style={{
              position: "absolute",
              top: -4,
              right: -4,
              background: "var(--sev-red, #d64545)",
              color: "#fff",
              borderRadius: 9,
              fontSize: 10,
              lineHeight: "14px",
              minWidth: 14,
              height: 14,
              padding: "0 3px",
              fontWeight: 700,
              textAlign: "center",
            }}
          >
            {unread > 99 ? "99+" : unread}
          </span>
        )}
      </button>
      {open && (
        <div
          style={{
            position: "absolute",
            right: 0,
            top: "calc(100% + 6px)",
            width: 340,
            maxHeight: 360,
            overflowY: "auto",
            background: "var(--surface-1, #fff)",
            border: "1px solid var(--line, #d8dde3)",
            borderRadius: 8,
            boxShadow: "0 8px 24px rgba(0,0,0,0.18)",
            zIndex: 60,
          }}
        >
          <div
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "space-between",
              padding: "8px 10px",
              borderBottom: "1px solid var(--line, #d8dde3)",
            }}
          >
            <span style={{ fontWeight: 700, fontSize: 12 }}>Notifications</span>
            {unread > 0 && (
              <button
                className="btn ghost sm"
                style={{ fontSize: 11 }}
                disabled={busy}
                onClick={markAll}
              >
                Mark all read
              </button>
            )}
          </div>
          {items.length === 0 && (
            <div style={{ padding: 14, fontSize: 12, color: "var(--ink-3)" }}>
              No notifications.
            </div>
          )}
          {items.map(function (n) {
            var clickable = !!n.exceptionId;
            return (
              <div
                key={n.id}
                role={clickable ? "button" : undefined}
                tabIndex={clickable ? 0 : undefined}
                title={clickable ? "Open exception" : undefined}
                onClick={
                  clickable
                    ? function () {
                        clickItem(n);
                      }
                    : undefined
                }
                onKeyDown={
                  clickable
                    ? function (e) {
                        if (e.key === "Enter" || e.key === " ") {
                          e.preventDefault();
                          clickItem(n);
                        }
                      }
                    : undefined
                }
                style={{
                  padding: "9px 10px",
                  borderBottom: "1px solid var(--line, #edf0f3)",
                  background: n.readAt
                    ? "transparent"
                    : "var(--surface-2, #f6f8fa)",
                  display: "flex",
                  gap: 8,
                  cursor: clickable ? "pointer" : "default",
                }}
              >
                <span
                  style={{
                    marginTop: 5,
                    width: 7,
                    height: 7,
                    borderRadius: 4,
                    flex: "0 0 auto",
                    background: n.readAt
                      ? "transparent"
                      : "var(--sev-red, #d64545)",
                  }}
                />
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontSize: 12, lineHeight: 1.35 }}>
                    {n.message}
                  </div>
                  <div
                    style={{
                      fontSize: 10.5,
                      color: "var(--ink-3)",
                      marginTop: 3,
                    }}
                  >
                    {new Date(n.createdAt).toLocaleString()}
                  </div>
                  {goneId === n.id &&
                    (function () {
                      var line =
                        outcome && outcome.outcome
                          ? outcomeText(outcome.outcome)
                          : null;
                      if (line) {
                        // Off the board, but we know what happened — show the
                        // resolution summary instead of a dead end.
                        var o = outcome.outcome;
                        return (
                          <div
                            style={{
                              fontSize: 10.5,
                              marginTop: 3,
                              padding: "5px 7px",
                              borderRadius: 6,
                              background: "var(--surface-2, #f2f5f7)",
                              border: "1px solid var(--line, #e2e6ea)",
                            }}
                          >
                            <div
                              style={{
                                fontWeight: 700,
                                color: "var(--sev-green, #2f8a4c)",
                              }}
                            >
                              {line}
                            </div>
                            {o.note && (
                              <div
                                style={{
                                  color: "var(--ink-2)",
                                  marginTop: 2,
                                  whiteSpace: "pre-wrap",
                                }}
                              >
                                \"{o.note}\"
                              </div>
                            )}
                            <div
                              style={{ color: "var(--ink-3)", marginTop: 2 }}
                            >
                              No longer on the board.
                            </div>
                          </div>
                        );
                      }
                      if (outcome === null) {
                        return (
                          <div
                            style={{
                              fontSize: 10.5,
                              color: "var(--ink-3)",
                              marginTop: 3,
                            }}
                          >
                            Checking what happened…
                          </div>
                        );
                      }
                      return (
                        <div
                          style={{
                            fontSize: 10.5,
                            color: "var(--sev-amber, #b8860b)",
                            marginTop: 3,
                          }}
                        >
                          This exception is no longer on the board — it may have
                          been resolved or cleared.
                        </div>
                      );
                    })()}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

/* Admin → Notifications: which shops escalate labor exceptions to the HR
   team. Owner-editable; a saved list replaces the boss-assigned defaults
   entirely (unchecking everything genuinely means "no shops flagged"). */
function HrEscalationCard({ onToast }) {
  var shopIdsS = useState(null);
  var setShopIds = shopIdsS[1];
  var shopIds = shopIdsS[0];
  var isDefaultS = useState(false);
  var setIsDefault = isDefaultS[1];
  var isDefault = isDefaultS[0];
  var savingS = useState(false);
  var setSaving = savingS[1];
  var saving = savingS[0];
  var dirtyS = useState(false);
  var setDirty = dirtyS[1];
  var dirty = dirtyS[0];
  var errorS = useState("");
  var setError = errorS[1];
  var error = errorS[0];

  useEffect(function () {
    API.adminSettings()
      .then(function (r) {
        var hr = r.hrEscalation || {};
        setShopIds(hr.shopIds || []);
        setIsDefault(!!hr.isDefault);
      })
      .catch(function (e) {
        setError(e.message || "Failed to load HR escalation settings.");
      });
  }, []);

  var FL = window.FL;
  var shops = (FL && FL.SHOPS ? FL.SHOPS : []).slice().sort(function (a, b) {
    return (a.name || a.id).localeCompare(b.name || b.id);
  });

  var toggle = function (id) {
    if (!shopIds) return;
    var next =
      shopIds.indexOf(id) > -1
        ? shopIds.filter(function (s) {
            return s !== id;
          })
        : shopIds.concat([id]);
    setShopIds(next);
    setDirty(true);
  };

  var save = function () {
    if (!shopIds) return;
    setSaving(true);
    API.adminUpdateHrEscalation(shopIds)
      .then(function (r) {
        var hr = r.hrEscalation || {};
        setShopIds(hr.shopIds || []);
        setIsDefault(!!hr.isDefault);
        setDirty(false);
        if (onToast) onToast("HR escalation shops saved");
      })
      .catch(function (e) {
        if (onToast)
          onToast(e.message || "Failed to save HR escalation shops.", "danger");
      })
      .then(function () {
        setSaving(false);
      });
  };

  return (
    <div className="card" style={{ marginTop: 14, padding: 14 }}>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 8,
          marginBottom: 4,
        }}
      >
        <div style={{ fontWeight: 700 }}>HR escalation</div>
        {isDefault && <Tag tone="yellow">Boss defaults</Tag>}
      </div>
      <div style={{ fontSize: 12, color: "var(--ink-3)", marginBottom: 10 }}>
        Labor exceptions (missed clock-out, no break taken) in the shops checked
        here escalate to the HR team once they reach Critical — after the shop
        watcher was alerted. HR users get an in-app notification (topbar bell),
        plus an email when opted in under Users &amp; access.
      </div>
      {error && <div className="auth-msg err">{error}</div>}
      {!shopIds && !error && (
        <div style={{ fontSize: 12, color: "var(--ink-3)" }}>Loading…</div>
      )}
      {shopIds && (
        <div
          style={{
            display: "grid",
            gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
            gap: "4px 14px",
            marginBottom: 12,
          }}
        >
          {shops.map(function (s) {
            return (
              <label
                key={s.id}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 7,
                  fontSize: 12.5,
                  cursor: "pointer",
                  padding: "3px 0",
                }}
              >
                <input
                  type="checkbox"
                  checked={shopIds.indexOf(s.id) > -1}
                  onChange={function () {
                    toggle(s.id);
                  }}
                />
                <span>{s.name || s.id}</span>
              </label>
            );
          })}
        </div>
      )}
      {shopIds && (
        <button
          className="btn primary sm"
          disabled={saving || !dirty}
          onClick={save}
        >
          {saving ? "Saving\u2026" : "Save"}
        </button>
      )}
    </div>
  );
}

function NotificationsTab({ onToast }) {
  var _this = this;
  var data = useState(null);
  var setData = data[1];
  data = data[0];
  var smsEnabledS = useState(false);
  var setSmsEnabled = smsEnabledS[1];
  var smsEnabled = smsEnabledS[0];
  var savingS = useState(false);
  var setSaving = savingS[1];
  var saving = savingS[0];
  var savedS = useState(false);
  var setSaved = savedS[1];
  var saved = savedS[0];
  var phoneInputS = useState("");
  var setPhoneInput = phoneInputS[1];
  var phoneInput = phoneInputS[0];
  var phoneSavingS = useState(false);
  var setPhoneSaving = phoneSavingS[1];
  var phoneSaving = phoneSavingS[0];
  var phoneSavedS = useState(false);
  var setPhoneSaved = phoneSavedS[1];
  var phoneSaved = phoneSavedS[0];
  var testBusyS = useState(false);
  var setTestBusy = testBusyS[1];
  var testBusy = testBusyS[0];
  var testMsgS = useState("");
  var setTestMsg = testMsgS[1];
  var testMsg = testMsgS[0];
  var thresholdInputS = useState("12");
  var setThresholdInput = thresholdInputS[1];
  var thresholdInput = thresholdInputS[0];
  var thresholdSavingS = useState(false);
  var setThresholdSaving = thresholdSavingS[1];
  var thresholdSaving = thresholdSavingS[0];
  var thresholdSavedS = useState(false);
  var setThresholdSaved = thresholdSavedS[1];
  var thresholdSaved = thresholdSavedS[0];

  var load = function () {
    setSaved(false);
    setPhoneSaved(false);
    setThresholdSaved(false);
    API.adminNotifications()
      .then(function (r) {
        setData(r);
        setSmsEnabled(!!r.smsEnabled);
        setPhoneInput(r.ownerPhone || "");
        setThresholdInput(
          String(r.smsSafetyThreshold != null ? r.smsSafetyThreshold : 12),
        );
      })
      .catch(function (e) {
        if (onToast)
          onToast(
            e.message || "Failed to load notification settings.",
            "danger",
          );
      });
  };
  useEffect(function () {
    load();
  }, []);

  var toggle = function (next) {
    setSaved(false);
    setSaving(true);
    API.adminUpdateNotifications({ smsEnabled: next })
      .then(function (r) {
        setData(r);
        setSmsEnabled(!!r.smsEnabled);
        setSaved(true);
        if (onToast)
          onToast(
            next ? "SMS notifications enabled" : "SMS notifications disabled",
          );
      })
      .catch(function (e) {
        if (onToast)
          onToast(e.message || "Failed to update SMS setting.", "danger");
      })
      .then(function () {
        setSaving(false);
      });
  };

  var savePhone = function () {
    setPhoneSaved(false);
    setPhoneSaving(true);
    var phone = phoneInput.trim() || null;
    API.adminUpdateNotifications({ ownerPhone: phone })
      .then(function (r) {
        setData(r);
        setPhoneInput(r.ownerPhone || "");
        setPhoneSaved(true);
        if (onToast)
          onToast(phone ? "Phone number saved" : "Phone number cleared");
      })
      .catch(function (e) {
        if (onToast) onToast(e.message || "Failed to save phone.", "danger");
      })
      .then(function () {
        setPhoneSaving(false);
      });
  };

  var saveThreshold = function () {
    var val = parseInt(thresholdInput.trim(), 10);
    if (!Number.isInteger(val) || val < 0 || val > 1000) {
      if (onToast) onToast("Enter a whole number from 0 to 1000.", "danger");
      return;
    }
    setThresholdSaved(false);
    setThresholdSaving(true);
    API.adminUpdateNotifications({ smsSafetyThreshold: val })
      .then(function (r) {
        setData(r);
        setThresholdInput(
          String(r.smsSafetyThreshold != null ? r.smsSafetyThreshold : 12),
        );
        setThresholdSaved(true);
        if (onToast) onToast("Safety SMS threshold saved");
      })
      .catch(function (e) {
        if (onToast)
          onToast(e.message || "Failed to save threshold.", "danger");
      })
      .then(function () {
        setThresholdSaving(false);
      });
  };

  var sendTest = function () {
    setTestMsg("");
    setTestBusy(true);
    API.adminTestSms()
      .then(function (r) {
        setTestMsg("Test SMS sent to " + r.maskedTo + ".");
        if (onToast) onToast("Test SMS sent to " + r.maskedTo);
      })
      .catch(function (e) {
        setTestMsg(e.message || "Test SMS failed.");
        if (onToast) onToast(e.message || "Test SMS failed.", "danger");
      })
      .then(function () {
        setTestBusy(false);
      });
  };

  var configured = data && data.twilioConfigured;
  var dryRunOn = data && data.dryRunEnabled;
  var hasPhone = data && !!data.ownerPhone;
  var phoneInputOk = /^\+[1-9]\d{7,14}$/.test((phoneInput || "").trim());

  return React.createElement(
    "div",
    null,
    React.createElement(
      "div",
      {
        style: {
          display: "flex",
          alignItems: "flex-start",
          marginBottom: 12,
          gap: 12,
        },
      },
      React.createElement(
        "div",
        { className: "view-desc", style: { margin: 0 } },
        "Control how Flowline notifies people: agent SMS messages and manager alert emails. Twilio and SendGrid credentials are configured on the server.",
      ),
      React.createElement(
        "button",
        {
          className: "btn sm",
          style: { marginLeft: "auto", flex: "0 0 auto" },
          onClick: load,
        },
        "Refresh",
      ),
    ),

    !data &&
      React.createElement("div", { className: "empty" }, "Loading\u2026"),

    data &&
      React.createElement(
        React.Fragment,
        null,
        React.createElement(
          "div",
          {
            className: "card",
            style: { padding: 16, maxWidth: 540, marginBottom: 14 },
          },
          React.createElement(
            "div",
            { style: { fontWeight: 700, marginBottom: 10 } },
            "Twilio configuration",
          ),
          React.createElement(
            "div",
            {
              className: "card",
              style: {
                padding: 10,
                background: "var(--surface-2)",
                marginBottom: 12,
              },
            },
            React.createElement(
              "table",
              { style: { width: "100%", borderCollapse: "collapse" } },
              React.createElement(
                "tbody",
                null,
                React.createElement(
                  "tr",
                  null,
                  React.createElement(
                    "td",
                    {
                      style: { fontWeight: 600, paddingBottom: 6, width: 140 },
                    },
                    "Status",
                  ),
                  React.createElement(
                    "td",
                    null,
                    configured
                      ? React.createElement(
                          Tag,
                          { tone: "green" },
                          "Configured",
                        )
                      : React.createElement(
                          Tag,
                          { tone: "red" },
                          "Not configured",
                        ),
                  ),
                ),
                React.createElement(
                  "tr",
                  null,
                  React.createElement(
                    "td",
                    { style: { fontWeight: 600, paddingBottom: 6 } },
                    "Account SID",
                  ),
                  React.createElement(
                    "td",
                    { style: { fontFamily: "monospace", fontSize: 12 } },
                    data.maskedSid ||
                      React.createElement(
                        "span",
                        { style: { color: "var(--ink-3)" } },
                        "\u2014",
                      ),
                  ),
                ),
                React.createElement(
                  "tr",
                  null,
                  React.createElement(
                    "td",
                    { style: { fontWeight: 600 } },
                    "From number",
                  ),
                  React.createElement(
                    "td",
                    { style: { fontFamily: "monospace", fontSize: 12 } },
                    data.fromNumber ||
                      React.createElement(
                        "span",
                        { style: { color: "var(--ink-3)" } },
                        "\u2014",
                      ),
                  ),
                ),
                dryRunOn &&
                  React.createElement(
                    "tr",
                    null,
                    React.createElement(
                      "td",
                      { colSpan: 2, style: { paddingTop: 8 } },
                      React.createElement(
                        Tag,
                        { tone: "yellow" },
                        "AGENT_DRY_RUN=true",
                      ),
                      React.createElement(
                        "span",
                        {
                          style: {
                            color: "var(--ink-3)",
                            fontSize: 11,
                            marginLeft: 8,
                          },
                        },
                        "Server-level dry-run overrides SMS on/off \u2014 no live sends.",
                      ),
                    ),
                  ),
              ),
            ),
          ),

          React.createElement(
            "div",
            {
              style: {
                display: "flex",
                alignItems: "center",
                gap: 14,
                marginBottom: 8,
              },
            },
            React.createElement(
              "div",
              { style: { flex: 1 } },
              React.createElement(
                "div",
                { style: { fontWeight: 600 } },
                "SMS notifications",
              ),
              React.createElement(
                "div",
                { style: { color: "var(--ink-3)", fontSize: 11 } },
                "When on, the agent sends real SMS messages. When off, actions are logged only.",
              ),
            ),
            React.createElement(
              "button",
              {
                className: "btn sm" + (smsEnabled ? " primary" : ""),
                disabled: saving || !configured,
                style: { minWidth: 64 },
                onClick: function () {
                  toggle(!smsEnabled);
                },
                title: !configured
                  ? "Configure Twilio credentials on the server first"
                  : undefined,
              },
              saving ? "\u2026" : smsEnabled ? "On" : "Off",
            ),
          ),
          saved &&
            React.createElement(
              "div",
              { style: { color: "var(--green-ink)", fontSize: 12 } },
              "Saved \u2014 applied immediately.",
            ),
          !configured &&
            React.createElement(
              "div",
              { style: { color: "var(--ink-3)", fontSize: 12, marginTop: 4 } },
              "Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_FROM_NUMBER on the server to enable.",
            ),
        ),

        React.createElement(
          "div",
          {
            className: "card",
            style: { padding: 16, maxWidth: 540, marginBottom: 14 },
          },
          React.createElement(
            "div",
            { style: { fontWeight: 700, marginBottom: 4 } },
            "Your phone number",
          ),
          React.createElement(
            "div",
            {
              style: { color: "var(--ink-3)", fontSize: 12, marginBottom: 12 },
            },
            "Used to receive test SMS messages. Must be in E.164 format (e.g. +15551234567).",
          ),
          React.createElement(
            "div",
            {
              style: {
                display: "flex",
                gap: 8,
                alignItems: "center",
                marginBottom: 6,
              },
            },
            React.createElement("input", {
              className: "input",
              style: { flex: 1 },
              placeholder: "+15551234567",
              value: phoneInput,
              onChange: function (e) {
                setPhoneInput(e.target.value);
                setPhoneSaved(false);
              },
              disabled: phoneSaving,
            }),
            React.createElement(
              "button",
              {
                className: "btn sm",
                disabled:
                  phoneSaving ||
                  (phoneInput.trim() === (data.ownerPhone || "") &&
                    !phoneSaved) ||
                  (phoneInput.trim() && !phoneInputOk),
                onClick: savePhone,
                title:
                  phoneInput.trim() && !phoneInputOk
                    ? "Use E.164 format: +15551234567"
                    : undefined,
              },
              phoneSaving ? "\u2026" : "Save",
            ),
          ),
          phoneSaved &&
            React.createElement(
              "div",
              { style: { color: "var(--green-ink)", fontSize: 12 } },
              "Phone number saved.",
            ),
          phoneInput.trim() &&
            !phoneInputOk &&
            React.createElement(
              "div",
              { style: { color: "var(--red-ink)", fontSize: 12 } },
              "Must be E.164 format (e.g. +15551234567).",
            ),
        ),

        React.createElement(
          "div",
          { className: "card", style: { padding: 16, maxWidth: 540 } },
          React.createElement(
            "div",
            { style: { fontWeight: 700, marginBottom: 4 } },
            "Send a test SMS",
          ),
          React.createElement(
            "div",
            {
              style: { color: "var(--ink-3)", fontSize: 12, marginBottom: 12 },
            },
            "Sends a single test message to your saved phone number to confirm Twilio is working. SMS must be on and a phone number must be saved.",
          ),
          React.createElement(
            "div",
            {
              style: {
                display: "flex",
                gap: 8,
                alignItems: "center",
                marginBottom: 8,
              },
            },
            React.createElement(
              "div",
              {
                style: {
                  flex: 1,
                  fontSize: 13,
                  color: hasPhone ? "var(--ink-1)" : "var(--ink-3)",
                },
              },
              hasPhone
                ? "Sending to: " + data.maskedPhone
                : "No phone number saved.",
            ),
            React.createElement(
              "button",
              {
                className: "btn primary sm",
                disabled: testBusy || !smsEnabled || !hasPhone || dryRunOn,
                onClick: sendTest,
                title: !smsEnabled
                  ? "Enable SMS first"
                  : dryRunOn
                    ? "AGENT_DRY_RUN is true"
                    : !hasPhone
                      ? "Save your phone number above first"
                      : undefined,
              },
              testBusy ? "Sending\u2026" : "Send test",
            ),
          ),
          testMsg &&
            React.createElement(
              "div",
              {
                className:
                  "auth-msg" + (testMsg.indexOf("sent") > -1 ? " ok" : " err"),
              },
              testMsg,
            ),
        ),

        React.createElement(
          "div",
          {
            className: "card",
            style: { padding: 16, maxWidth: 540, marginBottom: 14 },
          },
          React.createElement(
            "div",
            { style: { fontWeight: 700, marginBottom: 4 } },
            "Safety SMS threshold",
          ),
          React.createElement(
            "div",
            {
              style: { color: "var(--ink-3)", fontSize: 12, marginBottom: 12 },
            },
            "Minimum weighted safety score before medium / low incidents text the watching manager. High and critical events always send immediately. Raise this to require more incidents before a text fires. Default: 12.",
          ),
          React.createElement(
            "div",
            {
              style: {
                display: "flex",
                gap: 8,
                alignItems: "center",
                marginBottom: 6,
              },
            },
            React.createElement("input", {
              className: "input",
              style: { width: 80 },
              type: "number",
              min: "0",
              max: "1000",
              step: "1",
              placeholder: "12",
              value: thresholdInput,
              onChange: function (e) {
                setThresholdInput(e.target.value);
                setThresholdSaved(false);
              },
              disabled: thresholdSaving,
            }),
            React.createElement(
              "span",
              { style: { fontSize: 12, color: "var(--ink-3)" } },
              "weighted score (0\u2013100)",
            ),
            React.createElement(
              "button",
              {
                className: "btn sm",
                disabled:
                  thresholdSaving ||
                  !/^\d+$/.test(thresholdInput.trim()) ||
                  parseInt(thresholdInput.trim(), 10) > 1000,
                onClick: saveThreshold,
              },
              thresholdSaving ? "\u2026" : "Save",
            ),
          ),
          thresholdSaved &&
            React.createElement(
              "div",
              {
                style: {
                  color: "var(--green-ink)",
                  fontSize: 12,
                  marginTop: 2,
                },
              },
              "Saved.",
            ),
        ),

        React.createElement(EmailNotificationsCard, {
          data: data,
          onData: setData,
          onToast: onToast,
        }),

        React.createElement(HrEscalationCard, { onToast: onToast }),
      ),
  );
}

/* Email notifications card — manager alert emails: SendGrid status, the
   org-level on/off toggle, a test send to the signed-in owner, and the roster
   of alert-eligible people with their per-person email opt-ins. */
function EmailNotificationsCard({ data, onData, onToast }) {
  const [saving, setSaving] = useState(false);
  const [testBusy, setTestBusy] = useState(false);
  const [testMsg, setTestMsg] = useState("");
  const [roster, setRoster] = useState(null);
  const [rosterErr, setRosterErr] = useState("");
  const [busyUserId, setBusyUserId] = useState(null);

  const emailEnabled = !!data.emailEnabled;
  const configured = !!data.sendgridConfigured;
  const mode = data.emailMode || "off";

  const ROSTER_ROLE_ORDER = {
    admin: 0,
    manager: 1,
  };
  const loadRoster = () => {
    setRosterErr("");
    API.adminUsers()
      .then((r) => {
        const eligible = (r.users || []).filter(function (u) {
          return (
            u.status === "active" && ROSTER_ROLE_ORDER[u.role] !== undefined
          );
        });
        eligible.sort(function (a, b) {
          const ra = ROSTER_ROLE_ORDER[a.role];
          const rb = ROSTER_ROLE_ORDER[b.role];
          if (ra !== rb) return ra - rb;
          return (a.name || a.email).localeCompare(b.name || b.email);
        });
        setRoster(eligible);
      })
      .catch(function (e) {
        setRosterErr(e.message || "Failed to load users.");
      });
  };
  useEffect(loadRoster, []);

  // Keep roster in sync when UsersTab (or any other panel) toggles a user's
  // email opt-in so both views stay consistent without a full page reload.
  useEffect(function () {
    var handler = function (e) {
      var detail = e.detail || {};
      setRoster(function (cur) {
        return (cur || []).map(function (x) {
          return x.id === detail.id
            ? Object.assign({}, x, {
                emailAlertsEnabled: detail.emailAlertsEnabled,
              })
            : x;
        });
      });
    };
    window.addEventListener("fl:userOptInChanged", handler);
    return function () {
      window.removeEventListener("fl:userOptInChanged", handler);
    };
  }, []);

  const toggle = (next) => {
    setSaving(true);
    API.adminUpdateNotifications({ emailEnabled: next })
      .then(function (r) {
        onData(r);
        if (onToast)
          onToast(
            next
              ? "Email notifications enabled"
              : "Email notifications disabled",
          );
      })
      .catch(function (e) {
        if (onToast)
          onToast(e.message || "Failed to update email setting.", "danger");
      })
      .then(function () {
        setSaving(false);
      });
  };

  const sendTest = () => {
    setTestMsg("");
    setTestBusy(true);
    API.adminTestEmail()
      .then(function (r) {
        setTestMsg("Test email sent to " + r.maskedTo + ".");
        if (onToast) onToast("Test email sent to " + r.maskedTo);
      })
      .catch(function (e) {
        setTestMsg(e.message || "Test email failed.");
        if (onToast) onToast(e.message || "Test email failed.", "danger");
      })
      .then(function () {
        setTestBusy(false);
      });
  };

  const flipUser = (u) => {
    const next = !u.emailAlertsEnabled;
    setBusyUserId(u.id);
    API.adminSetUserNotifications(u.id, next)
      .then(function (r) {
        setRoster(function (cur) {
          return (cur || []).map(function (x) {
            return x.id === u.id && r.user
              ? Object.assign({}, x, {
                  emailAlertsEnabled: r.user.emailAlertsEnabled,
                })
              : x;
          });
        });
        // Sync UsersTab (and any other panel) so they reflect the change
        // without a reload.
        window.dispatchEvent(
          new CustomEvent("fl:userOptInChanged", {
            detail: { id: u.id, emailAlertsEnabled: next },
          }),
        );
        if (onToast)
          onToast(
            (u.name || u.email) +
              (next
                ? " will now receive alert emails"
                : " will no longer receive alert emails"),
          );
      })
      .catch(function (e) {
        if (onToast) onToast(e.message || "Failed to update opt-in.", "danger");
      })
      .then(function () {
        setBusyUserId(null);
      });
  };

  const modeHint =
    mode === "live"
      ? "Live — alert emails go to opted-in people below."
      : mode === "test"
        ? "Test — every alert is redirected to the server's test inbox."
        : "Off at the server level (MANAGER_ALERT_MODE) — no alert emails send.";

  return (
    <div className="card" style={{ padding: 16, maxWidth: 540, marginTop: 14 }}>
      <div style={{ fontWeight: 700, marginBottom: 10 }}>
        Email notifications
      </div>
      <div
        className="card"
        style={{
          padding: 10,
          background: "var(--surface-2)",
          marginBottom: 12,
        }}
      >
        <table style={{ width: "100%", borderCollapse: "collapse" }}>
          <tbody>
            <tr>
              <td style={{ fontWeight: 600, paddingBottom: 6, width: 140 }}>
                Email service
              </td>
              <td>
                {configured ? (
                  <Tag tone="green">SendGrid connected</Tag>
                ) : (
                  <Tag tone="red">SendGrid not connected</Tag>
                )}
              </td>
            </tr>
            <tr>
              <td style={{ fontWeight: 600, paddingBottom: 6 }}>
                From address
              </td>
              <td style={{ fontFamily: "monospace", fontSize: 12 }}>
                {data.emailFrom || (
                  <span style={{ color: "var(--ink-3)" }}>—</span>
                )}
              </td>
            </tr>
            <tr>
              <td style={{ fontWeight: 600 }}>Delivery mode</td>
              <td>
                <Tag tone={mode === "live" ? "green" : "yellow"}>{mode}</Tag>
                <span
                  style={{
                    color: "var(--ink-3)",
                    fontSize: 11,
                    marginLeft: 8,
                  }}
                >
                  {modeHint}
                </span>
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 14,
          marginBottom: 8,
        }}
      >
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 600 }}>Manager alert emails</div>
          <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
            When off, no alert emails are sent for this organization — even in
            live mode. Intended alerts are still logged.
          </div>
        </div>
        <button
          className={"btn sm" + (emailEnabled ? " primary" : "")}
          disabled={saving}
          style={{ minWidth: 64 }}
          onClick={() => toggle(!emailEnabled)}
        >
          {saving ? "…" : emailEnabled ? "On" : "Off"}
        </button>
      </div>

      <div
        style={{
          display: "flex",
          gap: 8,
          alignItems: "center",
          paddingTop: 10,
          borderTop: "1px solid var(--line)",
          marginBottom: 8,
        }}
      >
        <div style={{ flex: 1, fontSize: 13 }}>
          <div style={{ fontWeight: 600 }}>Send a test email</div>
          <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
            {"Delivers one test message to " +
              (data.ownerEmail || "your account address") +
              " to confirm SendGrid is working."}
          </div>
        </div>
        <button
          className="btn primary sm"
          disabled={testBusy || !configured}
          title={
            !configured ? "Connect SendGrid on the server first" : undefined
          }
          onClick={sendTest}
        >
          {testBusy ? "Sending…" : "Send test"}
        </button>
      </div>
      {testMsg && (
        <div
          className={
            "auth-msg" + (testMsg.indexOf("sent") > -1 ? " ok" : " err")
          }
        >
          {testMsg}
        </div>
      )}

      <div
        style={{
          paddingTop: 10,
          borderTop: "1px solid var(--line)",
          marginTop: 10,
        }}
      >
        <div style={{ fontWeight: 600, marginBottom: 2 }}>
          Who receives alert emails
        </div>
        <div style={{ color: "var(--ink-3)", fontSize: 11, marginBottom: 6 }}>
          Alert-eligible people (owner, regional and shop managers). Each person
          must also be opted in here to receive live alerts.
        </div>
        {rosterErr && (
          <div style={{ color: "var(--red-ink)", fontSize: 12 }}>
            {rosterErr}
          </div>
        )}
        {!roster && !rosterErr && (
          <div className="empty is-loading">Loading…</div>
        )}
        {roster && roster.length === 0 && (
          <div className="empty">No alert-eligible users.</div>
        )}
        {roster &&
          roster.map((u) => (
            <div
              key={u.id}
              style={{
                display: "flex",
                alignItems: "center",
                gap: 10,
                padding: "6px 0",
                borderTop: "1px solid var(--line)",
              }}
            >
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontWeight: 600, fontSize: 13 }}>
                  {u.name || u.email}
                </div>
                <div
                  style={{
                    color: "var(--ink-3)",
                    fontSize: 11,
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                  }}
                >
                  {(ROLE_LABELS[u.role] || u.role) + " · " + u.email}
                </div>
              </div>
              <button
                className={"btn sm" + (u.emailAlertsEnabled ? " primary" : "")}
                disabled={busyUserId === u.id}
                style={{ minWidth: 64 }}
                onClick={() => flipUser(u)}
              >
                {busyUserId === u.id
                  ? "…"
                  : u.emailAlertsEnabled
                    ? "On"
                    : "Off"}
              </button>
            </div>
          ))}
      </div>
    </div>
  );
}

/* ============================================================
   OrgTab — manage the shop / department hierarchy at runtime
   ============================================================ */

/**
 * Build a nested tree from a flat list of entities. Each node gets a
 * `children` array of direct descendants; top-level nodes have no parent
 * among the set (or parentId === null).
 */
function buildOrgTree(entities) {
  var byId = {};
  entities.forEach(function (e) {
    byId[e.id] = Object.assign({}, e, { children: [] });
  });
  var roots = [];
  entities.forEach(function (e) {
    if (e.parentId && byId[e.parentId]) {
      byId[e.parentId].children.push(byId[e.id]);
    } else {
      roots.push(byId[e.id]);
    }
  });
  // Sort: departments before shops, then alphabetically.
  function sortNodes(nodes) {
    nodes.sort(function (a, b) {
      if (a.type !== b.type) return a.type === "department" ? -1 : 1;
      return a.name.localeCompare(b.name);
    });
    nodes.forEach(function (n) {
      sortNodes(n.children);
    });
    return nodes;
  }
  return sortNodes(roots);
}

/**
 * Collect all descendant ids of a node (including the node itself) so the
 * Move-to picker can exclude invalid targets.
 */
function collectDescendantIds(nodeId, allEntities) {
  var ids = new Set();
  var queue = [nodeId];
  while (queue.length > 0) {
    var cur = queue.pop();
    ids.add(cur);
    allEntities.forEach(function (e) {
      if (e.parentId === cur) queue.push(e.id);
    });
  }
  return ids;
}

function OrgTreeNode({
  node,
  depth,
  allEntities,
  onRename,
  onAdd,
  onDelete,
  onMove,
  busy,
}) {
  var pad = 6 + depth * 18;
  var [editing, setEditing] = useState(false);
  var [editVal, setEditVal] = useState(node.name);
  var [adding, setAdding] = useState(false);
  var [addName, setAddName] = useState("");
  var [addType, setAddType] = useState("shop");
  var [moving, setMoving] = useState(false);
  var [expanded, setExpanded] = useState(depth < 2);

  var hasChildren = node.children && node.children.length > 0;
  var canDelete = node.activeChildCount === 0;

  // Valid move targets: all shop/dept containers except this node and its descendants.
  var invalidIds = collectDescendantIds(node.id, allEntities || []);
  var moveOptions = (allEntities || []).filter(function (e) {
    return (
      (e.type === "shop" || e.type === "department") && !invalidIds.has(e.id)
    );
  });

  var startEdit = function () {
    setEditVal(node.name);
    setEditing(true);
  };
  var commitEdit = function () {
    var v = editVal.trim();
    if (v && v !== node.name) {
      onRename(node.id, v);
    }
    setEditing(false);
  };
  var cancelEdit = function () {
    setEditing(false);
    setEditVal(node.name);
  };

  var startAdd = function () {
    setAddName("");
    setAddType("shop");
    setAdding(true);
  };
  var commitAdd = function () {
    var v = addName.trim();
    if (!v) return;
    onAdd(v, addType, node.id);
    setAdding(false);
  };
  var cancelAdd = function () {
    setAdding(false);
  };

  var handleMoveSelect = function (e) {
    var val = e.target.value;
    if (val === "") return; // placeholder
    var newParentId = val === "__top__" ? null : val;
    setMoving(false);
    onMove(node.id, node.name, newParentId);
  };

  return (
    <div>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 6,
          paddingLeft: pad,
          paddingTop: 5,
          paddingBottom: 5,
          borderRadius: 4,
        }}
      >
        {/* expand / collapse toggle */}
        <button
          className="btn sm"
          style={{
            minWidth: 20,
            padding: "0 4px",
            fontFamily: "monospace",
            opacity: hasChildren || adding ? 1 : 0.3,
          }}
          onClick={function () {
            setExpanded(function (x) {
              return !x;
            });
          }}
          title={expanded ? "Collapse" : "Expand"}
        >
          {expanded ? "▾" : "▸"}
        </button>

        {/* type badge */}
        <span
          style={{
            fontSize: 10,
            fontWeight: 700,
            textTransform: "uppercase",
            letterSpacing: "0.05em",
            color: node.type === "shop" ? "var(--blue-ink)" : "var(--ink-3)",
            minWidth: 54,
          }}
        >
          {node.type}
        </span>

        {/* name / rename input */}
        {editing ? (
          <input
            autoFocus
            className="field"
            style={{ flex: 1, padding: "2px 6px", fontSize: 13 }}
            value={editVal}
            onChange={function (e) {
              setEditVal(e.target.value);
            }}
            onKeyDown={function (e) {
              if (e.key === "Enter") commitEdit();
              if (e.key === "Escape") cancelEdit();
            }}
            onBlur={commitEdit}
          />
        ) : (
          <span style={{ flex: 1, fontSize: 13, fontWeight: 500 }}>
            {node.name}
          </span>
        )}

        {/* action buttons */}
        {!editing && !moving && (
          <>
            <button
              className="btn sm"
              title="Rename"
              disabled={busy === node.id}
              onClick={startEdit}
              style={{ padding: "2px 7px" }}
            >
              ✎
            </button>
            <button
              className="btn sm"
              title="Add shop or department inside"
              onClick={startAdd}
              style={{ padding: "2px 7px" }}
            >
              +
            </button>
            <button
              className="btn sm"
              title="Move to a different parent"
              disabled={busy === node.id}
              onClick={function () {
                setMoving(true);
              }}
              style={{ padding: "2px 7px" }}
            >
              ↪
            </button>
            <button
              className="btn sm"
              title={
                canDelete
                  ? "Delete this unit"
                  : "Cannot delete — has active employees or sub-units"
              }
              disabled={!canDelete || busy === node.id}
              onClick={function () {
                onDelete(node.id, node.name);
              }}
              style={{
                padding: "2px 7px",
                opacity: canDelete ? 1 : 0.35,
                color: canDelete ? "var(--red-ink)" : undefined,
              }}
            >
              ✕
            </button>
          </>
        )}

        {/* inline move picker */}
        {moving && (
          <>
            <select
              className="field"
              autoFocus
              style={{ padding: "2px 4px", fontSize: 12, flex: 1 }}
              defaultValue=""
              onChange={handleMoveSelect}
            >
              <option value="" disabled>
                Move to…
              </option>
              <option value="__top__">— Top level (no parent) —</option>
              {moveOptions.map(function (opt) {
                var isCurrent = opt.id === node.parentId;
                return (
                  <option key={opt.id} value={opt.id} disabled={isCurrent}>
                    {opt.name}
                    {isCurrent ? " (current)" : ""}
                  </option>
                );
              })}
            </select>
            <button
              className="btn sm"
              onClick={function () {
                setMoving(false);
              }}
              style={{ padding: "2px 7px" }}
            >
              Cancel
            </button>
          </>
        )}
      </div>

      {/* inline add-child form */}
      {adding && (
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: 6,
            paddingLeft: pad + 18 + 6,
            paddingTop: 4,
            paddingBottom: 4,
          }}
        >
          <select
            className="field"
            style={{ padding: "2px 4px", fontSize: 12 }}
            value={addType}
            onChange={function (e) {
              setAddType(e.target.value);
            }}
          >
            <option value="shop">Shop</option>
            <option value="department">Department</option>
          </select>
          <input
            autoFocus
            className="field"
            style={{ flex: 1, padding: "2px 6px", fontSize: 13 }}
            placeholder="Name…"
            value={addName}
            onChange={function (e) {
              setAddName(e.target.value);
            }}
            onKeyDown={function (e) {
              if (e.key === "Enter") commitAdd();
              if (e.key === "Escape") cancelAdd();
            }}
          />
          <button className="btn sm primary" onClick={commitAdd}>
            Add
          </button>
          <button className="btn sm" onClick={cancelAdd}>
            Cancel
          </button>
        </div>
      )}

      {/* children */}
      {expanded &&
        node.children.map(function (child) {
          return (
            <OrgTreeNode
              key={child.id}
              node={child}
              depth={depth + 1}
              allEntities={allEntities}
              onRename={onRename}
              onAdd={onAdd}
              onDelete={onDelete}
              onMove={onMove}
              busy={busy}
            />
          );
        })}
    </div>
  );
}

function OrgTab({ onToast }) {
  var [entities, setEntities] = useState(null);
  var [busy, setBusy] = useState(null);

  var load = async function () {
    try {
      var r = await API.adminOrgEntities();
      setEntities(r.entities || []);
    } catch (e) {
      if (onToast) onToast(e.message || "Failed to load org chart.", "danger");
    }
  };

  useEffect(function () {
    load();
  }, []);

  var handleRename = async function (id, name) {
    setBusy(id);
    try {
      await API.adminRenameEntity(id, name);
      if (onToast) onToast("Renamed to: " + name);
      await load();
    } catch (e) {
      if (onToast) onToast(e.message || "Rename failed.", "danger");
    } finally {
      setBusy(null);
    }
  };

  var handleAdd = async function (name, type, parentId) {
    setBusy("adding");
    try {
      await API.adminCreateEntity(name, type, parentId);
      if (onToast)
        onToast((type === "shop" ? "Shop" : "Department") + " added: " + name);
      await load();
    } catch (e) {
      if (onToast) onToast(e.message || "Create failed.", "danger");
    } finally {
      setBusy(null);
    }
  };

  var handleAddTop = async function (name, type) {
    setBusy("adding");
    try {
      await API.adminCreateEntity(name, type, null);
      if (onToast)
        onToast((type === "shop" ? "Shop" : "Department") + " added: " + name);
      await load();
    } catch (e) {
      if (onToast) onToast(e.message || "Create failed.", "danger");
    } finally {
      setBusy(null);
    }
  };

  var handleDelete = async function (id, name) {
    if (!window.confirm("Delete " + name + "? This cannot be undone.")) return;
    setBusy(id);
    try {
      await API.adminDeleteEntity(id);
      if (onToast) onToast("Deleted: " + name);
      await load();
    } catch (e) {
      if (onToast) onToast(e.message || "Delete failed.", "danger");
    } finally {
      setBusy(null);
    }
  };

  var handleMove = async function (id, name, newParentId) {
    setBusy(id);
    try {
      await API.adminMoveEntity(id, newParentId);
      var dest = newParentId
        ? (entities || []).find(function (e) {
            return e.id === newParentId;
          })
        : null;
      var destLabel = dest ? dest.name : "top level";
      if (onToast) onToast(name + " moved to " + destLabel);
      await load();
    } catch (e) {
      if (onToast) onToast(e.message || "Move failed.", "danger");
    } finally {
      setBusy(null);
    }
  };

  var tree = entities ? buildOrgTree(entities) : [];

  return (
    <div className="admin-section">
      <div
        style={{
          display: "flex",
          alignItems: "baseline",
          justifyContent: "space-between",
          marginBottom: 10,
        }}
      >
        <div>
          <div style={{ fontWeight: 600, fontSize: 14 }}>
            Shops &amp; Departments
          </div>
          <div style={{ color: "var(--ink-3)", fontSize: 12, marginTop: 2 }}>
            Add, rename, or delete structural units. Employees are managed via
            the Timetracker sync and card-edit flow.
          </div>
        </div>
        <AddTopLevelButton onAdd={handleAddTop} busy={busy} />
      </div>

      {!entities && <div className="empty is-loading">Loading…</div>}
      {entities && tree.length === 0 && (
        <div className="empty">No shops or departments found.</div>
      )}
      {entities && tree.length > 0 && (
        <div
          style={{
            border: "1px solid var(--line)",
            borderRadius: 6,
            padding: "6px 0",
            background: "var(--canvas)",
          }}
        >
          {tree.map(function (node) {
            return (
              <OrgTreeNode
                key={node.id}
                node={node}
                depth={0}
                allEntities={entities}
                onRename={handleRename}
                onAdd={handleAdd}
                onDelete={handleDelete}
                onMove={handleMove}
                busy={busy}
              />
            );
          })}
        </div>
      )}
    </div>
  );
}

function AddTopLevelButton({ onAdd, busy }) {
  var [open, setOpen] = useState(false);
  var [name, setName] = useState("");
  var [type, setType] = useState("shop");

  var commit = function () {
    var v = name.trim();
    if (!v) return;
    onAdd(v, type);
    setOpen(false);
    setName("");
    setType("shop");
  };

  if (!open) {
    return (
      <button
        className="btn sm primary"
        disabled={busy !== null}
        onClick={function () {
          setOpen(true);
        }}
      >
        + New top-level unit
      </button>
    );
  }
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
      <select
        className="field"
        style={{ padding: "2px 4px", fontSize: 12 }}
        value={type}
        onChange={function (e) {
          setType(e.target.value);
        }}
      >
        <option value="shop">Shop</option>
        <option value="department">Department</option>
      </select>
      <input
        autoFocus
        className="field"
        style={{ padding: "3px 8px", fontSize: 13 }}
        placeholder="Name…"
        value={name}
        onChange={function (e) {
          setName(e.target.value);
        }}
        onKeyDown={function (e) {
          if (e.key === "Enter") commit();
          if (e.key === "Escape") {
            setOpen(false);
            setName("");
          }
        }}
      />
      <button className="btn sm primary" onClick={commit}>
        Add
      </button>
      <button
        className="btn sm"
        onClick={function () {
          setOpen(false);
          setName("");
        }}
      >
        Cancel
      </button>
    </div>
  );
}

const ADMIN_TABS = [
  { id: "users", label: "Users" },
  { id: "org", label: "Org" },
  { id: "thresholds", label: "Thresholds" },
  { id: "branches", label: "Branches" },
  { id: "notifications", label: "Notifications" },
  { id: "audit", label: "Audit log" },
  { id: "sources", label: "Data sources" },
  { id: "dataflow", label: "Data flow" },
  { id: "itsuite", label: "IT Suite" },
  { id: "system", label: "System" },
];

function AdminPanel({
  onReload,
  onToast,
  currentUserId,
  pendingInviteCard,
  onClearPendingInviteCard,
}) {
  // Force the Users tab open when an org-card Invite prefill is pending so
  // UsersTab is mounted before we hand it the prefill data.
  const [tab, setTab] = useState(pendingInviteCard ? "users" : "users");
  return (
    /* The data-flow graph needs more horizontal room than the form-shaped tabs:
       it renders a five-stage pipeline alongside a metadata panel. */
    <div
      className="canvas"
      style={{ maxWidth: tab === "dataflow" ? 1360 : 1040 }}
    >
      <div className="view-head">
        <div>
          <div className="view-title">Admin</div>
          <div className="view-desc">
            Accounts, audit trail, and system health. Admin-only.
          </div>
        </div>
      </div>

      <div className="admin-tabs">
        {ADMIN_TABS.map((t) => (
          <button
            key={t.id}
            className={"admin-tab" + (tab === t.id ? " active" : "")}
            onClick={() => setTab(t.id)}
          >
            {t.label}
          </button>
        ))}
      </div>

      {tab === "users" && (
        <UsersTab
          onToast={onToast}
          currentUserId={currentUserId}
          pendingInviteCard={pendingInviteCard}
          onClearPendingInviteCard={onClearPendingInviteCard}
        />
      )}
      {tab === "org" && <OrgTab onToast={onToast} />}
      {tab === "thresholds" && <ThresholdsTab onToast={onToast} />}
      {tab === "branches" && <BranchMappingTab onToast={onToast} />}
      {tab === "notifications" && <NotificationsTab onToast={onToast} />}
      {tab === "audit" && <AuditTab />}
      {tab === "sources" && (
        <SourcesTab onReload={onReload} onToast={onToast} />
      )}
      {tab === "dataflow" && <DataFlowTab onToast={onToast} />}
      {tab === "itsuite" && <ITSuiteTab />}
      {tab === "system" && <SystemTab />}
    </div>
  );
}

/* One unit row in the browse tree (recursive: child units, then people managers-first). */
function OrgNavUnit({
  unit,
  depth,
  expanded,
  toggle,
  nav,
  onPickUnit,
  onPickPerson,
  pathUnitId,
  onInviteCard,
}) {
  const FL = window.FL;
  const isOpen = !!expanded[unit.id];
  const childUnits = FL.childUnitsOf(unit.id);
  const mgmt = FL.managementOf(unit.id);
  const people = mgmt.managers.concat(mgmt.reports);
  const st = FL.unitStatus(unit.id);
  const flagged = st.counts.red + st.counts.orange + st.counts.yellow;
  const hasKids = childUnits.length > 0 || people.length > 0;
  const unitCur = nav.view === "unit" && nav.unitId === unit.id;
  const unitInPath = pathUnitId === unit.id && !unitCur;
  const pad = 8 + depth * 14;
  return (
    <div>
      <div
        className={
          "orgnav-shoprow" +
          (unitCur ? " cur" : "") +
          (unitInPath ? " inpath" : "")
        }
        style={{ paddingLeft: pad }}
      >
        <button
          className={"orgnav-tw" + (isOpen ? " open" : "")}
          onClick={() => toggle(unit.id)}
          aria-label={isOpen ? "Collapse" : "Expand"}
          aria-expanded={isOpen}
        >
          {hasKids ? "▸" : ""}
        </button>
        <button className="orgnav-pick" onClick={() => onPickUnit(unit.id)}>
          <Dot sev={st.sev} style="filled" size="sm" />
          <span className="orgnav-name">{unit.name}</span>
          {flagged > 0 ? (
            <span className={"orgnav-badge " + st.sev}>{flagged}</span>
          ) : (
            <span className="orgnav-meta">{people.length}</span>
          )}
        </button>
      </div>
      {isOpen &&
        childUnits.map((cu) => (
          <OrgNavUnit
            key={cu.id}
            unit={cu}
            depth={depth + 1}
            expanded={expanded}
            toggle={toggle}
            nav={nav}
            onPickUnit={onPickUnit}
            onPickPerson={onPickPerson}
            pathUnitId={pathUnitId}
            onInviteCard={onInviteCard}
          />
        ))}
      {isOpen &&
        people.map((p) => {
          const isMgr = mgmt.managers.indexOf(p) !== -1;
          var login = (FL.loginByEntity || {})[p.id];
          return (
            <div key={p.id} style={{ display: "flex", alignItems: "stretch" }}>
              <button
                className={
                  "orgnav-row orgnav-person" +
                  (nav.view === "person" && nav.personId === p.id ? " cur" : "")
                }
                style={{ paddingLeft: pad + 22, flex: 1, minWidth: 0 }}
                onClick={() => onPickPerson(p.id)}
              >
                <span className="orgnav-tw ghost" />
                <Dot sev={FL.personStatus(p.id).sev} style="filled" size="sm" />
                <span className="orgnav-name">{p.name}</span>
                <span className="orgnav-meta">
                  {isMgr ? "Manager · " + p.role : p.role}
                </span>
                {login && (
                  <span
                    title={
                      login.status === "active"
                        ? "Has login: " + login.email
                        : "Invite pending: " + login.email
                    }
                    style={{
                      fontSize: 10,
                      marginLeft: 6,
                      color:
                        login.status === "active"
                          ? "var(--green-ink, #22a05a)"
                          : "var(--amber-ink, #9a6b00)",
                    }}
                  >
                    {login.status === "active" ? "✓" : "~"}
                  </span>
                )}
              </button>
              {onInviteCard && !login && (
                <button
                  className="btn ghost sm"
                  style={{
                    fontSize: 10,
                    padding: "1px 6px",
                    flexShrink: 0,
                    alignSelf: "center",
                    marginRight: 4,
                  }}
                  title={"Invite " + p.name + " to this org card"}
                  onClick={function (e) {
                    e.stopPropagation();
                    onInviteCard(p.id, p.name);
                  }}
                >
                  Invite
                </button>
              )}
            </div>
          );
        })}
      {isOpen && !hasKids && (
        <div className="orgnav-empty" style={{ paddingLeft: pad + 22 }}>
          No people in this unit.
        </div>
      )}
    </div>
  );
}

/* ======  ROLE-AWARE UI COMPONENTS  ====== */

/* Context strip — one line below topbar showing the signed-in user's scope.
   executive: org name | manager: "Your Shop: [name]" | individual: own name | staff-filtered: role badge */
function ContextStrip({ user, tier }) {
  var FL = window.FL;
  if (tier === "executive") return null;
  var content = null;
  if (tier === "manager") {
    var watched = FL.watchedIds(user);
    var shopNames = watched.map(function (id) {
      var node = FL.unitById[id];
      return node ? node.name : id;
    });
    var shopLabel = shopNames.length > 1 ? "Your Shops: " : "Your Shop: ";
    content = (
      <span className="ctx-label">
        {shopLabel}
        <strong>{shopNames.length ? shopNames.join(", ") : "Your shop"}</strong>
      </span>
    );
  } else if (tier === "individual") {
    var personNode = user.scopeEntityId
      ? FL.peopleById[user.scopeEntityId]
      : null;
    var personName = personNode ? personNode.name : user.name || user.email;
    content = <span className="ctx-label">{personName}</span>;
  } else if (tier === "staff-filtered") {
    var roleLabel =
      user.role === "hr"
        ? "HR View"
        : user.role === "billing"
          ? "Billing View"
          : "AR View";
    content = <span className="ctx-badge">{roleLabel}</span>;
  }
  if (!content) return null;
  return <div className="ctx-strip">{content}</div>;
}

/* Coaching candidates — ranked list of repeat-offender patterns surfaced by
   the coaching engine. Reads FL.COACHING populated during load(); re-renders on
   every version bump because App receives version as a prop and all children
   re-render. Does NOT use useState to cache coaching data so stale renders
   cannot hide fresh results after auto-refresh. */
function CoachingCandidatesPanel({ onSelectPerson }) {
  var FL = window.FL;
  var candidates = FL.COACHING || [];
  if (!candidates.length) return null;
  return (
    <div className="canvas" style={{ maxWidth: 820, paddingBottom: 0 }}>
      <div className="view-head" style={{ marginBottom: 12 }}>
        <div>
          <div className="view-title">Coaching candidates</div>
          <div className="view-desc">
            Employees with repeated exceptions in the coaching window — click to
            view their profile.
          </div>
        </div>
        <div style={{ marginLeft: "auto" }}>
          <Tag tone="orange">
            {candidates.length} pattern{candidates.length !== 1 ? "s" : ""}
          </Tag>
        </div>
      </div>
      <div className="card" style={{ padding: 4, marginBottom: 22 }}>
        <table className="admin-table">
          <thead>
            <tr>
              <th>Employee</th>
              <th>Rule</th>
              <th style={{ textAlign: "right" }}>Count (window)</th>
              <th style={{ textAlign: "right" }}>Trend</th>
            </tr>
          </thead>
          <tbody>
            {candidates.map(function (c) {
              var trendIcon =
                c.trend === "rising" ? "↑" : c.trend === "falling" ? "↓" : "→";
              var trendTone =
                c.trend === "rising"
                  ? "red"
                  : c.trend === "falling"
                    ? "green"
                    : "neutral";
              return (
                <tr
                  key={c.entityId + ":" + c.ruleId}
                  style={{ cursor: onSelectPerson ? "pointer" : "default" }}
                  onClick={function () {
                    if (onSelectPerson) onSelectPerson(c.entityId);
                  }}
                >
                  <td>
                    <div style={{ fontWeight: 650 }}>{c.entityName}</div>
                  </td>
                  <td>
                    <span style={{ fontSize: 12, color: "var(--ink-2)" }}>
                      {FL.humanize ? FL.humanize(c.ruleId) : c.ruleId}
                    </span>
                  </td>
                  <td style={{ textAlign: "right", fontWeight: 650 }}>
                    {c.currentCount}×
                    {c.priorCount > 0 && (
                      <span
                        style={{
                          fontSize: 11,
                          color: "var(--ink-3)",
                          fontWeight: 400,
                          marginLeft: 4,
                        }}
                      >
                        ({c.priorCount} prior)
                      </span>
                    )}
                  </td>
                  <td style={{ textAlign: "right" }}>
                    <Tag tone={trendTone}>
                      {trendIcon} {c.trend}
                    </Tag>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* Shop switcher — shown for managers who watch multiple shops, so they can
   navigate directly to any watched shop from the dashboard without needing an
   admin scope change. */
function ShopSwitcherPanel({ user, currentUnitId, onGoUnit }) {
  var FL = window.FL;
  var watched = FL.watchedIds(user);
  if (watched.length <= 1) return null;
  return (
    <div className="canvas" style={{ maxWidth: 820, paddingBottom: 0 }}>
      <div className="view-head" style={{ marginBottom: 10 }}>
        <div>
          <div className="view-title">Your shops</div>
          <div className="view-desc">Switch between shops you cover.</div>
        </div>
      </div>
      <div
        style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 22 }}
      >
        {watched.map(function (shopId) {
          var unit = FL.unitById[shopId];
          if (!unit) return null;
          var st = FL.unitStatus(shopId);
          var isCurrent = shopId === currentUnitId;
          return (
            <button
              key={shopId}
              className={"btn" + (isCurrent ? " primary" : "")}
              style={{ display: "flex", alignItems: "center", gap: 6 }}
              onClick={function () {
                onGoUnit(shopId);
              }}
            >
              <Dot sev={st.sev} style="filled" size="sm" />
              {unit.name}
            </button>
          );
        })}
      </div>
    </div>
  );
}

/* Tackle first — a manager's Escalated & Critical OPEN items across their crew,
   worst-then-oldest first, pinned to the top of their shop landing so on entry
   they immediately see who + what to hit. Reads already-scoped FL data; it does
   NOT compute or change severity/routing. Clicking a row opens that exception
   (which carries its own "How to handle this" playbook). */
function TriagePanel({ unitId, onOpenExc }) {
  var FL = window.FL;
  var mgmt = FL.managementOf(unitId);
  var people = mgmt.managers.concat(mgmt.reports);
  var rank = { red: 0, orange: 1 };
  var items = [];
  people.forEach(function (p) {
    var xs = FL.personStatus(p.id).exceptions || [];
    xs.forEach(function (x) {
      var openish = x.status === "open" || !x.status;
      if (openish && (x.current === "red" || x.current === "orange")) {
        items.push({ exc: x, personName: p.name });
      }
    });
  });
  items.sort(function (a, b) {
    var d = rank[a.exc.current] - rank[b.exc.current];
    if (d !== 0) return d;
    return (b.exc.elapsedMin || 0) - (a.exc.elapsedMin || 0);
  });

  return (
    <div className="canvas" style={{ maxWidth: 820, paddingBottom: 0 }}>
      <div className="view-head" style={{ marginBottom: 12 }}>
        <div>
          <div className="view-title">Tackle first</div>
          <div className="view-desc">
            {items.length
              ? "Escalated and critical items in your shop, worst first — open one to see what to do."
              : "Escalated and critical items in your shop show up here first."}
          </div>
        </div>
        {items.length > 0 && (
          <div style={{ marginLeft: "auto" }}>
            <Tag tone="red">{items.length} need action</Tag>
          </div>
        )}
      </div>

      {items.length === 0 ? (
        <div
          className="card"
          style={{
            padding: 16,
            display: "flex",
            gap: 10,
            alignItems: "center",
            marginBottom: 22,
          }}
        >
          <Dot sev="green" style="filled" />
          <div>
            <b>All clear.</b>{" "}
            <span style={{ color: "var(--ink-3)" }}>
              Nothing critical or escalated in your shop right now.
            </span>
          </div>
        </div>
      ) : (
        <div
          className="card triage-list"
          style={{ padding: 4, marginBottom: 22 }}
        >
          {items.map(function (it, i) {
            var x = it.exc;
            return (
              <button
                key={x.id}
                onClick={function () {
                  onOpenExc(x.id);
                }}
                style={{
                  all: "unset",
                  cursor: "pointer",
                  display: "flex",
                  width: "100%",
                  boxSizing: "border-box",
                  alignItems: "center",
                  gap: 10,
                  padding: "11px 12px",
                  borderTop: i ? "1px solid var(--border)" : "none",
                }}
              >
                <Dot
                  sev={x.current}
                  style="filled"
                  pulse={x.current === "red"}
                />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontWeight: 650, fontSize: 13.5 }}>
                    {it.personName}
                  </div>
                  <div
                    style={{
                      color: "var(--ink-2)",
                      fontSize: 12,
                      overflow: "hidden",
                      textOverflow: "ellipsis",
                      whiteSpace: "nowrap",
                    }}
                  >
                    {x.plainSummary || x.title}
                  </div>
                </div>
                <div style={{ textAlign: "right", flexShrink: 0 }}>
                  <Tag tone={x.current === "red" ? "red" : "orange"}>
                    {x.current === "red" ? "Critical" : "Escalated"}
                  </Tag>
                  <div
                    style={{
                      color: "var(--ink-3)",
                      fontSize: 11,
                      marginTop: 3,
                    }}
                  >
                    open {fmtElapsed(x.elapsedMin)}
                  </div>
                </div>
                <span style={{ color: "var(--ink-3)", fontSize: 16 }}>›</span>
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

/* Crew summary — shown for shop managers on their scoped unit.
   One row per employee: name, open exception count, worst severity dot. */
function CrewSummaryPanel({ unitId, onSelectPerson }) {
  var FL = window.FL;
  var mgmt = FL.managementOf(unitId);
  var people = mgmt.managers.concat(mgmt.reports);
  if (!people.length) return null;
  return (
    <div className="canvas" style={{ maxWidth: 820, paddingBottom: 0 }}>
      <div className="view-head" style={{ marginBottom: 12 }}>
        <div>
          <div className="view-title">Your crew</div>
          <div className="view-desc">
            Everyone in your shop — click a person to see their exceptions.
          </div>
        </div>
      </div>
      <div className="card" style={{ padding: 4, marginBottom: 22 }}>
        <table className="admin-table">
          <thead>
            <tr>
              <th>Employee</th>
              <th style={{ textAlign: "right" }}>Open exceptions</th>
              <th style={{ textAlign: "right" }}>Status</th>
            </tr>
          </thead>
          <tbody>
            {people.map(function (p) {
              var ps = FL.personStatus(p.id);
              var openExc = ps.exceptions.filter(function (x) {
                return x.status === "open" || !x.status;
              }).length;
              var sevTone =
                ps.sev === "green"
                  ? "green"
                  : ps.sev === "red"
                    ? "red"
                    : ps.sev === "orange"
                      ? "orange"
                      : "yellow";
              var sevText =
                ps.sev === "green"
                  ? "In flow"
                  : ps.sev === "red"
                    ? "Critical"
                    : ps.sev === "orange"
                      ? "Escalated"
                      : "Flagged";
              return (
                <tr
                  key={p.id}
                  style={{ cursor: "pointer" }}
                  onClick={function () {
                    onSelectPerson(p.id);
                  }}
                >
                  <td>
                    <div
                      style={{ display: "flex", alignItems: "center", gap: 8 }}
                    >
                      <Dot sev={ps.sev} style="filled" size="sm" />
                      <div>
                        <div style={{ fontWeight: 650 }}>{p.name}</div>
                        <div style={{ color: "var(--ink-3)", fontSize: 11 }}>
                          {p.role}
                        </div>
                      </div>
                    </div>
                  </td>
                  <td style={{ textAlign: "right" }}>
                    {openExc > 0 ? (
                      openExc
                    ) : (
                      <span style={{ color: "var(--ink-3)" }}>—</span>
                    )}
                  </td>
                  <td style={{ textAlign: "right" }}>
                    <Tag tone={sevTone}>{sevText}</Tag>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* Staff-filtered banner — dismissible note for hr/billing/ar users explaining
   which exception types are visible so they're not confused by empty rows. */
function StaffFilteredBanner({ role }) {
  var [dismissed, setDismissed] = useState(false);
  if (dismissed) return null;
  var msg = "";
  if (role === "hr")
    msg =
      "Showing labor exceptions only (missed clock-outs and no-break violations)";
  else if (role === "billing" || role === "ar")
    msg = "Showing billing exceptions only (invoice exceptions)";
  if (!msg) return null;
  return (
    <div className="staff-filter-banner">
      <span>{msg}</span>
      <button
        className="btn ghost sm"
        style={{ flexShrink: 0 }}
        onClick={function () {
          setDismissed(true);
        }}
      >
        Dismiss
      </button>
    </div>
  );
}

/* ======  ORG NAVIGATOR (expandable tree switcher)  ====== */
function OrgNavigator({
  nav,
  onGoRegion,
  onGoUnit,
  onGoPerson,
  hideOrgWide,
  onInviteCard,
}) {
  const FL = window.FL;
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [expanded, setExpanded] = useState({});
  const rootRef = useRef(null);
  const inputRef = useRef(null);

  const curUnit = FL.unitById[nav.unitId];
  const curPerson = nav.view === "person" ? FL.peopleById[nav.personId] : null;
  const pathUnitId = curPerson
    ? curPerson.unitId
    : nav.view === "unit"
      ? nav.unitId
      : null;

  // When the panel opens, expand the path to the current location and focus search.
  useEffect(() => {
    if (!open) return undefined;
    const next = {};
    if (pathUnitId)
      FL.unitPath(pathUnitId).forEach((u) => {
        next[u.id] = true;
      });
    setExpanded(next);
    setQuery("");
    const id = setTimeout(() => {
      if (inputRef.current) inputRef.current.focus();
    }, 20);
    return () => clearTimeout(id);
  }, [open]);

  // Close on outside-click and Escape.
  useEffect(() => {
    if (!open) return undefined;
    const onDown = (e) => {
      if (rootRef.current && !rootRef.current.contains(e.target))
        setOpen(false);
    };
    const onKey = (e) => {
      if (e.key === "Escape") setOpen(false);
    };
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);

  const toggle = (id) =>
    setExpanded((prev) => {
      const n = Object.assign({}, prev);
      if (n[id]) delete n[id];
      else n[id] = true;
      return n;
    });

  const pickUnit = (id) => {
    onGoUnit(id);
    setOpen(false);
  };
  const pickPerson = (id) => {
    onGoPerson(id);
    setOpen(false);
  };
  const pickRegion = () => {
    onGoRegion();
    setOpen(false);
  };

  let locLabel = FL.orgName;
  if (nav.view === "admin") locLabel = "Admin";
  else if (curPerson) locLabel = curPerson.name;
  else if (nav.view === "unit" && curUnit) locLabel = curUnit.name;

  const q = query.trim().toLowerCase();
  let results = null;
  if (q) {
    results = [];
    FL.UNITS.forEach((u) => {
      if (u.name.toLowerCase().indexOf(q) !== -1)
        results.push({
          kind: u.type === "shop" ? "shop" : "dept",
          id: u.id,
          name: u.name,
          path: u.deptName || FL.orgName,
          sev: FL.unitStatus(u.id).sev,
        });
    });
    FL.PEOPLE.forEach((p) => {
      if (p.name.toLowerCase().indexOf(q) !== -1) {
        const un = FL.unitById[p.unitId];
        results.push({
          kind: "person",
          id: p.id,
          name: p.name,
          path: un ? un.name : "",
          sev: FL.personStatus(p.id).sev,
        });
      }
    });
    results = results.slice(0, 50);
  }

  const pickResult = (r) => {
    if (r.kind === "person") pickPerson(r.id);
    else pickUnit(r.id);
  };

  return (
    <div className="orgnav" ref={rootRef}>
      <button
        className={"btn sm orgnav-trigger" + (open ? " primary" : "")}
        onClick={() => setOpen((o) => !o)}
        aria-expanded={open}
        aria-haspopup="true"
        title="Browse organization"
      >
        <span className="orgnav-cur">{locLabel}</span>
        <span className="orgnav-caret">▾</span>
      </button>

      {open && (
        <div
          className="orgnav-panel"
          role="dialog"
          aria-label="Browse organization"
        >
          <div className="orgnav-search">
            <input
              ref={inputRef}
              className="orgnav-input"
              type="text"
              placeholder="Search people, shops, departments…"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
          </div>

          <div className="orgnav-body">
            {!q && !hideOrgWide && (
              <button
                className={
                  "orgnav-row orgnav-org" +
                  (nav.view === "region" ? " cur" : "")
                }
                onClick={pickRegion}
              >
                <Avatar
                  kind="dept"
                  size={18}
                  square
                  dotStyle="filled"
                  pulse={false}
                />
                <span className="orgnav-name">{FL.orgName}</span>
                <span className="orgnav-meta">org-wide</span>
              </button>
            )}

            {!q &&
              FL.topUnits().map((d) => (
                <OrgNavUnit
                  key={d.id}
                  unit={d}
                  depth={0}
                  expanded={expanded}
                  toggle={toggle}
                  nav={nav}
                  onPickUnit={pickUnit}
                  onPickPerson={pickPerson}
                  pathUnitId={pathUnitId}
                  onInviteCard={onInviteCard}
                />
              ))}

            {q && results.length === 0 && (
              <div className="orgnav-empty">No matches for \"{query}\".</div>
            )}
            {q &&
              results.map((r) => (
                <button
                  key={r.kind + ":" + r.id}
                  className="orgnav-row orgnav-result"
                  onClick={() => pickResult(r)}
                >
                  <Dot sev={r.sev} style="filled" size="sm" />
                  <span className="orgnav-name">{r.name}</span>
                  <span className="orgnav-path">{r.path}</span>
                  <span className="orgnav-kind">{r.kind}</span>
                </button>
              ))}
          </div>
        </div>
      )}
    </div>
  );
}

/* ======  CHANGE PASSWORD MODAL  ====== */
function ChangePasswordModal({ onClose, onSuccess, onSignOutAll }) {
  var [currentPw, setCurrentPw] = useState("");
  var [newPw, setNewPw] = useState("");
  var [err, setErr] = useState("");
  var [busy, setBusy] = useState(false);
  var [signOutAllBusy, setSignOutAllBusy] = useState(false);
  var [confirmSignOutAll, setConfirmSignOutAll] = useState(false);

  var submit = async function (e) {
    e.preventDefault();
    setErr("");
    setBusy(true);
    try {
      await API.changePassword(currentPw, newPw);
      onSuccess();
    } catch (e2) {
      setErr(e2.message || "Failed to change password.");
    } finally {
      setBusy(false);
    }
  };

  var handleSignOutAll = async function () {
    setSignOutAllBusy(true);
    try {
      await onSignOutAll();
    } finally {
      setSignOutAllBusy(false);
    }
  };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div
        className="auth-card"
        style={{ position: "relative", zIndex: 1 }}
        onClick={function (e) {
          e.stopPropagation();
        }}
      >
        <div className="auth-title">Change password</div>
        {err && <div className="auth-msg err">{err}</div>}
        <form onSubmit={submit}>
          <div className="field">
            <label>Current password</label>
            <input
              className="input"
              type="password"
              value={currentPw}
              onChange={function (e) {
                setCurrentPw(e.target.value);
              }}
              autoComplete="current-password"
              required
            />
          </div>
          <div className="field">
            <label>New password</label>
            <input
              className="input"
              type="password"
              value={newPw}
              onChange={function (e) {
                setNewPw(e.target.value);
              }}
              autoComplete="new-password"
              required
            />
          </div>
          <PasswordRequirements />
          <button
            className="btn primary auth-btn"
            type="submit"
            disabled={busy}
          >
            {busy ? "Saving…" : "Change password"}
          </button>
        </form>
        <button
          className="btn ghost auth-btn"
          style={{ marginTop: 8 }}
          type="button"
          onClick={onClose}
        >
          Cancel
        </button>
        <div
          style={{
            marginTop: 16,
            paddingTop: 12,
            borderTop: "1px solid var(--border, #e4e4e7)",
          }}
        >
          {!confirmSignOutAll ? (
            <button
              className="btn ghost auth-btn sm"
              type="button"
              onClick={function () {
                setConfirmSignOutAll(true);
              }}
            >
              Sign out of all devices
            </button>
          ) : (
            <div>
              <div className="auth-desc" style={{ margin: "0 0 8px" }}>
                This signs you out everywhere, including this device. Continue?
              </div>
              <button
                className="btn ghost auth-btn sm"
                type="button"
                disabled={signOutAllBusy}
                onClick={handleSignOutAll}
              >
                {signOutAllBusy ? "Signing out…" : "Yes, sign out everywhere"}
              </button>
              <button
                className="btn ghost auth-btn sm"
                style={{ marginLeft: 8 }}
                type="button"
                onClick={function () {
                  setConfirmSignOutAll(false);
                }}
              >
                Never mind
              </button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ======  TWO-FACTOR AUTHENTICATION  ====== */

/**
 * Enrolment walkthrough: scan the QR (or type the key), prove possession with
 * a live code, then save the one-time recovery codes. Used both inside the
 * account modal and standalone on the mandatory-setup screen for admins.
 */
function TwoFactorEnroll({ onDone }) {
  var [step, setStep] = useState("start"); // start | confirm | codes
  var [enrollment, setEnrollment] = useState(null);
  var [code, setCode] = useState("");
  var [recoveryCodes, setRecoveryCodes] = useState([]);
  var [saved, setSaved] = useState(false);
  var [err, setErr] = useState("");
  var [busy, setBusy] = useState(false);

  var begin = async function () {
    setErr("");
    setBusy(true);
    try {
      var r = await API.twoFactorEnroll();
      setEnrollment(r);
      setStep("confirm");
    } catch (e) {
      setErr(e.message || "Could not start setup.");
    } finally {
      setBusy(false);
    }
  };

  var confirm = async function (e) {
    e.preventDefault();
    setErr("");
    setBusy(true);
    try {
      var r = await API.twoFactorConfirm(code.trim());
      setRecoveryCodes(r.recoveryCodes || []);
      setStep("codes");
    } catch (e2) {
      setErr(e2.message || "That code didn't work.");
    } finally {
      setBusy(false);
    }
  };

  if (step === "start") {
    return (
      <div>
        {err && <div className="auth-msg err">{err}</div>}
        <div className="auth-desc" style={{ marginBottom: 12 }}>
          Two-step verification asks for a 6-digit code from an authenticator
          app (Google Authenticator, 1Password, Authy…) whenever you sign in, so
          a stolen password alone isn't enough to reach your account.
        </div>
        <button
          className="btn primary auth-btn"
          type="button"
          onClick={begin}
          disabled={busy}
        >
          {busy ? "Working…" : "Set up two-step verification"}
        </button>
      </div>
    );
  }

  if (step === "confirm") {
    return (
      <form onSubmit={confirm}>
        {err && <div className="auth-msg err">{err}</div>}
        <div className="auth-desc" style={{ marginBottom: 10 }}>
          Scan this with your authenticator app, then type the 6-digit code it
          shows.
        </div>
        <div style={{ textAlign: "center", marginBottom: 10 }}>
          <img
            src={enrollment.qrDataUrl}
            alt="Two-factor setup QR code"
            width="220"
            height="220"
            style={{ background: "#fff", borderRadius: 8, padding: 6 }}
          />
        </div>
        <div className="field">
          <label>Can't scan? Enter this key by hand</label>
          <input
            className="input"
            readOnly
            value={enrollment.manualKey}
            data-testid="totp-manual-key"
            onFocus={function (e) {
              e.target.select();
            }}
          />
        </div>
        <div className="field">
          <label>6-digit code</label>
          <input
            className="input"
            value={code}
            onChange={function (e) {
              setCode(e.target.value);
            }}
            placeholder="123456"
            inputMode="numeric"
            autoComplete="one-time-code"
            data-testid="totp-confirm-code"
            required
          />
        </div>
        <button className="btn primary auth-btn" type="submit" disabled={busy}>
          {busy ? "Verifying…" : "Verify and turn on"}
        </button>
      </form>
    );
  }

  return (
    <div data-testid="recovery-codes">
      <div className="auth-msg ok">Two-step verification is on.</div>
      <div className="auth-desc" style={{ marginBottom: 10 }}>
        Save these recovery codes somewhere safe — a password manager, or
        printed and locked away. Each one works exactly once, and they are the
        only way in if you lose your phone. This is the only time they'll be
        shown.
      </div>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "1fr 1fr",
          gap: 6,
          fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
          fontSize: 13,
          padding: 10,
          borderRadius: 8,
          border: "1px solid var(--border-strong)",
          marginBottom: 10,
        }}
      >
        {recoveryCodes.map(function (rc) {
          return <div key={rc}>{rc}</div>;
        })}
      </div>
      <label
        style={{
          display: "flex",
          gap: 8,
          alignItems: "center",
          marginBottom: 10,
        }}
      >
        <input
          type="checkbox"
          checked={saved}
          data-testid="recovery-codes-saved"
          onChange={function (e) {
            setSaved(e.target.checked);
          }}
        />
        <span>I've saved my recovery codes</span>
      </label>
      <button
        className="btn primary auth-btn"
        type="button"
        disabled={!saved}
        onClick={function () {
          onDone();
        }}
      >
        Done
      </button>
    </div>
  );
}

/**
 * Account-settings entry point: shows current state, runs enrolment, and lets
 * a user who is not *required* to keep a second factor turn theirs off by
 * re-entering their password.
 */
function TwoFactorModal({ user, onClose, onChanged }) {
  var [status, setStatus] = useState(null);
  var [password, setPassword] = useState("");
  var [err, setErr] = useState("");
  var [busy, setBusy] = useState(false);

  useEffect(function () {
    var cancelled = false;
    API.twoFactorStatus()
      .then(function (r) {
        if (!cancelled) setStatus(r);
      })
      .catch(function (e) {
        if (!cancelled) setErr(e.message || "Could not load status.");
      });
    return function () {
      cancelled = true;
    };
  }, []);

  var disable = async function (e) {
    e.preventDefault();
    setErr("");
    setBusy(true);
    try {
      await API.twoFactorDisable(password);
      onChanged("Two-step verification turned off");
    } catch (e2) {
      setErr(e2.message || "Could not turn it off.");
    } finally {
      setBusy(false);
    }
  };

  var body;
  if (!status && !err) {
    body = <div className="auth-desc">Loading…</div>;
  } else if (status && status.enabled) {
    body = (
      <div>
        <div className="auth-msg ok">
          Two-step verification is on for {user.email}.
        </div>
        <div className="auth-desc" style={{ marginBottom: 10 }}>
          {status.recoveryCodesRemaining} recovery{" "}
          {status.recoveryCodesRemaining === 1 ? "code" : "codes"} left.
        </div>
        {status.required ? (
          <div className="auth-desc">
            Admin accounts must keep two-step verification on, so it can't be
            turned off here. If you've lost your authenticator, use a recovery
            code — or ask another admin to clear it from the admin panel.
          </div>
        ) : (
          <form onSubmit={disable}>
            <div className="field">
              <label>Confirm your password to turn it off</label>
              <input
                className="input"
                type="password"
                value={password}
                onChange={function (e) {
                  setPassword(e.target.value);
                }}
                autoComplete="current-password"
                data-testid="twofactor-disable-password"
                required
              />
            </div>
            <button className="btn auth-btn" type="submit" disabled={busy}>
              {busy ? "Working…" : "Turn off two-step verification"}
            </button>
          </form>
        )}
      </div>
    );
  } else if (status) {
    body = (
      <TwoFactorEnroll
        onDone={function () {
          onChanged("Two-step verification is on");
        }}
      />
    );
  } else {
    body = null;
  }

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div
        className="auth-card"
        style={{ position: "relative", zIndex: 1 }}
        onClick={function (e) {
          e.stopPropagation();
        }}
      >
        <div className="auth-title">Two-step verification</div>
        {err && <div className="auth-msg err">{err}</div>}
        {body}
        <button
          className="btn ghost auth-btn"
          style={{ marginTop: 8 }}
          type="button"
          onClick={onClose}
        >
          Close
        </button>
      </div>
    </div>
  );
}

/**
 * Mandatory enrolment gate. An admin without a second factor lands here
 * instead of the app: the admin API refuses them until they enrol, so walking
 * them through setup is the only useful thing this screen can do.
 */
function TwoFactorRequiredScreen({ user, onDone, onSignOut }) {
  return (
    <div className="auth-wrap">
      <div className="auth-card">
        <div className="auth-brand">
          <span className="brand-mark" />
          <span className="brand-name">FLOWLINE</span>
        </div>
        <div className="auth-title">Set up two-step verification</div>
        <div className="auth-desc">
          Admin accounts can reassign access, reset passwords and read the audit
          trail, so {user.email} needs a second factor before continuing.
        </div>
        <TwoFactorEnroll onDone={onDone} />
        <div className="auth-alt">
          <button className="auth-link" onClick={onSignOut}>
            Sign out
          </button>
        </div>
      </div>
    </div>
  );
}

/* ======  APP SHELL  ====== */
/* ---- source-health staleness banner ---- */
function relTime(iso) {
  if (!iso) return "never";
  const ms = Date.now() - new Date(iso).getTime();
  const min = Math.round(ms / 60000);
  if (min < 1) return "just now";
  if (min < 60) return min + " min ago";
  const hr = Math.round(min / 60);
  if (hr < 24) return hr + " hr ago";
  return Math.round(hr / 24) + " d ago";
}

const SOURCE_LABEL = {
  samsara: "Samsara",
  timetracking: "Timetracker",
  fullbay: "Fullbay",
  payments: "Payment Hub",
};

// Polls /api/source-health on the same cadence as the app refresh and renders a
// compact warning strip ONLY when a live source is stale or its last refresh
// had failures. Renders nothing on the happy path (no new chrome, so the
// exception-card E2E suites are unaffected). Dismissable for the session.
function SourceHealthBanner({ version }) {
  const [health, setHealth] = useState(null);
  const [dismissed, setDismissed] = useState(false);

  useEffect(
    function () {
      let alive = true;
      API.sourceHealth()
        .then(function (r) {
          if (alive) setHealth(r);
        })
        .catch(function () {
          /* health is best-effort; never block the board on it */
        });
      return function () {
        alive = false;
      };
    },
    [version],
  );

  if (dismissed || !health || !health.sources) return null;
  const problems = health.sources.filter(function (s) {
    return s.mode === "live" && (s.stale || s.lastFailures > 0);
  });

  // The Fullbay scraper pull has its own freshness, distinct from the mapping
  // refresh above: the board can look "refreshed" while no new snapshot has
  // actually landed. Warn when a pull is overdue or repeatedly failing.
  const fullbaySrc = health.sources.filter(function (s) {
    return s.source === "fullbay";
  })[0];
  const pull = fullbaySrc && fullbaySrc.pull ? fullbaySrc.pull : null;
  const pullProblems = [];
  if (pull) {
    if (pull.wip.stale) {
      pullProblems.push(
        "⚠ Fullbay WIP pull overdue — data last landed " +
          relTime(pull.wip.lastLandedAt),
      );
    }
    if (pull.employeeStats.stale) {
      pullProblems.push(
        "⚠ Fullbay Employee Statistics pull overdue — last landed " +
          relTime(pull.employeeStats.lastLandedAt),
      );
    }
    // A failed last attempt warns even before staleness kicks in: `error` is a
    // broken run, `empty` is an export with no rows (a dead session, not a
    // quiet shop). Skip the per-report line when staleness already covers it.
    const attemptFailed = function (r) {
      return r.lastStatus === "error" || r.lastStatus === "empty";
    };
    if (!pull.wip.stale && attemptFailed(pull.wip)) {
      pullProblems.push(
        "⚠ Fullbay WIP: last pull attempt failed — data last landed " +
          relTime(pull.wip.lastLandedAt),
      );
    }
    if (!pull.employeeStats.stale && attemptFailed(pull.employeeStats)) {
      pullProblems.push(
        "⚠ Fullbay Employee Statistics: last pull attempt failed — data last landed " +
          relTime(pull.employeeStats.lastLandedAt),
      );
    }
    if (pull.consecutiveFailures > 0) {
      pullProblems.push(
        "⚠ Fullbay pull failing (" + pull.consecutiveFailures + " in a row)",
      );
    }
  }

  if (problems.length === 0 && pullProblems.length === 0) return null;

  return (
    <div
      className="source-health-banner"
      role="status"
      style={{
        display: "flex",
        alignItems: "center",
        gap: 10,
        flexWrap: "wrap",
        padding: "8px 12px",
        margin: "8px 0",
        borderRadius: 8,
        background: "var(--warn-bg, #3a2a00)",
        border: "1px solid var(--orange, #d08700)",
        fontSize: 13,
      }}
    >
      {problems.map(function (s) {
        const label = SOURCE_LABEL[s.source] || s.source;
        const msg = s.stale
          ? "⚠ " + label + " data last updated " + relTime(s.refreshedAt)
          : "⚠ " + label + ": last refresh had errors";
        return (
          <span key={s.source} style={{ fontWeight: 600 }}>
            {msg}
          </span>
        );
      })}
      {pullProblems.map(function (msg, i) {
        return (
          <span key={"pull-" + i} style={{ fontWeight: 600 }}>
            {msg}
          </span>
        );
      })}
      <button
        className="btn ghost sm"
        style={{ marginLeft: "auto", fontSize: 12 }}
        onClick={function () {
          setDismissed(true);
        }}
      >
        Dismiss
      </button>
    </div>
  );
}

function App({ version, user, onSignOut, onSignOutAll, onReload }) {
  const FL = window.FL;
  const tier = FL.userTier(user);
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  // Initial nav view is scoped to the user's tier.
  // manager  → land on their shop unit; multi-shop watchers land on the
  //            region overview so every watched shop is on screen
  // individual → land on their own person view (scopeEntityId)
  // executive / staff-filtered → region overview
  const initNav = (function () {
    if (tier === "individual" && user.scopeEntityId) {
      var p0 = FL.peopleById[user.scopeEntityId];
      return {
        view: "person",
        unitId: p0 ? p0.unitId : null,
        personId: user.scopeEntityId,
      };
    }
    if (
      tier === "manager" &&
      user.scopeEntityId &&
      FL.watchedIds(user).length <= 1
    ) {
      return { view: "unit", unitId: user.scopeEntityId, personId: null };
    }
    var fu = FL.topUnits()[0]
      ? FL.topUnits()[0].id
      : FL.UNITS[0]
        ? FL.UNITS[0].id
        : null;
    return { view: "region", unitId: fu, personId: null };
  })();
  const [nav, setNav] = useState(initNav);
  const [sel, setSel] = useState(null);
  const [filterSev, setFilterSev] = useState(null);
  const [filterRule, setFilterRule] = useState(null);
  const [showFlagged, setShowFlagged] = useState(true);
  const [regionMode, setRegionMode] = useState("supervision");
  const [pendingId, setPendingId] = useState(null);
  const [branchFilter, setBranchFilter] = useState(
    window.FL.branchFilter || "",
  );
  const [toasts, setToasts] = useState([]);
  const toastTimers = useRef({});
  const seenAckedCaseIds = useRef(new Set());
  const [showChangePw, setShowChangePw] = useState(false);
  const [showTwoFactor, setShowTwoFactor] = useState(false);
  const isOwner = user.role === "admin";

  // Owner-only: fetch the reverse login-link view (entityId → { email,
  // status }) so org chart cards can show who has a login. Refreshed on each
  // auto-reload tick; non-owners never call the endpoint so FL.loginByEntity
  // stays empty and no badges render. loginRev bumps to re-render the chart.
  const [, setLoginRev] = useState(0);
  useEffect(
    function () {
      if (!isOwner) {
        // Clear any badges left by a previous owner session in this runtime
        // (e.g. owner signs out, non-owner signs in) so login emails are
        // never shown to non-owner roles.
        window.FL.loginByEntity = {};
        return;
      }
      var cancelled = false;
      window.API.adminEntityLogins()
        .then(function (data) {
          if (cancelled) return;
          window.FL.loginByEntity = (data && data.logins) || {};
          setLoginRev(function (n) {
            return n + 1;
          });
        })
        .catch(function (_e) {
          /* non-fatal: chart simply renders without login badges */
        });
      return function () {
        cancelled = true;
      };
    },
    [isOwner, version],
  );

  useEffect(function () {
    return function () {
      var timers = toastTimers.current;
      Object.keys(timers).forEach(function (id) {
        clearTimeout(timers[id]);
      });
    };
  }, []);

  const dismissToast = function (id) {
    clearTimeout(toastTimers.current[id]);
    delete toastTimers.current[id];
    setToasts(function (prev) {
      return prev.map(function (t) {
        return t.id === id ? { id: t.id, msg: t.msg, exiting: true } : t;
      });
    });
    setTimeout(function () {
      setToasts(function (prev) {
        return prev.filter(function (t) {
          return t.id !== id;
        });
      });
    }, 220);
  };

  const showToast = function (msg, kind) {
    var id = Date.now() + "-" + Math.random();
    setToasts(function (prev) {
      return prev.concat({
        id: id,
        msg: msg,
        kind: kind || "",
        exiting: false,
      });
    });
    toastTimers.current[id] = setTimeout(function () {
      dismissToast(id);
    }, 4000);
  };

  // After each auto-refresh (version bump > 0), compare FL.PREV_CASES_BY_ID
  // with FL.CASES_BY_ID to surface any case that just moved to "acknowledged".
  // Iterating by caseId (not exceptionId) ensures that two cases for the same
  // exception — e.g. from a reopen→recontact flow — each produce their own toast
  // instead of the second one being silently dropped by the exceptionId index.
  // seenAckedCaseIds deduplicates toasts when two rapid polls arrive carrying
  // the same transition (e.g. two tabs open or a transient reload race).
  useEffect(
    function () {
      if (!version) return;
      var prev = FL.PREV_CASES_BY_ID || {};
      var curr = FL.CASES_BY_ID || {};
      var seen = seenAckedCaseIds.current;

      // Prune ids for cases that have left the active list or moved to resolved.
      seen.forEach(function (caseId) {
        var c = curr[caseId];
        if (!c || c.status === "resolved") seen.delete(caseId);
      });

      Object.keys(curr).forEach(function (caseId) {
        var c = curr[caseId];
        var p = prev[caseId];
        if (
          c.status === "acknowledged" &&
          (!p || p.status !== "acknowledged")
        ) {
          if (!seen.has(caseId)) {
            seen.add(caseId);
            showToast("Case acknowledged by employee");
          }
        }
      });
    },
    [version],
  );

  // Branch (shop) filter: narrows the exceptions feed to a single home branch.
  // Setting it stamps FL.branchFilter (read by FL.load) and reloads, so the
  // server returns only that branch's exceptions and every view derives from it.
  const branchOptions = FL.branchGroups();
  const onBranchChange = async (val) => {
    window.FL.branchFilter = val || null;
    setBranchFilter(val);
    await onReload();
  };

  useEffect(() => {
    document.body.classList.toggle("dark", !!t.dark);
  }, [t.dark]);

  const sum = FL.summary();

  const SEV_OPTIONS = [
    {
      value: null,
      label: "All severities",
      hint: "Show exceptions at every severity level",
    },
    {
      value: "red",
      label: "Critical",
      sev: "red",
      count: sum.sev.red,
      hint: "Exceptions needing immediate attention",
    },
    {
      value: "orange",
      label: "Escalated",
      sev: "orange",
      count: sum.sev.orange,
      hint: "Exceptions escalated up the org tree",
    },
    {
      value: "yellow",
      label: "Flagged",
      sev: "yellow",
      count: sum.sev.yellow,
      hint: "Exceptions flagged for review",
    },
  ];
  const GROUP_OPTIONS = [
    {
      value: "supervision",
      label: "Supervision",
      hint: "Group by who reports to whom (supervision hierarchy)",
    },
    {
      value: "branch",
      label: "Shop / Branch",
      hint: "Group people and trucks by their home shop / branch",
    },
  ];

  const goRegion = () => {
    setNav({ view: "region", unitId: nav.unitId, personId: null });
    setSel(null);
  };
  const goUnit = (id) => {
    setNav({ view: "unit", unitId: id, personId: null });
    setSel(null);
  };
  const goPerson = (id) => {
    const p = FL.peopleById[id];
    setNav({ view: "person", unitId: p ? p.unitId : nav.unitId, personId: id });
    setSel(null);
  };
  const goAdmin = () => {
    setNav({ view: "admin", unitId: nav.unitId, personId: null });
    setSel(null);
  };

  // Owner-only: open the admin invite form pre-filled for a specific org card.
  // State is lifted here so the pending invite survives the navigate-to-admin
  // re-render (a window event would fire before UsersTab mounts).
  const [pendingInviteCard, setPendingInviteCard] = useState(null);
  const handleInviteCard = isOwner
    ? function (entityId, name) {
        setPendingInviteCard({ entityId: entityId, name: name });
        setNav({ view: "admin", unitId: nav.unitId, personId: null });
        setSel(null);
      }
    : null;

  const openExc = (id) => setSel({ kind: "exc", id });
  const openPerson = (id) => setSel({ kind: "person", id });
  const openEntity = (ref) => setSel(ref);

  const onFilterByRule = function (ruleId) {
    setFilterRule(ruleId);
    setSel(null);
  };
  const clearFilterRule = function () {
    setFilterRule(null);
  };

  const ACT_TOAST = {
    resolve: "Exception resolved",
    acknowledge: "Exception acknowledged",
    snooze: "Exception snoozed",
    reopen: "Exception reopened",
  };

  const act = async (excId, action, extra) => {
    setPendingId(excId);
    try {
      await API.resolution(excId, action, extra);
      await onReload();
      if (ACT_TOAST[action]) showToast(ACT_TOAST[action]);
    } catch (e) {
      showToast(e.message || "Action failed.", "danger");
    } finally {
      setPendingId(null);
    }
  };

  const crumbUnitId =
    nav.view === "person"
      ? FL.peopleById[nav.personId]
        ? FL.peopleById[nav.personId].unitId
        : null
      : nav.view === "unit"
        ? nav.unitId
        : null;
  const crumbPath = crumbUnitId ? FL.unitPath(crumbUnitId) : [];

  // Client-side mirror of server WRITE_ACTION_ROLES (auth/visibility.ts): every
  // level except `employee` may act on exceptions/cases. The server still
  // enforces — this just avoids showing buttons that would only ever 403.
  const canWrite = user.role !== "employee";

  return (
    <ActionContext.Provider value={{ act, pendingId, canWrite }}>
      <div className="app">
        <div className="topbar">
          <div className="brand">
            <span className="brand-mark" />
            <span className="brand-name">FLOWLINE</span>
            <span className="brand-sub">WorkForce Services</span>
          </div>
          <div className="topbar-spacer" />
          <div className="sevbar">
            <FilterDropdown
              label="Severity"
              value={filterSev}
              options={SEV_OPTIONS}
              onChange={setFilterSev}
              title="Filter exceptions by severity level"
            />
            <div
              className="sevchip flow"
              title="People and units currently in flow (no open exceptions) — informational only, not a filter"
            >
              <Dot sev="green" style="filled" />
              <span className="num">{sum.inFlow}</span>
              <span className="lbl">In flow</span>
            </div>
          </div>
          <div className="usermenu">
            <NotificationsBell onOpenExc={openExc} />
            <div className="user-pill">
              <Avatar kind="person" size={22} />
              <div>
                <div className="uname">{user.name || user.email}</div>
                <div className="urole">{user.role}</div>
              </div>
            </div>
            <button
              className="btn ghost sm"
              onClick={function () {
                setShowChangePw(true);
              }}
            >
              Change password
            </button>
            <button
              className="btn ghost sm"
              data-testid="open-twofactor"
              onClick={function () {
                setShowTwoFactor(true);
              }}
            >
              Two-step verification
            </button>
            <button className="btn ghost sm" onClick={onSignOut}>
              Sign out
            </button>
          </div>
        </div>
        {showChangePw && (
          <ChangePasswordModal
            onClose={function () {
              setShowChangePw(false);
            }}
            onSuccess={function () {
              setShowChangePw(false);
              showToast("Password changed");
            }}
            onSignOutAll={onSignOutAll}
          />
        )}
        {showTwoFactor && (
          <TwoFactorModal
            user={user}
            onClose={function () {
              setShowTwoFactor(false);
            }}
            onChanged={function (msg) {
              setShowTwoFactor(false);
              showToast(msg);
              // The 2FA flag rides on /api/auth/me, so re-boot the shell to
              // pick up the new state (and clear an admin's enrolment gate).
              if (onReload) onReload();
            }}
          />
        )}

        <ContextStrip user={user} tier={tier} />

        <SourceHealthBanner version={version} />

        <div className="crumbrow">
          {/* manager tier: suppress the org-wide crumb so they can't navigate up to a level they can't act on */}
          {tier === "manager" ? (
            <span className="crumb crumb-static">{FL.orgName}</span>
          ) : (
            <button
              className={"crumb" + (nav.view === "region" ? " current" : "")}
              onClick={goRegion}
            >
              {FL.orgName}
            </button>
          )}
          {(nav.view === "unit" || nav.view === "person") &&
            crumbPath.map((u, i) => {
              const isCurrentUnitView =
                nav.view === "unit" && i === crumbPath.length - 1;
              return (
                <React.Fragment key={u.id}>
                  <span className="crumb-sep">›</span>
                  <button
                    className={"crumb" + (isCurrentUnitView ? " current" : "")}
                    onClick={() => goUnit(u.id)}
                  >
                    {u.name}
                  </button>
                </React.Fragment>
              );
            })}
          {nav.view === "person" && FL.peopleById[nav.personId] && (
            <>
              <span className="crumb-sep">›</span>
              <button className="crumb current">
                {FL.peopleById[nav.personId].name}
              </button>
            </>
          )}
          {nav.view === "admin" && (
            <>
              <span className="crumb-sep">›</span>
              <button className="crumb current">Admin</button>
            </>
          )}

          <div className="crumb-actions">
            {filterRule !== null && (
              <div
                style={{ display: "inline-flex", alignItems: "center", gap: 4 }}
              >
                <span className="tag neutral" style={{ fontSize: 11 }}>
                  {filterRule
                    ? "Rule: " +
                      (FL.humanize ? FL.humanize(filterRule) : filterRule)
                    : "All exceptions"}
                </span>
                <button
                  className="btn ghost sm"
                  style={{ fontSize: 11 }}
                  onClick={clearFilterRule}
                >
                  × Back
                </button>
              </div>
            )}
            {filterRule === null && nav.view !== "admin" && (
              <button
                className="btn ghost sm"
                style={{ fontSize: 11 }}
                onClick={() => setFilterRule("")}
                title="Show a flat, searchable list of all open exceptions with rule and severity filters"
              >
                All exceptions
              </button>
            )}
            {isOwner && (
              <button
                className={
                  "btn ghost sm" + (nav.view === "admin" ? " primary" : "")
                }
                onClick={goAdmin}
              >
                Admin
              </button>
            )}
            {nav.view === "region" && (
              <FilterDropdown
                label="Group by"
                value={regionMode}
                options={GROUP_OPTIONS}
                onChange={setRegionMode}
                title="Choose how units are grouped on the region view"
              />
            )}
            {nav.view === "region" && regionMode === "supervision" && (
              <button
                type="button"
                className={"toggle-switch" + (showFlagged ? " on" : "")}
                onClick={() => setShowFlagged((f) => !f)}
                role="switch"
                aria-checked={showFlagged}
                title="When on, only shows departments and shops that currently have an open exception. Turn off to also see units that are fully in flow."
              >
                <span className="track" />
                Out of flow only
              </button>
            )}
            {nav.view !== "admin" && tier !== "individual" && (
              <div
                className="branchsel"
                title="Filter exceptions down to a single shop / branch"
              >
                <span className="filterdd-label">Branch</span>
                <select
                  className="select"
                  style={{ height: 30, fontSize: 12, padding: "0 8px" }}
                  value={branchFilter}
                  onChange={(e) => onBranchChange(e.target.value)}
                >
                  <option value="">All branches</option>
                  {branchOptions.map((g) => (
                    <option key={g.id} value={g.id}>
                      {g.name}
                    </option>
                  ))}
                </select>
              </div>
            )}
            {/* individual tier has no org to navigate — hide the navigator entirely */}
            {tier !== "individual" && (
              <OrgNavigator
                nav={nav}
                onGoRegion={goRegion}
                onGoUnit={goUnit}
                onGoPerson={goPerson}
                hideOrgWide={tier === "manager"}
                onInviteCard={handleInviteCard}
              />
            )}
          </div>
        </div>

        <div className="main">
          {tier === "staff-filtered" && nav.view !== "admin" && (
            <StaffFilteredBanner role={user.role} />
          )}
          {filterRule !== null && nav.view !== "admin" && (
            <RuleExceptionsView
              ruleId={filterRule}
              onClear={clearFilterRule}
              onOpenExc={openExc}
            />
          )}
          {filterRule === null &&
            nav.view === "region" &&
            regionMode === "supervision" && (
              <RegionView
                t={t}
                filterSev={filterSev}
                showFlagged={showFlagged}
                onOpenUnit={goUnit}
                onOpenEntity={openEntity}
              />
            )}
          {filterRule === null &&
            nav.view === "region" &&
            regionMode === "branch" && (
              <BranchView
                t={t}
                filterSev={filterSev}
                onOpenEntity={openEntity}
              />
            )}
          {/* Coaching candidates — show on region view (multi-shop manager home)
              or unit view when it's one of their watched shops. Reads
              FL.COACHING directly in render so auto-refresh always surfaces
              fresh data. */}
          {filterRule === null &&
            (nav.view === "region" ||
              (nav.view === "unit" &&
                FL.watchedIds(user).indexOf(nav.unitId) !== -1)) &&
            tier === "manager" && (
              <CoachingCandidatesPanel onSelectPerson={goPerson} />
            )}
          {/* Shop switcher — only shown on unit view when manager covers > 1 shop */}
          {filterRule === null && nav.view === "unit" && tier === "manager" && (
            <ShopSwitcherPanel
              user={user}
              currentUnitId={nav.unitId}
              onGoUnit={goUnit}
            />
          )}
          {filterRule === null &&
            nav.view === "unit" &&
            tier === "manager" &&
            FL.watchedIds(user).indexOf(nav.unitId) !== -1 && (
              <>
                <TriagePanel unitId={nav.unitId} onOpenExc={openExc} />
                <CrewSummaryPanel
                  unitId={nav.unitId}
                  onSelectPerson={goPerson}
                />
              </>
            )}
          {filterRule === null && nav.view === "unit" && (
            <UnitOrgChart
              unitId={nav.unitId}
              t={t}
              filterSev={filterSev}
              selectedId={sel ? sel.kind + ":" + sel.id : null}
              onSelectPerson={openPerson}
              onSelectEntity={openEntity}
              onOpenUnit={goUnit}
              quietGreens={t.quietGreens}
              showLogins={isOwner}
              user={user}
              onReload={onReload}
            />
          )}
          {filterRule === null && nav.view === "person" && (
            <PersonDay
              personId={nav.personId}
              t={t}
              onSelectExc={openExc}
              onFilterByRule={onFilterByRule}
            />
          )}
          {nav.view === "admin" && (
            <AdminPanel
              onReload={onReload}
              onToast={showToast}
              currentUserId={user.id}
              pendingInviteCard={pendingInviteCard}
              onClearPendingInviteCard={function () {
                setPendingInviteCard(null);
              }}
            />
          )}
        </div>

        <Drawer
          sel={sel}
          t={t}
          onClose={() => setSel(null)}
          onOpenExc={openExc}
          onOpenPerson={(id) => {
            setSel(null);
            goPerson(id);
          }}
          onOpenDay={(id) => {
            setSel(null);
            goPerson(id);
          }}
          onFilterByRule={onFilterByRule}
        />

        <SettingsPanel t={t} setTweak={setTweak} />

        {toasts.length > 0 && (
          <div className="fl-toast-stack">
            {(function () {
              var TOAST_CAP = 5;
              var hiddenCount =
                toasts.length > TOAST_CAP ? toasts.length - TOAST_CAP : 0;
              var visibleToasts =
                hiddenCount > 0
                  ? toasts.slice(toasts.length - TOAST_CAP)
                  : toasts;
              return [
                hiddenCount > 0 ? (
                  <div key="__more" className="fl-toast-more">
                    +{hiddenCount} more notification
                    {hiddenCount === 1 ? "" : "s"}
                  </div>
                ) : null,
                visibleToasts.map(function (toast) {
                  return (
                    <div
                      key={toast.id}
                      className={
                        "fl-toast" +
                        (toast.kind ? " " + toast.kind : "") +
                        (toast.exiting ? " fl-toast-out" : "")
                      }
                    >
                      <span>{toast.msg}</span>
                      <button
                        className="fl-toast-x"
                        onClick={function () {
                          dismissToast(toast.id);
                        }}
                      >
                        ✕
                      </button>
                    </div>
                  );
                }),
              ];
            })()}
          </div>
        )}
      </div>
    </ActionContext.Provider>
  );
}

/* ======  ROOT (auth + data orchestration)  ====== */
function Root() {
  const [phase, setPhase] = useState("loading"); // loading | auth | pending | ready | error
  const [user, setUser] = useState(null);
  const [devLoginEnabled, setDevLoginEnabled] = useState(false);
  const [version, setVersion] = useState(0);
  const [errMsg, setErrMsg] = useState("");
  const [errKind, setErrKind] = useState("unknown"); // "network" | "server" | "unknown"

  const initialReset = (() => {
    try {
      return new URLSearchParams(window.location.search).get("reset") || "";
    } catch (_e) {
      return "";
    }
  })();

  const initialInvite = (() => {
    try {
      return new URLSearchParams(window.location.search).get("invite") || "";
    } catch (_e) {
      return "";
    }
  })();

  const boot = async () => {
    setPhase("loading");
    setErrMsg("");
    setErrKind("unknown");
    try {
      const me = await API.me();
      setDevLoginEnabled(!!me.devLoginEnabled);
      // Adopt the server's password rules so every password form states the
      // requirements the server will actually apply.
      if (me.passwordPolicy && me.passwordPolicy.requirements) {
        PASSWORD_POLICY = me.passwordPolicy;
      }
      if (!me.authenticated || !me.user) {
        setPhase("auth");
        return;
      }
      setUser(me.user);
      if (me.user.status !== "active") {
        setPhase("pending");
        return;
      }
      // Admins must have a second factor before the admin API will serve them,
      // so walk them through enrolment instead of dropping them into a panel
      // that would 403 on every call.
      if (me.user.twoFactorRequired && !me.user.twoFactorEnabled) {
        setPhase("twofactor");
        return;
      }
      await window.FL.load();
      setPhase("ready");
    } catch (e) {
      // A 401/403 during the initial load means the session is not valid —
      // show the login screen instead of a generic error.
      if (e && (e.status === 401 || e.status === 403)) {
        setPhase("auth");
        return;
      }
      // Classify the error so the UI can give actionable guidance.
      const isNetwork = !e || !e.status || e instanceof TypeError;
      setErrKind(
        isNetwork ? "network" : e.status >= 500 ? "server" : "unknown",
      );
      setErrMsg(e && e.message ? e.message : "Failed to load.");
      setPhase("error");
    }
  };

  useEffect(() => {
    boot();
  }, []);

  // Auto-refresh every 30 seconds while the app is live so supervisors see
  // status changes (including employee acknowledgements) without manual refresh.
  useEffect(
    function () {
      if (phase !== "ready") return undefined;
      var id = setInterval(function () {
        reload();
      }, 30000);
      return function () {
        clearInterval(id);
      };
    },
    [phase],
  );

  const reload = async () => {
    try {
      await window.FL.load();
      setVersion((v) => v + 1);
    } catch (e) {
      // A 401/403 on a background poll means the session expired — redirect to
      // login rather than silently leaving the user looking at stale data.
      if (e && (e.status === 401 || e.status === 403)) {
        setPhase("auth");
        return;
      }
      // Transient network / server errors on background polls are non-fatal:
      // leave the dashboard showing the last-good data. Repeated toasts from a
      // flaky connection would be more disruptive than silent retry.
    }
  };
  // Expose reload globally so browser tests can trigger a version bump without
  // relying on the 30-second auto-poll interval.
  window.__flReload = reload;

  const signOut = async () => {
    try {
      await API.logout();
    } catch (_e) {}
    // Drop owner-only login-link data so the next session (possibly a
    // non-owner in the same tab) can never render badges from stale state.
    window.FL.loginByEntity = {};
    // Flip the phase before clearing the user. Under legacy ReactDOM.render,
    // state updates in this async callback are not batched, so each triggers a
    // synchronous re-render; setting phase first guarantees the intermediate
    // render shows the auth screen rather than App with a null user.
    setPhase("auth");
    setUser(null);
  };

  // Revokes every session for this account (all devices, including this
  // one), then runs the same local cleanup as signOut — logout-all already
  // clears this device's cookie server-side.
  const signOutAll = async () => {
    try {
      await API.logoutAll();
    } catch (_e) {}
    window.FL.loginByEntity = {};
    setPhase("auth");
    setUser(null);
  };

  if (phase === "loading") {
    return (
      <div className="auth-wrap">
        <div className="auth-card" style={{ textAlign: "center" }}>
          <div className="brand-mark" style={{ margin: "0 auto 14px" }} />
          <div className="auth-desc" style={{ margin: 0 }}>
            Loading exception data…
          </div>
        </div>
      </div>
    );
  }
  if (phase === "error") {
    const isNetwork = errKind === "network";
    const isServer = errKind === "server";
    return (
      <div className="auth-wrap">
        <div className="auth-card">
          <div className="auth-title">
            {isNetwork ? "Can't reach the server" : "Something went wrong"}
          </div>
          <div className="auth-msg err">{errMsg}</div>
          {isNetwork && (
            <div
              className="auth-desc"
              style={{ marginBottom: 12, marginTop: -4 }}
            >
              Check your network connection and try again.
            </div>
          )}
          {isServer && (
            <div
              className="auth-desc"
              style={{ marginBottom: 12, marginTop: -4 }}
            >
              The server returned an error. If this keeps happening, contact{" "}
              <a href="mailto:support@flowline.app">support@flowline.app</a>.
            </div>
          )}
          <button className="btn primary auth-btn" onClick={boot}>
            Retry
          </button>
        </div>
      </div>
    );
  }
  if (phase === "auth") {
    return (
      <AuthScreen
        devLoginEnabled={devLoginEnabled}
        initialReset={initialReset}
        initialInvite={initialInvite}
        onAuthed={boot}
      />
    );
  }
  if (phase === "pending") {
    return <PendingScreen user={user} onSignOut={signOut} />;
  }
  if (phase === "twofactor" && user) {
    return (
      <TwoFactorRequiredScreen user={user} onDone={boot} onSignOut={signOut} />
    );
  }
  // App requires an authenticated user. Guard against any transient render where
  // phase is "ready" but the user has been cleared (e.g. mid sign-out), which
  // would otherwise throw on user.role.
  if (phase === "ready" && user) {
    return (
      <App
        version={version}
        user={user}
        onSignOut={signOut}
        onSignOutAll={signOutAll}
        onReload={reload}
      />
    );
  }
  return null;
}

ReactDOM.render(<Root />, document.getElementById("root"));
