turash/bugulma/frontend/pages/admin/UsersListPage.tsx
2025-12-15 10:06:41 +01:00

217 lines
6.8 KiB
TypeScript

import { DataTable } from '@/components/admin/DataTable.tsx';
import { Badge, Button, Input } from '@/components/ui';
import { useDeactivateUser, useUpdateUserRole, useUsers } from '@/hooks/api/useAdminAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import type { User } from '@/services/admin-api.ts';
import { Edit, Plus, UserCheck, UserX } from 'lucide-react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const UsersListPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState('');
const [roleFilter, setRoleFilter] = useState<string | undefined>();
const [statusFilter, setStatusFilter] = useState<boolean | undefined>();
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 25;
const { data, isLoading } = useUsers({
search: searchTerm || undefined,
role: roleFilter,
isActive: statusFilter,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
});
const { mutate: deactivateUser } = useDeactivateUser();
const { mutate: updateRole } = useUpdateUserRole();
const handleDeactivate = (user: User) => {
if (
window.confirm(
t('adminPage.users.confirmDeactivate', { name: user.name }) || `Deactivate ${user.name}?`
)
) {
deactivateUser(user.id, {
onSuccess: () => {
// Query will refetch automatically
},
});
}
};
const handleRoleChange = (user: User, newRole: string) => {
updateRole(
{ id: user.id, role: newRole },
{
onSuccess: () => {
// Query will refetch automatically
},
}
);
};
const formatDate = (dateString: string | null | undefined) => {
if (!dateString) return t('adminPage.users.never') || 'Never';
try {
const date = new Date(dateString);
return (
date.toLocaleDateString() +
' ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
);
} catch {
return t('adminPage.users.never') || 'Never';
}
};
const columns = [
{
key: 'name',
header: t('adminPage.users.table.name') || 'Name',
render: (user: User) => <span className="font-medium">{user.name}</span>,
},
{
key: 'email',
header: t('adminPage.users.table.email') || 'Email',
render: (user: User) => <span className="text-muted-foreground">{user.email}</span>,
},
{
key: 'role',
header: t('adminPage.users.table.role') || 'Role',
render: (user: User) => (
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>{user.role}</Badge>
),
},
{
key: 'status',
header: t('adminPage.users.table.status') || 'Status',
render: (user: User) => (
<Badge variant={user.isActive ? 'success' : 'destructive'}>
{user.isActive
? t('adminPage.users.active') || 'Active'
: t('adminPage.users.inactive') || 'Inactive'}
</Badge>
),
},
{
key: 'lastLogin',
header: t('adminPage.users.table.lastLogin') || 'Last Login',
render: (user: User) => (
<span className="text-sm text-muted-foreground">{formatDate(user.lastLoginAt)}</span>
),
},
];
const actions = [
{
label: t('adminPage.users.edit') || 'Edit',
icon: <Edit className="h-4 w-4" />,
onClick: (user: User) => navigate(`/admin/users/${user.id}/edit`),
},
{
label: t('adminPage.users.changeRole') || 'Change Role',
onClick: (user: User) => handleRoleChange(user, user.role === 'admin' ? 'user' : 'admin'),
},
{
label: t('adminPage.users.toggleStatus') || 'Toggle Status',
variant: 'destructive' as const,
onClick: (user: User) => handleDeactivate(user),
},
];
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{t('adminPage.users.title') || 'Users'}</h1>
<p className="text-muted-foreground">
{t('adminPage.users.description') || 'Manage user accounts, roles, and permissions'}
</p>
</div>
<Button onClick={() => navigate('/admin/users/new')}>
<Plus className="h-4 w-4 mr-2" />
{t('adminPage.users.newUser') || 'New User'}
</Button>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder={t('adminPage.users.searchPlaceholder') || 'Search by name or email...'}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex gap-2">
<Button
variant={roleFilter === undefined ? 'default' : 'outline'}
onClick={() => setRoleFilter(undefined)}
>
{t('adminPage.users.allRoles') || 'All Roles'}
</Button>
<Button
variant={roleFilter === 'admin' ? 'default' : 'outline'}
onClick={() => setRoleFilter('admin')}
>
{t('adminPage.users.admin') || 'Admin'}
</Button>
<Button
variant={roleFilter === 'user' ? 'default' : 'outline'}
onClick={() => setRoleFilter('user')}
>
{t('adminPage.users.user') || 'User'}
</Button>
</div>
<div className="flex gap-2">
<Button
variant={statusFilter === undefined ? 'default' : 'outline'}
onClick={() => setStatusFilter(undefined)}
>
{t('adminPage.users.allStatuses') || 'All'}
</Button>
<Button
variant={statusFilter === true ? 'default' : 'outline'}
onClick={() => setStatusFilter(true)}
>
<UserCheck className="h-4 w-4 mr-2" />
{t('adminPage.users.active') || 'Active'}
</Button>
<Button
variant={statusFilter === false ? 'default' : 'outline'}
onClick={() => setStatusFilter(false)}
>
<UserX className="h-4 w-4 mr-2" />
{t('adminPage.users.inactive') || 'Inactive'}
</Button>
</div>
</div>
{/* Table */}
<DataTable
columns={columns}
data={data?.users || []}
getRowId={(user) => user.id}
isLoading={isLoading}
actions={actions}
pagination={
data
? {
currentPage,
totalPages: Math.ceil(data.total / pageSize),
pageSize,
totalItems: data.total,
onPageChange: setCurrentPage,
}
: undefined
}
/>
</div>
);
};
export default UsersListPage;