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
@@ -0,0 +1,180 @@
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
import Stack from '@mui/material/Stack'
import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { Link as RouterLink, useParams } from 'react-router-dom'
import { fetchMyOrder, payOrderStub, postOrderMessage } from '@/entities/order/api/order-api'
import { formatPriceRub } from '@/shared/lib/format-price'
type AddressSnapshot = {
label?: string | null
recipientName?: string
recipientPhone?: string
addressLine?: string
comment?: string | null
}
export function OrderDetailPage() {
const { id } = useParams()
const qc = useQueryClient()
const [text, setText] = useState('')
const orderQuery = useQuery({
queryKey: ['me', 'orders', id],
queryFn: () => fetchMyOrder(id!),
enabled: Boolean(id),
})
const payMut = useMutation({
mutationFn: () => payOrderStub(id!),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['me', 'orders', id] }),
})
const msgMut = useMutation({
mutationFn: () => postOrderMessage(id!, text.trim()),
onSuccess: async () => {
setText('')
await qc.invalidateQueries({ queryKey: ['me', 'orders', id] })
},
})
const order = orderQuery.data?.item
const address = useMemo((): AddressSnapshot | null => {
if (!order?.addressSnapshotJson) return null
try {
return JSON.parse(order.addressSnapshotJson) as AddressSnapshot
} catch {
return null
}
}, [order])
if (!id) return <Alert severity="error">Некорректный заказ.</Alert>
if (orderQuery.isLoading) return <Typography>Загрузка</Typography>
if (orderQuery.isError || !order) return <Alert severity="error">Не удалось загрузить заказ.</Alert>
return (
<Box>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ sm: 'center' }} sx={{ mb: 2 }}>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4">Заказ #{order.id.slice(-6)}</Typography>
<Typography color="text.secondary">Статус: {order.status}</Typography>
</Box>
<Button component={RouterLink} to="/me/orders" variant="outlined">
К списку
</Button>
</Stack>
<Stack spacing={2} sx={{ maxWidth: 900 }}>
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
<Typography variant="h6" gutterBottom>
Позиции
</Typography>
<Stack spacing={1}>
{order.items.map((i) => (
<Stack key={i.id} direction="row" spacing={2} alignItems="center">
<Box sx={{ flexGrow: 1 }}>
<Typography sx={{ fontWeight: 700 }}>{i.titleSnapshot}</Typography>
<Typography color="text.secondary" variant="body2">
{i.qty} × {formatPriceRub(i.priceCentsSnapshot)}
</Typography>
</Box>
<Typography sx={{ fontWeight: 700 }}>{formatPriceRub(i.priceCentsSnapshot * i.qty)}</Typography>
</Stack>
))}
</Stack>
<Divider sx={{ my: 2 }} />
<Typography variant="h6">Итого: {formatPriceRub(order.totalCents)}</Typography>
</Box>
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
<Typography variant="h6" gutterBottom>
Доставка
</Typography>
{address ? (
<>
<Typography sx={{ fontWeight: 700 }}>{address.addressLine}</Typography>
<Typography color="text.secondary" variant="body2">
Получатель: {address.recipientName} · {address.recipientPhone}
</Typography>
{address.comment && (
<Typography color="text.secondary" variant="body2">
Комментарий: {address.comment}
</Typography>
)}
</>
) : (
<Typography color="text.secondary">Адрес не распознан.</Typography>
)}
{order.comment && (
<Typography color="text.secondary" variant="body2" sx={{ mt: 1 }}>
Комментарий к заказу: {order.comment}
</Typography>
)}
</Box>
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
<Typography variant="h6" gutterBottom>
Оплата
</Typography>
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
Пока это заглушка. Позже подключим реальную оплату.
</Typography>
<Button variant="contained" onClick={() => payMut.mutate()} disabled={payMut.isPending}>
Оплатить (заглушка)
</Button>
</Box>
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
<Typography variant="h6" gutterBottom>
Чат по заказу
</Typography>
<Stack spacing={1} sx={{ mb: 2 }}>
{order.messages.map((m) => (
<Box
key={m.id}
sx={{
p: 1.25,
borderRadius: 2,
bgcolor: m.authorType === 'admin' ? 'grey.100' : 'primary.50',
border: 1,
borderColor: 'divider',
}}
>
<Typography variant="caption" color="text.secondary">
{m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()}
</Typography>
<Typography sx={{ whiteSpace: 'pre-wrap' }}>{m.text}</Typography>
</Box>
))}
{order.messages.length === 0 && <Typography color="text.secondary">Пока сообщений нет.</Typography>}
</Stack>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ sm: 'flex-end' }}>
<TextField
label="Сообщение"
value={text}
onChange={(e) => setText(e.target.value)}
fullWidth
multiline
minRows={2}
/>
<Button
variant="contained"
onClick={() => msgMut.mutate()}
disabled={msgMut.isPending || !text.trim()}
sx={{ minWidth: 160 }}
>
Отправить
</Button>
</Stack>
</Box>
</Stack>
</Box>
)
}