import { useEffect, useMemo, useState } from 'react' import Alert from '@mui/material/Alert' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Dialog from '@mui/material/Dialog' import DialogActions from '@mui/material/DialogActions' import DialogContent from '@mui/material/DialogContent' import DialogTitle from '@mui/material/DialogTitle' import Divider from '@mui/material/Divider' import Rating from '@mui/material/Rating' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import axios from 'axios' import { Link as RouterLink, useParams } from 'react-router-dom' import { confirmOrderReceived, fetchMyOrder, fetchOrderReviewEligibility, payOrderStub, postOrderMessage, } from '@/entities/order/api/order-api' import { postProductReview, uploadReviewImage } from '@/entities/product/api/reviews-api' import { markOrderMessagesRead } from '@/entities/user/api/messages-api' import { formatPriceRub } from '@/shared/lib/format-price' import { orderStatusLabelRu } from '@/shared/lib/order-status-labels' import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent' import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor' function reviewSubmitErrorMessage(err: unknown): string { if (axios.isAxiosError(err)) { const status = err.response?.status const raw = err.response?.data const apiMsg = raw && typeof raw === 'object' && 'error' in raw && typeof (raw as { error: unknown }).error === 'string' ? (raw as { error: string }).error : null if (status === 409 || apiMsg?.toLowerCase().includes('уже')) { return 'Вы уже оставляли отзыв на этот товар.' } return apiMsg || err.message || 'Не удалось отправить отзыв' } if (err instanceof Error) return err.message return 'Не удалось отправить отзыв' } type AddressSnapshot = { deliveryType?: 'delivery' | 'pickup' 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 [reviewTarget, setReviewTarget] = useState<{ productId: string; title: string } | null>(null) const [reviewRating, setReviewRating] = useState(5) const [reviewText, setReviewText] = useState('') const [reviewImageUrl, setReviewImageUrl] = useState(null) const orderQuery = useQuery({ queryKey: ['me', 'orders', id], queryFn: () => fetchMyOrder(id!), enabled: Boolean(id), }) const payMut = useMutation({ mutationFn: () => payOrderStub(id!), onSuccess: () => Promise.all([ qc.invalidateQueries({ queryKey: ['me', 'orders', id] }), qc.invalidateQueries({ queryKey: ['me', 'orders'] }), ]), }) const confirmMut = useMutation({ mutationFn: () => confirmOrderReceived(id!), onSuccess: () => Promise.all([ qc.invalidateQueries({ queryKey: ['me', 'orders', id] }), qc.invalidateQueries({ queryKey: ['me', 'orders'] }), ]), }) const msgMut = useMutation({ mutationFn: () => postOrderMessage(id!, text.trim()), onSuccess: async () => { setText('') await qc.invalidateQueries({ queryKey: ['me', 'orders', id] }) await qc.invalidateQueries({ queryKey: ['me', 'conversations'] }) }, }) const order = orderQuery.data?.item const canSendMessage = text.replace(/<[^>]*>/g, ' ').trim().length > 0 const eligibilityQuery = useQuery({ queryKey: ['me', 'orders', id, 'review-eligibility'], queryFn: () => fetchOrderReviewEligibility(id!), enabled: Boolean(id && order?.status === 'DONE'), }) const reviewMut = useMutation({ mutationFn: async () => { if (!reviewTarget) return const t = reviewText.trim() await postProductReview(reviewTarget.productId, { rating: reviewRating, text: t.length ? t : null, imageUrl: reviewImageUrl, }) }, onSuccess: async () => { setReviewTarget(null) setReviewRating(5) setReviewText('') setReviewImageUrl(null) await qc.invalidateQueries({ queryKey: ['me', 'orders', id, 'review-eligibility'] }) }, }) const uploadReviewImageMut = useMutation({ mutationFn: (file: File) => uploadReviewImage(file), onSuccess: ({ url }) => setReviewImageUrl(url), }) useEffect(() => { if (!id || orderQuery.status !== 'success' || !order) return void (async () => { await markOrderMessagesRead(id).catch(() => undefined) await qc.invalidateQueries({ queryKey: ['me', 'messages', 'unread-count'] }) })() }, [id, order, orderQuery.status, qc]) 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 Некорректный заказ. if (orderQuery.isLoading) return Загрузка… if (orderQuery.isError || !order) return Не удалось загрузить заказ. return ( Заказ #{order.id.slice(-6)} Статус: {orderStatusLabelRu(order.status)} Позиции {order.items.map((i) => ( {i.titleSnapshot} {i.qty} × {formatPriceRub(i.priceCentsSnapshot)} {formatPriceRub(i.priceCentsSnapshot * i.qty)} ))} Товары: {formatPriceRub(order.itemsSubtotalCents)} {order.deliveryType === 'delivery' && ( Доставка: {formatPriceRub(order.deliveryFeeCents)} )} Итого: {formatPriceRub(order.totalCents)} Получение Способ: {order.deliveryType === 'pickup' ? 'Самовывоз' : 'Доставка'} {order.deliveryType === 'delivery' && ( <> {address ? ( <> {address.addressLine} Получатель: {address.recipientName} · {address.recipientPhone} {address.comment && ( Комментарий: {address.comment} )} ) : ( Адрес не распознан. )} )} {order.deliveryType === 'pickup' && ( Адрес доставки не требуется. Мы свяжемся для согласования самовывоза. )} {order.comment && ( Комментарий к заказу: {order.comment} )} Оплата {order.status === 'PENDING_PAYMENT' && ( <> Пока это заглушка. После нажатия заказ перейдёт в статус «Проверка оплаты». )} {order.status === 'PAYMENT_VERIFICATION' && ( Оплата отправлена на проверку. Мы проверим поступление и обновим статус. )} {!['PENDING_PAYMENT', 'PAYMENT_VERIFICATION'].includes(order.status) && ( На этом этапе действий по оплате в этом блоке не требуется. )} {(order.deliveryType === 'delivery' && order.status === 'SHIPPED') || (order.deliveryType === 'pickup' && order.status === 'READY_FOR_PICKUP') ? ( Получение заказа {order.deliveryType === 'delivery' ? 'Когда забрали посылку у курьера или на пункте выдачи — подтвердите получение.' : 'Когда забрали заказ самовывозом — подтвердите получение.'} ) : null} {order.status === 'DONE' && eligibilityQuery.isSuccess && eligibilityQuery.data.canReview && ( Отзывы Поделитесь впечатлением о товарах. Отзывы появляются после модерации. {eligibilityQuery.data.items.map((row) => ( {row.title} ))} )} Чат по заказу {order.messages.map((m) => ( {m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()} ))} {order.messages.length === 0 && Пока сообщений нет.} { if (reviewMut.isPending) return setReviewTarget(null) setReviewRating(5) setReviewText('') setReviewImageUrl(null) }} fullWidth maxWidth="sm" > Отзыв: {reviewTarget?.title} Оценка { if (v !== null) setReviewRating(v) }} /> {reviewImageUrl && ( )} {reviewImageUrl && ( )} {uploadReviewImageMut.isError && ( Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp. )} {reviewMut.isError && ( {reviewSubmitErrorMessage(reviewMut.error)} )} ) }