Files
shop-server/client/src/pages/admin-users/ui/AdminUsersPage.tsx
T
2026-05-21 12:02:29 +05:00

264 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Stack from '@mui/material/Stack'
import TableCell from '@mui/material/TableCell'
import TablePagination from '@mui/material/TablePagination'
import TableRow from '@mui/material/TableRow'
import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useUnit } from 'effector-react'
import { Controller, useForm } from 'react-hook-form'
import { Link as RouterLink } from 'react-router-dom'
import { createAdminUser, deleteAdminUser, fetchAdminUsers, updateAdminUser } from '@/entities/user/api/user-api'
import type { AdminUser } from '@/entities/user/model/types'
import { getErrorMessage } from '@/shared/lib/get-error-message'
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
import { useEditDialogState } from '@/shared/lib/use-edit-dialog-state'
import { $user } from '@/shared/model/auth'
import { AdminDialog } from '@/shared/ui/AdminDialog/AdminDialog'
import { AdminTable } from '@/shared/ui/AdminTable'
import { EntityRowActions } from '@/shared/ui/EntityRowActions'
type UserFormState = {
email: string
name: string
}
const emptyUserForm = (): UserFormState => ({ email: '', name: '' })
function formatDt(v: string) {
try {
const d = new Date(v)
if (Number.isNaN(d.getTime())) return '—'
return d.toLocaleString()
} catch {
return '—'
}
}
export function AdminUsersPage() {
const queryClient = useQueryClient()
const { dialogOpen, editing, openCreateDialog, openEditDialog, closeDialog } = useEditDialogState<AdminUser>()
const [qInput, setQInput] = useState('')
const [q, setQ] = useState('')
const [page, setPage] = useState(0)
const [rowsPerPage, setRowsPerPage] = useState(20)
const currentUser = useUnit($user)
const currentUserId = currentUser?.id
const userForm = useForm<UserFormState>({
defaultValues: emptyUserForm(),
mode: 'onChange',
})
useEffect(() => {
const t = window.setTimeout(() => {
setQ(qInput.trim())
setPage(0)
}, 250)
return () => window.clearTimeout(t)
}, [qInput])
const usersQuery = useQuery({
queryKey: ['admin', 'users', { q, page, rowsPerPage }],
queryFn: () =>
fetchAdminUsers({
q: q || undefined,
page: page + 1,
pageSize: rowsPerPage,
}),
})
const createMut = useMutation({
mutationFn: async () => {
const v = userForm.getValues()
await createAdminUser({
email: v.email.trim(),
name: v.name.trim() || null,
})
},
onSuccess: () => {
void invalidateQueryKeys(queryClient, [['admin', 'users']])
closeDialog()
},
})
const updateMut = useMutation({
mutationFn: async () => {
const v = userForm.getValues()
await updateAdminUser(editing!.id, {
email: v.email.trim(),
name: v.name.trim() || null,
})
},
onSuccess: () => {
void invalidateQueryKeys(queryClient, [['admin', 'users']])
closeDialog()
},
})
const deleteMut = useMutation({
mutationFn: async (id: string) => deleteAdminUser(id),
onSuccess: () => {
void invalidateQueryKeys(queryClient, [['admin', 'users']])
},
})
const mutationError = createMut.error ?? updateMut.error ?? deleteMut.error
const openCreate = () => {
userForm.reset(emptyUserForm())
openCreateDialog()
}
const openEdit = (u: AdminUser) => {
openEditDialog(u)
userForm.reset({
email: u.email,
name: u.name ?? '',
})
}
const emailValue = userForm.watch('email')
const isSaveDisabled = !emailValue.trim() || createMut.isPending || updateMut.isPending
const users = usersQuery.data?.items ?? []
const total = usersQuery.data?.total ?? 0
return (
<Box>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ mb: 2, alignItems: { sm: 'center' }, justifyContent: 'space-between' }}
>
<Typography variant="h4">Админка пользователи</Typography>
<Button component={RouterLink} to="/admin" variant="outlined">
Товары
</Button>
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Управление пользователями доступно пользователю с правами администратора.
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2, alignItems: { sm: 'center' } }}>
<Button variant="contained" onClick={openCreate}>
Новый пользователь
</Button>
<TextField
size="small"
label="Поиск (email/имя)"
value={qInput}
onChange={(e) => setQInput(e.target.value)}
fullWidth
/>
</Stack>
{usersQuery.isError && (
<Alert severity="error" sx={{ mb: 2 }}>
Ошибка загрузки. Проверьте, что сервер запущен, и у вас есть права администратора.
</Alert>
)}
{mutationError && (
<Alert severity="error" sx={{ mb: 2 }}>
{getErrorMessage(mutationError)}
</Alert>
)}
<AdminTable
columns={[
{ key: 'email', label: 'Почта' },
{ key: 'name', label: 'Имя' },
{ key: 'createdAt', label: 'Создан' },
{ key: 'updatedAt', label: 'Обновлён' },
{ key: 'actions', label: 'Действия', align: 'right' },
]}
loading={usersQuery.isLoading}
error={usersQuery.isError ? 'Ошибка загрузки.' : null}
>
{users.length === 0 && !usersQuery.isLoading ? (
<TableRow>
<TableCell colSpan={5} sx={{ color: 'text.secondary' }}>
Пользователей пока нет.
</TableCell>
</TableRow>
) : (
users.map((u) => (
<TableRow key={u.id} hover>
<TableCell>{u.email}</TableCell>
<TableCell>{u.name ?? '—'}</TableCell>
<TableCell>{formatDt(u.createdAt)}</TableCell>
<TableCell>{formatDt(u.updatedAt)}</TableCell>
<TableCell align="right">
<EntityRowActions
onEdit={() => openEdit(u)}
onDelete={u.id === currentUserId ? undefined : () => deleteMut.mutate(u.id)}
deleteDisabled={deleteMut.isPending}
confirmDeleteMessage={`Удалить пользователя ${u.email}?`}
/>
</TableCell>
</TableRow>
))
)}
</AdminTable>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={(_, p) => setPage(p)}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={(e) => {
setRowsPerPage(Number(e.target.value))
setPage(0)
}}
rowsPerPageOptions={[10, 20, 50, 100]}
/>
<AdminDialog
open={dialogOpen}
onClose={closeDialog}
title={editing ? 'Редактировать пользователя' : 'Новый пользователь'}
maxWidth="xs"
actions={
<>
<Button onClick={closeDialog}>Отмена</Button>
<Button
variant="contained"
onClick={() => (editing ? updateMut.mutate() : createMut.mutate())}
disabled={isSaveDisabled}
>
{editing ? 'Сохранить' : 'Создать'}
</Button>
</>
}
>
<Stack spacing={2} sx={{ mt: 1 }}>
<Controller
control={userForm.control}
name="email"
render={({ field }) => (
<TextField
label="Почта"
fullWidth
required
disabled={Boolean(editing && editing.id === currentUserId)}
{...field}
/>
)}
/>
<Controller
control={userForm.control}
name="name"
render={({ field }) => <TextField label="Имя/ник" fullWidth {...field} />}
/>
</Stack>
</AdminDialog>
</Box>
)
}