remove some files

This commit is contained in:
hamid zarghami
2026-02-16 10:29:08 +03:30
parent c00f0c4856
commit 3ea39c9efe
11 changed files with 26 additions and 1472 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../components/Input'
import Textarea from '../../components/Textarea'
-196
View File
@@ -1,196 +0,0 @@
import { type FC } from 'react'
import Select from '@/components/Select'
import Input from '@/components/Input'
import Td from '@/components/Td'
import SwitchComponent from '@/components/Switch'
import { useFormik } from 'formik'
import type { CategoriesItemType, CreateCategoryType } from './types/TicketTypes'
import * as Yup from 'yup'
import Button from '@/components/Button'
import { useCreateCategory, useGetCategories } from './hooks/useTicketData'
import Textarea from '@/components/Textarea'
import type { ErrorType } from '@/helpers/types'
import { toast } from 'react-toastify'
const TicketCategory: FC = () => {
// TODO: این hook باید بعداً وصل شود - مربوط به users است
// const getGroups = useGetGroups()
const createCategory = useCreateCategory()
const getCategories = useGetCategories()
const formik = useFormik<CreateCategoryType>({
initialValues: {
title: '',
description: '',
userGroupId: '',
isActive: true
},
validationSchema: Yup.object({
title: Yup.string().required('این فیلد الزامی است'),
userGroupId: Yup.string().required('این فیلد الزامی است'),
description: Yup.string().required('این فیلد الزامی است'),
}),
onSubmit: (values: CreateCategoryType) => {
createCategory.mutate(values, {
onSuccess: () => {
formik.resetForm()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
})
return (
<div className='mt-4'>
<div>
دستهبندی تیکتها
</div>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1'>
<div className='flex w-full justify-between items-center'>
<div className='w-28'>
<Select
items={[
{
label: '',
value: ''
}
]}
placeholder='وضعیت'
className='border'
/>
</div>
<div>
<Input
variant='search'
placeholder='جستجو'
className='bg-white border'
/>
</div>
</div>
<div className='bg-white mt-5 py-3 xl:px-10 px-4 rounded-3xl flex-1'>
<div className='relative overflow-x-auto rounded-3xl w-full'>
<table className='w-full text-sm '>
<thead className='thead'>
<tr>
<Td text='عنوان' />
<Td text='وضعیت' />
</tr>
</thead>
<tbody>
{
getCategories.data?.data?.ticketCategories?.map((item: CategoriesItemType) => (
<tr key={item.id} className='tr'>
<Td text={item.title} />
<Td text={item.isActive ? 'فعال' : 'غیرفعال'} />
</tr>
))
}
</tbody>
</table>
</div>
</div>
</div>
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
افزودن دستهبندی
</div>
<div className='mt-6 flex justify-between items-center'>
<div className='text-xs'>
وضعیت دستهبندی
</div>
<SwitchComponent
active={formik.values.isActive}
onChange={(value) => formik.setFieldValue('isActive', value)}
/>
</div>
<div className='mt-6'>
<Input
label='عنوان دسته‌بندی'
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
</div>
<div className='mt-6'>
<Textarea
label='توضیحات'
{...formik.getFieldProps('description')}
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
/>
</div>
<div className='mt-6'>
<Select
label='کاربران'
// TODO: این items باید بعداً از getGroups.data گرفته شود
items={[
// موقت: داده‌های mock برای نمایش UI
// getGroups.data?.data?.usersGroup?.map((item: GroupItemType) => ({
// label: item.name,
// value: item.id
// }))
]}
{...formik.getFieldProps('userGroupId')}
error_text={formik.touched.userGroupId && formik.errors.userGroupId ? formik.errors.userGroupId : ''}
placeholder='انتخاب کنید'
className='border'
/>
</div>
<div className='mt-6'>
<Button
label='ثبت'
onClick={() => formik.handleSubmit()}
isLoading={createCategory.isPending}
/>
</div>
{/* <div className='mt-6 border border-dashed border-border rounded-xl p-4 flex gap-2 flex-wrap'>
<div className='bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg'>
<div className=' '>
سیما فرهادی
</div>
<CloseCircle
variant='Bold'
className='size-4'
color='red'
/>
</div>
<div className='bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg'>
<div className=' '>
سیما فرهادی
</div>
<CloseCircle
variant='Bold'
className='size-4'
color='red'
/>
</div>
<div className='bg-[#EBEEF5] flex gap-2 text-xs items-center px-2 py-2 rounded-lg'>
<div className=' '>
سیما فرهادی
</div>
<CloseCircle
variant='Bold'
className='size-4'
color='red'
/>
</div>
</div> */}
</div>
</div>
</div>
)
}
export default TicketCategory
-236
View File
@@ -1,236 +0,0 @@
import { type FC } from 'react'
import Input from '@/components/Input'
import Select from '@/components/Select'
import Textarea from '@/components/Textarea'
import UploadBox from '@/components/UploadBox'
import Button from '@/components/Button'
import { TickCircle, TickSquare } from 'iconsax-react'
import { useCreateTicket, useGetCategories } from './hooks/useTicketData'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import type { ErrorType } from '@/helpers/types'
import { useNavigate } from 'react-router-dom'
import type { CreateTicketAdminType } from './types/TicketTypes'
import { toast } from 'react-toastify'
const CreateTicket: FC = () => {
const navigate = useNavigate()
// const [files, setFiles] = useState<File[]>([])
const getCategories = useGetCategories()
const createTicket = useCreateTicket()
const formik = useFormik<CreateTicketAdminType>({
initialValues: {
subject: '',
message: '',
categoryId: '',
priority: 'LOW',
attachmentUrls: [],
userId: ''
},
validationSchema: Yup.object({
subject: Yup.string().required('این فیلد الزامی است'),
message: Yup.string().required('این فیلد الزامی است'),
categoryId: Yup.string().required('این فیلد الزامی است'),
priority: Yup.string().required('این فیلد الزامی است'),
}),
onSubmit: async (values) => {
// TODO: آپلود فایل باید بعداً وصل شود - نیاز به multiUpload API دارد
// 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
// }
delete values.attachmentUrls
if (values.danakServiceId === '') {
delete values.danakServiceId
}
createTicket.mutate(values, {
onSuccess: (data) => {
toast.success('تیکت با موفقیت ثبت شد')
formik.resetForm()
navigate(`/ticket/detail/${data.data?.ticketId}`)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
})
return (
<div className='mt-4 pb-12'>
<div>
تیکت جدید
</div>
{
getCategories.isPending ?
<div>در حال بارگذاری...</div>
:
<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>
پیام خود را ثبت کنید
</div>
<div className='mt-5'>
<Select
label='مشتری'
placeholder='انتخاب کنید'
// TODO: این items باید بعداً از getCustomers.data گرفته شود
items={[
// موقت: داده‌های mock برای نمایش UI
// getCustomers.data?.data?.customers?.map((item: CustomerItemType) => ({
// label: item.firstName + ' ' + item.lastName,
// value: item.id
// }))
]}
{...formik.getFieldProps('userId')}
/>
</div>
<div className='mt-6 rowTwoInput'>
<Input
label='موضوع'
placeholder='موضوع خود را وارد کنید'
name='subject'
value={formik.values.subject}
onChange={formik.handleChange}
error_text={formik.touched.subject && formik.errors.subject ? formik.errors.subject : ''}
/>
<Select
label='اولویت'
items={[
{
label: 'کم',
value: 'LOW'
},
{
label: 'متوسط',
value: 'MEDIUM'
},
{
label: 'زیاد',
value: 'HIGH'
}
]}
placeholder='انتخاب کنید'
className='border'
name='priority'
value={formik.values.priority}
onChange={formik.handleChange}
error_text={formik.touched.priority && formik.errors.priority ? formik.errors.priority : ''}
/>
</div>
<div className='mt-6 rowTwoInput'>
<Select
label='دسته‌بندی'
items={getCategories.data?.data?.ticketCategories?.map((item: { id: string, title: string }) => ({
label: item.title,
value: item.id
})
) ?? []}
placeholder='انتخاب کنید'
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'>
<Textarea
label='توضیحات'
placeholder='توضیحات خود را وارد کنید'
name='message'
value={formik.values.message}
onChange={formik.handleChange}
error_text={formik.touched.message && formik.errors.message ? formik.errors.message : ''}
/>
</div>
<div className='mt-6'>
<UploadBox
label='پیوست'
isMultiple
onChange={() => { }}
/>
</div>
<div className='mt-8 flex justify-end'>
<Button
className='xl:max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={createTicket.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
ارسال
</div>
</div>
</Button>
</div>
</div>
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
راهنمای ارسال تیکت
</div>
<div className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
</div>
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از داناک می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
</div>
</div>
</div>
}
</div>
)
}
export default CreateTicket
-352
View File
@@ -1,352 +0,0 @@
import { type FC, Fragment, useState } from 'react'
import Input from '@/components/Input'
import Textarea from '@/components/Textarea'
import UploadBox from '@/components/UploadBox'
import Button from '@/components/Button'
import { CloseCircle, InfoCircle, Paperclip2 } from 'iconsax-react'
import { useNavigate, useParams } from 'react-router-dom'
import { useAddMessageTicket, useCloseTicket, useGetMessages } from './hooks/useTicketData'
import moment from 'moment-jalaali'
import { useFormik } from 'formik'
import type { AddMessageTicketType, TicketMessageType } from './types/TicketTypes'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import type { ErrorType } from '@/helpers/types'
import { clx } from '@/helpers/utils'
import { Paths } from '@/config/Paths'
import ReferTicket from './components/ReferTicket'
const TicketDetail: FC = () => {
const navigate = useNavigate()
const { id } = useParams()
const getMessages = useGetMessages(id ? id : '')
const [setFiles] = useState<File[]>([])
const addMessage = useAddMessageTicket(id ? id : '')
const [isResetFiles, setIsResetFiles] = useState<boolean>(false)
const closeTicket = useCloseTicket()
const formik = useFormik<AddMessageTicketType>({
initialValues: {
content: '',
attachmentUrls: [],
},
validationSchema: Yup.object().shape({
content: Yup.string().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
// }
delete values.attachmentUrls
addMessage.mutate(values, {
onSuccess: () => {
toast.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('تیکت با موفقیت بسته شد')
navigate(Paths.ticket.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
return (
<div className='mt-4'>
<div className='flex gap-1'>
<div>شماره تیکت</div>
<div>{getMessages.data?.data?.ticket?.numericId || '12345'}</div>
</div>
{
getMessages.isPending ?
<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'>
<div className='gap-6 rowTwoInput'>
<Input
label='انتخاب سرویس شما'
value={(getMessages.data?.data?.ticket?.danakService as any)?.name || 'طراحی گرافیک'}
readOnly
/>
<Input
label='نام کاربر'
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName || 'علی احمدی'}
readOnly
/>
<Input
label='پلن کاربر'
value={(getMessages.data?.data?.supportPlan as any)?.userSupportPlan?.supportPlan?.name || 'پلن طلایی'}
readOnly
/>
</div>
<div className='mt-8 xl:text-sm text-xs'>
{getMessages.data?.data?.ticket?.subject || 'مشکل در دریافت فایل نهایی پروژه'}
</div>
{
getMessages.data?.data?.messages && getMessages.data.data.messages.length > 0 ? (
getMessages.data.data.messages.map((item: TicketMessageType) => {
if (item?.author?.roles[0]?.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'>
{'پیوست' + ' ' + Number(index + 1)}
</a>
</div>
)
})
}
</div>
</div>
)
} else {
return (
<div key={item.id} 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'>
کارشناس
</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'>
{'پیوست' + ' ' + Number(index + 1)}
</a>
</div>
)
})
}
</div>
<div className='flex gap-2'></div>
</div>
</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 dltr end mt-6 text-xs text-description'>
1403/08/01 14: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'>
کارشناس
</div>
<div>رضا محمدی</div>
</div>
<div className='leading-7 mt-4'>
سلام، بله حتما. فایل نهایی پروژه شما در حال بررسی نهایی است و تا پایان امروز برای شما ارسال خواهد شد.
</div>
<div className='flex dltr mt-6 text-xs text-description'>
1403/08/01 15:45
</div>
</div>
</div>
</>
)
}
{
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
<Fragment>
<div className='mt-9'>
<Textarea
label='پاسخ'
placeholder='توضیحات خود را وارد کنید'
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='پیوست'
onChange={setFiles}
isMultiple
isReset={isResetFiles}
/>
</div>
<div className='flex justify-end mt-6'>
<Button
label='ارسال'
className='max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={addMessage.isPending}
/>
</div>
</Fragment>
}
<div className='h-8'></div>
</div>
<div className={`h-fit lg:sticky left-0 lg:top-0 xl:w-[300px] w-full`}>
{
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
<div className='bg-white xl:p-6 p-4 rounded-3xl flex gap-4'>
<Button
label='ارسال پاسخ'
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>
بستن تیکت
</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'>
اطلاعات تیکت
</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'
)}>
{
getMessages.data?.data?.ticket?.status === 'PENDING' ? 'در انتظار' :
getMessages.data?.data?.ticket?.status === 'ANSWERED' ? 'پاسخ داده شده' :
'بسته شده'
}
</div>
</div>
<div className='mt-14 text-xs w-full'>
<div className='flex justify-between text-description'>
<div>شماره تیکت</div>
<div>
{getMessages.data?.data?.ticket?.numericId || '12345'}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>ارجاع به</div>
<div>
{getMessages.data?.data?.ticket?.assignedTo ? (getMessages.data?.data?.ticket?.assignedTo as any)?.firstName + ' ' + (getMessages.data?.data?.ticket?.assignedTo as any)?.lastName : 'رضا محمدی'}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>تاریخ</div>
<div className='dltr'>
{getMessages.data?.data?.ticket?.createdAt ? moment(getMessages.data?.data?.ticket?.createdAt).format('jYYYY/jMM/jDD HH:mm') : '1403/08/01 14:30'}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>دستهبندی</div>
<div>
{getMessages.data?.data?.ticket?.category?.title || 'پشتیبانی فنی'}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>اولویت</div>
<div>
{getMessages.data?.data?.ticket?.priority === 'low' ? 'کم' : getMessages.data?.data?.ticket?.priority === 'medium' ? 'متوسط' : getMessages.data?.data?.ticket?.priority === 'high' ? 'زیاد' : 'متوسط'}
</div>
</div>
</div>
<div className='mt-10 border-t text-xs pt-5 border-border items-center flex gap-2'>
<InfoCircle size={20} color='black' />
<div>
بهروزرسانی از طریق پیامک
</div>
</div>
</div>
{
<ReferTicket refetch={getMessages.refetch} />
}
</div>
</div>
}
</div>
)
}
export default TicketDetail
-147
View File
@@ -1,147 +0,0 @@
import { type FC, useEffect, useMemo, useState } from 'react'
import { Eye, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
import Tabs from '@/components/Tabs'
import Table from '@/components/Table'
import type { ColumnType } from '@/components/types/TableTypes'
import { Link, useSearchParams } from 'react-router-dom'
import { useGetTickets } from './hooks/useTicketData'
import moment from 'moment-jalaali'
import type { TicketItemType } from './types/TicketTypes'
import OpenTicket from './components/OpenTicket'
const TicketList: FC = () => {
const [page, setPage] = useState<number>(1)
const [searchParams] = useSearchParams()
const [customerId, setCustomerId] = useState<string>('')
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED' | ''>('PENDING')
const getTicket = useGetTickets(activeTab, customerId, page)
useEffect(() => {
const urlCustomerId = searchParams.get('user')
if (urlCustomerId) {
setCustomerId(urlCustomerId)
setActiveTab('')
}
}, [searchParams])
const columns = useMemo<ColumnType<TicketItemType>[]>(() => [
{
title: 'شماره',
key: 'numericId',
render: (item) => item.numericId + ''
},
{
title: 'عنوان',
key: 'subject'
},
{
title: 'کاربر',
key: 'user',
render: (item) => item.user.firstName + ' ' + item.user.lastName
},
{
title: 'ارجاع به',
key: 'assignedTo',
render: (item) => item?.assignedTo?.firstName + ' ' + item?.assignedTo?.lastName
},
{
title: 'دسته‌بندی',
key: 'category',
render: (item) => item?.category?.title
},
{
title: 'سرویس',
key: 'danakService',
render: (item) => item?.danakService?.name
},
{
title: 'تاریخ',
key: 'updatedAt',
render: (item) => (
<div className='dltr text-right'>
{moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
</div>
)
},
{
title: 'وضعیت',
key: 'status',
render: (item) =>
item.status === 'PENDING' ? 'در حال بررسی' :
item.status === 'ANSWERED' ? 'پاسخ داده شده' :
item.status === 'CLOSED' ? 'بسته شده' : item.status
},
{
title: 'اولویت',
key: 'priority',
render: (item) =>
item.priority === 'LOW' ? 'کم' :
item.priority === 'MEDIUM' ? 'متوسط' :
item.priority === 'HIGH' ? 'زیاد' : item.priority
},
{
title: 'جزئیات',
key: 'actions',
render: (item) => (
<div className='flex items-center gap-2'>
<Link to={`/ticket/detail/${item.id}`}>
<Eye size={20} color='#8C90A3' />
</Link>
{item.status === 'CLOSED' && (
<OpenTicket refetch={getTicket.refetch} id={item.id} />
)}
</div>
)
}
], [getTicket.refetch])
return (
<div className='mt-4'>
<div className='flex justify-between items-center'>
<div>
تیکتها
</div>
</div>
<div className='mt-14'>
<Tabs
active={activeTab}
items={[
{
icon: <MessageTime color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
label: 'در حال بررسی',
value: 'PENDING'
},
{
icon: <MessageTick color={activeTab === 'ANSWERED' ? 'black' : '#8C90A3'} size={22} />,
label: 'پاسخ داده شده',
value: 'ANSWERED'
},
{
icon: <MessageRemove color={activeTab === 'CLOSED' ? 'black' : '#8C90A3'} size={22} />,
label: 'بسته شده',
value: 'CLOSED'
},
]}
onChange={(value) => setActiveTab(value as 'ANSWERED' | 'PENDING' | 'CLOSED')}
/>
</div>
<Table
columns={columns}
data={getTicket.data?.data?.tickets}
isLoading={getTicket.isLoading}
noDataMessage='هیچ تیکتی یافت نشد'
pagination={(getTicket.data?.data?.pager?.totalPages ?? 0) > 1 ? {
currentPage: page,
totalPages: getTicket.data?.data?.pager?.totalPages ?? 1,
onPageChange: setPage
} : undefined}
/>
</div>
)
}
export default TicketList
@@ -1,34 +0,0 @@
import { type FC } from 'react'
import Button from '@/components/Button'
import { useOpenTicket } from '../hooks/useTicketData'
type Props = {
id: string,
refetch: () => void
}
const OpenTicket: FC<Props> = ({ id, refetch }) => {
const { mutate: openTicket, isPending } = useOpenTicket()
const handleOpenTicket = () => {
openTicket(id, {
onSuccess: () => {
refetch()
}
})
}
return (
<div>
<Button
label={'باز کردن تیکت'}
onClick={handleOpenTicket}
className='px-4 h-8 text-xs'
isLoading={isPending}
/>
</div>
)
}
export default OpenTicket
@@ -1,76 +0,0 @@
import { type FC } from 'react'
import { useParams } from 'react-router-dom'
import { useReferTicket } from '../hooks/useTicketData'
import Button from '@/components/Button'
import { toast } from 'react-toastify'
import type { ErrorType } from '@/helpers/types'
type Props = {
refetch: () => void
}
const ReferTicket: FC<Props> = ({ refetch }) => {
const { id } = useParams()
// const [userId, setUserId] = useState<string>('')
// TODO: این hook باید بعداً وصل شود - مربوط به users است
// const getUsers = useGetUsers('', 'tickets')
const referTicket = useReferTicket()
const handleReferTicket = () => {
referTicket.mutate({ id: id as string, userId: '' }, {
onSuccess: () => {
refetch()
toast.success('تیکت با موفقیت ارجاع داده شد')
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
return (
<div className={'bg-white p-6 rounded-3xl mt-8'}>
<div className='flex justify-between'>
<div className='text-sm'>
ارجاع تیکت
</div>
</div>
<div className="mt-4">
{/* TODO: این بخش باید بعداً وصل شود - نیاز به getUsers.data دارد */}
{/* {
getUsers.data?.data?.users?.map((item: UserItemType) => (
<div key={item.id} className='flex gap-2 items-center'>
<div>
<CheckBoxComponent
checked={userId === item.id}
onChange={(e) => setUserId(e.target.checked ? item.id : '')}
/>
</div>
<div className='text-xs leading-5'>
{item.firstName + ' ' + item.lastName}
</div>
</div>
))
} */}
<div className='text-xs text-gray-500 text-center py-4'>
لیست کاربران در حال حاضر در دسترس نیست
</div>
<div className='flex justify-end mt-4'>
<Button
label='ارجاع تیکت'
onClick={handleReferTicket}
isLoading={referTicket.isPending}
className='w-fit px-7'
/>
</div>
</div>
</div>
)
}
export default ReferTicket
-143
View File
@@ -1,143 +0,0 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
// import * as api from "../service/TicketServiec";
import type {
AddMessageTicketType,
CreateCategoryType,
CreateTicketAdminType,
} from "../types/TicketTypes";
// Mock data برای نمایش UI
const mockTicketsData = {
data: {
tickets: [],
pager: {
totalPages: 1,
currentPage: 1,
},
},
};
const mockMessagesData = {
data: {
ticket: {
id: "",
numericId: 0,
subject: "",
status: "PENDING",
priority: "LOW",
user: {
firstName: "",
lastName: "",
},
assignedTo: null,
category: {
title: "",
},
danakService: null,
createdAt: new Date().toISOString(),
},
messages: [],
supportPlan: null,
},
};
const mockCategoriesData = {
data: {
ticketCategories: [],
},
};
export const useGetTickets = (
status: string,
customerId?: string,
page?: number
) => {
return useQuery({
queryKey: ["tickets", status, customerId, page],
// TODO: API call کامنت شده - باید بعداً وصل شود
// queryFn: () => api.getTickets(status, customerId, page),
queryFn: () => Promise.resolve(mockTicketsData),
});
};
export const useGetMessages = (id: string) => {
return useQuery({
queryKey: ["ticket-messages", id],
// TODO: API call کامنت شده - باید بعداً وصل شود
// queryFn: () => api.getMessages(id),
queryFn: () => Promise.resolve(mockMessagesData),
enabled: !!id,
});
};
export const useAddMessageTicket = (id: string) => {
const queryClient = useQueryClient();
return useMutation({
// TODO: API call کامنت شده - باید بعداً وصل شود
// mutationFn: (variables: AddMessageTicketType) =>
// api.addMessageTicket(id, variables),
mutationFn: (_variables: AddMessageTicketType) =>
Promise.resolve({ data: { success: true } }),
onSuccess: () => {
queryClient.refetchQueries({
queryKey: ["ticket-messages", id],
});
},
});
};
export const useCloseTicket = () => {
return useMutation({
// TODO: API call کامنت شده - باید بعداً وصل شود
// mutationFn: (id: string) => api.closeTicket(id),
mutationFn: (_id: string) => Promise.resolve({ data: { success: true } }),
});
};
export const useCreateCategory = () => {
return useMutation({
// TODO: API call کامنت شده - باید بعداً وصل شود
// mutationFn: (variables: CreateCategoryType) =>
// api.createCategory(variables),
mutationFn: (_variables: CreateCategoryType) =>
Promise.resolve({ data: { success: true } }),
});
};
export const useGetCategories = () => {
return useQuery({
queryKey: ["categories"],
// TODO: API call کامنت شده - باید بعداً وصل شود
// queryFn: () => api.getCategories(),
queryFn: () => Promise.resolve(mockCategoriesData),
});
};
export const useReferTicket = () => {
return useMutation({
// TODO: API call کامنت شده - باید بعداً وصل شود
// mutationFn: (variables: { id: string; userId: string }) =>
// api.referTicket(variables.id, variables.userId),
mutationFn: (_variables: { id: string; userId: string }) =>
Promise.resolve({ data: { success: true } }),
});
};
export const useOpenTicket = () => {
return useMutation({
// TODO: API call کامنت شده - باید بعداً وصل شود
// mutationFn: (id: string) => api.openTicket(id),
mutationFn: (_id: string) => Promise.resolve({ data: { success: true } }),
});
};
export const useCreateTicket = () => {
return useMutation({
// TODO: API call کامنت شده - باید بعداً وصل شود
// mutationFn: (variables: CreateTicketAdminType) =>
// api.createTicket(variables),
mutationFn: (_variables: CreateTicketAdminType) =>
Promise.resolve({ data: { ticketId: "mock-ticket-id" } }),
});
};
-82
View File
@@ -1,82 +0,0 @@
// import axios from "@/config/axios";
import type {
AddMessageTicketType,
CreateCategoryType,
CreateTicketAdminType,
} from "../types/TicketTypes";
// TODO: تمام API calls کامنت شده‌اند - باید بعداً وصل شوند
export const getTickets = async (
_status: string,
_customerId?: string,
_page?: number
) => {
// const params = new URLSearchParams();
// if (status) params.append("status", status);
// if (customerId) params.append("userId", customerId);
// if (page) params.append("page", page.toString());
// const query = params.toString();
// const { data } = await axios.get(`/tickets/admin?${query}`);
// return data;
return { data: { tickets: [], pager: { totalPages: 1 } } };
};
export const getMessages = async (_id: string) => {
// const { data } = await axios.get(`/tickets/${id}/messages`);
// return data;
return {
data: {
ticket: null,
messages: [],
supportPlan: null,
},
};
};
export const addMessageTicket = async (
_id: string,
_params: AddMessageTicketType
) => {
// const { data } = await axios.post(`/tickets/${id}/messages`, params);
// return data;
return { data: { success: true } };
};
export const closeTicket = async (_id: string) => {
// const { data } = await axios.post(`/tickets/${id}/close`);
// return data;
return { data: { success: true } };
};
export const createCategory = async (_params: CreateCategoryType) => {
// const { data } = await axios.post(`/tickets/category`, params);
// return data;
return { data: { success: true } };
};
export const getCategories = async () => {
// const { data } = await axios.get(`/tickets/categories`);
// return data;
return { data: { ticketCategories: [] } };
};
export const referTicket = async (_id: string, _userId: string) => {
// const { data } = await axios.post(`/tickets/${id}/refer`, {
// adminId: userId,
// });
// return data;
return { data: { success: true } };
};
export const openTicket = async (_id: string) => {
// const { data } = await axios.post(`/tickets/${id}/open`);
// return data;
return { data: { success: true } };
};
export const createTicket = async (_params: CreateTicketAdminType) => {
// const { data } = await axios.post(`/tickets/admin`, params);
// return data;
return { data: { ticketId: "mock-ticket-id" } };
};
-91
View File
@@ -1,91 +0,0 @@
export type TicketItemType = {
id: string;
numericId: number;
subject: string;
category: {
title: string;
};
user: {
id: string;
firstName: string;
lastName: string;
};
assignedTo: {
id: string;
firstName: string;
lastName: string;
};
updatedAt: string;
status: string;
priority: string;
danakService: {
id: string;
name: string;
description: string;
icon: string;
link: string;
isActive: boolean;
isDanakSuggest: boolean;
metaDescription: string;
serviceLanguage: string;
softwareLanguage: string;
title: string;
userCount: number;
createdAt: string;
updatedAt: 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;
roles: {
name: string;
}[];
};
attachments: {
attachmentUrl: string;
id: string;
}[];
};
export type CreateCategoryType = {
title: string;
description: string;
userGroupId: string;
isActive: boolean;
};
export type CategoriesItemType = {
id: string;
title: string;
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
};
export type CreateTicketAdminType = {
subject: string;
priority: "LOW" | "MEDIUM" | "HIGH";
danakServiceId?: string;
categoryId: string;
message: string;
attachmentUrls?: string[];
userId: string;
};
+25 -114
View File
@@ -13,10 +13,6 @@ import CreateProduct from "@/pages/product/Create";
import CreateFeature from "@/pages/Features/Create";
import PrintService from "@/pages/service/PrintService";
import DetailPerfomaInvoice from "@/pages/invoice/DetailPerfomaInvoice";
import TicketList from "@/pages/ticket/TicketList";
import CreateTicket from "@/pages/ticket/CreateTicket";
import TicketDetail from "@/pages/ticket/Detail";
import TicketCategory from "@/pages/ticket/Category";
import AnnouncementList from "@/pages/annoncement/List";
import AnnouncementUpdate from "@/pages/annoncement/Update";
import AnnouncementCreate from "@/pages/annoncement/Create";
@@ -62,142 +58,57 @@ const MainRouter: FC = () => {
<Routes>
<Route path="/" element={<Home />} />
<Route path={Paths.home} element={<Home />} />
<Route
path={Paths.perfomaInvoice.list}
element={<ProformaInvoice />}
/>
<Route
path={Paths.perfomaInvoice.detail + ":id"}
element={<DetailPerfomaInvoice />}
/>
<Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} />
<Route path={Paths.perfomaInvoice.detail + ":id"} element={<DetailPerfomaInvoice />} />
<Route path={Paths.requests.list} element={<RequestList />} />
<Route path={Paths.product.list} element={<ProductList />} />
<Route path={Paths.product.create} element={<CreateProduct />} />
<Route
path={Paths.product.update + ":id"}
element={<UpdateProduct />}
/>
<Route
path={Paths.product.attribute.create + ":id"}
element={<CreateAttribute />}
/>
<Route
path={Paths.product.attribute.list + ":id"}
element={<AttributeList />}
/>
<Route
path={Paths.product.attribute.update + ":id"}
element={<UpdateAttribute />}
/>
<Route
path={Paths.product.attributeValue.list + ":id"}
element={<AttributeValues />}
/>
<Route
path={Paths.product.category.list}
element={<CategoryList />}
/>
<Route
path={Paths.product.category.create}
element={<CreateCategory />}
/>
<Route
path={Paths.product.category.update + ":id"}
element={<UpdateCategory />}
/>
<Route path={Paths.product.update + ":id"} element={<UpdateProduct />} />
<Route path={Paths.product.attribute.create + ":id"} element={<CreateAttribute />} />
<Route path={Paths.product.attribute.list + ":id"} element={<AttributeList />} />
<Route path={Paths.product.attribute.update + ":id"} element={<UpdateAttribute />} />
<Route path={Paths.product.attributeValue.list + ":id"} element={<AttributeValues />} />
<Route path={Paths.product.category.list} element={<CategoryList />} />
<Route path={Paths.product.category.create} element={<CreateCategory />} />
<Route path={Paths.product.category.update + ":id"} element={<UpdateCategory />} />
<Route
path={Paths.formBuilder.list + ":id/:type"}
element={<FormBuilderList />}
/>
<Route
path={Paths.formBuilder.create + ":id/:type"}
element={<FormBuilderCreate />}
/>
<Route
path={Paths.formBuilder.update + ":id"}
element={<FormBuilderUpdate />}
/>
<Route
path={Paths.formBuilder.values + ":id"}
element={<FormBuilderValues />}
/>
<Route path={Paths.formBuilder.list + ":id/:type"} element={<FormBuilderList />} />
<Route path={Paths.formBuilder.create + ":id/:type"} element={<FormBuilderCreate />} />
<Route path={Paths.formBuilder.update + ":id"} element={<FormBuilderUpdate />} />
<Route path={Paths.formBuilder.values + ":id"} element={<FormBuilderValues />} />
<Route path={Paths.order.list} element={<OrdersList />} />
<Route
path={Paths.order.details + ":id"}
element={<OrderDetail />}
/>
<Route path={Paths.order.details + ":id"} element={<OrderDetail />} />
<Route path={Paths.order.create} element={<NewOrder />} />
<Route path={Paths.order.edit + ":id"} element={<EditOrder />} />
<Route path={Paths.print.list} element={<SectionList />} />
<Route path={Paths.print.create} element={<CreateSection />} />
<Route
path={Paths.print.update + ":id"}
element={<UpdateSection />}
/>
<Route
path={Paths.print.form + ":itemId/:orderId"}
element={<PrintForm />}
/>
<Route path={Paths.print.update + ":id"} element={<UpdateSection />} />
<Route path={Paths.print.form + ":itemId/:orderId"} element={<PrintForm />} />
<Route path={Paths.users.list} element={<UsersList />} />
<Route path={Paths.admin.list} element={<AdminList />} />
<Route path={Paths.admin.create} element={<CreateAdmin />} />
<Route
path={Paths.admin.update + ":id"}
element={<EditAdmin />}
/>
<Route path={Paths.admin.update + ":id"} element={<EditAdmin />} />
<Route path={Paths.features.list} element={<FeaturesList />} />
<Route path={Paths.features.create} element={<CreateFeature />} />
<Route path={Paths.service.print} element={<PrintService />} />
<Route path={Paths.ticket.list} element={<TicketList />} />
<Route path={Paths.ticket.create} element={<CreateTicket />} />
<Route
path={Paths.ticket.detail + ":id"}
element={<TicketDetail />}
/>
<Route
path={Paths.ticket.category}
element={<TicketCategory />}
/>
<Route
path={Paths.announcement.list}
element={<AnnouncementList />}
/>
<Route
path={Paths.announcement.detail + ":id"}
element={<AnnouncementUpdate />}
/>
<Route
path={Paths.announcement.create}
element={<AnnouncementCreate />}
/>
<Route path={Paths.announcement.list} element={<AnnouncementList />} />
<Route path={Paths.announcement.detail + ":id"} element={<AnnouncementUpdate />} />
<Route path={Paths.announcement.create} element={<AnnouncementCreate />} />
<Route
path={Paths.criticisms.list}
element={<CriticismsList />}
/>
<Route
path={Paths.criticisms.detail + ":id"}
element={<CriticismsDetail />}
/>
<Route path={Paths.criticisms.list} element={<CriticismsList />} />
<Route path={Paths.criticisms.detail + ":id"} element={<CriticismsDetail />} />
<Route path={Paths.learning.list} element={<LearningList />} />
<Route
path={Paths.learning.create}
element={<LearningCreate />}
/>
<Route
path={Paths.learning.category}
element={<LearningCategory />}
/>
<Route path={Paths.learning.create} element={<LearningCreate />} />
<Route path={Paths.learning.category} element={<LearningCategory />} />
</Routes>
</div>
</div>