import { useEffect, useState, type FormEvent, type KeyboardEvent } from "react"; import { adminApi } from "../../auth/api"; import { roleChipClass, roleFromKind, sortRoles } from "../../auth/roles"; import { admin9Api, type ReconcileAnswer, type RefreshRequested } from "../../api/admin"; import type { AdLookup, AdminUser, GroupRow, PinState } from "../../auth/types"; import { useAuth } from "../../auth/useAuth"; import { errorText, fmtTime } from "../../lib/format"; import { useLoad, type Load } from "../../lib/useLoad"; import { FeedTab, ResourcesTab } from "./FeedTabs"; import { AccessLogTab } from "./AccessLogTab"; import { ChecklistsTab } from "./ChecklistsTab"; import { DepartmentsTab } from "./DepartmentsTab"; import { NotificationsTab } from "./NotificationsTab"; import { PresenceTab } from "./PresenceTab"; import { ProfilesTab } from "./ProfilesTab"; import { SecurityTab } from "./SecurityTab"; import { StampVerifyTab } from "./StampVerifyTab"; import { StorageTab } from "./StorageTab"; type Tab = | "users" | "lookup" | "devices" | "groups" | "attempts" | "feed" | "resources" | "storage" | "departments" | "profiles" | "checklists" | "security" | "accesslog" | "presence" | "stampverify" | "notifications"; const TABS: ReadonlyArray<{ id: Tab; label: string }> = [ { id: "users", label: "Users" }, { id: "lookup", label: "Look up AD account" }, { id: "devices", label: "Devices" }, { id: "groups", label: "Group map" }, { id: "attempts", label: "Sign-in attempts" }, { id: "feed", label: "Feed" }, { id: "resources", label: "VISUAL resources" }, { id: "storage", label: "Storage" }, { id: "departments", label: "Departments" }, { id: "profiles", label: "Profiles" }, { id: "checklists", label: "Checklists" }, { id: "security", label: "Security" }, { id: "accesslog", label: "Access log" }, { id: "presence", label: "Who is in" }, { id: "stampverify", label: "Stamp check" }, { id: "notifications", label: "Notifications" }, ]; const ROLES_NOTE = "Roles come from AD groups. Ask IT to change them."; function Problem({ load }: { load: Load }) { if (load.state === "loading") return
Loading
; if (load.state === "error") return
{load.message}
; return null; } function RoleChips({ roles, label }: { roles: readonly string[]; label: string }) { const { roleLabel } = useAuth(); if (roles.length === 0) return No roles; return ( ); } function GroupChips({ groups }: { groups: readonly string[] }) { if (groups.length === 0) return No Packet Station groups; return ( ); } function PinPill({ pin }: { pin: PinState }) { if (!pin.set) return None; if (pin.disabled) return Disabled; return Set; } /* ---------- Users ---------- */ function UserDetail({ id, onChanged, onClose }: { id: string; onChanged: () => void; onClose: () => void }) { const [load, reload] = useLoad((s) => adminApi.user(id, s), id); const [reason, setReason] = useState(""); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); const clearPin = async (e: FormEvent) => { e.preventDefault(); if (reason.trim().length < 8) { setMsg({ ok: false, text: "Give a reason of at least 8 characters." }); return; } setBusy(true); try { await adminApi.clearPin(id, reason.trim()); setReason(""); setMsg({ ok: true, text: "PIN cleared. The user sets a new one from Account." }); reload(); onChanged(); } catch (err) { setMsg({ ok: false, text: errorText(err) }); } finally { setBusy(false); } }; if (load.state !== "ok") { return (
); } const u = load.data; return (

{u.display_name}

{u.sam_account}
Employee ID
{u.emp_no ?? Missing}
Email
{u.email ?? "-"}
Last sign-in
{fmtTime(u.last_signin_at)}
Read from AD
{fmtTime(u.ad_synced_at)}
Groups{u.groups_source === "cache" ? " (cached, AD unreachable)" : " (live)"}
Roles
{u.note || ROLES_NOTE}

Sessions

{u.sessions.map((s) => ( ))} {u.sessions.length === 0 ? ( ) : null}
Started Last seen Sign-in Device Address
{fmtTime(s.created_at)} {fmtTime(s.last_seen_at)} {s.kind === "pin" ? "PIN" : "Password"} {s.device?.name ?? "-"} {s.ip ?? "-"}
Not signed in.

PIN

{u.pin.set ? (
) : null} {msg ? (
{msg.text}
) : null}
); } function UsersTab() { const [q, setQ] = useState(""); const [query, setQuery] = useState(""); const [selected, setSelected] = useState(null); const [load, reload] = useLoad((s) => adminApi.users(query, s), query); // Search as the admin types, a moment after the last key. useEffect(() => { const t = window.setTimeout(() => setQuery(q.trim()), 250); return () => window.clearTimeout(t); }, [q]); const rows: AdminUser[] = load.state === "ok" ? load.data : []; const open = (id: string) => setSelected((cur) => (cur === id ? null : id)); const onRowKey = (e: KeyboardEvent, id: string) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(id); } }; return (
e.preventDefault()}>
{load.state === "ok" ? (
{rows.map((u) => ( open(u.id)} onKeyDown={(e) => onRowKey(e, u.id)} > ))} {rows.length === 0 ? ( ) : null}
Name Account Employee ID Roles PIN Status
{u.display_name} {u.sam_account} {u.emp_no ?? ""} {u.flags.emp_missing ? Missing : null} {u.flags.emp_duplicate ? Duplicate : null} {u.flags.disabled ? ( Disabled in AD ) : u.active ? ( Active ) : ( Inactive )}
No users match.
) : null} {selected ? setSelected(null)} /> : null}
); } /* ---------- Look up AD account ---------- */ function LookupTab() { const [login, setLogin] = useState(""); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const submit = async (e: FormEvent) => { e.preventDefault(); if (!login.trim()) return; setBusy(true); setError(null); try { setResult(await adminApi.lookup(login.trim())); } catch (err) { setResult(null); setError(errorText(err)); } finally { setBusy(false); } }; const u = result?.user ?? null; return (
Reads one account live from AD. Nothing is saved.
{error ? (
{error}
) : null} {result && !result.found ?
No account by that name in AD.
: null} {result && result.found ? (

{u?.display_name ?? login}

{u?.sam_account ?? ""} {result.disabled ? Disabled in AD : Enabled}
UPN
{u?.upn ?? "-"}
Email
{u?.email ?? "-"}
Employee ID
{u?.emp_no ?? Missing}
Groups
Roles
) : null}
); } /* ---------- Devices ---------- */ function DevicesTab() { const [load, reload] = useLoad((s) => adminApi.devices(s)); const [confirm, setConfirm] = useState(null); const [error, setError] = useState(null); const revoke = async (id: string) => { setError(null); try { await adminApi.revokeDevice(id); setConfirm(null); reload(); } catch (err) { setError(errorText(err)); } }; const rows = load.state === "ok" ? load.data : []; return (
Revoking a device ends every PIN session on it.
{error ? (
{error}
) : null} {load.state === "ok" ? (
{rows.map((d) => ( ))} {rows.length === 0 ? ( ) : null}
Name Registered by Registered Last seen Sessions Status
{d.name} {d.registered_by?.display_name ?? "-"} {fmtTime(d.registered_at)} {fmtTime(d.last_seen_at)} {d.active_sessions} {d.revoked_at ? Revoked : Active} {d.revoked_at ? null : confirm === d.id ? ( <> {" "} ) : ( )}
No devices registered.
) : null}
); } /* ---------- Group map ---------- */ function InAdPill({ inAd }: { inAd: boolean | null }) { if (inAd === true) return In AD; if (inAd === false) return Missing in AD; return AD unreachable; } type ReportValue = string[] | number | undefined; const count = (v: ReportValue) => (Array.isArray(v) ? v.length : (v ?? 0)); const names = (v: ReportValue) => (Array.isArray(v) ? v : []); function ReconcileReportView({ load, onReload }: { load: Load; onReload: () => void }) { if (load.state !== "ok") return ; const { run, report } = load.data; const r = report ?? {}; const tiles: Array<[number, string, boolean]> = [ [count(r.groups_missing), "Groups missing", count(r.groups_missing) > 0], [count(r.groups_extra), "Extra groups", false], [r.users_seen ?? 0, "Users seen", false], [r.created ?? 0, "Created", false], [r.updated ?? 0, "Updated", false], [r.deactivated ?? 0, "Deactivated", (r.deactivated ?? 0) > 0], [r.sessions_ended ?? 0, "Sessions ended", (r.sessions_ended ?? 0) > 0], [count(r.emp_missing), "No employee ID", count(r.emp_missing) > 0], [count(r.emp_duplicate), "Duplicate employee IDs", count(r.emp_duplicate) > 0], ]; return (

Latest reconcile

{run ? `${run.status}, finished ${fmtTime(run.finished_at)}` : "Not run yet. It runs nightly at 01:30."}
{run ? (
{tiles.map(([n, l, hot]) => (
{n}
{l}
))}
) : null} {names(r.groups_missing).length ? (
Missing groups: {names(r.groups_missing).join(", ")}
) : null} {names(r.emp_missing).length ? (
No employee ID in AD: {names(r.emp_missing).join(", ")}
) : null} {names(r.emp_duplicate).length ? (
Employee IDs used by more than one account: {names(r.emp_duplicate).join(", ")}
) : null}
); } function GroupsTab() { const { roleLabel } = useAuth(); const [load, reload] = useLoad((s) => adminApi.groups(s)); const [report, reloadReport] = useLoad((s) => admin9Api.reconcile(s)); const [busy, setBusy] = useState(false); const [requested, setRequested] = useState(null); const [error, setError] = useState(null); const refresh = async () => { setBusy(true); setError(null); try { setRequested(await admin9Api.refreshAd()); reload(); reloadReport(); } catch (err) { setError(errorText(err)); } finally { setBusy(false); } }; const map = load.state === "ok" ? load.data : null; const missing = map ? map.groups.filter((g) => g.in_ad === false).length : 0; const label = (g: GroupRow) => roleLabel(roleFromKind(g.role, g.dept_code)); return (
The groups Packet Station reads, from the departments table. {map?.checked_at ? ` Checked ${fmtTime(map.checked_at)}.` : ""}
{error ? (
{error}
) : null} {requested ? (
Refresh requested. The worker runs the AD reconcile within a minute {requested.pending > 1 ? `, after ${requested.pending - 1} other ${requested.pending === 2 ? "request" : "requests"}` : ""}. Then show the latest report.
) : null} {map && !map.ad_reachable ?
AD is unreachable. Showing the expected groups only.
: null} {map && missing > 0 ? (
{missing === 1 ? "1 group is" : `${missing} groups are`} missing in AD. Ask IT to create {missing === 1 ? "it" : "them"}.
) : null} {map ? (
{map.groups.map((g) => ( ))}
Group Role In AD
{g.name} {label(g)}
) : null} {map ? (

Other groups in the OU

{map.extra_in_ou.length ? ( <>
Packet Station does not use these.
    {map.extra_in_ou.map((g) => (
  • {g}
  • ))}
) : (
None.
)}
) : null}
); } /* ---------- Sign-in attempts ---------- */ function AttemptsTab() { const [load, reload] = useLoad((s) => adminApi.signinAttempts(100, s)); const rows = load.state === "ok" ? load.data : []; return (
Recent failed attempts. Newest first.
{load.state === "ok" ? (
{rows.map((a, i) => ( ))} {rows.length === 0 ? ( ) : null}
When Account Kind Reason Address
{fmtTime(a.at)} {a.account} {a.kind} {a.reason ? a.reason.replace(/_/g, " ") : "-"} {a.ip ?? "-"}
No failed attempts.
) : null}
); } /** /admin: users, AD, devices, groups, sign-ins, the feed, storage and the phase 9 tabs. Needs a password session. */ export function AdminPage() { const { me } = useAuth(); const [tab, setTab] = useState("users"); const head = (
Admin

Users and access

); if (me?.session.kind !== "password") { return ( <> {head}
Admin needs a password sign-in. Sign out and sign in with your domain password.
); } return ( <> {head} {/* Sixteen tabs: they wrap into rows rather than scroll sideways on a narrow screen. */}
{TABS.map((t) => ( ))}
{tab === "users" ? : null} {tab === "lookup" ? : null} {tab === "devices" ? : null} {tab === "groups" ? : null} {tab === "attempts" ? : null} {tab === "feed" ? : null} {tab === "resources" ? : null} {tab === "storage" ? : null} {tab === "departments" ? : null} {tab === "profiles" ? : null} {tab === "checklists" ? : null} {tab === "security" ? : null} {tab === "accesslog" ? : null} {tab === "presence" ? : null} {tab === "stampverify" ? : null} {tab === "notifications" ? : null}
); }