ticket list + create ticket
This commit is contained in:
@@ -1,30 +1,15 @@
|
|||||||
import { type FC, useState } from 'react'
|
import { type FC, useState } from 'react'
|
||||||
// import { useTranslation } from 'react-i18next'
|
|
||||||
import Input from '../../components/Input'
|
import Input from '../../components/Input'
|
||||||
import Select from '../../components/Select'
|
|
||||||
// import Textarea from '../../components/Textarea'
|
|
||||||
import UploadBox from '../../components/UploadBox'
|
import UploadBox from '../../components/UploadBox'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { TickCircle, TickSquare } from 'iconsax-react'
|
import { TickCircle, TickSquare } from 'iconsax-react'
|
||||||
// import { useCreateTicket, useGetCategoriesTicket, useMultiUpload } from './hooks/useTicketData'
|
import { useCreateOrReplyTicket } from './hooks/useTicketData'
|
||||||
// import { useGetProfile } from '../profile/hooks/useProfileData'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { staticCategoriesData, staticProfileData, staticServicesData } from './data/staticData'
|
|
||||||
// import PageLoading from '../../components/PageLoading'
|
|
||||||
// import { useFormik } from 'formik'
|
|
||||||
// import { type CreateTicketType } from './types/TicketTypes'
|
|
||||||
// import * as Yup from 'yup'
|
|
||||||
// import { toast } from '../../components/Toast'
|
|
||||||
// import { type ErrorType } from '../../helpers/types'
|
|
||||||
// import { useNavigate } from 'react-router-dom'
|
|
||||||
// import { Pages } from '../../config/Pages'
|
|
||||||
// import { useGetMyServices } from '../service/hooks/useServiceData'
|
|
||||||
// import { MyServicesItem } from '../service/types/ServiecTypes'
|
|
||||||
import { fa } from '@/locale/fa'
|
import { fa } from '@/locale/fa'
|
||||||
import Textarea from '@/components/Textarea'
|
import Textarea from '@/components/Textarea'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
const CreateTicket: FC = () => {
|
const CreateTicket: FC = () => {
|
||||||
|
|
||||||
// const { t } = useTranslation('global')
|
|
||||||
const t = (key: string): string => {
|
const t = (key: string): string => {
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let value: unknown = fa;
|
let value: unknown = fa;
|
||||||
@@ -33,65 +18,26 @@ const CreateTicket: FC = () => {
|
|||||||
}
|
}
|
||||||
return (value as string) || key;
|
return (value as string) || key;
|
||||||
}
|
}
|
||||||
// const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [_files, setFiles] = useState<File[]>([])
|
const [subject, setSubject] = useState('')
|
||||||
const [percent] = useState<number>(0)
|
const [content, setContent] = useState('')
|
||||||
// const getProfile = useGetProfile()
|
const [files, setFiles] = useState<File[]>([])
|
||||||
// const getCategories = useGetCategoriesTicket()
|
|
||||||
// const multiUpload = useMultiUpload(setPercent)
|
|
||||||
// const createTicket = useCreateTicket()
|
|
||||||
// const getMyservice = useGetMyServices('')
|
|
||||||
|
|
||||||
// استفاده از دادههای استاتیک
|
const createTicket = useCreateOrReplyTicket()
|
||||||
const getProfile = {
|
|
||||||
isPending: false,
|
|
||||||
data: staticProfileData
|
|
||||||
}
|
|
||||||
const getCategories = {
|
|
||||||
isPending: false,
|
|
||||||
data: staticCategoriesData
|
|
||||||
}
|
|
||||||
const multiUpload = {
|
|
||||||
isPending: false,
|
|
||||||
mutateAsync: () => Promise.resolve()
|
|
||||||
}
|
|
||||||
const createTicket = {
|
|
||||||
isPending: false,
|
|
||||||
mutate: () => { }
|
|
||||||
}
|
|
||||||
const getMyservice = {
|
|
||||||
isPending: false,
|
|
||||||
data: staticServicesData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock formik
|
const handleSubmit = () => {
|
||||||
const formik = {
|
if (!subject.trim() || !content.trim()) return
|
||||||
values: {
|
createTicket.mutate(
|
||||||
subject: '',
|
{
|
||||||
message: '',
|
subject: subject.trim(),
|
||||||
categoryId: '',
|
content: content.trim(),
|
||||||
priority: 'LOW',
|
attachments: files.length ? files.map(() => ({})) : [],
|
||||||
danakServiceId: '',
|
|
||||||
title: '',
|
|
||||||
attachmentUrls: []
|
|
||||||
},
|
},
|
||||||
handleChange: () => { },
|
{
|
||||||
handleSubmit: () => { },
|
onSuccess: () => navigate(Paths.tickets.list),
|
||||||
resetForm: () => { },
|
onError: (err: unknown) => console.error(err),
|
||||||
touched: {
|
|
||||||
subject: false,
|
|
||||||
priority: false,
|
|
||||||
danakServiceId: false,
|
|
||||||
categoryId: false,
|
|
||||||
message: false
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
subject: '',
|
|
||||||
priority: '',
|
|
||||||
danakServiceId: '',
|
|
||||||
categoryId: '',
|
|
||||||
message: ''
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -101,104 +47,26 @@ const CreateTicket: FC = () => {
|
|||||||
{t('ticket.new_ticket')}
|
{t('ticket.new_ticket')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
|
||||||
getProfile.isPending || getCategories.isPending ?
|
|
||||||
<div>Loading...</div>
|
|
||||||
:
|
|
||||||
<div className='flex gap-6 xl:mt-8 mt-4'>
|
<div className='flex gap-6 xl:mt-8 mt-4'>
|
||||||
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||||
<div>
|
<div>
|
||||||
{t('ticket.submit_your_message')}
|
{t('ticket.submit_your_message')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 rowTwoInput'>
|
<div className='mt-6'>
|
||||||
<Input
|
|
||||||
label={t('ticket.name')}
|
|
||||||
readOnly
|
|
||||||
placeholder={getProfile?.data?.data?.user?.firstName + ' ' + getProfile?.data?.data?.user?.lastName}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={t('ticket.email')}
|
|
||||||
readOnly
|
|
||||||
placeholder={getProfile?.data?.data?.user?.email}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className='mt-6 rowTwoInput'>
|
|
||||||
<Input
|
<Input
|
||||||
label={t('ticket.subject')}
|
label={t('ticket.subject')}
|
||||||
placeholder={t('ticket.enter_your_subject')}
|
placeholder={t('ticket.enter_your_subject')}
|
||||||
name='subject'
|
value={subject}
|
||||||
value={formik.values.subject}
|
onChange={(e) => setSubject(e.target.value)}
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.subject && formik.errors.subject ? formik.errors.subject : ''}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('ticket.priority')}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
label: t('ticket.low'),
|
|
||||||
value: 'LOW'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('ticket.medium'),
|
|
||||||
value: 'MEDIUM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('ticket.high'),
|
|
||||||
value: 'HIGH'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
placeholder={t('select')}
|
|
||||||
className='border'
|
|
||||||
name='priority'
|
|
||||||
value={formik.values.priority}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.priority && formik.errors.priority ? formik.errors.priority : ''}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 rowTwoInput'>
|
|
||||||
<Select
|
|
||||||
label={t('ticket.select_your_service')}
|
|
||||||
placeholder={t('ticket.not_once')}
|
|
||||||
className='border'
|
|
||||||
name='danakServiceId'
|
|
||||||
items={getMyservice.data?.data?.subscriptions?.map((item: Record<string, unknown>) => {
|
|
||||||
const plan = item.plan as Record<string, unknown>;
|
|
||||||
const service = plan?.service as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
label: service?.name as string,
|
|
||||||
value: service?.id as string
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.danakServiceId && formik.errors.danakServiceId ? formik.errors.danakServiceId : ''}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label={t('ticket.category')}
|
|
||||||
items={getCategories.data?.data?.ticketCategories?.map((item: { id: string, title: string }) => ({
|
|
||||||
label: item.title,
|
|
||||||
value: item.id
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
placeholder={t('select')}
|
|
||||||
className='border'
|
|
||||||
name='categoryId'
|
|
||||||
value={formik.values.categoryId}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.categoryId && formik.errors.categoryId ? formik.errors.categoryId : ''}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<Textarea
|
<Textarea
|
||||||
label={t('ticket.your_description')}
|
label={t('ticket.your_description')}
|
||||||
name='message'
|
value={content}
|
||||||
value={formik.values.message}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.message && formik.errors.message ? formik.errors.message : ''}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -213,9 +81,8 @@ const CreateTicket: FC = () => {
|
|||||||
<div className='mt-8 flex justify-end'>
|
<div className='mt-8 flex justify-end'>
|
||||||
<Button
|
<Button
|
||||||
className='xl:max-w-[100px]'
|
className='xl:max-w-[100px]'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={handleSubmit}
|
||||||
isLoading={createTicket.isPending || multiUpload.isPending}
|
isLoading={createTicket.isPending}
|
||||||
percentage={percent}
|
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<TickCircle size={20} color='black' />
|
<TickCircle size={20} color='black' />
|
||||||
@@ -260,10 +127,6 @@ const CreateTicket: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-79
@@ -1,28 +1,18 @@
|
|||||||
import { type FC, Fragment, useState } from 'react'
|
import { type FC, Fragment, useState } from 'react'
|
||||||
// import { useTranslation } from 'react-i18next'
|
|
||||||
import Input from '../../components/Input'
|
import Input from '../../components/Input'
|
||||||
import Textarea from '@/components/Textarea'
|
import Textarea from '@/components/Textarea'
|
||||||
import UploadBox from '../../components/UploadBox'
|
import UploadBox from '../../components/UploadBox'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { CloseCircle, InfoCircle, Paperclip2, Send2 } from 'iconsax-react'
|
import { CloseCircle, InfoCircle, Paperclip2, Send2 } from 'iconsax-react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { type MessageType, type AttachmentType } from './types/TicketTypes'
|
import { type MessageType, type AttachmentType } from './types/TicketTypes'
|
||||||
// import { useAddMessageTicket, useCloseTicket, useGetMessages, useMultiUpload } from './hooks/useTicketData'
|
import { useGetTicketById, useCreateOrReplyTicket } from './hooks/useTicketData'
|
||||||
import { staticTicketDetailData } from './data/staticData'
|
|
||||||
// import PageLoading from '../../components/PageLoading'
|
|
||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
// import { useFormik } from 'formik'
|
|
||||||
// import { type AddMessageTicketType } from './types/TicketTypes'
|
|
||||||
// import * as Yup from 'yup'
|
|
||||||
// import { toast } from '../../components/Toast'
|
|
||||||
// import { type ErrorType } from '../../helpers/types'
|
|
||||||
import { clx } from '../../helpers/utils'
|
import { clx } from '../../helpers/utils'
|
||||||
import { fa } from '@/locale/fa'
|
import { fa } from '@/locale/fa'
|
||||||
// import { Pages } from '../../config/Pages'
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
const TicketDetail: FC = () => {
|
const TicketDetail: FC = () => {
|
||||||
|
|
||||||
// const { t } = useTranslation('global')
|
|
||||||
const t = (key: string) => {
|
const t = (key: string) => {
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let value: unknown = fa;
|
let value: unknown = fa;
|
||||||
@@ -32,57 +22,45 @@ const TicketDetail: FC = () => {
|
|||||||
return (value as string) || key;
|
return (value as string) || key;
|
||||||
}
|
}
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
// const { id: _id } = useParams()
|
const { id } = useParams<{ id: string }>()
|
||||||
const [, setFiles] = useState<File[]>([])
|
const [content, setContent] = useState('')
|
||||||
const [percent] = useState<number>(0)
|
const [files, setFiles] = useState<File[]>([])
|
||||||
// const getMessages = useGetMessages(id ? id : '')
|
|
||||||
// const addMessage = useAddMessageTicket(id ? id : '')
|
|
||||||
// const multiUpload = useMultiUpload(setPercent)
|
|
||||||
// const closeTicket = useCloseTicket()
|
|
||||||
|
|
||||||
// استفاده از دادههای استاتیک
|
const getTicket = useGetTicketById(id ?? '')
|
||||||
const getMessages = {
|
const addMessage = useCreateOrReplyTicket()
|
||||||
isPending: false,
|
|
||||||
data: staticTicketDetailData
|
|
||||||
}
|
|
||||||
const addMessage = {
|
|
||||||
isPending: false,
|
|
||||||
mutate: () => { },
|
|
||||||
mutateAsync: () => Promise.resolve()
|
|
||||||
}
|
|
||||||
const multiUpload = {
|
|
||||||
isPending: false,
|
|
||||||
mutateAsync: () => Promise.resolve()
|
|
||||||
}
|
|
||||||
const closeTicket = {
|
|
||||||
isPending: false,
|
|
||||||
mutate: () => { }
|
|
||||||
}
|
|
||||||
const [isResetFiles] = useState<boolean>(false)
|
|
||||||
|
|
||||||
// Mock formik
|
const raw = getTicket.data?.data
|
||||||
const formik = {
|
const ticket = raw?.ticket ?? raw
|
||||||
values: {
|
const messages: MessageType[] = raw?.messages ?? raw?.ticket?.messages ?? []
|
||||||
content: '',
|
|
||||||
attachmentUrls: [],
|
const handleSendReply = () => {
|
||||||
|
if (!id || !content.trim()) return
|
||||||
|
addMessage.mutate(
|
||||||
|
{
|
||||||
|
parentId: id,
|
||||||
|
subject: (ticket as { subject?: string })?.subject ?? '',
|
||||||
|
content: content.trim(),
|
||||||
|
attachments: files.length ? files.map(() => ({})) : [],
|
||||||
},
|
},
|
||||||
handleChange: () => { },
|
{
|
||||||
handleSubmit: () => { },
|
onSuccess: () => {
|
||||||
resetForm: () => { },
|
setContent('')
|
||||||
touched: { content: false },
|
setFiles([])
|
||||||
errors: { content: '' }
|
getTicket.refetch()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hadleCloseTicket = () => {
|
const hadleCloseTicket = () => {
|
||||||
closeTicket.mutate()
|
navigate(Paths.tickets.list)
|
||||||
navigate('/tickets')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSendAnswer = () => {
|
const handleSendAnswer = () => {
|
||||||
const messageEndRef = document.getElementById('content');
|
const messageEndRef = document.getElementById('content')
|
||||||
if (messageEndRef) {
|
if (messageEndRef) {
|
||||||
messageEndRef.scrollIntoView({ behavior: 'smooth' });
|
messageEndRef.scrollIntoView({ behavior: 'smooth' })
|
||||||
messageEndRef.focus();
|
messageEndRef.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,31 +70,31 @@ const TicketDetail: FC = () => {
|
|||||||
<div>{t('ticket.ticket_number')}</div>
|
<div>{t('ticket.ticket_number')}</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
getMessages.isPending ?
|
getTicket.isPending ?
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
:
|
:
|
||||||
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
|
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
|
||||||
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||||
<Input
|
<Input
|
||||||
label={t('ticket.subject')}
|
label={t('ticket.subject')}
|
||||||
value={getMessages.data?.data?.ticket?.subject}
|
value={(ticket as { subject?: string })?.subject}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<div className='gap-6 rowTwoInput mt-5'>
|
<div className='gap-6 rowTwoInput mt-5'>
|
||||||
<Input
|
<Input
|
||||||
label={t('ticket.select_your_service')}
|
label={t('ticket.select_your_service')}
|
||||||
value={getMessages.data?.data?.ticket?.danakService?.name}
|
value={(ticket as { danakService?: { name?: string } })?.danakService?.name}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('ticket.user_name')}
|
label={t('ticket.user_name')}
|
||||||
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
|
value={[(ticket as { user?: { firstName?: string; lastName?: string } })?.user?.firstName, (ticket as { user?: { lastName?: string } })?.user?.lastName].filter(Boolean).join(' ')}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
getMessages.data?.data?.messages.map((item: MessageType) => {
|
messages.map((item: MessageType) => {
|
||||||
if (item?.author?.roles[0]?.name === 'user') {
|
if (item?.author?.roles[0]?.name === 'user') {
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'>
|
<div key={item.id} className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'>
|
||||||
@@ -184,16 +162,14 @@ const TicketDetail: FC = () => {
|
|||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
|
(ticket as { status?: string })?.status !== 'CLOSED' &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='mt-9'>
|
<div className='mt-9'>
|
||||||
<Textarea
|
<Textarea
|
||||||
id='content'
|
id='content'
|
||||||
placeholder={t('ticket.your_description')}
|
placeholder={t('ticket.your_description')}
|
||||||
name='content'
|
value={content}
|
||||||
value={formik.values.content}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
onChange={formik.handleChange}
|
|
||||||
error_text={formik.touched.content && formik.errors.content ? formik.errors.content : ''}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,16 +178,14 @@ const TicketDetail: FC = () => {
|
|||||||
label={t('ticket.attachment')}
|
label={t('ticket.attachment')}
|
||||||
onChange={setFiles}
|
onChange={setFiles}
|
||||||
isMultiple
|
isMultiple
|
||||||
isReset={isResetFiles}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-end mt-6'>
|
<div className='flex justify-end mt-6'>
|
||||||
<Button
|
<Button
|
||||||
className='xl:max-w-[100px]'
|
className='xl:max-w-[100px]'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={handleSendReply}
|
||||||
percentage={percent}
|
isLoading={addMessage.isPending}
|
||||||
isLoading={addMessage.isPending || multiUpload.isPending}
|
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<Send2 size={20} color='black' />
|
<Send2 size={20} color='black' />
|
||||||
@@ -227,7 +201,7 @@ const TicketDetail: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={`h-fit lg:sticky left-0 lg:top-0 xl:w-[300px] w-full`}>
|
<div className={`h-fit lg:sticky left-0 lg:top-0 xl:w-[300px] w-full`}>
|
||||||
{
|
{
|
||||||
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
|
(ticket as { status?: string })?.status !== 'CLOSED' &&
|
||||||
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
|
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
|
||||||
<Button
|
<Button
|
||||||
label={t('ticket.send_answer')}
|
label={t('ticket.send_answer')}
|
||||||
@@ -237,7 +211,6 @@ const TicketDetail: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
className='bg-[#D52903] xl:h-10 h-8'
|
className='bg-[#D52903] xl:h-10 h-8'
|
||||||
onClick={hadleCloseTicket}
|
onClick={hadleCloseTicket}
|
||||||
isLoading={closeTicket.isPending}
|
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 text-xs items-center'>
|
<div className='flex gap-2 text-xs items-center'>
|
||||||
<CloseCircle
|
<CloseCircle
|
||||||
@@ -253,7 +226,7 @@ const TicketDetail: FC = () => {
|
|||||||
}
|
}
|
||||||
<div className={clx(
|
<div className={clx(
|
||||||
'bg-white p-6 rounded-3xl mt-8',
|
'bg-white p-6 rounded-3xl mt-8',
|
||||||
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'mt-0'
|
(ticket as { status?: string })?.status === 'CLOSED' && 'mt-0'
|
||||||
)}>
|
)}>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
@@ -261,10 +234,10 @@ const TicketDetail: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={clx(
|
<div className={clx(
|
||||||
'h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400',
|
'h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400',
|
||||||
getMessages.data?.data?.ticket?.status === 'PENDING' && 'bg-orange-100 text-orange-700',
|
(ticket as { status?: string })?.status === 'PENDING' && 'bg-orange-100 text-orange-700',
|
||||||
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'bg-red-100 text-red-400'
|
(ticket as { status?: string })?.status === 'CLOSED' && 'bg-red-100 text-red-400'
|
||||||
)}>
|
)}>
|
||||||
{t(`ticket.${getMessages.data?.data?.ticket?.status}`)}
|
{t(`ticket.${(ticket as { status?: string })?.status}`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,31 +245,33 @@ const TicketDetail: FC = () => {
|
|||||||
<div className='flex justify-between text-description'>
|
<div className='flex justify-between text-description'>
|
||||||
<div>{t('ticket.ticket_number')}</div>
|
<div>{t('ticket.ticket_number')}</div>
|
||||||
<div>
|
<div>
|
||||||
{getMessages.data?.data?.ticket?.numericId}
|
{(ticket as { numericId?: string })?.numericId}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex mt-6 justify-between text-description'>
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
<div>{t('ticket.asignto')}</div>
|
<div>{t('ticket.asignto')}</div>
|
||||||
<div>
|
<div>
|
||||||
{getMessages.data?.data?.ticket?.assignedTo ? getMessages.data?.data?.ticket?.assignedTo?.firstName + ' ' + getMessages.data?.data?.ticket?.assignedTo?.lastName : ''}
|
{(ticket as { assignedTo?: { firstName?: string; lastName?: string } })?.assignedTo
|
||||||
|
? [(ticket as { assignedTo?: { firstName?: string } })?.assignedTo?.firstName, (ticket as { assignedTo?: { lastName?: string } })?.assignedTo?.lastName].filter(Boolean).join(' ')
|
||||||
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex mt-6 justify-between text-description'>
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
<div>{t('date')}</div>
|
<div>{t('date')}</div>
|
||||||
<div className='dltr'>
|
<div className='dltr'>
|
||||||
{moment(getMessages.data?.data?.ticket?.createdAt).format('jYYYY/jMM/jDD HH:mm')}
|
{moment((ticket as { createdAt?: string })?.createdAt).format('jYYYY/jMM/jDD HH:mm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex mt-6 justify-between text-description'>
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
<div>{t('ticket.category')}</div>
|
<div>{t('ticket.category')}</div>
|
||||||
<div>
|
<div>
|
||||||
{getMessages.data?.data?.ticket?.category?.title}
|
{(ticket as { category?: { title?: string } })?.category?.title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex mt-6 justify-between text-description'>
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
<div>{t('ticket.priority')}</div>
|
<div>{t('ticket.priority')}</div>
|
||||||
<div>
|
<div>
|
||||||
{getMessages.data?.data?.ticket?.priority === 'low' ? t('ticket.low') : getMessages.data?.data?.ticket?.priority === 'medium' ? t('ticket.medium') : t('ticket.high')}
|
{(ticket as { priority?: string })?.priority === 'low' ? t('ticket.low') : (ticket as { priority?: string })?.priority === 'medium' ? t('ticket.medium') : t('ticket.high')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,75 +4,36 @@ import { Add, Eye } from 'iconsax-react'
|
|||||||
import Tabs from '../../components/Tabs'
|
import Tabs from '../../components/Tabs'
|
||||||
import Table from '../../components/Table'
|
import Table from '../../components/Table'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { staticTicketsData } from './data/staticData'
|
|
||||||
import { t } from '@/locale'
|
import { t } from '@/locale'
|
||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import type { ColumnType } from '@/components/types/TableTypes'
|
import type { ColumnType } from '@/components/types/TableTypes'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
|
import { useGetTickets } from './hooks/useTicketData'
|
||||||
// تعریف type برای تیکت
|
import type { TicketListItemType } from './types/TicketTypes'
|
||||||
interface TicketItem {
|
|
||||||
id: string;
|
|
||||||
numericId: string;
|
|
||||||
subject: string;
|
|
||||||
status: string;
|
|
||||||
priority: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
category: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
danakService: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
};
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TicketList: FC = () => {
|
const TicketList: FC = () => {
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED' | ''>('')
|
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED' | ''>('')
|
||||||
|
const getTickets = useGetTickets(activeTab || undefined)
|
||||||
|
|
||||||
// استفاده از دادههای استاتیک
|
const tickets: TicketListItemType[] = getTickets.data?.data ?? []
|
||||||
const getTickets = {
|
|
||||||
isPending: false,
|
|
||||||
data: staticTicketsData
|
|
||||||
}
|
|
||||||
|
|
||||||
// تعریف ستونهای جدول
|
const columns: ColumnType<TicketListItemType>[] = [
|
||||||
const columns: ColumnType<TicketItem>[] = [
|
|
||||||
{
|
{
|
||||||
title: t('ticket.number'),
|
title: t('ticket.number'),
|
||||||
key: 'numericId',
|
key: 'id',
|
||||||
render: (item) => item.numericId
|
render: (item) => item.id.slice(-8).toUpperCase()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('ticket.title'),
|
title: t('ticket.title'),
|
||||||
key: 'subject',
|
key: 'subject',
|
||||||
render: (item) => item.subject
|
render: (item) => item.subject
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('ticket.category'),
|
|
||||||
key: 'category',
|
|
||||||
render: (item) => item.category?.title
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('ticket.service'),
|
|
||||||
key: 'danakService',
|
|
||||||
render: (item) => item.danakService?.name
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('ticket.date'),
|
title: t('ticket.date'),
|
||||||
key: 'updatedAt',
|
key: 'createdAt',
|
||||||
render: (item) => (
|
render: (item) => (
|
||||||
<div className='dltr text-right'>
|
<div className='dltr text-right'>
|
||||||
{moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
|
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -82,9 +43,13 @@ const TicketList: FC = () => {
|
|||||||
render: (item) => t(`ticket.${item.status}`)
|
render: (item) => t(`ticket.${item.status}`)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('ticket.priority'),
|
title: t('ticket.user_name'),
|
||||||
key: 'priority',
|
key: 'user',
|
||||||
render: (item) => t(`ticket.${item.priority}`)
|
render: (item) => {
|
||||||
|
const u = item.user
|
||||||
|
const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || u.phone || '-'
|
||||||
|
return name
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('ticket.detail'),
|
title: t('ticket.detail'),
|
||||||
@@ -104,7 +69,7 @@ const TicketList: FC = () => {
|
|||||||
{t('ticket.tickets')}
|
{t('ticket.tickets')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to="/tickets/create">
|
<Link to={Paths.tickets.create}>
|
||||||
<Button
|
<Button
|
||||||
className='w-[172px]'
|
className='w-[172px]'
|
||||||
>
|
>
|
||||||
@@ -145,7 +110,7 @@ const TicketList: FC = () => {
|
|||||||
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={getTickets.data?.data?.tickets || []}
|
data={tickets}
|
||||||
isLoading={getTickets.isPending}
|
isLoading={getTickets.isPending}
|
||||||
noDataMessage="هیچ تیکتی یافت نشد"
|
noDataMessage="هیچ تیکتی یافت نشد"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,14 +1,37 @@
|
|||||||
import * as api from "../service/TicketService";
|
import * as api from "../service/TicketService";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { type AddMessageTicketType, type CreateTicketType } from "../types/TicketTypes";
|
import { type AddMessageTicketType, type CreateTicketPayload, type CreateTicketType } from "../types/TicketTypes";
|
||||||
|
|
||||||
export const useGetTickets = (status: string) => {
|
export const useGetTickets = (status?: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["tickets", status],
|
queryKey: ["public-tickets", status ?? ""],
|
||||||
queryFn: () => api.getTickets(status),
|
queryFn: () => api.getTickets(status),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** جزئیات یک تیکت از GET /public/tickets/{id} */
|
||||||
|
export const useGetTicketById = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["public-ticket", id],
|
||||||
|
queryFn: () => api.getTicketById(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ایجاد تیکت جدید (بدون parentId) یا ارسال پیام (با parentId) */
|
||||||
|
export const useCreateOrReplyTicket = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreateTicketPayload) => api.createOrReplyTicket(variables),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["public-tickets"] });
|
||||||
|
if (variables.parentId) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["public-ticket", variables.parentId] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateTicket = () => {
|
export const useCreateTicket = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (variables: CreateTicketType) => api.createTicket(variables),
|
mutationFn: (variables: CreateTicketType) => api.createTicket(variables),
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
import axios from "../../../config/axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import { type AddMessageTicketType, type CreateTicketType } from "../types/TicketTypes";
|
import axios from "@/config/axios";
|
||||||
|
import {
|
||||||
|
type AddMessageTicketType,
|
||||||
|
type CreateTicketPayload,
|
||||||
|
type CreateTicketType,
|
||||||
|
type GetTicketsResponseType,
|
||||||
|
} from "../types/TicketTypes";
|
||||||
|
|
||||||
export const getTickets = async (status: string) => {
|
/** لیست تیکتها - GET /public/tickets */
|
||||||
let query = ``;
|
export const getTickets = async (status?: string): Promise<GetTicketsResponseType> => {
|
||||||
if (status) {
|
const query = status ? `?status=${status}` : "";
|
||||||
query = `?status=${status}`;
|
const { data } = await axios.get<GetTicketsResponseType>(`/public/tickets${query}`);
|
||||||
}
|
|
||||||
const { data } = await axios.get(`/tickets${query}`);
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** جزئیات یک تیکت - GET /public/tickets/{id} */
|
||||||
|
export const getTicketById = async (id: string) => {
|
||||||
|
const { data } = await axios.get(`/public/tickets/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ایجاد تیکت جدید (بدون parentId) یا ارسال پیام (با parentId) - POST /public/tickets */
|
||||||
|
export const createOrReplyTicket = async (params: CreateTicketPayload) => {
|
||||||
|
const { data } = await axios.post('/public/tickets', params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @deprecated استفاده از createOrReplyTicket با/بدون parentId */
|
||||||
export const createTicket = async (params: CreateTicketType) => {
|
export const createTicket = async (params: CreateTicketType) => {
|
||||||
const { data } = await axios.post(`/tickets`, params);
|
const { data } = await axios.post(`/tickets`, params);
|
||||||
return data;
|
return data;
|
||||||
@@ -25,8 +42,8 @@ export const uploadSingle = async (
|
|||||||
callback?: (value: number) => void
|
callback?: (value: number) => void
|
||||||
) => {
|
) => {
|
||||||
const { data } = await axios.post(`/uploader/single-file`, params, {
|
const { data } = await axios.post(`/uploader/single-file`, params, {
|
||||||
onUploadProgress: (progressEvent: any) => {
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
||||||
if (progressEvent.bytes) {
|
if (progressEvent.bytes && progressEvent.total) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(
|
callback(
|
||||||
Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||||
@@ -46,8 +63,8 @@ export const uploadMultiple = async (
|
|||||||
callback?: (value: number) => void
|
callback?: (value: number) => void
|
||||||
) => {
|
) => {
|
||||||
const { data } = await axios.post(`/uploader/multi-file`, params, {
|
const { data } = await axios.post(`/uploader/multi-file`, params, {
|
||||||
onUploadProgress: (progressEvent: any) => {
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
||||||
if (progressEvent.bytes) {
|
if (progressEvent.bytes && progressEvent.total) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(
|
callback(
|
||||||
Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||||
|
|||||||
@@ -1,3 +1,44 @@
|
|||||||
|
import type { BaseResponse } from "@/shared/types/Types";
|
||||||
|
|
||||||
|
/** کاربر در لیست تیکت (پاسخ GET /public/tickets) */
|
||||||
|
export type TicketUserType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
gender: boolean;
|
||||||
|
maxCredit: number;
|
||||||
|
addresse: string | null;
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** آیتم تیکت در لیست (پاسخ GET /public/tickets) */
|
||||||
|
export type TicketListItemType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
parent: unknown | null;
|
||||||
|
subject: string;
|
||||||
|
content: string;
|
||||||
|
status: string;
|
||||||
|
attachments: unknown[];
|
||||||
|
admin: unknown | null;
|
||||||
|
user: TicketUserType;
|
||||||
|
children: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetTicketsResponseType = BaseResponse<TicketListItemType[]>;
|
||||||
|
|
||||||
|
/** برای ایجاد تیکت جدید (بدون parentId) یا ارسال پیام (با parentId) */
|
||||||
|
export type CreateTicketPayload = {
|
||||||
|
parentId?: string;
|
||||||
|
subject: string;
|
||||||
|
content: string;
|
||||||
|
attachments?: Record<string, unknown>[];
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateTicketType = {
|
export type CreateTicketType = {
|
||||||
title: string;
|
title: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user