Added title field to profiles
X-Lovable-Edit-ID: edt-59875660-d6c5-4bcb-8e53-7933a1edd63b Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user