order ticket

This commit is contained in:
hamid zarghami
2026-01-28 16:01:00 +03:30
parent a5ea0174ee
commit 10245bdb08
9 changed files with 577 additions and 80 deletions
+5 -79
View File
@@ -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
+21
View File
@@ -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),
});
};
+14
View File
@@ -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;
};
+21
View File
@@ -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[]>;