@@ -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 (
|
||||
<div className="mt-4 flex items-center justify-center min-h-[240px]">
|
||||
<span className="text-sm text-[#8C90A3]">در حال بارگذاری...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError || !request) {
|
||||
return (
|
||||
<div className="mt-4 flex flex-col items-center justify-center min-h-[240px] gap-4">
|
||||
<span className="text-sm text-[#8C90A3]">درخواست یافت نشد</span>
|
||||
<Link
|
||||
to={Paths.myRequests}
|
||||
className="flex items-center gap-1 text-sm text-[#0047FF] hover:underline"
|
||||
>
|
||||
<ArrowRight2 size={18} color="#0047FF" />
|
||||
بازگشت به لیست درخواستها
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-[#eceef6] p-6">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="text-sm text-[#8C90A3]">
|
||||
درخواست #{data?.data?.requestNumber}
|
||||
<div className="mt-4 pb-8">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="text-lg font-light text-black">
|
||||
درخواست #{request.requestNumber}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mt-2 text-xs text-[#8C90A3]">
|
||||
<Calendar size={14} />
|
||||
<span>
|
||||
تاریخ ثبت:{' '}
|
||||
{moment(request.createdAt).format('jYYYY/jMM/jDD HH:mm')}
|
||||
</span>
|
||||
{items.length > 0 && (
|
||||
<>
|
||||
<span className="text-[#D8DCE8]">|</span>
|
||||
<span>{items.length} قلم</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={Paths.myRequests}
|
||||
className="flex items-center gap-1 text-sm text-[#8C90A3] hover:text-black transition-colors shrink-0"
|
||||
>
|
||||
<ArrowRight2 size={18} color="currentColor" />
|
||||
بازگشت به لیست
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Order Information Section */}
|
||||
<div className="bg-white rounded-2xl p-6 mb-6">
|
||||
<div className='flex justify-between items-center'>
|
||||
<h2 className="text-lg font-light mb-6">اطلاعات درخواست</h2>
|
||||
</div>
|
||||
<div className='flex flex-col gap-10'>
|
||||
{
|
||||
data?.data?.items?.map((item) => {
|
||||
const productImage = item.product?.images?.[0]
|
||||
<section className="bg-white rounded-2xl p-6 md:p-8">
|
||||
<h2 className="text-sm text-black mb-6">اطلاعات درخواست</h2>
|
||||
|
||||
return (
|
||||
<div key={item.product.id}>
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Product Image */}
|
||||
<div className='size-[86px] rounded-lg overflow-hidden bg-gray-100'>
|
||||
<img
|
||||
src={productImage || placeholderProductImage}
|
||||
alt={item.product?.title ?? 'محصول'}
|
||||
className='size-full object-cover'
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = placeholderProductImage
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<p className="text-sm text-[#8C90A3]">قلمی برای این درخواست ثبت نشده است.</p>
|
||||
) : (
|
||||
<div>
|
||||
{items.map((item, index) => (
|
||||
<RequestDetailItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
index={index}
|
||||
total={items.length}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Order Details */}
|
||||
<div className="flex-1 flex gap-20 pr-10">
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className="text-xs text-desc">عنوان:</div>
|
||||
<div className="text-sm font-medium text-black">{item.product.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Description */}
|
||||
<div className="mt-6 pt-6 border-t border-dashed border-desc">
|
||||
<div className="text-sm font-medium mb-2 text-desc">شرح درخواست:</div>
|
||||
<p className="text-sm font-light text-black leading-6">
|
||||
{item.description || '—'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Attributes from field */}
|
||||
{item.attributes?.length ? (
|
||||
<>
|
||||
<div className='font-bold mb-5 mt-7'>ویژگیها</div>
|
||||
<div className='flex flex-col gap-3'>
|
||||
{item.attributes.map((attr) => (
|
||||
<div className='flex gap-3 items-center' key={attr.fieldId}>
|
||||
<div className='text-sm text-gray-500'>{attr.field?.name}:</div>
|
||||
<div className='text-sm -mt-px'>{attr.value ?? '—'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Attachments */}
|
||||
{item.attachments?.length ? (
|
||||
<div className="mt-6 pt-6 border-t border-dashed border-desc">
|
||||
<div className="text-sm font-medium mb-3 text-desc">پیوستها:</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{item.attachments.map((att) => (
|
||||
att.type === 'voice' ? (
|
||||
<div key={att.url} className="flex items-center gap-2 p-3 bg-[#f5f6fa] rounded-xl">
|
||||
<audio controls src={att.url} className="max-w-[240px] h-9" />
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
key={att.url}
|
||||
href={att.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block size-20 rounded-lg overflow-hidden border border-desc hover:opacity-90"
|
||||
>
|
||||
<img src={att.url} alt="پیوست" className="size-full object-cover" />
|
||||
</a>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section - White Card */}
|
||||
<TicketSection />
|
||||
<section className="mt-6">
|
||||
<TicketSection />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<Props> = ({ 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 (
|
||||
<article className={index > 0 ? 'pt-8 mt-8 border-t border-[#EBEDF5]' : ''}>
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="size-20 rounded-xl overflow-hidden bg-[#F5F7FC] shrink-0">
|
||||
<img
|
||||
src={productImage || placeholderProductImage}
|
||||
alt={item.product?.title ?? 'محصول'}
|
||||
className="size-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = placeholderProductImage
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[#8C90A3]">عنوان:</span>
|
||||
<span className="text-sm font-medium text-black">{item.product.title}</span>
|
||||
</div>
|
||||
{total > 1 && (
|
||||
<span className="text-xs text-[#8C90A3] bg-[#F5F7FC] rounded-full px-2.5 py-0.5">
|
||||
قلم {index + 1} از {total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 pt-5 border-t border-dashed border-[#D8DCE8]">
|
||||
<div className="text-xs text-[#8C90A3] mb-2">شرح درخواست</div>
|
||||
<p className="text-sm text-black leading-7 whitespace-pre-wrap">
|
||||
{item.description || '—'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{item.attributes?.length > 0 && (
|
||||
<div className="mt-5 pt-5 border-t border-dashed border-[#D8DCE8]">
|
||||
<div className="text-xs text-[#8C90A3] mb-3">ویژگیها</div>
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
{item.attributes.map((attr) => (
|
||||
<div
|
||||
key={attr.fieldId}
|
||||
className="flex items-center gap-2 bg-[#F5F7FC] rounded-lg px-3 py-2"
|
||||
>
|
||||
<span className="text-xs text-[#8C90A3] shrink-0">
|
||||
{attr.field?.name}:
|
||||
</span>
|
||||
<span className="text-sm text-black truncate">
|
||||
{attr.value ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fileAttachments.length > 0 && (
|
||||
<div className="mt-5 pt-5 border-t border-dashed border-[#D8DCE8]">
|
||||
<div className="text-xs text-[#8C90A3] mb-3 flex items-center gap-1.5">
|
||||
<Paperclip2 size={16} />
|
||||
پیوستها
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{fileAttachments.map((att) =>
|
||||
isImageUrl(att.url) ? (
|
||||
<a
|
||||
key={att.url}
|
||||
href={att.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block size-20 rounded-xl overflow-hidden border border-[#EBEDF5] hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<img
|
||||
src={att.url}
|
||||
alt="پیوست"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
key={att.url}
|
||||
type="button"
|
||||
onClick={() => handleOpenLink(att.url)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-[#F5F7FC] hover:bg-[#eceef6] transition-colors text-[#0047FF] text-xs"
|
||||
>
|
||||
<Paperclip2 size={16} color="#0047FF" />
|
||||
<span>
|
||||
{getFileNameAndExtensionFromUrl(att.url).fileName ||
|
||||
'فایل ضمیمه'}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{voiceAttachments.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{voiceAttachments.map((att) => (
|
||||
<VoicePlayer key={att.url} url={att.url} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default RequestDetailItem
|
||||
@@ -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<Props> = ({
|
||||
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 (
|
||||
<div className={isAdmin ? 'flex justify-end' : ''}>
|
||||
<div
|
||||
className={`bg-[#F5F7FC] rounded-3xl p-5 max-w-[min(100%,520px)] ${
|
||||
isAdmin ? 'rounded-tl-none' : 'rounded-tr-none'
|
||||
}`}
|
||||
>
|
||||
{isAdmin && senderName && (
|
||||
<div className="flex gap-1 text-xs mb-2 text-[#8C90A3]">
|
||||
<span className="font-medium text-black">{senderLabel}:</span>
|
||||
<span>{senderName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content && (
|
||||
<p className="text-sm text-black leading-7 whitespace-pre-wrap">{content}</p>
|
||||
)}
|
||||
|
||||
{fileAttachments.length > 0 && (
|
||||
<div className="flex gap-3 flex-wrap mt-3">
|
||||
{fileAttachments.map((attach) => (
|
||||
<button
|
||||
key={attach.url}
|
||||
type="button"
|
||||
onClick={() => handleOpenLink(attach.url)}
|
||||
className="flex cursor-pointer items-center gap-1.5 text-[#0047FF] hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Paperclip2 size={18} color="#0047FF" />
|
||||
<span className="text-xs">
|
||||
{getFileNameAndExtensionFromUrl(attach.url).fileName}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{voiceAttachments.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{voiceAttachments.map((voice) => (
|
||||
<VoicePlayer key={voice.url} url={voice.url} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createdAt && (
|
||||
<div className="mt-3 text-[11px] text-[#8C90A3] text-left" dir="ltr">
|
||||
{moment(createdAt).format('jYYYY/jMM/jDD HH:mm')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TicketMessage
|
||||
@@ -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<File[]>([])
|
||||
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 (
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
<div className="bg-white rounded-2xl p-6 md:p-8">
|
||||
<h2 className="text-sm text-black mb-2">گفتگو با پشتیبانی</h2>
|
||||
<p className="text-xs text-[#8C90A3] mb-6">
|
||||
سوالات یا توضیحات تکمیلی خود را با تیم پشتیبانی در میان بگذارید.
|
||||
</p>
|
||||
|
||||
{
|
||||
data?.data?.map((item) => {
|
||||
if (item.user)
|
||||
<div className="space-y-4 min-h-[80px]">
|
||||
{isLoadingTickets && tickets.length === 0 && (
|
||||
<p className="text-xs text-[#8C90A3]">در حال بارگذاری پیامها...</p>
|
||||
)}
|
||||
|
||||
{!isLoadingTickets && tickets.length === 0 && (
|
||||
<div className="rounded-xl bg-[#F5F7FC] px-4 py-6 text-center">
|
||||
<p className="text-sm text-[#8C90A3]">هنوز پیامی ثبت نشده است.</p>
|
||||
<p className="text-xs text-[#8C90A3] mt-1">
|
||||
اولین پیام خود را در فرم زیر ارسال کنید.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tickets.map((item) => {
|
||||
if (item.user) {
|
||||
return (
|
||||
<div key={item.id} className='bg-[#F5F7FC] rounded-4xl rounded-tr-none mt-6 p-6 max-w-[55%]'>
|
||||
<div className='text-sm font-light text-black leading-6'>
|
||||
{item.content}
|
||||
</div>
|
||||
|
||||
{/* attachments (non-voice) */}
|
||||
<div className="flex gap-3 flex-wrap mt-3">
|
||||
{item.attachments
|
||||
?.filter((a) => a.type !== 'voice')
|
||||
.map((attach) => (
|
||||
<div
|
||||
key={attach.url}
|
||||
onClick={() => handleOpenLink(attach.url)}
|
||||
className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]"
|
||||
>
|
||||
<Paperclip2 size={20} color="#0047FF" />
|
||||
<div className="text-xs">
|
||||
{getFileNameAndExtensionFromUrl(attach.url).fileName}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* voice messages */}
|
||||
{item.attachments
|
||||
?.filter((a) => a.type === 'voice')
|
||||
.map((voice) => (
|
||||
<VoicePlayer key={voice.url} url={voice.url} />
|
||||
))}
|
||||
</div>
|
||||
<TicketMessage
|
||||
key={item.id}
|
||||
content={item.content}
|
||||
attachments={item.attachments}
|
||||
createdAt={item.createdAt}
|
||||
/>
|
||||
)
|
||||
else if (item.admin)
|
||||
}
|
||||
|
||||
if (item.admin) {
|
||||
return (
|
||||
<div className='flex justify-end'>
|
||||
<div className='bg-[#F5F7FC] rounded-4xl rounded-tl-none mt-6 p-6 max-w-[55%]'>
|
||||
<div className='flex gap-1 text-sm mb-2'>
|
||||
<div className='font-bold'>پشتیبان : </div>
|
||||
<div>{item.admin?.firstName + ' ' + item?.admin?.lastName}</div>
|
||||
</div>
|
||||
<div className='text-sm font-light text-black leading-6'>
|
||||
{item.content}
|
||||
</div>
|
||||
|
||||
{/* attachments (non-voice) */}
|
||||
<div className="flex gap-3 flex-wrap mt-3">
|
||||
{item.attachments
|
||||
?.filter((a) => a.type !== 'voice')
|
||||
.map((attach) => (
|
||||
<div
|
||||
key={attach.url}
|
||||
onClick={() => handleOpenLink(attach.url)}
|
||||
className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]"
|
||||
>
|
||||
<Paperclip2 size={20} color="#0047FF" />
|
||||
<div className="text-xs">
|
||||
{getFileNameAndExtensionFromUrl(attach.url).fileName}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* voice messages */}
|
||||
{item.attachments
|
||||
?.filter((a) => a.type === 'voice')
|
||||
.map((voice) => (
|
||||
<VoicePlayer key={voice.url} url={voice.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<TicketMessage
|
||||
key={item.id}
|
||||
content={item.content}
|
||||
attachments={item.attachments}
|
||||
createdAt={item.createdAt}
|
||||
isAdmin
|
||||
senderName={`${item.admin.firstName} ${item.admin.lastName}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
<div className="mt-6">
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-[#EBEDF5]">
|
||||
<div className="text-sm mb-2 text-black">پیام شما</div>
|
||||
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full h-40 bg-white border border-border rounded-xl p-4 text-sm resize-none outline-none"
|
||||
placeholder=""
|
||||
className="w-full h-32 bg-white border border-border rounded-xl p-4 text-sm resize-none outline-none focus:border-[#0047FF] transition-colors"
|
||||
placeholder="متن پیام خود را بنویسید..."
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={isRecording ? stopRecording : startRecording}
|
||||
className="absolute left-4 bottom-4 bg-[#FFF1D7] size-8 rounded-lg flex items-center justify-center"
|
||||
className="absolute left-4 bottom-4 bg-[#FFF1D7] size-8 rounded-lg flex items-center justify-center hover:opacity-90 transition-opacity"
|
||||
aria-label={isRecording ? 'توقف ضبط' : 'ضبط صدا'}
|
||||
>
|
||||
<Microphone
|
||||
size={20}
|
||||
@@ -192,8 +167,10 @@ const TicketSection: FC = () => {
|
||||
{audioUrl && (
|
||||
<div className="w-full h-12 bg-white border border-border rounded-xl flex items-center px-3 gap-3 mt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePlayPause}
|
||||
className="w-8 h-8 rounded-full bg-black flex items-center justify-center shrink-0"
|
||||
aria-label={isPlaying ? 'توقف' : 'پخش'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause size={16} color="#fff" variant="Bold" />
|
||||
@@ -211,7 +188,7 @@ const TicketSection: FC = () => {
|
||||
className="w-[2px] rounded-full transition-all"
|
||||
style={{
|
||||
height: `${Math.random() * 18 + 4}px`,
|
||||
backgroundColor: passed ? '#0047FF' : '#E5E7EB'
|
||||
backgroundColor: passed ? '#0047FF' : '#E5E7EB',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -223,21 +200,16 @@ const TicketSection: FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6">
|
||||
<UploadBox
|
||||
label="فایل های ضمیمه"
|
||||
isMultiple={true}
|
||||
onChange={setFiles}
|
||||
/>
|
||||
<UploadBox label="فایلهای ضمیمه" isMultiple onChange={setFiles} />
|
||||
</div>
|
||||
|
||||
<div className="flex mt-10 justify-end">
|
||||
<div className="flex mt-8 justify-end">
|
||||
<Button
|
||||
label="ارسال"
|
||||
label="ارسال پیام"
|
||||
onClick={handleSubmit}
|
||||
className='w-[150px]'
|
||||
isLoading={singleUpload.isPending || multiUpload.isPending || addTicket.isPending}
|
||||
className="w-[150px]"
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user