admin ticket
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
VITE_TOKEN_NAME = 'admin_token'
|
VITE_TOKEN_NAME = 'admin_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||||
VITE_BASE_URL = 'https://danak-dsc-api.run.danakcorp.com'
|
# VITE_BASE_URL = 'https://danak-dsc-api.run.danakcorp.com'
|
||||||
# VITE_BASE_URL = 'http://192.168.0.207:3500'
|
VITE_BASE_URL = 'http://192.168.0.221:3500'
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { FC, InputHTMLAttributes } from 'react'
|
import { FC, InputHTMLAttributes } from 'react'
|
||||||
|
import Error from './Error'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string,
|
label: string,
|
||||||
|
error_text?: string
|
||||||
} & InputHTMLAttributes<HTMLTextAreaElement>
|
} & InputHTMLAttributes<HTMLTextAreaElement>
|
||||||
|
|
||||||
const Textarea: FC<Props> = (props: Props) => {
|
const Textarea: FC<Props> = (props: Props) => {
|
||||||
@@ -18,6 +20,11 @@ const Textarea: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
</textarea>
|
</textarea>
|
||||||
|
|
||||||
|
{
|
||||||
|
props.error_text &&
|
||||||
|
<Error errorText={props.error_text} />
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,57 @@
|
|||||||
import { FC, useCallback, useState } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string,
|
label: string,
|
||||||
|
isMultiple?: boolean,
|
||||||
|
onChange?: (file: File[]) => void;
|
||||||
|
isReset?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadBox: FC<Props> = (props: Props) => {
|
const UploadBox: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [file, setFile] = useState<File>()
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
setFile(acceptedFiles[0])
|
if (props.isMultiple) {
|
||||||
}, [])
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(array);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange([acceptedFiles[0]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [files])
|
||||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||||
|
|
||||||
|
const handleRemove = (index: number) => {
|
||||||
|
const array = [...files]
|
||||||
|
array.splice(index, 1)
|
||||||
|
setFiles(array)
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(array)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (props.isReset) {
|
||||||
|
setFiles([])
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [props.isReset])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -28,15 +63,27 @@ const UploadBox: FC<Props> = (props: Props) => {
|
|||||||
{t('select_file')}
|
{t('select_file')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className='text-description text-xs'>
|
<div className='lg:flex gap-2 hidden'>
|
||||||
{
|
{
|
||||||
file ?
|
files?.map((item, index) => (
|
||||||
file.name
|
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||||
:
|
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||||
t('no_select_file')
|
<div className='text-xs'>{item.name}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='lg:hidden gap-2 flex flex-wrap mt-4'>
|
||||||
|
{
|
||||||
|
files?.map((item, index) => (
|
||||||
|
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||||
|
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||||
|
<div className='text-xs'>{item.name}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -208,8 +208,21 @@
|
|||||||
"add_category": "اضافه کردن دسته بندی",
|
"add_category": "اضافه کردن دسته بندی",
|
||||||
"status_category": "وضعیت دسته بندی",
|
"status_category": "وضعیت دسته بندی",
|
||||||
"title_category": "عنوان دسته بندی",
|
"title_category": "عنوان دسته بندی",
|
||||||
"users": "کاربران"
|
"users": "کاربران",
|
||||||
|
"MEDIUM": "متوسط",
|
||||||
|
"LOW": "پایین",
|
||||||
|
"HIGH": "بالا",
|
||||||
|
"PENDING": "در انتظار",
|
||||||
|
"ANSWERED": "پاسخ داده شده",
|
||||||
|
"CLOSED": "بسته شده",
|
||||||
|
"asignto": "کارشناس پاسخگویی",
|
||||||
|
"category": "دسته بندی",
|
||||||
|
"low": "پایین",
|
||||||
|
"medium": "متوسط",
|
||||||
|
"high": "بالا"
|
||||||
},
|
},
|
||||||
|
"attach": "پیوست",
|
||||||
|
"date": "تاریخ",
|
||||||
"status": "وضعیت",
|
"status": "وضعیت",
|
||||||
"title": "عنوان",
|
"title": "عنوان",
|
||||||
"ads": {
|
"ads": {
|
||||||
|
|||||||
+281
-125
@@ -1,14 +1,89 @@
|
|||||||
import { FC } from 'react'
|
import { FC, Fragment, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 } from 'iconsax-react'
|
import { CloseCircle, InfoCircle, Paperclip2 } from 'iconsax-react'
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { useAddMessageTicket, useCloseTicket, useGetMessages } from './hooks/useTicketData'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { AddMessageTicketType, TicketMessageType } from './types/TicketTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
import { clx } from '../../helpers/utils'
|
||||||
|
import { Pages } from '../../config/Pages'
|
||||||
|
import { useMultiUpload } from '../service/hooks/useServiceData'
|
||||||
|
|
||||||
const TicketDetail: FC = () => {
|
const TicketDetail: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { id } = useParams()
|
||||||
|
const getMessages = useGetMessages(id ? id : '')
|
||||||
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
const addMessage = useAddMessageTicket(id ? id : '')
|
||||||
|
const multiUpload = useMultiUpload()
|
||||||
|
const [isResetFiles, setIsResetFiles] = useState<boolean>(false)
|
||||||
|
const closeTicket = useCloseTicket()
|
||||||
|
|
||||||
|
const formik = useFormik<AddMessageTicketType>({
|
||||||
|
initialValues: {
|
||||||
|
content: '',
|
||||||
|
attachmentUrls: [],
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object().shape({
|
||||||
|
content: Yup.string().required(t('errors.required')),
|
||||||
|
}),
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
if (files.length > 0) {
|
||||||
|
const images = new FormData()
|
||||||
|
files.forEach(file => {
|
||||||
|
images.append('files', file)
|
||||||
|
})
|
||||||
|
await multiUpload.mutateAsync(images, {
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
values.attachmentUrls = await data.data.map((item: { url: string }) => item?.url)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
delete values.attachmentUrls
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('success'))
|
||||||
|
formik.resetForm()
|
||||||
|
setFiles([])
|
||||||
|
setIsResetFiles(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsResetFiles(false)
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hadleCloseTicket = () => {
|
||||||
|
closeTicket.mutate(id ? id : '', {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('success'))
|
||||||
|
navigate(Pages.ticket.list)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -16,139 +91,220 @@ const TicketDetail: FC = () => {
|
|||||||
<div>{t('ticket.ticket_number')}</div>
|
<div>{t('ticket.ticket_number')}</div>
|
||||||
<div>12312</div>
|
<div>12312</div>
|
||||||
</div>
|
</div>
|
||||||
<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'>
|
getMessages.isPending ?
|
||||||
<div className='gap-6 rowTwoInput'>
|
<PageLoading />
|
||||||
<Input
|
:
|
||||||
label={t('ticket.select_your_service')}
|
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
|
||||||
value={'دی منو'}
|
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||||
readOnly
|
<div className='gap-6 rowTwoInput'>
|
||||||
/>
|
<Input
|
||||||
<Input
|
label={t('ticket.select_your_service')}
|
||||||
label={t('ticket.user_name')}
|
value={'دی منو'}
|
||||||
value={'مهرداد مظفری'}
|
readOnly
|
||||||
readOnly
|
/>
|
||||||
/>
|
<Input
|
||||||
</div>
|
label={t('ticket.user_name')}
|
||||||
|
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
|
||||||
<div className='mt-8 xl:text-sm text-xs'>
|
readOnly
|
||||||
موضوع درخواست پشتیبانی
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div 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 className='leading-7'>
|
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد،
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-end mt-6 text-xs text-description'>
|
|
||||||
10:07 | 1403/09/30
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='w-full flex justify-end'>
|
|
||||||
<div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'>
|
|
||||||
<div className='flex gap-1'>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{t('ticket.expert')}
|
|
||||||
</div>
|
|
||||||
<div>سیما فرهادی</div>
|
|
||||||
</div>
|
|
||||||
<div className='leading-7 mt-4'>
|
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد،
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-end mt-6 text-xs text-description'>
|
|
||||||
10:07 | 1403/09/30
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-9'>
|
|
||||||
<Textarea
|
|
||||||
label={t('ticket.answer')}
|
|
||||||
placeholder={t('ticket.your_description')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-7'>
|
|
||||||
<UploadBox
|
|
||||||
label={t('ticket.attachment')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end mt-6'>
|
|
||||||
<Button
|
|
||||||
label={t('ticket.send')}
|
|
||||||
className='max-w-[100px]'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='h-8'></div>
|
|
||||||
</div>
|
|
||||||
<div className={`h-fit xl:w-sidebar w-full`}>
|
|
||||||
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
|
|
||||||
<Button
|
|
||||||
label={t('ticket.send_answer')}
|
|
||||||
className='text-xs xl:h-10 h-8'
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className='bg-[#D52903] xl:h-10 h-8'
|
|
||||||
>
|
|
||||||
<div className='flex gap-2 text-xs items-center'>
|
|
||||||
<CloseCircle
|
|
||||||
color='white'
|
|
||||||
size={20}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
{t('ticket.close_ticket')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
|
||||||
</div>
|
<div className='mt-8 xl:text-sm text-xs'>
|
||||||
<div className='bg-white p-6 rounded-3xl mt-8'>
|
{getMessages.data?.data?.ticket?.subject}
|
||||||
<div className='flex justify-between'>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('ticket.info_ticket')}
|
|
||||||
</div>
|
|
||||||
<div className='h-7 px-3 rounded-xl flex items-center text-xs bg-green-100 text-green-400'>
|
|
||||||
{t('ticket.answered')}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
getMessages.data?.data?.messages.map((item: TicketMessageType) => {
|
||||||
|
if (item?.author?.role?.name === 'user') {
|
||||||
|
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 className='leading-7'>
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
<div className='flex dltr end mt-6 text-xs text-description'>
|
||||||
|
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2 flex-wrap text-sm'>
|
||||||
|
{
|
||||||
|
item.attachments && item?.attachments?.map((item, index: number) => {
|
||||||
|
return (
|
||||||
|
<div className='text-[#0047FF] mt-2 flex gap-1 items-center' key={item.id}>
|
||||||
|
<Paperclip2 size={20} color='#0047FF' />
|
||||||
|
<a href={item.attachmentUrl} target='_blank' rel='noreferrer'>
|
||||||
|
{t('attach') + ' ' + Number(index + 1)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className='w-full flex justify-end'>
|
||||||
|
<div className='mt-6 xl:text-sm text-xs bg-[#EBEDF5] p-6 rounded-3xl rounded-tl-none xl:max-w-[70%] max-w-[90%]'>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{t('ticket.expert')}
|
||||||
|
</div>
|
||||||
|
<div>{item.author?.firstName + ' ' + item?.author?.lastName}</div>
|
||||||
|
</div>
|
||||||
|
<div className='leading-7 mt-4'>
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
<div className='flex dltr mt-6 text-xs text-description'>
|
||||||
|
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2 flex-wrap text-sm'>
|
||||||
|
{
|
||||||
|
item.attachments && item?.attachments?.map((item, index: number) => {
|
||||||
|
return (
|
||||||
|
<div className='text-[#0047FF] mt-2 flex gap-1 items-center' key={item.id}>
|
||||||
|
<Paperclip2 size={20} color='#0047FF' />
|
||||||
|
<a href={item.attachmentUrl} target='_blank' rel='noreferrer'>
|
||||||
|
{t('attach') + ' ' + Number(index + 1)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-2'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
|
||||||
|
<Fragment>
|
||||||
|
<div className='mt-9'>
|
||||||
|
<Textarea
|
||||||
|
label={t('ticket.answer')}
|
||||||
|
placeholder={t('ticket.your_description')}
|
||||||
|
value={formik.values.content}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
name='content'
|
||||||
|
error_text={formik.touched.content && formik.errors.content ? formik.errors.content : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-7'>
|
||||||
|
<UploadBox
|
||||||
|
label={t('ticket.attachment')}
|
||||||
|
onChange={setFiles}
|
||||||
|
isMultiple
|
||||||
|
isReset={isResetFiles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end mt-6'>
|
||||||
|
<Button
|
||||||
|
label={t('ticket.send')}
|
||||||
|
className='max-w-[100px]'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={addMessage.isPending || multiUpload.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
<div className='h-8'></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={`h-fit lg:sticky left-0 lg:top-0 xl:w-sidebar w-full`}>
|
||||||
|
{
|
||||||
|
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
|
||||||
|
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
|
||||||
|
<Button
|
||||||
|
label={t('ticket.send_answer')}
|
||||||
|
className='text-xs xl:h-10 h-8'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className='bg-[#D52903] xl:h-10 h-8'
|
||||||
|
onClick={hadleCloseTicket}
|
||||||
|
isLoading={closeTicket.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 text-xs items-center'>
|
||||||
|
<CloseCircle
|
||||||
|
color='white'
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{t('ticket.close_ticket')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className={clx(
|
||||||
|
'bg-white p-6 rounded-3xl mt-8',
|
||||||
|
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'mt-0'
|
||||||
|
)}>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
{t('ticket.info_ticket')}
|
||||||
|
</div>
|
||||||
|
<div className={clx(
|
||||||
|
'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',
|
||||||
|
getMessages.data?.data?.ticket?.status === 'CLOSED' && 'bg-red-100 text-red-400'
|
||||||
|
)}>
|
||||||
|
{t(`ticket.${getMessages.data?.data?.ticket?.status}`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-14 text-xs w-full'>
|
<div className='mt-14 text-xs w-full'>
|
||||||
<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}
|
||||||
</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.ticket_number')}</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 : ''}
|
||||||
</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.ticket_number')}</div>
|
<div>{t('date')}</div>
|
||||||
<div>
|
<div className='dltr'>
|
||||||
۱۲۳۴۵۵
|
{moment(getMessages.data?.data?.ticket?.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.ticket_number')}</div>
|
<div>{t('ticket.category')}</div>
|
||||||
<div>
|
<div>
|
||||||
۱۲۳۴۵۵
|
{getMessages.data?.data?.ticket?.category?.title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex mt-6 justify-between text-description'>
|
||||||
|
<div>{t('ticket.priority')}</div>
|
||||||
|
<div>
|
||||||
|
{getMessages.data?.data?.ticket?.priority === 'low' ? t('ticket.low') : getMessages.data?.data?.ticket?.priority === 'medium' ? t('ticket.medium') : t('ticket.high')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-10 border-t text-xs pt-5 border-border items-center flex gap-2'>
|
<div className='mt-10 border-t text-xs pt-5 border-border items-center flex gap-2'>
|
||||||
<InfoCircle size={20} color='black' />
|
<InfoCircle size={20} color='black' />
|
||||||
<div>
|
<div>
|
||||||
{t('ticket.update_sms')}
|
{t('ticket.update_sms')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '../../components/Button'
|
import { Eye, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
|
||||||
import { Add, Eye, Message2, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
|
|
||||||
import Tabs from '../../components/Tabs'
|
import Tabs from '../../components/Tabs'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
|
import { useGetTickets } from './hooks/useTicketData'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { TicketItemType } from './types/TicketTypes'
|
||||||
|
|
||||||
const TicketList: FC = () => {
|
const TicketList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [activeTab, setActiveTab] = useState<string>('answered')
|
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED'>('ANSWERED')
|
||||||
|
const getTicket = useGetTickets(activeTab)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -19,7 +22,7 @@ const TicketList: FC = () => {
|
|||||||
{t('ticket.tickets')}
|
{t('ticket.tickets')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to={Pages.ticket.create}>
|
{/* <Link to={Pages.ticket.create}>
|
||||||
<Button
|
<Button
|
||||||
className='w-[172px]'
|
className='w-[172px]'
|
||||||
>
|
>
|
||||||
@@ -30,7 +33,7 @@ const TicketList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-14'>
|
<div className='mt-14'>
|
||||||
@@ -38,27 +41,27 @@ const TicketList: FC = () => {
|
|||||||
active={activeTab}
|
active={activeTab}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
icon: <MessageTick color={activeTab === 'answered' ? 'black' : '#8C90A3'} size={22} />,
|
icon: <MessageTick color={activeTab === 'ANSWERED' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
label: t('ticket.answered'),
|
label: t('ticket.answered'),
|
||||||
value: 'answered'
|
value: 'ANSWERED'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <MessageTime color={activeTab === 'checking' ? 'black' : '#8C90A3'} size={22} />,
|
icon: <MessageTime color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
label: t('ticket.checking'),
|
label: t('ticket.checking'),
|
||||||
value: 'checking'
|
value: 'PENDING'
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// icon: <Message2 color={activeTab === 'wating' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
|
// label: t('ticket.wait_answered'),
|
||||||
|
// value: 'wating'
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
icon: <Message2 color={activeTab === 'wating' ? 'black' : '#8C90A3'} size={22} />,
|
icon: <MessageRemove color={activeTab === 'CLOSED' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
label: t('ticket.wait_answered'),
|
|
||||||
value: 'wating'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <MessageRemove color={activeTab === 'closed' ? 'black' : '#8C90A3'} size={22} />,
|
|
||||||
label: t('ticket.closed'),
|
label: t('ticket.closed'),
|
||||||
value: 'closed'
|
value: 'CLOSED'
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onChange={setActiveTab}
|
onChange={(value) => setActiveTab(value as 'ANSWERED' | 'PENDING' | 'CLOSED')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -77,34 +80,28 @@ const TicketList: FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className='tr'>
|
{
|
||||||
<Td text={t('ticket.number')} />
|
getTicket.data?.data?.tickets?.map((item: TicketItemType) => (
|
||||||
<Td text={t('ticket.title')} />
|
<tr key={item.id} className='tr'>
|
||||||
<Td text={t('ticket.team')} />
|
<Td text={item.numericId + ''} />
|
||||||
<Td text={t('ticket.date')} />
|
<Td text={item.subject} />
|
||||||
<Td text={t('ticket.status')} />
|
<Td text={item?.category?.title} />
|
||||||
<Td text={t('ticket.priority')} />
|
<Td text={''}>
|
||||||
<Td text={''}>
|
<div className='dltr text-right'>
|
||||||
<Link to={Pages.ticket.detail + '1'}>
|
{moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
|
||||||
<Eye size={20} color='#8C90A3' />
|
</div>
|
||||||
</Link>
|
</Td>
|
||||||
</Td>
|
<Td text={t(`ticket.${item.status}`)} />
|
||||||
<Td text={''} />
|
<Td text={t(`ticket.${item.priority}`)} />
|
||||||
</tr>
|
<Td text={''}>
|
||||||
<tr className='tr'>
|
<Link to={Pages.ticket.detail + item.id}>
|
||||||
<Td text={t('ticket.number')} />
|
<Eye size={20} color='#8C90A3' />
|
||||||
<Td text={t('ticket.title')} />
|
</Link>
|
||||||
<Td text={t('ticket.team')} />
|
</Td>
|
||||||
<Td text={t('ticket.date')} />
|
<Td text={''} />
|
||||||
<Td text={t('ticket.status')} />
|
</tr>
|
||||||
<Td text={t('ticket.priority')} />
|
))
|
||||||
<Td text={''}>
|
}
|
||||||
<Link to={Pages.ticket.detail + '1'}>
|
|
||||||
<Eye size={20} color='#8C90A3' />
|
|
||||||
</Link>
|
|
||||||
</Td>
|
|
||||||
<Td text={''} />
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/TicketServiec";
|
||||||
|
import { AddMessageTicketType } from "../types/TicketTypes";
|
||||||
|
|
||||||
|
export const useGetTickets = (status: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["tickets", status],
|
||||||
|
queryFn: () => api.getTickets(status),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMessages = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["ticket-messages", id],
|
||||||
|
queryFn: () => api.getMessages(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddMessageTicket = (id: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: AddMessageTicketType) =>
|
||||||
|
api.addMessageTicket(id, variables),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({
|
||||||
|
queryKey: ["ticket-messages", id],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCloseTicket = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.closeTicket(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import { AddMessageTicketType } from "../types/TicketTypes";
|
||||||
|
|
||||||
|
export const getTickets = async (status: string) => {
|
||||||
|
const { data } = await axios.get(`/tickets/admin?status=${status}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessages = async (id: string) => {
|
||||||
|
const { data } = await axios.get(`/tickets/${id}/messages`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addMessageTicket = async (
|
||||||
|
id: string,
|
||||||
|
params: AddMessageTicketType
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.post(`/tickets/${id}/messages`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeTicket = async (id: string) => {
|
||||||
|
const { data } = await axios.post(`/tickets/${id}/close`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export type TicketItemType = {
|
||||||
|
id: string;
|
||||||
|
numericId: number;
|
||||||
|
subject: string;
|
||||||
|
category: {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddMessageTicketType = {
|
||||||
|
content: string;
|
||||||
|
attachmentUrls?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketMessageType = {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
attachmentUrls: string[];
|
||||||
|
createdAt: string;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
author: {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attachments: {
|
||||||
|
attachmentUrl: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user