From 7e7bade80ccd8de1c0640a28cbd006035906ef83 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 21 May 2026 21:05:22 +0500 Subject: [PATCH] feat: avatars in order messages --- .../src/entities/order/api/admin-order-api.ts | 9 +++- .../src/features/order-chat/ui/OrderChat.tsx | 34 +++++++++--- .../order-detail/ui/OrderDetailContent.tsx | 42 ++++++++++++--- .../features/user/user-menu/ui/UserMenu.tsx | 2 +- client/src/shared/ui/ChatMessageBubble.tsx | 49 ++++++++++++------ server/prisma/prisma/dev.db | Bin 364544 -> 364544 bytes server/src/routes/api/admin-orders.js | 2 +- 7 files changed, 103 insertions(+), 35 deletions(-) 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 eda88e12626c812198dc9721c0f994d09ad00901..f8989a0952fa99a7b414714fe94fdfa10532c26e 100644 GIT binary patch delta 733 zcmZozAl9%zY=Si7%!xA2j59YTWX@-^G&V6XG@N{Xz8pK4X=<|h%lxzR?K5-AEes3{ zj7>9h5)I1pOtZ7nOBm;QyD`TxDYMwKawg{%q~&MkRG0%5nSm4)rR7=XW@HqA6*X;h zV0^?c2U0D`qRq+~;Ogh%>E|9F;27!a>K6i46TtYUUN|$SpaN({VsW{7eo;oEWpbif z`~UNd+y9?u%AH@2%^7+5skw=nIl6}C=9b1l1oD7|LrH3JiC%JkF4*h{2pJO0$ZPM< z00ykA3=B9Oot|2vTaZ|ZY^14)Lu!R+Vs1fBD%{u_u(37U3YgFF8)0)AlMJ&x?x5M$ zz-(AA0@RlR^sp%?&~mfW($e!Ylcw+Z&#a~d3@)$*aZq<+#=!L6|IFe*6DmL^Sb$7O zuFA8>ttd=`na~6_p$Vr?x3EB*j^P9pr(+nuO@QULylQ4n5h%XXKyhVRW@K!hVNh&n zWNcyzvXp5tgxqGp@`fMiMogvK99WLqTW01I8UqbU1{soJm{w$JR#gafE=EcKrzWOq Vu))>a8d%=g0}aO1zU%#ne*8!EG&(Uj3=L;FUJmMnptfAGXLy+drL#3QUe16 zV?%@dlH}~(()}0jSGN^%s`6Lii(R;jEc;_if(Ol zV0^?c2U0D`qRq+~;Ogh%>E|9F;27!a>K6i46TtYUUf9y0JP~L{Vp@@DN?LhAN=a36 z`~UNd+y9?u%AH@2%^7+5skw=nIl6}C=9b3h5D!>5l%y7y=q2apg3aCzAwz;0dF}lf zz<`yNfdQwZ(^E@y3lb}ljWn}xNUab}%q_@Cg&TVnZ0yx-1qPDcDjr2)WIGeV7UMQ73S># 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' } }, },