base commit

This commit is contained in:
@kirill.komarov
2026-04-29 19:14:34 +05:00
parent c1773e5c57
commit bfc9661d22
25 changed files with 1885 additions and 3 deletions
+1
View File
@@ -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>
)
}