base commit

This commit is contained in:
@kirill.komarov
2026-05-03 19:57:12 +05:00
parent 9139a24093
commit fe10f25b8c
53 changed files with 2064 additions and 1071 deletions
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
@@ -12,42 +12,22 @@ 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 { Controller, useForm } from 'react-hook-form'
import { fetchAdminReviews, moderateReview } from '@/entities/review/api/admin-review-api'
import { clearAdminToken, getAdminToken, setAdminToken } from '@/shared/lib/admin-token'
type TokenFormState = { token: string }
import { getErrorMessage } from '@/shared/lib/get-error-message'
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
export function AdminReviewsPage() {
const qc = useQueryClient()
const [token, setTokenState] = useState<string | null>(() => getAdminToken())
const tokenForm = useForm<TokenFormState>({ defaultValues: { token: '' }, mode: 'onChange' })
useEffect(() => {
tokenForm.reset({ token: '' })
}, [token, tokenForm])
const saveToken = () => {
const t = tokenForm.getValues('token').trim()
if (!t) {
clearAdminToken()
setTokenState(null)
return
}
setAdminToken(t)
setTokenState(t)
}
const [status] = useState('pending')
const reviewsQuery = useQuery({
queryKey: ['admin', 'reviews', token],
queryFn: () => fetchAdminReviews(token!, { status: 'pending', page: 1, pageSize: 50 }),
enabled: Boolean(token),
queryKey: ['admin', 'reviews', status],
queryFn: () => fetchAdminReviews({ status, page: 1, pageSize: 50 }),
})
const modMut = useMutation({
mutationFn: (params: { id: string; action: 'approve' | 'reject' }) =>
moderateReview(token!, params.id, params.action),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['admin', 'reviews'] }),
mutationFn: (params: { id: string; action: 'approve' | 'reject' }) => moderateReview(params.id, params.action),
onSuccess: () => void invalidateQueryKeys(qc, [['admin', 'reviews']]),
})
const error = modMut.error
@@ -61,84 +41,62 @@ export function AdminReviewsPage() {
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Введите API-токен из <code>ADMIN_API_TOKEN</code>.
Модерация отзывов доступна пользователю с правами администратора.
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2 }}>
<Controller
control={tokenForm.control}
name="token"
render={({ field }) => (
<TextField
label="Токен (Bearer)"
type="password"
fullWidth
{...field}
placeholder={token ? '••••••••' : ''}
/>
)}
/>
<Button variant="contained" onClick={saveToken} sx={{ minWidth: 140 }}>
Сохранить
</Button>
<TextField size="small" label="Статус" value="pending" disabled />
</Stack>
{!token && <Alert severity="info">После сохранения токена появится список отзывов на модерации.</Alert>}
{reviewsQuery.isError && <Alert severity="error">Не удалось загрузить отзывы.</Alert>}
{error && <Alert severity="error">{getErrorMessage(error)}</Alert>}
{token && (
<>
{reviewsQuery.isError && <Alert severity="error">Не удалось загрузить отзывы.</Alert>}
{error && <Alert severity="error">{(error as Error).message}</Alert>}
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Товар</TableCell>
<TableCell>Пользователь</TableCell>
<TableCell>Оценка</TableCell>
<TableCell>Текст</TableCell>
<TableCell align="right">Действия</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((r) => (
<TableRow key={r.id} hover>
<TableCell>{r.product.title}</TableCell>
<TableCell>{r.user.email}</TableCell>
<TableCell>
<Chip label={String(r.rating)} size="small" />
</TableCell>
<TableCell>{r.text ?? '—'}</TableCell>
<TableCell align="right">
<Button
size="small"
onClick={() => modMut.mutate({ id: r.id, action: 'approve' })}
disabled={modMut.isPending}
>
Одобрить
</Button>
<Button
size="small"
color="error"
onClick={() => modMut.mutate({ id: r.id, action: 'reject' })}
disabled={modMut.isPending}
>
Отклонить
</Button>
</TableCell>
</TableRow>
))}
{reviewsQuery.isSuccess && items.length === 0 && (
<TableRow>
<TableCell colSpan={5} sx={{ color: 'text.secondary' }}>
Нет отзывов на модерации.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</>
)}
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Товар</TableCell>
<TableCell>Пользователь</TableCell>
<TableCell>Оценка</TableCell>
<TableCell>Текст</TableCell>
<TableCell align="right">Действия</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((r) => (
<TableRow key={r.id} hover>
<TableCell>{r.product.title}</TableCell>
<TableCell>{r.user.email}</TableCell>
<TableCell>
<Chip label={String(r.rating)} size="small" />
</TableCell>
<TableCell>{r.text ?? '—'}</TableCell>
<TableCell align="right">
<Button
size="small"
onClick={() => modMut.mutate({ id: r.id, action: 'approve' })}
disabled={modMut.isPending}
>
Одобрить
</Button>
<Button
size="small"
color="error"
onClick={() => modMut.mutate({ id: r.id, action: 'reject' })}
disabled={modMut.isPending}
>
Отклонить
</Button>
</TableCell>
</TableRow>
))}
{reviewsQuery.isSuccess && items.length === 0 && (
<TableRow>
<TableCell colSpan={5} sx={{ color: 'text.secondary' }}>
Нет отзывов на модерации.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Box>
)
}