import { createFileRoute } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { api } from '@/lib/api-client' import { userListOptions, userRolesOptions, userMutations, type UserRecord } from '@/api/users' import { roleListOptions, rbacMutations } from '@/api/rbac' import { usePagination } from '@/hooks/use-pagination' import { DataTable, type Column } from '@/components/shared/data-table' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { MoreVertical, Shield, Plus, X, KeyRound, Search, UserCheck, UserX } from 'lucide-react' import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth.store' export const Route = createFileRoute('/_authenticated/users')({ validateSearch: (search: Record) => ({ page: Number(search.page) || 1, limit: Number(search.limit) || 25, q: (search.q as string) || undefined, sort: (search.sort as string) || undefined, order: (search.order as 'asc' | 'desc') || 'asc', }), component: UsersPage, }) function ManageRolesDialog({ user, open, onClose }: { user: UserRecord; open: boolean; onClose: () => void }) { const queryClient = useQueryClient() const { data: userRolesData } = useQuery(userRolesOptions(user.id)) const { data: allRolesData } = useQuery(roleListOptions()) const [selectedRoleId, setSelectedRoleId] = useState('') const userRoles = userRolesData?.data ?? [] const allRoles = allRolesData?.data ?? [] const assignedIds = new Set(userRoles.map((r) => r.id)) const availableRoles = allRoles.filter((r) => !assignedIds.has(r.id)) const assignMutation = useMutation({ mutationFn: (roleId: string) => rbacMutations.assignRole(user.id, roleId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }) setSelectedRoleId('') toast.success('Role assigned') }, onError: (err) => toast.error(err.message), }) const removeMutation = useMutation({ mutationFn: (roleId: string) => rbacMutations.removeRole(user.id, roleId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }) toast.success('Role removed') }, onError: (err) => toast.error(err.message), }) return ( !o && onClose()}> Manage Roles — {user.firstName} {user.lastName}

Assigned Roles

{userRoles.length === 0 ? (

No roles assigned

) : (
{userRoles.map((r) => (
{r.name} {r.isSystem && System}
))}
)}
{availableRoles.length > 0 && (
)}
) } function UsersPage() { const queryClient = useQueryClient() const hasPermission = useAuthStore((s) => s.hasPermission) const { params, setPage, setSearch, setSort } = usePagination() const [searchInput, setSearchInput] = useState(params.q ?? '') const [managingUser, setManagingUser] = useState(null) const { data, isLoading } = useQuery(userListOptions(params)) const statusMutation = useMutation({ mutationFn: ({ userId, isActive }: { userId: string; isActive: boolean }) => userMutations.toggleStatus(userId, isActive), onSuccess: (_data, vars) => { queryClient.invalidateQueries({ queryKey: ['users'] }) toast.success(vars.isActive ? 'User enabled' : 'User disabled') }, onError: (err) => toast.error(err.message), }) function handleSearchSubmit(e: React.FormEvent) { e.preventDefault() setSearch(searchInput) } const userColumns: Column[] = [ { key: 'name', header: 'Name', sortable: true, render: (row) => ( {row.firstName} {row.lastName} ), }, { key: 'email', header: 'Email', sortable: true, render: (row) => {row.email}, }, { key: 'roles', header: 'Roles', render: (row) => row.roles.length === 0 ? ( No roles ) : (
{row.roles.map((r) => ( {r.name} ))}
), }, { key: 'status', header: 'Status', render: (row) => row.isActive ? ( Active ) : ( Disabled ), }, { key: 'created_at', header: 'Created', sortable: true, render: (row) => new Date(row.createdAt).toLocaleDateString(), }, { key: 'actions', header: '', render: (row) => ( {hasPermission('users.edit') && ( setManagingUser(row)}> Manage Roles )} {hasPermission('users.admin') && ( { try { const res = await api.post<{ resetLink: string }>(`/v1/auth/reset-password/${row.id}`, {}) await navigator.clipboard.writeText(res.resetLink) toast.success('Reset link copied to clipboard') } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to generate link') } }}> Reset Password Link )} {hasPermission('users.admin') && ( statusMutation.mutate({ userId: row.id, isActive: !row.isActive })} > {row.isActive ? ( <> Disable User ) : ( <> Enable User )} )} ), }, ] return (

Users

{managingUser && ( setManagingUser(null)} /> )}
setSearchInput(e.target.value)} className="pl-9" />
) }