base commit
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
@@ -19,7 +19,6 @@ 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 {
|
||||
fetchAdminOrder,
|
||||
fetchAdminOrders,
|
||||
@@ -27,15 +26,14 @@ import {
|
||||
setAdminOrderStatus,
|
||||
} from '@/entities/order/api/admin-order-api'
|
||||
import { ORDER_STATUSES, getAdminNextOrderStatuses } from '@/shared/constants/order'
|
||||
import { clearAdminToken, getAdminToken, setAdminToken } from '@/shared/lib/admin-token'
|
||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||
import { groupOrdersByStatus } from '@/shared/lib/group-orders-by-status'
|
||||
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
|
||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||
|
||||
type TokenFormState = { token: string }
|
||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||
|
||||
export function AdminOrdersPage() {
|
||||
const qc = useQueryClient()
|
||||
const [token, setTokenState] = useState<string | null>(() => getAdminToken())
|
||||
const [q, setQ] = useState('')
|
||||
const [status, setStatus] = useState('')
|
||||
const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup' | ''>('')
|
||||
@@ -43,54 +41,38 @@ export function AdminOrdersPage() {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [msg, setMsg] = useState('')
|
||||
|
||||
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 ordersQuery = useQuery({
|
||||
queryKey: ['admin', 'orders', token, { q, status, deliveryType }],
|
||||
queryKey: ['admin', 'orders', { q, status, deliveryType }],
|
||||
queryFn: () =>
|
||||
fetchAdminOrders(token!, {
|
||||
fetchAdminOrders({
|
||||
q: q.trim() || undefined,
|
||||
status: status || undefined,
|
||||
deliveryType: deliveryType || undefined,
|
||||
}),
|
||||
enabled: Boolean(token),
|
||||
})
|
||||
|
||||
const orderDetailQuery = useQuery({
|
||||
queryKey: ['admin', 'orders', 'detail', token, selectedId],
|
||||
queryFn: () => fetchAdminOrder(token!, selectedId!),
|
||||
enabled: Boolean(token && selectedId),
|
||||
queryKey: ['admin', 'orders', 'detail', selectedId],
|
||||
queryFn: () => fetchAdminOrder(selectedId!),
|
||||
enabled: Boolean(selectedId),
|
||||
})
|
||||
|
||||
const statusMut = useMutation({
|
||||
mutationFn: (next: string) => setAdminOrderStatus(token!, selectedId!, next),
|
||||
mutationFn: (next: string) => setAdminOrderStatus(selectedId!, next),
|
||||
onSuccess: async () => {
|
||||
await qc.invalidateQueries({ queryKey: ['admin', 'orders'] })
|
||||
await qc.invalidateQueries({ queryKey: ['admin', 'orders', 'detail'] })
|
||||
await qc.invalidateQueries({ queryKey: ['admin', 'orders', 'summary'] })
|
||||
await invalidateQueryKeys(qc, [
|
||||
['admin', 'orders'],
|
||||
['admin', 'orders', 'detail'],
|
||||
['admin', 'orders', 'summary'],
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const msgMut = useMutation({
|
||||
mutationFn: () => postAdminOrderMessage(token!, selectedId!, msg.trim()),
|
||||
mutationFn: () => postAdminOrderMessage(selectedId!, msg.trim()),
|
||||
onSuccess: async () => {
|
||||
setMsg('')
|
||||
await qc.invalidateQueries({ queryKey: ['admin', 'orders', 'detail'] })
|
||||
await invalidateQueryKeys(qc, [['admin', 'orders', 'detail']])
|
||||
},
|
||||
})
|
||||
|
||||
@@ -99,7 +81,15 @@ export function AdminOrdersPage() {
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const items = ordersQuery.data?.items ?? []
|
||||
const items = useMemo(() => ordersQuery.data?.items ?? [], [ordersQuery.data?.items])
|
||||
const groupedItems = useMemo(
|
||||
() =>
|
||||
groupOrdersByStatus(items, ORDER_STATUSES).map((group) => ({
|
||||
statusCode: group.status,
|
||||
items: group.items,
|
||||
})),
|
||||
[items],
|
||||
)
|
||||
|
||||
const detail = orderDetailQuery.data?.item
|
||||
|
||||
@@ -115,97 +105,74 @@ export function AdminOrdersPage() {
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Введите API-токен из <code>ADMIN_API_TOKEN</code> (сохраняется в sessionStorage).
|
||||
Управление заказами доступно пользователю с правами администратора.
|
||||
</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="Поиск (id/email)" value={q} onChange={(e) => setQ(e.target.value)} fullWidth />
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<InputLabel id="status-label">Статус</InputLabel>
|
||||
<Select
|
||||
labelId="status-label"
|
||||
label="Статус"
|
||||
value={status}
|
||||
onChange={(e) => setStatus(String(e.target.value))}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Все</em>
|
||||
</MenuItem>
|
||||
{ORDER_STATUSES.map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{orderStatusLabelRu(s)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<InputLabel id="delivery-type-label">Способ получения</InputLabel>
|
||||
<Select
|
||||
labelId="delivery-type-label"
|
||||
label="Способ получения"
|
||||
value={deliveryType}
|
||||
onChange={(e) => {
|
||||
const v = String(e.target.value)
|
||||
if (v === '' || v === 'delivery' || v === 'pickup') setDeliveryType(v)
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Все</em>
|
||||
</MenuItem>
|
||||
<MenuItem value="delivery">Доставка</MenuItem>
|
||||
<MenuItem value="pickup">Самовывоз</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
|
||||
{!token && <Alert severity="info">После сохранения токена появится список заказов.</Alert>}
|
||||
{ordersQuery.isError && <Alert severity="error">Не удалось загрузить заказы.</Alert>}
|
||||
|
||||
{token && (
|
||||
<>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Поиск (id/email)"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<InputLabel id="status-label">Статус</InputLabel>
|
||||
<Select
|
||||
labelId="status-label"
|
||||
label="Статус"
|
||||
value={status}
|
||||
onChange={(e) => setStatus(String(e.target.value))}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Все</em>
|
||||
</MenuItem>
|
||||
{ORDER_STATUSES.map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{orderStatusLabelRu(s)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<InputLabel id="delivery-type-label">Способ получения</InputLabel>
|
||||
<Select
|
||||
labelId="delivery-type-label"
|
||||
label="Способ получения"
|
||||
value={deliveryType}
|
||||
onChange={(e) => {
|
||||
const v = String(e.target.value)
|
||||
if (v === '' || v === 'delivery' || v === 'pickup') setDeliveryType(v)
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Все</em>
|
||||
</MenuItem>
|
||||
<MenuItem value="delivery">Доставка</MenuItem>
|
||||
<MenuItem value="pickup">Самовывоз</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
|
||||
{ordersQuery.isError && <Alert severity="error">Не удалось загрузить заказы.</Alert>}
|
||||
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Покупатель</TableCell>
|
||||
<TableCell>Создан</TableCell>
|
||||
<TableCell>Сумма</TableCell>
|
||||
<TableCell>Позиций</TableCell>
|
||||
<TableCell align="right">Действия</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{groupedItems.map((group) => (
|
||||
<Fragment key={`group:${group.statusCode}`}>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Покупатель</TableCell>
|
||||
<TableCell>Статус</TableCell>
|
||||
<TableCell>Сумма</TableCell>
|
||||
<TableCell>Позиций</TableCell>
|
||||
<TableCell align="right">Действия</TableCell>
|
||||
<TableCell colSpan={6} sx={{ fontWeight: 700, bgcolor: 'action.hover' }}>
|
||||
{orderStatusLabelRu(group.statusCode)} ({group.items.length})
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map((o) => (
|
||||
{group.items.map((o) => (
|
||||
<TableRow key={o.id} hover>
|
||||
<TableCell>{o.id.slice(-8)}</TableCell>
|
||||
<TableCell>{o.user.email}</TableCell>
|
||||
<TableCell>{orderStatusLabelRu(o.status)}</TableCell>
|
||||
<TableCell>{new Date(o.createdAt).toLocaleString('ru-RU')}</TableCell>
|
||||
<TableCell>{formatPriceRub(o.totalCents)}</TableCell>
|
||||
<TableCell>{o.itemsCount}</TableCell>
|
||||
<TableCell align="right">
|
||||
@@ -215,17 +182,17 @@ export function AdminOrdersPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{ordersQuery.isSuccess && items.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ color: 'text.secondary' }}>
|
||||
Заказов пока нет.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{ordersQuery.isSuccess && items.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ color: 'text.secondary' }}>
|
||||
Заказов пока нет.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} fullWidth maxWidth="md">
|
||||
<DialogTitle>Заказ</DialogTitle>
|
||||
@@ -282,14 +249,9 @@ export function AdminOrdersPage() {
|
||||
</Stack>
|
||||
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ alignItems: { sm: 'flex-end' } }}>
|
||||
<TextField
|
||||
label="Ответ админа"
|
||||
value={msg}
|
||||
onChange={(e) => setMsg(e.target.value)}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1, width: '100%' }}>
|
||||
<RichTextMessageEditor value={msg} onChange={setMsg} placeholder="Ответ админа" />
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => msgMut.mutate()}
|
||||
|
||||
Reference in New Issue
Block a user