Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-11 14:46:22 +00:00
co-authored by renee-png
parent f21d24c556
commit 1cabb9b383
+97 -4
View File
@@ -68,6 +68,7 @@ interface UserRow {
full_name: string;
created_at: string;
hourly_rate: number | null;
title: string | null;
role: AppRole | null;
}
@@ -79,16 +80,27 @@ const ROLE_LABEL: Record<AppRole, string> = {
staff: "Staff",
};
const TITLE_OPTIONS = [
"Attorney",
"Paralegal",
"Legal Assistant",
"Admin",
"Other",
] as const;
type UserTitle = (typeof TITLE_OPTIONS)[number];
function UsersPage() {
const { user: currentUser } = useAuth();
const [users, setUsers] = useState<UserRow[]>([]);
const [loading, setLoading] = useState(true);
const [inviteOpen, setInviteOpen] = useState(false);
const [search, setSearch] = useState("");
const [titleFilter, setTitleFilter] = useState<string>("all");
const load = async () => {
setLoading(true);
const [{ data: profiles }, { data: rolesData }] = await Promise.all([
supabase.from("profiles").select("id, email, full_name, created_at, hourly_rate"),
supabase.from("profiles").select("id, email, full_name, created_at, hourly_rate, title"),
supabase.from("user_roles").select("user_id, role"),
]);
const roleMap = new Map<string, AppRole>();
@@ -99,6 +111,7 @@ function UsersPage() {
full_name: p.full_name,
created_at: p.created_at,
hourly_rate: p.hourly_rate,
title: p.title ?? null,
role: roleMap.get(p.id) ?? null,
}));
rows.sort((a, b) => a.email.localeCompare(b.email));
@@ -147,6 +160,38 @@ function UsersPage() {
load();
};
const handleTitleChange = async (userId: string, title: string | null) => {
const { error } = await supabase
.from("profiles")
.update({ title })
.eq("id", userId);
if (error) {
toast.error("Could not update title", { description: error.message });
return;
}
toast.success("Title updated");
load();
};
const filteredUsers = users.filter((u) => {
if (titleFilter !== "all") {
if (titleFilter === "__none__" ? u.title : u.title !== titleFilter) return false;
}
const q = search.trim().toLowerCase();
if (!q) return true;
return (
u.email.toLowerCase().includes(q) ||
(u.full_name ?? "").toLowerCase().includes(q) ||
(u.title ?? "").toLowerCase().includes(q)
);
});
const titleCounts = TITLE_OPTIONS.reduce<Record<string, number>>((acc, t) => {
acc[t] = users.filter((u) => u.title === t).length;
return acc;
}, {});
const noTitleCount = users.filter((u) => !u.title).length;
return (
<PageContainer>
<PageHeader
@@ -173,14 +218,41 @@ function UsersPage() {
<Card>
<CardHeader>
<CardTitle className="font-serif">Team members</CardTitle>
<CardDescription>{users.length} total</CardDescription>
<CardDescription>
{filteredUsers.length} of {users.length} shown
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row gap-2 mb-4">
<div className="relative flex-1">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search by name, email, or title"
className="pl-8"
/>
</div>
<Select value={titleFilter} onValueChange={setTitleFilter}>
<SelectTrigger className="w-full sm:w-[220px]">
<SelectValue placeholder="Filter by title" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All titles ({users.length})</SelectItem>
<SelectItem value="__none__">No title ({noTitleCount})</SelectItem>
{TITLE_OPTIONS.map((t) => (
<SelectItem key={t} value={t}>
{t} ({titleCounts[t] ?? 0})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : users.length === 0 ? (
) : filteredUsers.length === 0 ? (
<div className="text-sm text-muted-foreground py-8 text-center">No users yet.</div>
) : (
<Table>
@@ -188,13 +260,14 @@ function UsersPage() {
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead className="w-[170px]">Title</TableHead>
<TableHead>Role</TableHead>
<TableHead className="w-[140px]">Hourly rate</TableHead>
<TableHead className="w-[120px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((u) => {
{filteredUsers.map((u) => {
const isSelf = u.id === currentUser?.id;
return (
<TableRow key={u.id}>
@@ -207,6 +280,26 @@ function UsersPage() {
)}
</TableCell>
<TableCell className="text-muted-foreground">{u.email}</TableCell>
<TableCell>
<Select
value={u.title ?? "__none__"}
onValueChange={(v) =>
handleTitleChange(u.id, v === "__none__" ? null : v)
}
>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="No title" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">No title</SelectItem>
{TITLE_OPTIONS.map((t) => (
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Select
value={u.role ?? undefined}