diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx new file mode 100644 index 0000000..1b971fe --- /dev/null +++ b/src/components/UserAvatar.tsx @@ -0,0 +1,42 @@ +import { type FC } from 'react' +import { usePresignedUrl } from '@/pages/uploader/hooks/usePresignedUrl' +import { clx } from '@/helpers/utils' + +type Props = { + src?: string | null + firstName?: string | null + lastName?: string | null + className?: string + textClassName?: string +} + +const UserAvatar: FC = ({ + src, + firstName, + lastName, + className = 'size-10', + textClassName = 'text-xs', +}) => { + const resolvedSrc = usePresignedUrl(src ?? undefined) + const initials = + `${String(firstName ?? '').charAt(0)}${String(lastName ?? '').charAt(0)}`.trim() || + '?' + + return ( +
+ {resolvedSrc ? ( + + ) : ( + initials + )} +
+ ) +} + +export default UserAvatar diff --git a/src/pages/order/components/OrderDetailSidebar.tsx b/src/pages/order/components/OrderDetailSidebar.tsx index 196616e..1eb1faf 100644 --- a/src/pages/order/components/OrderDetailSidebar.tsx +++ b/src/pages/order/components/OrderDetailSidebar.tsx @@ -1,10 +1,11 @@ import { type FC, useState } from 'react' import { Link } from 'react-router-dom' -import { Call, DocumentDownload, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react' +import { Call, DocumentDownload, Edit2, Location, Paperclip2, Receipt21 } from 'iconsax-react' import { clx } from '@/helpers/utils' import { getFileNameAndExtensionFromUrl } from '@/config/func' import { getPresignedUrl } from '@/pages/uploader/service/UploaderService' import { Paths } from '@/config/Paths' +import UserAvatar from '@/components/UserAvatar' import { getAttachmentUrl, formatJalaliDateTime, @@ -51,18 +52,20 @@ const PersonCard: FC<{ name: string subtitle?: string phone?: string - icon?: 'user' | 'designer' -}> = ({ title, name, subtitle, phone, icon = 'user' }) => ( + avatarUrl?: string | null + firstName?: string | null + lastName?: string | null +}> = ({ title, name, subtitle, phone, avatarUrl, firstName, lastName }) => (

{title}

-
- {icon === 'designer' ? ( - - ) : ( - - )} -
+

{name}

{subtitle &&

{subtitle}

} @@ -174,6 +177,9 @@ const OrderDetailSidebar: FC = ({ title="اطلاعات تماس" name={getUserDisplayName(order.user)} phone={order.user.phone} + avatarUrl={order.user.avatarUrl} + firstName={order.user.firstName} + lastName={order.user.lastName} /> {order.user.addresse && (
@@ -193,6 +199,9 @@ const OrderDetailSidebar: FC = ({ name={getCreatorDisplayName(order.creator)} subtitle="ادمین ثبت‌کننده" phone={order.creator.phone} + avatarUrl={order.creator.avatarUrl} + firstName={order.creator.firstName} + lastName={order.creator.lastName} /> )} {(order?.designers ?? []).map((orderDesigner) => ( @@ -202,7 +211,9 @@ const OrderDetailSidebar: FC = ({ name={getCreatorDisplayName(orderDesigner.designer)} subtitle="مسئول طراحی" phone={orderDesigner.designer?.phone} - icon="designer" + avatarUrl={orderDesigner.designer?.avatarUrl} + firstName={orderDesigner.designer?.firstName} + lastName={orderDesigner.designer?.lastName} /> ))} diff --git a/src/pages/order/components/TicketSection.tsx b/src/pages/order/components/TicketSection.tsx index 1528562..80d144e 100644 --- a/src/pages/order/components/TicketSection.tsx +++ b/src/pages/order/components/TicketSection.tsx @@ -1,6 +1,7 @@ import Button from '@/components/Button' import RefreshButton from '@/components/RefreshButton' import UploadBox from '@/components/UploadBox' +import UserAvatar from '@/components/UserAvatar' import { Microphone, Paperclip2 } from 'iconsax-react' import { useState, type FC } from 'react' import { useVoiceRecorder } from '@/hooks/useVoiceRecorder' @@ -97,41 +98,49 @@ const TicketSection: FC = () => { data?.data?.map((item) => { if (item.user) return ( -
-
- {item.content} -
+
+ +
+
+ {item.content} +
- {/* attachments (non-voice) */} -
- {item.attachments - ?.filter((a) => a.type !== 'voice') - .map((attach) => ( -
handleOpenLink(attach.url)} - className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]" - > - -
- {getFileNameAndExtensionFromUrl(attach.url).fileName} + {/* attachments (non-voice) */} +
+ {item.attachments + ?.filter((a) => a.type !== 'voice') + .map((attach) => ( +
handleOpenLink(attach.url)} + className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]" + > + +
+ {getFileNameAndExtensionFromUrl(attach.url).fileName} +
-
+ ))} +
+ + {/* voice messages */} + {item.attachments + ?.filter((a) => a.type === 'voice') + .map((voice) => ( + ))}
- - {/* voice messages */} - {item.attachments - ?.filter((a) => a.type === 'voice') - .map((voice) => ( - - ))}
) else if (item.admin) return ( -
-
+
+
طراح :
{item.admin?.firstName + ' ' + item?.admin?.lastName}
@@ -165,6 +174,12 @@ const TicketSection: FC = () => { ))}
+
) }) @@ -255,4 +270,4 @@ const TicketSection: FC = () => { ) } -export default TicketSection \ No newline at end of file +export default TicketSection diff --git a/src/pages/order/types/Types.ts b/src/pages/order/types/Types.ts index d8eabd0..8b5ee0f 100644 --- a/src/pages/order/types/Types.ts +++ b/src/pages/order/types/Types.ts @@ -100,6 +100,7 @@ export type OrderListResponseType = BaseResponse; export type UserType = { addresse?: string | null; + avatarUrl?: string | null; createdAt?: string; deletedAt?: string | null; firstName?: string | null; @@ -130,6 +131,7 @@ export type OrderDetailCreatorType = { lastName: string; role: string; phone: string; + avatarUrl?: string | null; }; export type OrderDesignerType = { @@ -174,12 +176,19 @@ export type TicketType = { lastName: string; phone: string; id: string; + avatarUrl?: string | null; }; attachments: AttachmentsType[]; content: string; createdAt: string; id: number; - user?: string; + user?: { + id: string; + firstName?: string | null; + lastName?: string | null; + phone?: string; + avatarUrl?: string | null; + } | null; }; export interface MyOrderType extends RowDataType { diff --git a/src/pages/requests/Detail.tsx b/src/pages/requests/Detail.tsx index ff88bae..cc02850 100644 --- a/src/pages/requests/Detail.tsx +++ b/src/pages/requests/Detail.tsx @@ -1,7 +1,7 @@ import { type FC } from 'react' import { Link, useParams } from 'react-router-dom' import moment from 'moment-jalaali' -import { Calendar, Profile2User, TickSquare } from 'iconsax-react' +import { Calendar, TickSquare } from 'iconsax-react' import { useGetRequestDetail } from './hooks/useRequestData' import RequestDetailItem from './components/RequestDetailItem' import TicketSection from './components/TicketSection' @@ -9,6 +9,7 @@ import { Paths } from '@/config/Paths' import BackButton from '@/components/BackButton' import Button from '@/components/Button' import RefreshButton from '@/components/RefreshButton' +import UserAvatar from '@/components/UserAvatar' const RequestDetail: FC = () => { const { id } = useParams() @@ -79,7 +80,13 @@ const RequestDetail: FC = () => { <> |
- + {customerName}
diff --git a/src/pages/requests/components/TicketMessage.tsx b/src/pages/requests/components/TicketMessage.tsx index 7e9c268..18d16a2 100644 --- a/src/pages/requests/components/TicketMessage.tsx +++ b/src/pages/requests/components/TicketMessage.tsx @@ -4,6 +4,7 @@ import { Paperclip2 } from 'iconsax-react' import { getFileNameAndExtensionFromUrl } from '@/config/func' import { getPresignedUrl } from '@/pages/uploader/service/UploaderService' import VoicePlayer from '@/components/VoicePlayer' +import UserAvatar from '@/components/UserAvatar' import type { AttachmentsType } from '@/pages/order/types/Types' type Props = { @@ -13,6 +14,9 @@ type Props = { senderLabel?: string createdAt?: string isAdmin?: boolean + avatarUrl?: string | null + firstName?: string | null + lastName?: string | null } const TicketMessage: FC = ({ @@ -22,6 +26,9 @@ const TicketMessage: FC = ({ senderLabel = 'پشتیبان', createdAt, isAdmin = false, + avatarUrl, + firstName, + lastName, }) => { const fileAttachments = attachments.filter((a) => a.type !== 'voice') const voiceAttachments = attachments.filter((a) => a.type === 'voice') @@ -31,8 +38,18 @@ const TicketMessage: FC = ({ window.open(url, '_blank', 'noopener,noreferrer') } + const avatar = ( + + ) + return ( -
+
+ {!isAdmin && avatar}
= ({
)}
+ {isAdmin && avatar}
) } diff --git a/src/pages/requests/components/TicketSection.tsx b/src/pages/requests/components/TicketSection.tsx index 4c1a4af..960d168 100644 --- a/src/pages/requests/components/TicketSection.tsx +++ b/src/pages/requests/components/TicketSection.tsx @@ -124,7 +124,16 @@ const TicketSection: FC = ({ customerName }) => { attachments={item.attachments} createdAt={item.createdAt} senderLabel="مشتری" - senderName={customerName} + senderName={ + customerName || + [item.user.firstName, item.user.lastName] + .filter(Boolean) + .join(' ') || + item.user.phone + } + avatarUrl={item.user.avatarUrl} + firstName={item.user.firstName} + lastName={item.user.lastName} /> ) } @@ -139,6 +148,9 @@ const TicketSection: FC = ({ customerName }) => { isAdmin senderLabel="پشتیبان" senderName={`${item.admin.firstName} ${item.admin.lastName}`} + avatarUrl={item.admin.avatarUrl} + firstName={item.admin.firstName} + lastName={item.admin.lastName} /> ) } diff --git a/src/pages/requests/types/Types.ts b/src/pages/requests/types/Types.ts index a9df77d..af2ea78 100644 --- a/src/pages/requests/types/Types.ts +++ b/src/pages/requests/types/Types.ts @@ -47,6 +47,7 @@ export type RequestDetailUserType = { maxCredit: number; addresse: string | null; phone: string; + avatarUrl?: string | null; }; export type RequestDetailProductType = { diff --git a/src/shared/Header.tsx b/src/shared/Header.tsx index 99d0d35..9ff806b 100644 --- a/src/shared/Header.tsx +++ b/src/shared/Header.tsx @@ -1,5 +1,6 @@ import { type FC, useEffect, useState } from 'react' import Input from '@/components/Input' +import UserAvatar from '@/components/UserAvatar' import { ArrowDown2, CloseCircle, HambergerMenu, Logout } from 'iconsax-react' import { Link } from 'react-router-dom' import { useLocation } from 'react-router-dom' @@ -21,7 +22,6 @@ const Header: FC = () => { const displayName = [admin?.firstName, admin?.lastName].filter(Boolean).join(' ').trim() const roleTitle = admin?.role?.title || admin?.role?.name || '' - const initials = `${String(admin?.firstName ?? '').charAt(0)}${String(admin?.lastName ?? '').charAt(0)}` useEffect(() => { setPopoverKey((prevKey) => prevKey + 1); @@ -53,9 +53,13 @@ const Header: FC = () => {
-
- {initials} -
+
@@ -77,9 +81,13 @@ const Header: FC = () => { setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' />
-
- {initials} -
+
{displayName || String(admin.phone ?? '')}
@@ -113,4 +121,4 @@ const Header: FC = () => { ) } -export default Header \ No newline at end of file +export default Header