diff --git a/src/pages/requests/Detail.tsx b/src/pages/requests/Detail.tsx index e0a38f6..ff88bae 100644 --- a/src/pages/requests/Detail.tsx +++ b/src/pages/requests/Detail.tsx @@ -1,53 +1,119 @@ import { type FC } from 'react' -import { useGetRequestDetail } from './hooks/useRequestData' import { Link, useParams } from 'react-router-dom' -import RequestItem from './components/RequestItem' +import moment from 'moment-jalaali' +import { Calendar, Profile2User, TickSquare } from 'iconsax-react' +import { useGetRequestDetail } from './hooks/useRequestData' +import RequestDetailItem from './components/RequestDetailItem' +import TicketSection from './components/TicketSection' import { Paths } from '@/config/Paths' import BackButton from '@/components/BackButton' import Button from '@/components/Button' import RefreshButton from '@/components/RefreshButton' -import { TickSquare } from 'iconsax-react' -import TicketSection from '../order/components/TicketSection' const RequestDetail: FC = () => { const { id } = useParams() - const { data, refetch, isFetching } = useGetRequestDetail(id!) + const { data, refetch, isFetching, isPending, isError } = useGetRequestDetail(id!) + const request = data?.data + const items = request?.items ?? [] + + const customerName = + [request?.user?.firstName, request?.user?.lastName].filter(Boolean).join(' ') || + request?.user?.phone || + undefined + + if (isPending && !request) { + return ( +
+ در حال بارگذاری... +
+ ) + } + + if (isError || !request) { + return ( +
+ درخواست یافت نشد + +
+ ) + } return ( -
- {/* Header Section */} -
+
+
+
refetch()} isLoading={isFetching} /> - +
-
- درخواست #{data?.data?.requestNumber} -
- - {/* Request Information Section */} -
-
-

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

-
-
- {data?.data?.items?.map((item) => ( - - ))} +
+
+

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

+
+
+ + + تاریخ ثبت:{' '} + {moment(request.createdAt).format('jYYYY/jMM/jDD HH:mm')} + +
+ {items.length > 0 && ( + <> + | + {items.length} قلم + + )} + {customerName && ( + <> + | +
+ + {customerName} +
+ + )} + {request.user?.phone && ( + <> + | + {request.user.phone} + + )} +
- +
+

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

+ + {items.length === 0 ? ( +

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

+ ) : ( +
+ {items.map((item, index) => ( + + ))} +
+ )} +
+ +
) } diff --git a/src/pages/requests/components/RequestDetailItem.tsx b/src/pages/requests/components/RequestDetailItem.tsx new file mode 100644 index 0000000..44dd161 --- /dev/null +++ b/src/pages/requests/components/RequestDetailItem.tsx @@ -0,0 +1,163 @@ +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 { useGetEntityField } from '@/pages/formBuilder/hooks/useFormBuilderData' +import type { RequestDetailItemType } from '../types/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 { data: fields } = useGetEntityField(String(item.product?.id ?? '')) + const attributes = item.attributes ?? [] + const productImage = item.product?.images?.[0] + + const getAttributeFieldId = (attribute: NonNullable[number]) => + attribute.fieldId ?? attribute.attributeId ?? attribute.field?.id + + const fieldRows = + fields?.data?.map((field) => ({ + id: field.id, + name: field.name, + value: attributes.find( + (attribute) => String(getAttributeFieldId(attribute)) === String(field.id) + )?.value, + })) ?? [] + + const matchedFieldIds = new Set(fieldRows.map((field) => String(field.id))) + const extraAttributeRows = attributes + .filter((attribute) => { + const fieldId = getAttributeFieldId(attribute) + return fieldId && !matchedFieldIds.has(String(fieldId)) + }) + .map((attribute) => ({ + id: String(getAttributeFieldId(attribute)), + name: attribute.field?.name ?? String(getAttributeFieldId(attribute)), + value: attribute.value, + })) + + const attributeRows = [...fieldRows, ...extraAttributeRows] + 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 || '—'} +

