base commit

This commit is contained in:
@kirill.komarov
2026-04-30 22:34:55 +05:00
parent 123d86091d
commit 9139a24093
46 changed files with 2023 additions and 153 deletions
@@ -1,13 +1,204 @@
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 Chip from '@mui/material/Chip'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemText from '@mui/material/ListItemText'
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 { Link as RouterLink } from 'react-router-dom'
import { fetchMyOrder, postOrderMessage } from '@/entities/order/api/order-api'
import { fetchMyConversations, markOrderMessagesRead } from '@/entities/user/api/messages-api'
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
export function MessagesPage() {
const qc = useQueryClient()
const [selectedId, setSelectedId] = useState<string | null>(null)
const [text, setText] = useState('')
const listQuery = useQuery({
queryKey: ['me', 'conversations'],
queryFn: fetchMyConversations,
})
const conversationsList = useMemo(() => listQuery.data?.items ?? [], [listQuery.data?.items])
const activeThreadId = useMemo(() => {
return selectedId ?? conversationsList[0]?.orderId ?? null
}, [selectedId, conversationsList])
const orderQuery = useQuery({
queryKey: ['me', 'orders', activeThreadId],
queryFn: () => fetchMyOrder(activeThreadId!),
enabled: Boolean(activeThreadId),
})
useEffect(() => {
if (!activeThreadId || orderQuery.status !== 'success') return
void (async () => {
await markOrderMessagesRead(activeThreadId).catch(() => undefined)
await qc.invalidateQueries({ queryKey: ['me', 'messages', 'unread-count'] })
await qc.invalidateQueries({ queryKey: ['me', 'conversations'] })
})()
}, [activeThreadId, orderQuery.status, qc])
const msgMut = useMutation({
mutationFn: () => postOrderMessage(activeThreadId!, text.trim()),
onSuccess: async () => {
setText('')
await qc.invalidateQueries({ queryKey: ['me', 'orders', activeThreadId] })
await qc.invalidateQueries({ queryKey: ['me', 'conversations'] })
},
})
const order = orderQuery.data?.item
return (
<Box>
<Typography variant="h4" gutterBottom>
Сообщения
</Typography>
<Typography color="text.secondary">Скоро здесь появятся сообщения и уведомления.</Typography>
<Typography color="text.secondary" variant="body2" sx={{ mb: 2 }}>
Переписка по всем заказам. Последнее сообщение в каждом заказе в списке слева.
</Typography>
{listQuery.isError && <Alert severity="error">Не удалось загрузить переписки.</Alert>}
{listQuery.isSuccess && conversationsList.length === 0 && (
<Alert severity="info">
Пока нет сообщений в заказах. Их отправит администратор или напишите сами на странице заказа.
</Alert>
)}
{conversationsList.length > 0 && (
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ alignItems: 'flex-start' }}>
<Box
sx={{
width: { xs: '100%', md: 320 },
flexShrink: 0,
border: 1,
borderColor: 'divider',
borderRadius: 2,
bgcolor: 'background.paper',
maxHeight: 520,
overflow: 'auto',
}}
>
<List disablePadding>
{conversationsList.map((c) => (
<ListItem
key={c.orderId}
disablePadding
secondaryAction={
c.unreadCount > 0 ? (
<Chip sx={{ mr: 1 }} size="small" color="error" label={String(c.unreadCount)} />
) : null
}
>
<ListItemButton selected={activeThreadId === c.orderId} onClick={() => setSelectedId(c.orderId)}>
<ListItemText
primary={
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Typography component="span" sx={{ fontWeight: 700 }}>
{c.orderId.slice(-6)}
</Typography>
<Typography component="span" variant="caption" color="text.secondary">
· {orderStatusLabelRu(c.status)}
</Typography>
</Stack>
}
secondaryTypographyProps={{
sx: {
mt: 0.5,
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
},
}}
secondary={c.preview}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
<Box
sx={{
flexGrow: 1,
minWidth: 0,
border: 1,
borderColor: 'divider',
borderRadius: 2,
p: 2,
bgcolor: 'background.paper',
}}
>
{!activeThreadId && <Typography color="text.secondary">Выберите заказ.</Typography>}
{activeThreadId && orderQuery.isLoading && <Typography>Загрузка чата</Typography>}
{activeThreadId && orderQuery.isError && <Alert severity="error">Не удалось загрузить заказ.</Alert>}
{order && (
<>
<Stack direction="row" sx={{ mb: 2, justifyContent: 'space-between', flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h6">
Чат заказа {order.id.slice(-6)}{' '}
<Typography component="span" variant="body2" color="text.secondary">
({orderStatusLabelRu(order.status)})
</Typography>
</Typography>
<Button component={RouterLink} to={`/me/orders/${order.id}`} size="small" variant="outlined">
Открыть заказ
</Button>
</Stack>
<Stack spacing={1} sx={{ mb: 2, maxHeight: 360, overflow: 'auto' }}>
{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} sx={{ alignItems: { sm: 'flex-end' } }}>
<TextField
label="Сообщение"
value={text}
onChange={(e) => setText(e.target.value)}
fullWidth
multiline
minRows={2}
/>
<Button
variant="contained"
sx={{ minWidth: 140 }}
onClick={() => msgMut.mutate()}
disabled={msgMut.isPending || !text.trim()}
>
Отправить
</Button>
</Stack>
</>
)}
</Box>
</Stack>
)}
</Box>
)
}