From f61a1db4a7c0b6aae1913418e1fc2038170d024b Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 9 Jul 2026 17:59:54 +0330 Subject: [PATCH] up --- src/pages/request/RequestDetail.tsx | 176 +++++++------- .../request/components/RequestDetailItem.tsx | 134 +++++++++++ .../request/components/TicketMessage.tsx | 86 +++++++ .../request/components/TicketSection.tsx | 214 ++++++++---------- 4 files changed, 391 insertions(+), 219 deletions(-) create mode 100644 src/pages/request/components/RequestDetailItem.tsx create mode 100644 src/pages/request/components/TicketMessage.tsx diff --git a/src/pages/request/RequestDetail.tsx b/src/pages/request/RequestDetail.tsx index c0958d4..e1a7bcc 100644 --- a/src/pages/request/RequestDetail.tsx +++ b/src/pages/request/RequestDetail.tsx @@ -1,114 +1,94 @@ import { type FC } from 'react' -import { useParams } from 'react-router-dom' +import { Link, useParams } from 'react-router-dom' +import moment from 'moment-jalaali' +import { ArrowRight2, Calendar } from 'iconsax-react' +import { Paths } from '@/config/Paths' import { useGetRequestDetails } from './hooks/useRequestData' +import RequestDetailItem from './components/RequestDetailItem' import TicketSection from './components/TicketSection' -import placeholderProductImage from '@/assets/images/placeholder-product.svg' const RequestDetail: FC = () => { - const { id } = useParams() - const { data } = useGetRequestDetails(id!) + const { data, isPending, isError } = useGetRequestDetails(id!) + const request = data?.data + const items = request?.items ?? [] + + if (isPending && !request) { + return ( +
+ در حال بارگذاری... +
+ ) + } + + if (isError || !request) { + return ( +
+ درخواست یافت نشد + + + بازگشت به لیست درخواست‌ها + +
+ ) + } return ( -
- {/* Header Section */} -
-
- درخواست #{data?.data?.requestNumber} +
+
+
+

+ درخواست #{request.requestNumber} +

+
+ + + تاریخ ثبت:{' '} + {moment(request.createdAt).format('jYYYY/jMM/jDD HH:mm')} + + {items.length > 0 && ( + <> + | + {items.length} قلم + + )} +
+ + + + بازگشت به لیست +
- {/* Order Information Section */} -
-
-

اطلاعات درخواست

-
-
- { - data?.data?.items?.map((item) => { - const productImage = item.product?.images?.[0] +
+

اطلاعات درخواست

- return ( -
-
- {/* Product Image */} -
- {item.product?.title { - e.currentTarget.src = placeholderProductImage - }} - /> -
+ {items.length === 0 ? ( +

قلمی برای این درخواست ثبت نشده است.

+ ) : ( +
+ {items.map((item, index) => ( + + ))} +
+ )} +
- {/* Order Details */} -
-
-
عنوان:
-
{item.product.title}
-
-
-
- - {/* Order Description */} -
-
شرح درخواست:
-

- {item.description || '—'} -

-
- - {/* Attributes from field */} - {item.attributes?.length ? ( - <> -
ویژگی‌ها
-
- {item.attributes.map((attr) => ( -
-
{attr.field?.name}:
-
{attr.value ?? '—'}
-
- ))} -
- - ) : null} - - {/* Attachments */} - {item.attachments?.length ? ( -
-
پیوست‌ها:
-
- {item.attachments.map((att) => ( - att.type === 'voice' ? ( -
-
- ) : ( - - پیوست - - ) - ))} -
-
- ) : null} -
- ) - }) - } -
-
- - {/* Bottom Section - White Card */} - +
+ +
) } diff --git a/src/pages/request/components/RequestDetailItem.tsx b/src/pages/request/components/RequestDetailItem.tsx new file mode 100644 index 0000000..a771f6d --- /dev/null +++ b/src/pages/request/components/RequestDetailItem.tsx @@ -0,0 +1,134 @@ +import { type FC } from 'react' +import { Paperclip2 } from 'iconsax-react' +import placeholderProductImage from '@/assets/images/placeholder-product.svg' +import { getFileNameAndExtensionFromUrl } from '@/config/func' +import VoicePlayer from '@/components/VoicePlayer' +import type { RequestDetailItemType } from '../type/Types' + +type Props = { + item: RequestDetailItemType + index: number + total: number +} + +const isImageUrl = (url: string) => /\.(jpe?g|png|gif|webp|bmp|svg)(\?|$)/i.test(url) + +const RequestDetailItem: FC = ({ item, index, total }) => { + const productImage = item.product?.images?.[0] + const voiceAttachments = item.attachments?.filter((a) => a.type === 'voice') ?? [] + const fileAttachments = item.attachments?.filter((a) => a.type !== 'voice') ?? [] + + const handleOpenLink = (url: string) => { + window.open(url, '_blank', 'noopener,noreferrer') + } + + return ( +
0 ? 'pt-8 mt-8 border-t border-[#EBEDF5]' : ''}> +
+
+ {item.product?.title { + e.currentTarget.src = placeholderProductImage + }} + /> +
+ +
+
+
+ عنوان: + {item.product.title} +
+ {total > 1 && ( + + قلم {index + 1} از {total} + + )} +
+ +
+
شرح درخواست
+

+ {item.description || '—'} +

+
+ + {item.attributes?.length > 0 && ( +
+
ویژگی‌ها
+
+ {item.attributes.map((attr) => ( +
+ + {attr.field?.name}: + + + {attr.value ?? '—'} + +
+ ))} +
+
+ )} + + {fileAttachments.length > 0 && ( +
+
+ + پیوست‌ها +
+
+ {fileAttachments.map((att) => + isImageUrl(att.url) ? ( + + پیوست + + ) : ( + + ) + )} +
+
+ )} + + {voiceAttachments.length > 0 && ( +
+ {voiceAttachments.map((att) => ( + + ))} +
+ )} +
+
+
+ ) +} + +export default RequestDetailItem diff --git a/src/pages/request/components/TicketMessage.tsx b/src/pages/request/components/TicketMessage.tsx new file mode 100644 index 0000000..3d5b266 --- /dev/null +++ b/src/pages/request/components/TicketMessage.tsx @@ -0,0 +1,86 @@ +import { type FC } from 'react' +import moment from 'moment-jalaali' +import { Paperclip2 } from 'iconsax-react' +import { getFileNameAndExtensionFromUrl } from '@/config/func' +import VoicePlayer from '@/components/VoicePlayer' +import type { AttachmentsType } from '../type/Types' + +type Props = { + content: string + attachments?: AttachmentsType[] + senderName?: string + senderLabel?: string + createdAt?: string + isAdmin?: boolean +} + +const TicketMessage: FC = ({ + content, + attachments = [], + senderName, + senderLabel = 'پشتیبان', + createdAt, + isAdmin = false, +}) => { + const fileAttachments = attachments.filter((a) => a.type !== 'voice') + const voiceAttachments = attachments.filter((a) => a.type === 'voice') + + const handleOpenLink = (url: string) => { + window.open(url, '_blank', 'noopener,noreferrer') + } + + return ( +
+
+ {isAdmin && senderName && ( +
+ {senderLabel}: + {senderName} +
+ )} + + {content && ( +

{content}

+ )} + + {fileAttachments.length > 0 && ( +
+ {fileAttachments.map((attach) => ( + + ))} +
+ )} + + {voiceAttachments.length > 0 && ( +
+ {voiceAttachments.map((voice) => ( + + ))} +
+ )} + + {createdAt && ( +
+ {moment(createdAt).format('jYYYY/jMM/jDD HH:mm')} +
+ )} +
+
+ ) +} + +export default TicketMessage diff --git a/src/pages/request/components/TicketSection.tsx b/src/pages/request/components/TicketSection.tsx index 3ebdafa..3ab7e49 100644 --- a/src/pages/request/components/TicketSection.tsx +++ b/src/pages/request/components/TicketSection.tsx @@ -1,26 +1,22 @@ import Button from '@/components/Button' import UploadBox from '@/components/UploadBox' -import { Microphone, Paperclip2 } from 'iconsax-react' +import { Microphone, Play, Pause } from 'iconsax-react' import { useState, type FC } from 'react' import { useVoiceRecorder } from '@/hooks/useVoiceRecorder' -import { Play, Pause } from 'iconsax-react' import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader' import type { AddTicketType } from '../type/Types' import { useAddTicket, useGetTickets } from '../hooks/useRequestData' import { useParams } from 'react-router-dom' import { toast } from '@/shared/toast' -import { extractErrorMessage, getFileNameAndExtensionFromUrl } from '@/config/func' -import VoicePlayer from '@/components/VoicePlayer' - - +import { extractErrorMessage } from '@/config/func' +import TicketMessage from './TicketMessage' const TicketSection: FC = () => { - const { id } = useParams() const [message, setMessage] = useState('') const [files, setFiles] = useState([]) const addTicket = useAddTicket() - const { data, refetch } = useGetTickets(id!) + const { data, refetch, isPending: isLoadingTickets } = useGetTickets(id!) const singleUpload = useSingleUpload() const multiUpload = useMultiUpload() @@ -34,152 +30,131 @@ const TicketSection: FC = () => { stopRecording, progress, audioFile, - resetRecorder + resetRecorder, } = useVoiceRecorder() + const tickets = data?.data ?? [] + const isSubmitting = + singleUpload.isPending || multiUpload.isPending || addTicket.isPending + const handleSubmit = async () => { + if (!message.trim() && !audioFile && files.length === 0) { + toast('لطفاً پیام یا فایل ضمیمه وارد کنید', 'error') + return + } + const params: AddTicketType = { attachments: [], - content: message + content: message.trim(), } + if (audioFile) { await singleUpload.mutateAsync(audioFile, { onSuccess: (data) => { params.attachments.push({ type: 'voice', - url: data?.data?.url + url: data?.data?.url, }) - } + }, }) } + if (files.length) { await multiUpload.mutateAsync(files, { onSuccess: (data) => { - data?.data?.map((item) => { + data?.data?.forEach((item) => { params.attachments.push({ type: 'uploads_attach', - url: item.url + url: item.url, }) }) - } + }, }) } - addTicket.mutate({ requestId: id!, params: params }, { - onSuccess: () => { - toast('پیام شما با موفقیت ارسال شد') - refetch() - setMessage('') - setFiles([]) - resetRecorder() - }, - onError: (error) => { - toast(extractErrorMessage(error), 'error') - }, - }) - } - - const handleOpenLink = (url: string) => { - window.open( - url, - '_blank' + addTicket.mutate( + { requestId: id!, params }, + { + onSuccess: () => { + toast('پیام شما با موفقیت ارسال شد') + refetch() + setMessage('') + setFiles([]) + resetRecorder() + }, + onError: (error) => { + toast(extractErrorMessage(error), 'error') + }, + } ) } return ( -
+
+

گفتگو با پشتیبانی

+

+ سوالات یا توضیحات تکمیلی خود را با تیم پشتیبانی در میان بگذارید. +

- { - data?.data?.map((item) => { - if (item.user) +
+ {isLoadingTickets && tickets.length === 0 && ( +

در حال بارگذاری پیام‌ها...

+ )} + + {!isLoadingTickets && tickets.length === 0 && ( +
+

هنوز پیامی ثبت نشده است.

+

+ اولین پیام خود را در فرم زیر ارسال کنید. +

+
+ )} + + {tickets.map((item) => { + if (item.user) { return ( -
-
- {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} -
-
- ))} -
- - {/* voice messages */} - {item.attachments - ?.filter((a) => a.type === 'voice') - .map((voice) => ( - - ))} -
+ ) - else if (item.admin) + } + + if (item.admin) { return ( -
-
-
-
پشتیبان :
-
{item.admin?.firstName + ' ' + item?.admin?.lastName}
-
-
- {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} -
-
- ))} -
- - {/* voice messages */} - {item.attachments - ?.filter((a) => a.type === 'voice') - .map((voice) => ( - - ))} -
-
+ ) - }) - } + } -
+ return null + })} +
+ +
پیام شما