order ticket
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
VITE_TOKEN_NAME = 'negareh_t'
|
||||
VITE_REFRESH_TOKEN_NAME = 'negareh_rt'
|
||||
VITE_API_BASE_URL = 'http://192.168.99.225:4000'
|
||||
VITE_API_BASE_URL = 'http://10.24.161.1:4000'
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Pause, Play } from "iconsax-react"
|
||||
import { useEffect, useRef, useState, type FC } from "react"
|
||||
|
||||
type VoicePlayerProps = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const VoicePlayer: FC<VoicePlayerProps> = ({ url }) => {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
|
||||
const update = () => {
|
||||
if (!audio.duration) return
|
||||
setProgress(audio.currentTime / audio.duration)
|
||||
}
|
||||
|
||||
audio.addEventListener('timeupdate', update)
|
||||
audio.addEventListener('ended', () => {
|
||||
setIsPlaying(false)
|
||||
setProgress(0)
|
||||
})
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', update)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggle = () => {
|
||||
if (!audioRef.current) return
|
||||
if (isPlaying) audioRef.current.pause()
|
||||
else audioRef.current.play()
|
||||
setIsPlaying(!isPlaying)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-12 bg-white border border-border rounded-xl flex items-center px-3 gap-3 mt-2">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="w-8 h-8 rounded-full bg-black flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause size={16} color="#fff" variant="Bold" />
|
||||
) : (
|
||||
<Play size={16} color="#fff" variant="Bold" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 flex items-center gap-[2px] h-8">
|
||||
{Array.from({ length: 60 }).map((_, i) => {
|
||||
const passed = i / 60 <= progress
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="w-[2px] rounded-full transition-all"
|
||||
style={{
|
||||
height: `${Math.random() * 18 + 4}px`,
|
||||
backgroundColor: passed ? '#0047FF' : '#E5E7EB'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<audio ref={audioRef} src={url} className="hidden" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VoicePlayer
|
||||
@@ -65,3 +65,24 @@ export const extractErrorMessage = (
|
||||
return fallbackMessage;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileNameAndExtensionFromUrl = (
|
||||
url: string,
|
||||
): { fileName: string; extension: string } => {
|
||||
try {
|
||||
const cleanUrl = url.split("?")[0].split("#")[0];
|
||||
const lastPart = cleanUrl.substring(cleanUrl.lastIndexOf("/") + 1);
|
||||
|
||||
if (!lastPart || !lastPart.includes(".")) {
|
||||
return { fileName: "", extension: "" };
|
||||
}
|
||||
|
||||
const lastDotIndex = lastPart.lastIndexOf(".");
|
||||
const fileName = lastPart.substring(0, lastDotIndex);
|
||||
const extension = lastPart.substring(lastDotIndex + 1);
|
||||
|
||||
return { fileName, extension };
|
||||
} catch {
|
||||
return { fileName: "", extension: "" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
|
||||
type UseVoiceRecorderOptions = {
|
||||
onRecordingComplete?: (audioBlob: Blob) => void;
|
||||
};
|
||||
|
||||
export const useVoiceRecorder = (options?: UseVoiceRecorderOptions) => {
|
||||
const { onRecordingComplete } = options || {};
|
||||
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
||||
const [audioFile, setAudioFile] = useState<File | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const chunksRef = useRef<Blob[]>([]);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
const startRecording = useCallback(async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
|
||||
mediaRecorderRef.current = mediaRecorder;
|
||||
chunksRef.current = [];
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
chunksRef.current.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
const audioBlob = new Blob(chunksRef.current, { type: "audio/webm" });
|
||||
const file = new File([audioBlob], "voice-message.webm", {
|
||||
type: "audio/webm",
|
||||
});
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
setAudioUrl(url);
|
||||
setAudioFile(file);
|
||||
onRecordingComplete?.(audioBlob);
|
||||
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
setIsRecording(true);
|
||||
} catch (err) {
|
||||
console.error("Microphone access error:", err);
|
||||
}
|
||||
}, [onRecordingComplete]);
|
||||
|
||||
const stopRecording = useCallback(() => {
|
||||
if (mediaRecorderRef.current && isRecording) {
|
||||
mediaRecorderRef.current.stop();
|
||||
setIsRecording(false);
|
||||
}
|
||||
}, [isRecording]);
|
||||
|
||||
const resetRecorder = useCallback(() => {
|
||||
if (mediaRecorderRef.current && isRecording) {
|
||||
mediaRecorderRef.current.stop();
|
||||
}
|
||||
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.currentTime = 0;
|
||||
}
|
||||
|
||||
if (audioUrl) {
|
||||
URL.revokeObjectURL(audioUrl);
|
||||
}
|
||||
|
||||
chunksRef.current = [];
|
||||
setIsRecording(false);
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setAudioUrl(null);
|
||||
setAudioFile(null);
|
||||
}, [audioUrl, isRecording]);
|
||||
|
||||
const togglePlayPause = useCallback(() => {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
}
|
||||
setIsPlaying((prev) => !prev);
|
||||
}, [isPlaying]);
|
||||
|
||||
const updateProgress = useCallback(() => {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
setCurrentTime(audioRef.current.currentTime);
|
||||
|
||||
if (isPlaying) {
|
||||
animationFrameRef.current = requestAnimationFrame(updateProgress);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
const audio = audioRef.current;
|
||||
|
||||
const onLoaded = () => setDuration(audio.duration);
|
||||
const onEnded = () => {
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
};
|
||||
|
||||
audio.addEventListener("loadedmetadata", onLoaded);
|
||||
audio.addEventListener("ended", onEnded);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener("loadedmetadata", onLoaded);
|
||||
audio.removeEventListener("ended", onEnded);
|
||||
};
|
||||
}, [audioUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying) updateProgress();
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [isPlaying, updateProgress]);
|
||||
|
||||
return {
|
||||
// state
|
||||
isRecording,
|
||||
isPlaying,
|
||||
audioUrl,
|
||||
audioFile,
|
||||
currentTime,
|
||||
duration,
|
||||
|
||||
// refs
|
||||
audioRef,
|
||||
|
||||
// actions
|
||||
startRecording,
|
||||
stopRecording,
|
||||
togglePlayPause,
|
||||
resetRecorder,
|
||||
|
||||
// helpers
|
||||
progress: duration ? currentTime / duration : 0,
|
||||
};
|
||||
};
|
||||
@@ -1,17 +1,14 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { Microphone, Paperclip2, Receipt1 } from 'iconsax-react'
|
||||
import Button from '@/components/Button'
|
||||
import UploadBox from '@/components/UploadBox'
|
||||
import Input from '@/components/Input'
|
||||
import { type FC } from 'react'
|
||||
import { Receipt1 } from 'iconsax-react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useGetOrderDetails } from './hooks/useOrderData'
|
||||
import TicketSection from './components/TicketSection'
|
||||
|
||||
const OrderDetail: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const { data } = useGetOrderDetails(id!)
|
||||
const [message, setMessage] = useState('')
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
|
||||
|
||||
const orderData = {
|
||||
orderId: '۱۲۲۴۵',
|
||||
@@ -22,9 +19,6 @@ const OrderDetail: FC = () => {
|
||||
description: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاق...'
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log('Submit:', { message, files })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen bg-[#eceef6] p-6">
|
||||
@@ -94,75 +88,7 @@ const OrderDetail: FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Bottom Section - White Card */}
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
{/* Designer Name */}
|
||||
<div className="mt-6">
|
||||
<Input
|
||||
label="نام طراح"
|
||||
placeholder="عباس حسینی"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div 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'>
|
||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد،
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between mt-3'>
|
||||
<div className='flex items-center gap-1.5 text-[#0047FF]'>
|
||||
<Paperclip2 size={20} color="#0047FF" />
|
||||
<div className='text-xs'>loremipsum.pdf</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>عباس حسینی</div>
|
||||
</div>
|
||||
<div className='text-sm font-light text-black leading-6'>
|
||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد،
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="mt-6">
|
||||
<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-[#f5f7fc] rounded-xl p-4 text-sm resize-none outline-none"
|
||||
placeholder=""
|
||||
/>
|
||||
<button className="absolute left-4 bottom-4 bg-[#FFF1D7] size-8 rounded-lg flex items-center justify-center">
|
||||
<Microphone size={20} color="black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6">
|
||||
<UploadBox
|
||||
label="فایل های ضمیمه"
|
||||
isMultiple={true}
|
||||
onChange={setFiles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-10 justify-end">
|
||||
<Button
|
||||
label="ارسال"
|
||||
onClick={handleSubmit}
|
||||
className='w-[150px]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TicketSection />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import Button from '@/components/Button'
|
||||
import Input from '@/components/Input'
|
||||
import UploadBox from '@/components/UploadBox'
|
||||
import { Microphone, Paperclip2 } 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/useOrderData'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { toast } from '@/shared/toast'
|
||||
import { extractErrorMessage, getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||
import VoicePlayer from '@/components/VoicePlayer'
|
||||
|
||||
|
||||
|
||||
const TicketSection: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const [message, setMessage] = useState('')
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const addTicket = useAddTicket()
|
||||
const { data, refetch } = useGetTickets(id!)
|
||||
const singleUpload = useSingleUpload()
|
||||
const multiUpload = useMultiUpload()
|
||||
|
||||
const {
|
||||
isRecording,
|
||||
isPlaying,
|
||||
audioUrl,
|
||||
audioRef,
|
||||
togglePlayPause,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
progress,
|
||||
audioFile,
|
||||
resetRecorder
|
||||
} = useVoiceRecorder()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const params: AddTicketType = {
|
||||
attachments: [],
|
||||
content: message
|
||||
}
|
||||
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?.map((item) => {
|
||||
params.attachments.push({
|
||||
type: 'uploads_attach',
|
||||
url: item.url
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addTicket.mutate({ orderId: id!, params: params }, {
|
||||
onSuccess: () => {
|
||||
toast('پیام شما با موفقیت ارسال شد')
|
||||
refetch()
|
||||
setMessage('')
|
||||
setFiles([])
|
||||
resetRecorder()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(extractErrorMessage(error), 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenLink = (url: string) => {
|
||||
window.open(
|
||||
url,
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl p-6">
|
||||
{/* Designer Name */}
|
||||
<div className="mt-6">
|
||||
<Input
|
||||
label="نام طراح"
|
||||
placeholder="عباس حسینی"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
data?.data?.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>
|
||||
)
|
||||
else 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>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="mt-6">
|
||||
<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=""
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={isRecording ? stopRecording : startRecording}
|
||||
className="absolute left-4 bottom-4 bg-[#FFF1D7] size-8 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Microphone
|
||||
size={20}
|
||||
color={isRecording ? '#EF4444' : 'black'}
|
||||
variant={isRecording ? 'Bold' : 'Outline'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{audioUrl && (
|
||||
<div className="w-full h-12 bg-white border border-border rounded-xl flex items-center px-3 gap-3 mt-3">
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
className="w-8 h-8 rounded-full bg-black flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause size={16} color="#fff" variant="Bold" />
|
||||
) : (
|
||||
<Play size={16} color="#fff" variant="Bold" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 flex items-center gap-[2px] h-8">
|
||||
{Array.from({ length: 60 }).map((_, i) => {
|
||||
const passed = i / 60 <= progress
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="w-[2px] rounded-full transition-all"
|
||||
style={{
|
||||
height: `${Math.random() * 18 + 4}px`,
|
||||
backgroundColor: passed ? '#0047FF' : '#E5E7EB'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<audio ref={audioRef} src={audioUrl} className="hidden" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6">
|
||||
<UploadBox
|
||||
label="فایل های ضمیمه"
|
||||
isMultiple={true}
|
||||
onChange={setFiles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-10 justify-end">
|
||||
<Button
|
||||
label="ارسال"
|
||||
onClick={handleSubmit}
|
||||
className='w-[150px]'
|
||||
isLoading={singleUpload.isPending || multiUpload.isPending || addTicket.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TicketSection
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/OrderService";
|
||||
import type { TabMyOrdersEnum } from "../enum/OrderEnum";
|
||||
import type { AddTicketType } from "../type/Types";
|
||||
|
||||
export const useGetProducts = () => {
|
||||
return useQuery({
|
||||
@@ -29,3 +30,23 @@ export const useGetOrderDetails = (id: string) => {
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetTickets = (orderId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["tickets", orderId],
|
||||
queryFn: () => api.getTickets(orderId),
|
||||
enabled: !!orderId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddTicket = () => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
orderId,
|
||||
params,
|
||||
}: {
|
||||
orderId: string;
|
||||
params: AddTicketType;
|
||||
}) => api.addTicket(orderId, params),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import axios from "@/config/axios";
|
||||
import type {
|
||||
AddTicketType,
|
||||
MyOrdersResponseType,
|
||||
OrderDetailResponseType,
|
||||
OrderType,
|
||||
ProductsResponseType,
|
||||
TicketsResponseType,
|
||||
} from "../type/Types";
|
||||
import { OrderStatusEnum, TabMyOrdersEnum } from "../enum/OrderEnum";
|
||||
|
||||
@@ -56,3 +58,15 @@ export const getOrderDetails = async (id: string) => {
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getTickets = async (orderId: string) => {
|
||||
const { data } = await axios.get<TicketsResponseType>(
|
||||
`/public/ticket/order/${orderId}`,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const addTicket = async (orderId: string, params: AddTicketType) => {
|
||||
const { data } = await axios.post(`/public/ticket/order/${orderId}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -99,3 +99,24 @@ export interface MyOrderType extends RowDataType {
|
||||
export type MyOrdersResponseType = BaseResponse<MyOrderType[]>;
|
||||
|
||||
export type OrderDetailResponseType = BaseResponse<MyOrderType>;
|
||||
|
||||
export type AddTicketType = {
|
||||
content: string;
|
||||
attachments: AttachmentsType[];
|
||||
};
|
||||
|
||||
export type TicketType = {
|
||||
admin?: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone: string;
|
||||
id: string;
|
||||
};
|
||||
attachments: AttachmentsType[];
|
||||
content: string;
|
||||
createdAt: string;
|
||||
id: number;
|
||||
user?: string;
|
||||
};
|
||||
|
||||
export type TicketsResponseType = BaseResponse<TicketType[]>;
|
||||
|
||||
Reference in New Issue
Block a user