+
+ + {attributeRows.length > 0 && ( +
+
ویژگی‌ها
+
+ {attributeRows.map((field) => ( +
+ + {field.name}: + + + {field.value || '—'} + +
+ ))} +
+
+ )} + + {fileAttachments.length > 0 && ( +
+
+ + پیوست‌ها +
+
+ {fileAttachments.map((att) => + isImageUrl(att.url) ? ( + + پیوست + + ) : ( + + ) + )} +
+
+ )} + + {voiceAttachments.length > 0 && ( +
+ {voiceAttachments.map((att) => ( + + ))} +
+ )} +
+
+
+ ) +} + +export default RequestDetailItem diff --git a/src/pages/requests/components/RequestItem.tsx b/src/pages/requests/components/RequestItem.tsx deleted file mode 100644 index c388860..0000000 --- a/src/pages/requests/components/RequestItem.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { type FC } from 'react' -import type { RequestDetailItemType } from '../types/Types' -import { useGetEntityField } from '@/pages/formBuilder/hooks/useFormBuilderData' -import { Paperclip2 } from 'iconsax-react' -import placeholderProductImage from '@/assets/images/placeholder-product.svg' - -type Props = { - item: RequestDetailItemType -} - -const RequestItem: FC = ({ item }) => { - const { data: fields } = useGetEntityField(String(item.product?.id ?? '')) - const attributes = item.attributes ?? [] - - const getAttributeFieldId = (attribute: NonNullable[number]) => - attribute.fieldId ?? attribute.attributeId ?? attribute.field?.id - - const fieldRows = fields?.data?.map((field) => ({ - id: field.id, - name: field.name, - value: attributes.find((attribute) => String(getAttributeFieldId(attribute)) === String(field.id))?.value, - })) ?? [] - - const matchedFieldIds = new Set(fieldRows.map((field) => String(field.id))) - const extraAttributeRows = attributes - .filter((attribute) => { - const fieldId = getAttributeFieldId(attribute) - return fieldId && !matchedFieldIds.has(String(fieldId)) - }) - .map((attribute) => ({ - id: String(getAttributeFieldId(attribute)), - name: attribute.field?.name ?? String(getAttributeFieldId(attribute)), - value: attribute.value, - })) - const attributeRows = [...fieldRows, ...extraAttributeRows] - - const handleOpenAttachment = (url: string) => { - window.open(url, '_blank') - } - - const productImage = item.product?.images?.[0] - - return ( -
-
- {/* Product Image */} -
- {item.product?.title { - e.currentTarget.src = placeholderProductImage - }} - /> -
- - {/* Request Details */} -
-
-
عنوان:
-
{item.product?.title}
-
-
-
- - {/* Description */} - {item.description && ( -
-
شرح درخواست:
-

- {item.description} -

-
- )} - - {/* Attributes */} - {attributeRows.length ? ( - <> -
ویژگی ها
- { - attributeRows.map((field) => ( -
-
{field.name}:
-
{field.value || '-'}
-
- )) - } - - ) : null} - - {/* Attachments */} - {item.attachments?.length ? ( -
-
ضمایم:
-
- {item.attachments - ?.filter((url): url is string => typeof url === 'string') - .map((url, index) => ( - - ))} -
-
- ) : null} -
- ) -} - -export default RequestItem diff --git a/src/pages/requests/components/TicketMessage.tsx b/src/pages/requests/components/TicketMessage.tsx new file mode 100644 index 0000000..d8a386e --- /dev/null +++ b/src/pages/requests/components/TicketMessage.tsx @@ -0,0 +1,84 @@ +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 '@/pages/order/types/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 ( +
+
+
+ {senderLabel}: + {senderName ? {senderName} : null} +
+ + {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/requests/components/TicketSection.tsx b/src/pages/requests/components/TicketSection.tsx new file mode 100644 index 0000000..8d9e964 --- /dev/null +++ b/src/pages/requests/components/TicketSection.tsx @@ -0,0 +1,234 @@ +import Button from '@/components/Button' +import RefreshButton from '@/components/RefreshButton' +import UploadBox from '@/components/UploadBox' +import { Microphone, Play, Pause } from 'iconsax-react' +import { useState, type FC } from 'react' +import { useVoiceRecorder } from '@/hooks/useVoiceRecorder' +import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader' +import type { AddTicketType } from '@/pages/order/types/Types' +import { useAddTicket, useGetTickets } from '@/pages/order/hooks/useOrderData' +import { useParams } from 'react-router-dom' +import { toast } from '@/shared/toast' +import { extractErrorMessage } from '@/config/func' +import TicketMessage from './TicketMessage' + +type Props = { + customerName?: string +} + +const TicketSection: FC = ({ customerName }) => { + const { id } = useParams() + const [message, setMessage] = useState('') + const [files, setFiles] = useState([]) + const addTicket = useAddTicket() + const { data, refetch, isFetching, isPending: isLoadingTickets } = useGetTickets(id!) + const singleUpload = useSingleUpload() + const multiUpload = useMultiUpload() + + const { + isRecording, + isPlaying, + audioUrl, + audioRef, + togglePlayPause, + startRecording, + stopRecording, + progress, + audioFile, + 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.trim(), + } + + if (audioFile) { + await singleUpload.mutateAsync(audioFile, { + onSuccess: (data) => { + params.attachments.push({ + type: 'voice', + url: data?.data?.url, + }) + }, + }) + } + + if (files.length) { + await multiUpload.mutateAsync(files, { + onSuccess: (data) => { + data?.data?.forEach((item) => { + params.attachments.push({ + type: 'uploads_attach', + url: item.url, + }) + }) + }, + }) + } + + addTicket.mutate( + { orderId: id!, params }, + { + onSuccess: () => { + toast('پیام شما با موفقیت ارسال شد') + refetch() + setMessage('') + setFiles([]) + resetRecorder() + }, + onError: (error) => { + toast(extractErrorMessage(error), 'error') + }, + } + ) + } + + return ( +
+
+
+

گفتگو با مشتری

+

+ پیام‌ها و فایل‌های مرتبط با درخواست +

+
+ refetch()} isLoading={isFetching} /> +
+ +
+
+ {isLoadingTickets && tickets.length === 0 && ( +

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

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

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

+

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

+
+ )} + + {tickets.map((item) => { + if (item.user) { + return ( + + ) + } + + if (item.admin) { + return ( + + ) + } + + return null + })} +
+ +
+
پیام شما
+ +
+