feat: add user management components including header, loading states, and table with pagination

This commit is contained in:
JSC
2025-08-17 11:44:08 +02:00
parent 75ecd26e06
commit 46bfcad271
5 changed files with 515 additions and 130 deletions

View File

@@ -1,7 +1,14 @@
import { AppLayout } from '@/components/AppLayout'
import { AppPagination } from '@/components/AppPagination'
import { UsersHeader, type UserSortField, type SortOrder, type UserStatus } from '@/components/admin/UsersHeader'
import {
UsersEmpty,
UsersError,
UsersLoading,
} from '@/components/admin/UsersLoadingStates'
import { UsersTable } from '@/components/admin/UsersTable'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
@@ -12,20 +19,10 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Sheet, SheetContent } from '@/components/ui/sheet'
import { Skeleton } from '@/components/ui/skeleton'
import { Switch } from '@/components/ui/switch'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { type Plan, adminService } from '@/lib/api/services/admin'
import type { User } from '@/types/auth'
import { formatDate } from '@/utils/format-date'
import { Edit, UserCheck, UserX } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
@@ -40,6 +37,21 @@ export function UsersPage() {
const [users, setUsers] = useState<User[]>([])
const [plans, setPlans] = useState<Plan[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Search and filtering state
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState<UserSortField>('name')
const [sortOrder, setSortOrder] = useState<SortOrder>('asc')
const [statusFilter, setStatusFilter] = useState<UserStatus>('all')
// Pagination state
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalCount, setTotalCount] = useState(0)
const [pageSize, setPageSize] = useState(10)
// Edit user state
const [editingUser, setEditingUser] = useState<User | null>(null)
const [editData, setEditData] = useState<EditUserData>({
name: '',
@@ -49,26 +61,65 @@ export function UsersPage() {
})
const [saving, setSaving] = useState(false)
useEffect(() => {
loadData()
}, [])
// Debounce search query
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery)
const loadData = async () => {
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearchQuery(searchQuery)
}, 300)
return () => clearTimeout(handler)
}, [searchQuery])
const fetchUsers = async () => {
try {
const [usersData, plansData] = await Promise.all([
adminService.listUsers(),
setLoading(true)
setError(null)
const [usersResponse, plansData] = await Promise.all([
adminService.listUsers({
page: currentPage,
limit: pageSize,
search: debouncedSearchQuery.trim() || undefined,
sort_by: sortBy,
sort_order: sortOrder,
status_filter: statusFilter !== 'all' ? statusFilter : undefined,
}),
adminService.listPlans(),
])
setUsers(usersData)
setUsers(usersResponse.users)
setTotalPages(usersResponse.total_pages)
setTotalCount(usersResponse.total)
setPlans(plansData)
} catch (error) {
toast.error('Failed to load data')
console.error('Error loading data:', error)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load users'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchUsers()
}, [debouncedSearchQuery, sortBy, sortOrder, statusFilter, currentPage, pageSize])
// Reset to page 1 when filters change
useEffect(() => {
if (currentPage !== 1) {
setCurrentPage(1)
}
}, [debouncedSearchQuery, sortBy, sortOrder, statusFilter, pageSize])
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
const handlePageSizeChange = (size: number) => {
setPageSize(size)
setCurrentPage(1) // Reset to first page when changing page size
}
const handleEditUser = (user: User) => {
setEditingUser(user)
setEditData({
@@ -111,7 +162,7 @@ export function UsersPage() {
toast.success('User enabled successfully')
}
// Reload data to get updated user status
loadData()
fetchUsers()
} catch (error) {
toast.error(`Failed to ${user.is_active ? 'disable' : 'enable'} user`)
console.error('Error toggling user status:', error)
@@ -126,47 +177,36 @@ export function UsersPage() {
)
}
const getStatusBadge = (isActive: boolean) => {
return (
<Badge variant={isActive ? 'default' : 'secondary'}>
{isActive ? 'Active' : 'Inactive'}
</Badge>
)
}
const renderContent = () => {
if (loading) {
return <UsersLoading />
}
if (error) {
return <UsersError error={error} onRetry={fetchUsers} />
}
if (users.length === 0) {
return <UsersEmpty searchQuery={searchQuery} statusFilter={statusFilter} />
}
if (loading) {
return (
<AppLayout
breadcrumb={{
items: [
{ label: 'Dashboard', href: '/' },
{ label: 'Admin' },
{ label: 'Users' },
],
}}
>
<div className="flex-1 rounded-xl bg-muted/50 p-4">
<div className="flex items-center justify-between mb-6">
<div>
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-64 mt-1" />
</div>
<Skeleton className="h-10 w-32" />
</div>
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
</CardHeader>
<CardContent>
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
</CardContent>
</Card>
</div>
</AppLayout>
<div className="space-y-4">
<UsersTable
users={users}
onEdit={handleEditUser}
onToggleStatus={handleToggleUserStatus}
/>
<AppPagination
currentPage={currentPage}
totalPages={totalPages}
totalCount={totalCount}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
itemName="users"
/>
</div>
)
}
@@ -181,72 +221,22 @@ export function UsersPage() {
}}
>
<div className="flex-1 rounded-xl bg-muted/50 p-4">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">User Management</h1>
<p className="text-muted-foreground">
Manage user accounts and permissions
</p>
</div>
<Button onClick={loadData} disabled={loading}>
Refresh
</Button>
</div>
<UsersHeader
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
sortBy={sortBy}
onSortByChange={setSortBy}
sortOrder={sortOrder}
onSortOrderChange={setSortOrder}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
onRefresh={fetchUsers}
loading={loading}
error={error}
userCount={totalCount}
/>
<Card>
<CardHeader>
<CardTitle>Users ({users.length})</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Plan</TableHead>
<TableHead>Credits</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map(user => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{getRoleBadge(user.role)}</TableCell>
<TableCell>{user.plan.name}</TableCell>
<TableCell>{user.credits.toLocaleString()}</TableCell>
<TableCell>{getStatusBadge(user.is_active)}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditUser(user)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleToggleUserStatus(user)}
>
{user.is_active ? (
<UserX className="h-4 w-4" />
) : (
<UserCheck className="h-4 w-4" />
)}
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{renderContent()}
</div>
{/* Edit User Sheet */}