diff --git a/client/src/entities/order/api/admin-order-api.ts b/client/src/entities/order/api/admin-order-api.ts index 2c38e16..6e20b8f 100644 --- a/client/src/entities/order/api/admin-order-api.ts +++ b/client/src/entities/order/api/admin-order-api.ts @@ -37,7 +37,14 @@ export type AdminOrderDetailResponse = { comment: string | null createdAt: string updatedAt: string - user: { id: string; email: string; displayName: string | null } + user: { + id: string + email: string + displayName: string | null + avatar?: string | null + avatarType?: string | null + avatarStyle?: string | null + } items: Array<{ id: string productId: string diff --git a/client/src/features/order-chat/ui/OrderChat.tsx b/client/src/features/order-chat/ui/OrderChat.tsx index a9c269d..978c28f 100644 --- a/client/src/features/order-chat/ui/OrderChat.tsx +++ b/client/src/features/order-chat/ui/OrderChat.tsx @@ -3,9 +3,12 @@ import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' +import { useUnit } from 'effector-react' +import { $user } from '@/shared/model/auth' import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble' import { OrderMessageBody } from '@/shared/ui/OrderMessageBody' import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor' +import { UserAvatar } from '@/shared/ui/UserAvatar' type Message = { id: string @@ -24,6 +27,7 @@ type Props = { export function OrderChat({ messages, isPending, onSend }: Props) { const [text, setText] = useState('') const canSend = text.replace(/<[^>]*>/g, ' ').trim().length > 0 + const currentUser = useUnit($user) const handleSend = () => { if (!canSend || isPending) return @@ -37,14 +41,28 @@ export function OrderChat({ messages, isPending, onSend }: Props) { Чат по заказу - {messages.map((m) => ( - - - {m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()} - - - - ))} + {messages.map((m) => { + const isAdminMsg = m.authorType === 'admin' + const avatarNode = isAdminMsg ? ( + + ) : currentUser ? ( + + ) : null + return ( + + + {isAdminMsg ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()} + + + + ) + })} {messages.length === 0 && Пока сообщений нет.} diff --git a/client/src/features/order-detail/ui/OrderDetailContent.tsx b/client/src/features/order-detail/ui/OrderDetailContent.tsx index db2d20c..721c478 100644 --- a/client/src/features/order-detail/ui/OrderDetailContent.tsx +++ b/client/src/features/order-detail/ui/OrderDetailContent.tsx @@ -9,6 +9,7 @@ import Select from '@mui/material/Select' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useUnit } from 'effector-react' import { postAdminOrderMessage, setAdminOrderStatus } from '@/entities/order/api/admin-order-api' import type { AdminOrderDetailResponse } from '@/entities/order/api/admin-order-api' import { deliveryCarrierLabelRu } from '@/shared/constants/delivery-carrier' @@ -17,9 +18,11 @@ import { formatPriceRub } from '@/shared/lib/format-price' import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys' import { parseOrderAddressSnapshot } from '@/shared/lib/order-address-snapshot' import { orderStatusLabelRu } from '@/shared/lib/order-status-labels' +import { $user } from '@/shared/model/auth' import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble' import { OrderMessageBody } from '@/shared/ui/OrderMessageBody' import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor' +import { UserAvatar } from '@/shared/ui/UserAvatar' import { DeliveryFeeAdjustmentForm } from './DeliveryFeeAdjustmentForm' export function OrderDetailContent({ detail, orderId }: { detail: AdminOrderDetailResponse['item']; orderId: string }) { @@ -56,6 +59,7 @@ export function OrderDetailContent({ detail, orderId }: { detail: AdminOrderDeta ) const canSendMessage = msg.replace(/<[^>]*>/g, ' ').trim().length > 0 + const currentUser = useUnit($user) return ( @@ -164,14 +168,36 @@ export function OrderDetailContent({ detail, orderId }: { detail: AdminOrderDeta Сообщения - {detail.messages.map((m) => ( - - - {m.authorType === 'admin' ? 'Админ (вы)' : 'Пользователь'} · {new Date(m.createdAt).toLocaleString()} - - - - ))} + {detail.messages.map((m) => { + const isAdminMsg = m.authorType === 'admin' + const avatarNode = isAdminMsg ? ( + currentUser && ( + + ) + ) : ( + + ) + return ( + + + {isAdminMsg ? 'Админ (вы)' : 'Пользователь'} · {new Date(m.createdAt).toLocaleString()} + + + + ) + })} {detail.messages.length === 0 && Нет сообщений.} diff --git a/client/src/features/user/user-menu/ui/UserMenu.tsx b/client/src/features/user/user-menu/ui/UserMenu.tsx index d714df7..ab661c3 100644 --- a/client/src/features/user/user-menu/ui/UserMenu.tsx +++ b/client/src/features/user/user-menu/ui/UserMenu.tsx @@ -1,10 +1,10 @@ import { useState } from 'react' +import PersonIcon from '@mui/icons-material/Person' import Badge from '@mui/material/Badge' import IconButton from '@mui/material/IconButton' import ListItemText from '@mui/material/ListItemText' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' -import PersonIcon from '@mui/icons-material/Person' import type { AuthUser } from '@/shared/model/auth' import { UserAvatar } from '@/shared/ui/UserAvatar' diff --git a/client/src/shared/ui/ChatMessageBubble.tsx b/client/src/shared/ui/ChatMessageBubble.tsx index 0fa56e5..b6e193c 100644 --- a/client/src/shared/ui/ChatMessageBubble.tsx +++ b/client/src/shared/ui/ChatMessageBubble.tsx @@ -1,29 +1,46 @@ import type { ReactNode } from 'react' import Box from '@mui/material/Box' +import Stack from '@mui/material/Stack' import { alpha } from '@mui/material/styles' type Author = 'admin' | 'user' -export function ChatMessageBubble(props: { authorType: Author; children: ReactNode }) { - const { authorType, children } = props +type Props = { + authorType: Author + avatar?: ReactNode + children: ReactNode +} + +export function ChatMessageBubble({ authorType, avatar, children }: Props) { + const isAdmin = authorType === 'admin' return ( - - authorType === 'admin' - ? alpha(theme.palette.grey[500], theme.palette.mode === 'dark' ? 0.28 : 0.14) - : alpha(theme.palette.primary.main, theme.palette.mode === 'dark' ? 0.28 : 0.1), + alignItems: 'flex-end', }} > - {children} - + {isAdmin && avatar && {avatar}} + + isAdmin + ? alpha(theme.palette.grey[500], theme.palette.mode === 'dark' ? 0.28 : 0.14) + : alpha(theme.palette.primary.main, theme.palette.mode === 'dark' ? 0.28 : 0.1), + }} + > + {children} + + {!isAdmin && avatar && {avatar}} + ) } diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db index eda88e1..f8989a0 100644 Binary files a/server/prisma/prisma/dev.db and b/server/prisma/prisma/dev.db differ diff --git a/server/src/routes/api/admin-orders.js b/server/src/routes/api/admin-orders.js index 3092e76..eef31db 100644 --- a/server/src/routes/api/admin-orders.js +++ b/server/src/routes/api/admin-orders.js @@ -73,7 +73,7 @@ export async function registerAdminOrderRoutes(fastify) { const order = await prisma.order.findUnique({ where: { id }, include: { - user: { select: { id: true, email: true, displayName: true } }, + user: { select: { id: true, email: true, displayName: true, avatar: true, avatarType: true, avatarStyle: true } }, items: true, messages: { orderBy: { createdAt: 'asc' } }, },