base commit
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user