import { useMemo, useState } from "react"; import { ApiError } from "../../api/client"; import type { UserAdmin } from "../../api/types"; import { useAuth } from "../../auth/AuthContext"; import { PageHeader } from "../../components/layout/PageHeader"; import { EditIcon, PlusIcon, RefreshIcon, SearchIcon, TrashIcon, } from "../../components/layout/icons"; import { Badge } from "../../components/ui/Badge"; import { Button } from "../../components/ui/Button"; import { Input } from "../../components/ui/Input"; import { Panel } from "../../components/ui/Panel"; import { EmptyState, ErrorState } from "../../components/ui/States"; import { StatusDot } from "../../components/ui/StatusDot"; import { roleLabel, roleTone, userStatusLabel, userStatusTone, } from "../../components/ui/status"; import { Table, type Column } from "../../components/ui/Table"; import { TableSkeleton } from "../../components/ui/TableSkeleton"; import { absoluteTime, relativeTime } from "../../lib/time"; import { CreateUserModal } from "./CreateUserModal"; import { DeleteUserDialog } from "./DeleteUserDialog"; import { EditUserModal } from "./EditUserModal"; import { useUsers } from "./hooks"; import "./users.css"; /** Compact permissions summary: admins are implicit-all; others list the set. */ function permissionsSummary(user: UserAdmin): string { if (user.role === "admin") return "All permissions"; if (user.permissions.length === 0) return "No permissions"; return user.permissions.join(", "); } export function UsersPage() { const { user: currentUser } = useAuth(); const usersQuery = useUsers(); const [filter, setFilter] = useState(""); const [createOpen, setCreateOpen] = useState(false); const [editFor, setEditFor] = useState(null); const [deleteFor, setDeleteFor] = useState(null); const { data } = usersQuery; const users = useMemo(() => data ?? [], [data]); const filtered = useMemo(() => { const q = filter.trim().toLowerCase(); if (!q) return users; return users.filter( (u) => u.username.toLowerCase().includes(q) || (u.email?.toLowerCase().includes(q) ?? false) || u.role.toLowerCase().includes(q), ); }, [users, filter]); const adminCount = useMemo( () => users.filter((u) => u.role === "admin").length, [users], ); const columns: Column[] = [ { key: "status", header: "", cellClass: "dt__status", render: (u) => ( ), }, { key: "username", header: "User", render: (u) => ( {u.username} {currentUser?.id === u.id && ( You )} {u.email && {u.email}} ), }, { key: "role", header: "Role", render: (u) => {roleLabel(u.role)}, }, { key: "permissions", header: "Permissions", render: (u) => ( {permissionsSummary(u)} ), }, { key: "status_label", header: "Status", render: (u) => ( {userStatusLabel(u.enabled)} ), }, { key: "created_at", header: "Created", render: (u) => ( {relativeTime(u.created_at)} ), }, { key: "last_login", header: "Last login", render: (u) => ( {u.last_login ? relativeTime(u.last_login) : "never"} ), }, { key: "actions", header: "", cellClass: "dt__actions", render: (u) => { const isSelf = currentUser?.id === u.id; return ( e.stopPropagation()} > {/* Self-delete is blocked client- and server-side; hide it on the signed-in admin's own row so it is never offered. */} {!isSelf && ( )} ); }, }, ]; return (
} />
setFilter(e.target.value)} aria-label="Filter users" />
{adminCount} admin ยท{" "} {users.length} total
{usersQuery.isLoading ? ( <> Loading users ) : usersQuery.isError ? ( void usersQuery.refetch()} > Retry } /> ) : filtered.length === 0 ? ( filter ? ( setFilter("")}> Clear filter } /> ) : ( setCreateOpen(true)}> Add user } /> ) ) : ( u.id} onRowClick={setEditFor} rowLabel={(u) => u.username} /> )} setCreateOpen(false)} /> setEditFor(null)} /> setDeleteFor(null)} /> ); }