import { createFileRoute } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useAuth, type AppRole } from "@/lib/auth"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer, PageHeader } from "@/components/app-shell"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Loader2, UserPlus, Trash2 } from "lucide-react"; import { toast } from "sonner"; export const Route = createFileRoute("/admin/users")({ component: () => ( ), }); interface UserRow { id: string; email: string; full_name: string; created_at: string; hourly_rate: number | null; role: AppRole | null; } const ROLE_LABEL: Record = { admin: "Administrator", attorney: "Attorney", staff: "Staff", }; function UsersPage() { const { user: currentUser } = useAuth(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [inviteOpen, setInviteOpen] = useState(false); 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("user_roles").select("user_id, role"), ]); const roleMap = new Map(); (rolesData ?? []).forEach((r) => roleMap.set(r.user_id, r.role as AppRole)); const rows: UserRow[] = (profiles ?? []).map((p: any) => ({ id: p.id, email: p.email, full_name: p.full_name, created_at: p.created_at, hourly_rate: p.hourly_rate, role: roleMap.get(p.id) ?? null, })); rows.sort((a, b) => a.email.localeCompare(b.email)); setUsers(rows); setLoading(false); }; useEffect(() => { load(); }, []); const handleRoleChange = async (userId: string, role: AppRole) => { const { error } = await supabase.functions.invoke("admin-update-role", { body: { user_id: userId, role }, }); if (error) { toast.error("Could not update role", { description: error.message }); return; } toast.success("Role updated"); load(); }; const handleDelete = async (userId: string) => { const { error } = await supabase.functions.invoke("admin-delete-user", { body: { user_id: userId }, }); if (error) { toast.error("Could not delete user", { description: error.message }); return; } toast.success("User deleted"); load(); }; const handleRateSave = async (userId: string, rate: number | null) => { const { error } = await supabase .from("profiles") .update({ hourly_rate: rate }) .eq("id", userId); if (error) { toast.error("Could not save rate", { description: error.message }); return; } toast.success("Rate updated"); load(); }; return ( { setInviteOpen(false); load(); }} /> } /> Team members {users.length} total {loading ? (
) : users.length === 0 ? (
No users yet.
) : ( Name Email Role Hourly rate {users.map((u) => { const isSelf = u.id === currentUser?.id; return ( {u.full_name || —} {isSelf && ( You )} {u.email} handleRateSave(u.id, v)} /> {!isSelf && ( Delete user? This will permanently remove {u.email} and revoke their access. Cancel handleDelete(u.id)}> Delete )} ); })}
)}
); } function RateInput({ value, onSave, }: { value: number | null; onSave: (v: number | null) => void | Promise; }) { const [text, setText] = useState(value != null ? String(value) : ""); useEffect(() => setText(value != null ? String(value) : ""), [value]); const dirty = text !== (value != null ? String(value) : ""); const commit = () => { if (!dirty) return; if (text.trim() === "") return onSave(null); const n = parseFloat(text); if (Number.isNaN(n) || n < 0) { toast.error("Invalid rate"); setText(value != null ? String(value) : ""); return; } onSave(n); }; return (
$ setText(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} placeholder="—" className="h-8 w-24" /> /hr
); } function InviteDialog({ onCreated }: { onCreated: () => void }) { const [email, setEmail] = useState(""); const [fullName, setFullName] = useState(""); const [password, setPassword] = useState(""); const [role, setRole] = useState("attorney"); const [submitting, setSubmitting] = useState(false); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); const { error } = await supabase.functions.invoke("admin-invite-user", { body: { email: email.trim(), password, full_name: fullName.trim(), role }, }); setSubmitting(false); if (error) { toast.error("Could not create user", { description: error.message }); return; } toast.success("User created", { description: "Share the temporary password securely with them.", }); setEmail(""); setFullName(""); setPassword(""); setRole("attorney"); onCreated(); }; return ( Invite user Create an account with a temporary password. Share it with them through a secure channel.
setFullName(e.target.value)} required />
setEmail(e.target.value)} required />
setPassword(e.target.value)} minLength={10} required />

Min 10 characters. The user can change it after first sign-in.

); }