base commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { AdminReviewsPage } from './ui/AdminReviewsPage'
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Table from '@mui/material/Table'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableCell from '@mui/material/TableCell'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
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 { Link as RouterLink } from 'react-router-dom'
|
||||
import { fetchAdminReviews, moderateReview } from '@/entities/review/api/admin-review-api'
|
||||
import { clearAdminToken, getAdminToken, setAdminToken } from '@/shared/lib/admin-token'
|
||||
|
||||
type TokenFormState = { token: string }
|
||||
|
||||
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 reviewsQuery = useQuery({
|
||||
queryKey: ['admin', 'reviews', token],
|
||||
queryFn: () => fetchAdminReviews(token!, { status: 'pending', page: 1, pageSize: 50 }),
|
||||
enabled: Boolean(token),
|
||||
})
|
||||
|
||||
const modMut = useMutation({
|
||||
mutationFn: (params: { id: string; action: 'approve' | 'reject' }) =>
|
||||
moderateReview(token!, params.id, params.action),
|
||||
onSuccess: () => void qc.invalidateQueries({ queryKey: ['admin', 'reviews'] }),
|
||||
})
|
||||
|
||||
const error = modMut.error
|
||||
|
||||
const items = reviewsQuery.data?.items ?? []
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2, alignItems: { sm: 'center' } }}>
|
||||
<Typography variant="h4" sx={{ flexGrow: 1 }}>
|
||||
Админка — отзывы
|
||||
</Typography>
|
||||
<Button component={RouterLink} to="/admin" variant="outlined">
|
||||
Товары
|
||||
</Button>
|
||||
<Button component={RouterLink} to="/admin/orders" variant="outlined">
|
||||
Заказы
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<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>
|
||||
</Stack>
|
||||
|
||||
{!token && <Alert severity="info">После сохранения токена появится список отзывов на модерации.</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>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user