ticket console

This commit is contained in:
hamid zarghami
2025-10-25 10:45:26 +03:30
parent 9c73185a13
commit 3615c72a93
12 changed files with 1331 additions and 1 deletions
+1
View File
@@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.vercel
+39
View File
@@ -0,0 +1,39 @@
import type { FC, InputHTMLAttributes } from 'react'
import Error from './Error'
import { clx } from '../helpers/utils'
type Props = {
label?: string,
error_text?: string
} & InputHTMLAttributes<HTMLTextAreaElement>
const Textarea: FC<Props> = (props: Props) => {
return (
<div className='w-full relative'>
{
props.label &&
<label className='text-sm'>
{props.label}
</label>
}
<textarea
{...props}
className={clx(
'border border-border leading-7 w-full rounded-xl px-4 py-3 min-h-[100px] mt-1 text-xs',
props.readOnly && 'bg-gray-100 border-0 text-description'
)}
>
</textarea>
{
props.error_text &&
<Error errorText={props.error_text} />
}
</div>
)
}
export default Textarea
+3 -1
View File
@@ -5,7 +5,9 @@ export const Paths = {
newOrder: '/new-order',
orderDetails: '/order/',
tickets: {
list: '/tickets',
list: '/tickets/list',
create: '/tickets/create',
detail: '/tickets/detail/',
},
announcement: {
list: '/announcement',
+52
View File
@@ -42,4 +42,56 @@ export const fa = {
uploadBox: {
selectFile: "انتخاب فایل",
},
ticket: {
tickets: "تیکت‌ها",
new_ticket: "تیکت جدید",
all: "همه",
checking: "در حال بررسی",
answered: "پاسخ داده شده",
closed: "بسته شده",
number: "شماره",
title: "عنوان",
category: "دسته‌بندی",
service: "سرویس",
date: "تاریخ",
status: "وضعیت",
priority: "اولویت",
detail: "جزئیات",
PENDING: "در حال بررسی",
ANSWERED: "پاسخ داده شده",
CLOSED: "بسته شده",
HIGH: "بالا",
MEDIUM: "متوسط",
LOW: "پایین",
subject: "موضوع",
select_your_service: "انتخاب سرویس شما",
user_name: "نام کاربر",
expert: "کارشناس",
answer: "پاسخ",
your_description: "توضیحات شما",
attachment: "پیوست",
send: "ارسال",
send_answer: "ارسال پاسخ",
close_ticket: "بستن تیکت",
info_ticket: "اطلاعات تیکت",
ticket_number: "شماره تیکت",
asignto: "واگذار شده به",
low: "پایین",
medium: "متوسط",
high: "بالا",
update_sms: "اطلاع‌رسانی از طریق پیامک",
name: "نام",
email: "ایمیل",
enter_your_subject: "موضوع خود را وارد کنید",
not_once: "یکبار نیست",
submit_your_message: "پیام خود را ارسال کنید",
title_hint: "راهنمای تیکت",
},
select: "انتخاب کنید",
description: "توضیحات",
attach: "پیوست",
success: "موفقیت‌آمیز",
errors: {
required: "این فیلد الزامی است",
},
};
+277
View File
@@ -0,0 +1,277 @@
import { type FC, useState } from 'react'
// import { useTranslation } from 'react-i18next'
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, useGetCategoriesTicket, useMultiUpload } from './hooks/useTicketData'
// import { useGetProfile } from '../profile/hooks/useProfileData'
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 { Helmet } from 'react-helmet-async'
import { fa } from '@/locale/fa'
import Textarea from '@/components/Textarea'
const CreateTicket: FC = () => {
// const { t } = useTranslation('global')
const t = (key: string): string => {
const keys = key.split('.');
let value: unknown = fa;
for (const k of keys) {
value = (value as Record<string, unknown>)?.[k];
}
return (value as string) || key;
}
// const navigate = useNavigate()
const [_files, setFiles] = useState<File[]>([])
const [percent] = useState<number>(0)
// const getProfile = useGetProfile()
// const getCategories = useGetCategoriesTicket()
// const multiUpload = useMultiUpload(setPercent)
// const createTicket = useCreateTicket()
// const getMyservice = useGetMyServices('')
// استفاده از داده‌های استاتیک
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 formik = {
values: {
subject: '',
message: '',
categoryId: '',
priority: 'LOW',
danakServiceId: '',
title: '',
attachmentUrls: []
},
handleChange: () => { },
handleSubmit: () => { },
resetForm: () => { },
touched: {
subject: false,
priority: false,
danakServiceId: false,
categoryId: false,
message: false
},
errors: {
subject: '',
priority: '',
danakServiceId: '',
categoryId: '',
message: ''
}
}
return (
<div className='mt-4 pb-12'>
{/* <Helmet>
<title>
داناک | ایجاد تیکت
</title>
</Helmet> */}
<div>
{t('ticket.new_ticket')}
</div>
{
getProfile.isPending || getCategories.isPending ?
<div>Loading...</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>
{t('ticket.submit_your_message')}
</div>
<div className='mt-6 rowTwoInput'>
<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
label={t('ticket.subject')}
placeholder={t('ticket.enter_your_subject')}
name='subject'
value={formik.values.subject}
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 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'>
<Textarea
label={t('ticket.your_description')}
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={t('ticket.attachment')}
isMultiple
onChange={setFiles}
/>
</div>
<div className='mt-8 flex justify-end'>
<Button
className='xl:max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={createTicket.isPending || multiUpload.isPending}
percentage={percent}
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='black' />
<div>
{t('ticket.send')}
</div>
</div>
</Button>
</div>
</div>
<div className='bg-white w-[300px] xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
{t('ticket.title_hint')}
</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
+325
View File
@@ -0,0 +1,325 @@
import { type FC, Fragment, useState } from 'react'
// import { useTranslation } from 'react-i18next'
import Input from '../../components/Input'
import Textarea from '@/components/Textarea'
import UploadBox from '../../components/UploadBox'
import Button from '../../components/Button'
import { CloseCircle, InfoCircle, Paperclip2, Send2 } from 'iconsax-react'
import { useNavigate } from 'react-router-dom'
import { type MessageType, type AttachmentType } from './types/TicketTypes'
// import { useAddMessageTicket, useCloseTicket, useGetMessages, useMultiUpload } from './hooks/useTicketData'
import { staticTicketDetailData } from './data/staticData'
// import PageLoading from '../../components/PageLoading'
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 { fa } from '@/locale/fa'
// import { Pages } from '../../config/Pages'
// import { Helmet } from 'react-helmet-async'
const TicketDetail: FC = () => {
// const { t } = useTranslation('global')
const t = (key: string) => {
const keys = key.split('.');
let value: unknown = fa;
for (const k of keys) {
value = (value as Record<string, unknown>)?.[k];
}
return (value as string) || key;
}
const navigate = useNavigate()
// const { id: _id } = useParams()
const [, setFiles] = useState<File[]>([])
const [percent] = useState<number>(0)
// const getMessages = useGetMessages(id ? id : '')
// const addMessage = useAddMessageTicket(id ? id : '')
// const multiUpload = useMultiUpload(setPercent)
// const closeTicket = useCloseTicket()
// استفاده از داده‌های استاتیک
const getMessages = {
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 formik = {
values: {
content: '',
attachmentUrls: [],
},
handleChange: () => { },
handleSubmit: () => { },
resetForm: () => { },
touched: { content: false },
errors: { content: '' }
}
const hadleCloseTicket = () => {
closeTicket.mutate()
navigate('/tickets')
}
const handleSendAnswer = () => {
const messageEndRef = document.getElementById('content');
if (messageEndRef) {
messageEndRef.scrollIntoView({ behavior: 'smooth' });
messageEndRef.focus();
}
}
return (
<div className='mt-4'>
{/* <Helmet>
<title>
داناک | تیکت
</title>
</Helmet> */}
<div className='flex gap-1'>
<div>{t('ticket.ticket_number')}</div>
</div>
{
getMessages.isPending ?
<div>Loading...</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'>
<Input
label={t('ticket.subject')}
value={getMessages.data?.data?.ticket?.subject}
readOnly
/>
<div className='gap-6 rowTwoInput mt-5'>
<Input
label={t('ticket.select_your_service')}
value={getMessages.data?.data?.ticket?.danakService?.name}
readOnly
/>
<Input
label={t('ticket.user_name')}
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
readOnly
/>
</div>
{
getMessages.data?.data?.messages.map((item: MessageType) => {
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((attachment: AttachmentType, index: number) => {
return (
<div className='text-[#0047FF] mt-2 flex gap-1 items-center' key={attachment.id}>
<Paperclip2 size={20} color='#0047FF' />
<a href={attachment.attachmentUrl} target='_blank' rel='noreferrer'>
{t('attach') + ' ' + Number(index + 1)}
</a>
</div>
)
})
}
</div>
</div>
)
} else {
return (
<div className='w-full flex justify-end' key={item.id}>
<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 end mt-6 text-xs text-description'>
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
</div>
</div>
<div className='flex gap-2 flex-wrap text-sm'>
{
item.attachments && item?.attachments?.map((attachment: AttachmentType, index: number) => {
return (
<div className='text-[#0047FF] mt-2 flex gap-1 items-center' key={attachment.id}>
<Paperclip2 size={20} color='#0047FF' />
<a href={attachment.attachmentUrl} target='_blank' rel='noreferrer'>
{t('attach') + ' ' + Number(index + 1)}
</a>
</div>
)
})
}
</div>
</div>
)
}
})
}
{
getMessages.data?.data?.ticket?.status !== 'CLOSED' &&
<Fragment>
<div className='mt-9'>
<Textarea
id='content'
placeholder={t('ticket.your_description')}
name='content'
value={formik.values.content}
onChange={formik.handleChange}
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
className='xl:max-w-[100px]'
onClick={() => formik.handleSubmit()}
percentage={percent}
isLoading={addMessage.isPending || multiUpload.isPending}
>
<div className='flex gap-2 items-center'>
<Send2 size={20} color='black' />
<div>
{t('ticket.send')}
</div>
</div>
</Button>
</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={t('ticket.send_answer')}
className='text-xs xl:h-10 h-8'
onClick={handleSendAnswer}
/>
<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 className='text-white'>
{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='flex justify-between text-description'>
<div>{t('ticket.ticket_number')}</div>
<div>
{getMessages.data?.data?.ticket?.numericId}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>{t('ticket.asignto')}</div>
<div>
{getMessages.data?.data?.ticket?.assignedTo ? getMessages.data?.data?.ticket?.assignedTo?.firstName + ' ' + getMessages.data?.data?.ticket?.assignedTo?.lastName : ''}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>{t('date')}</div>
<div className='dltr'>
{moment(getMessages.data?.data?.ticket?.createdAt).format('jYYYY/jMM/jDD HH:mm')}
</div>
</div>
<div className='flex mt-6 justify-between text-description'>
<div>{t('ticket.category')}</div>
<div>
{getMessages.data?.data?.ticket?.category?.title}
</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 className='mt-10 border-t text-xs pt-5 border-border items-center flex gap-2'>
<InfoCircle size={20} color='black' />
<div>
{t('ticket.update_sms')}
</div>
</div>
</div>
</div>
</div>
}
</div>
)
}
export default TicketDetail
+155
View File
@@ -0,0 +1,155 @@
import { type FC, useState } from 'react'
import Button from '../../components/Button'
import { Add, Eye } from 'iconsax-react'
import Tabs from '../../components/Tabs'
import Table from '../../components/Table'
import { Link } from 'react-router-dom'
import { staticTicketsData } from './data/staticData'
import { t } from '@/locale'
import moment from 'moment-jalaali'
import type { ColumnType } from '@/components/types/TableTypes'
// تعریف type برای تیکت
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 [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED' | ''>('')
// استفاده از داده‌های استاتیک
const getTickets = {
isPending: false,
data: staticTicketsData
}
// تعریف ستون‌های جدول
const columns: ColumnType<TicketItem>[] = [
{
title: t('ticket.number'),
key: 'numericId',
render: (item) => item.numericId
},
{
title: t('ticket.title'),
key: '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'),
key: 'updatedAt',
render: (item) => (
<div className='dltr text-right'>
{moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
</div>
)
},
{
title: t('ticket.status'),
key: 'status',
render: (item) => t(`ticket.${item.status}`)
},
{
title: t('ticket.priority'),
key: 'priority',
render: (item) => t(`ticket.${item.priority}`)
},
{
title: t('ticket.detail'),
key: 'actions',
render: (item) => (
<Link to={`/tickets/${item.id}`}>
<Eye size={20} color='#8C90A3' />
</Link>
)
}
]
return (
<div className='mt-4'>
<div className='flex justify-between items-center'>
<div>
{t('ticket.tickets')}
</div>
<Link to="/tickets/create">
<Button
className='w-[172px]'
>
<div className='flex gap-2 items-center'>
<Add size={20} color='black' />
<div>
{t('ticket.new_ticket')}
</div>
</div>
</Button>
</Link>
</div>
<div className='mt-14'>
<Tabs
activeTab={activeTab}
items={[
{
label: t('ticket.all'),
value: ''
},
{
label: t('ticket.checking'),
value: 'PENDING'
},
{
label: t('ticket.answered'),
value: 'ANSWERED'
},
{
label: t('ticket.closed'),
value: 'CLOSED'
},
]}
onTabChange={(value: string) => setActiveTab(value as 'ANSWERED' | 'PENDING' | 'CLOSED')}
/>
</div>
<Table
columns={columns}
data={getTickets.data?.data?.tickets || []}
isLoading={getTickets.isPending}
noDataMessage="هیچ تیکتی یافت نشد"
/>
</div>
)
}
export default TicketList
+289
View File
@@ -0,0 +1,289 @@
// داده‌های استاتیک برای بخش تیکت
export const staticTicketsData = {
data: {
tickets: [
{
id: "1",
numericId: "TK-001",
subject: "مشکل در اتصال به سرور",
status: "PENDING",
priority: "HIGH",
createdAt: "2024-01-15T10:30:00Z",
updatedAt: "2024-01-15T14:45:00Z",
category: {
id: "1",
title: "مشکلات فنی",
},
danakService: {
id: "1",
name: "سرویس ابری داناک",
},
user: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
},
},
{
id: "2",
numericId: "TK-002",
subject: "سوال در مورد صورتحساب",
status: "ANSWERED",
priority: "MEDIUM",
createdAt: "2024-01-14T09:15:00Z",
updatedAt: "2024-01-14T16:20:00Z",
category: {
id: "2",
title: "مالی",
},
danakService: {
id: "2",
name: "سرویس پرداخت",
},
user: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
},
},
{
id: "3",
numericId: "TK-003",
subject: "درخواست تغییر پلان",
status: "CLOSED",
priority: "LOW",
createdAt: "2024-01-13T11:00:00Z",
updatedAt: "2024-01-13T15:30:00Z",
category: {
id: "3",
title: "درخواست‌ها",
},
danakService: {
id: "1",
name: "سرویس ابری داناک",
},
user: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
},
},
{
id: "4",
numericId: "TK-004",
subject: "مشکل در آپلود فایل",
status: "PENDING",
priority: "HIGH",
createdAt: "2024-01-12T14:20:00Z",
updatedAt: "2024-01-12T14:20:00Z",
category: {
id: "1",
title: "مشکلات فنی",
},
danakService: {
id: "3",
name: "سرویس ذخیره‌سازی",
},
user: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
},
},
{
id: "5",
numericId: "TK-005",
subject: "سوال در مورد API",
status: "ANSWERED",
priority: "MEDIUM",
createdAt: "2024-01-11T08:45:00Z",
updatedAt: "2024-01-11T12:15:00Z",
category: {
id: "4",
title: "پشتیبانی فنی",
},
danakService: {
id: "4",
name: "سرویس API",
},
user: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
},
},
],
},
};
export const staticTicketDetailData = {
data: {
ticket: {
id: "1",
numericId: "TK-001",
subject: "مشکل در اتصال به سرور",
status: "PENDING",
priority: "HIGH",
createdAt: "2024-01-15T10:30:00Z",
updatedAt: "2024-01-15T14:45:00Z",
category: {
id: "1",
title: "مشکلات فنی",
},
danakService: {
id: "1",
name: "سرویس ابری داناک",
},
user: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
},
assignedTo: {
id: "2",
firstName: "علی",
lastName: "احمدی",
},
},
messages: [
{
id: "1",
content:
"سلام، من در اتصال به سرور مشکل دارم. هر بار که سعی می‌کنم به API متصل شوم، خطای timeout دریافت می‌کنم. لطفاً راهنمایی کنید.",
createdAt: "2024-01-15T10:30:00Z",
author: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
roles: [
{
name: "user",
},
],
},
attachments: [
{
id: "1",
attachmentUrl: "https://example.com/attachment1.pdf",
},
],
},
{
id: "2",
content:
"سلام احمد عزیز، متشکرم از گزارش مشکل. این مشکل معمولاً به دلیل تنظیمات فایروال یا محدودیت‌های شبکه رخ می‌دهد. لطفاً موارد زیر را بررسی کنید:\n\n1. بررسی تنظیمات فایروال\n2. اطمینان از دسترسی به پورت 443\n3. تست اتصال با VPN\n\nاگر مشکل ادامه داشت، لطفاً لاگ‌های مربوطه را ارسال کنید.",
createdAt: "2024-01-15T11:15:00Z",
author: {
id: "2",
firstName: "علی",
lastName: "احمدی",
roles: [
{
name: "admin",
},
],
},
attachments: [],
},
{
id: "3",
content:
"ممنون از راهنمایی. بررسی کردم و مشکل از فایروال بود. حالا درست کار می‌کند.",
createdAt: "2024-01-15T14:45:00Z",
author: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
roles: [
{
name: "user",
},
],
},
attachments: [],
},
],
},
};
export const staticCategoriesData = {
data: {
ticketCategories: [
{
id: "1",
title: "مشکلات فنی",
},
{
id: "2",
title: "مالی",
},
{
id: "3",
title: "درخواست‌ها",
},
{
id: "4",
title: "پشتیبانی فنی",
},
{
id: "5",
title: "پیشنهادات",
},
],
},
};
export const staticProfileData = {
data: {
user: {
id: "1",
firstName: "احمد",
lastName: "محمدی",
email: "ahmad.mohammadi@example.com",
},
},
};
export const staticServicesData = {
data: {
subscriptions: [
{
id: "1",
plan: {
service: {
id: "1",
name: "سرویس ابری داناک",
},
},
},
{
id: "2",
plan: {
service: {
id: "2",
name: "سرویس پرداخت",
},
},
},
{
id: "3",
plan: {
service: {
id: "3",
name: "سرویس ذخیره‌سازی",
},
},
},
{
id: "4",
plan: {
service: {
id: "4",
name: "سرویس API",
},
},
},
],
},
};
+72
View File
@@ -0,0 +1,72 @@
import * as api from "../service/TicketService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { type AddMessageTicketType, type CreateTicketType } from "../types/TicketTypes";
export const useGetTickets = (status: string) => {
return useQuery({
queryKey: ["tickets", status],
queryFn: () => api.getTickets(status),
});
};
export const useCreateTicket = () => {
return useMutation({
mutationFn: (variables: CreateTicketType) => api.createTicket(variables),
});
};
export const useGetCategoriesTicket = () => {
return useQuery({
queryKey: ["category-tickets"],
queryFn: () => api.getCategoriesTicket(),
});
};
export const useSingleUpload = (callback?: (value: number) => void) => {
return useMutation({
mutationFn: (variables: FormData) => api.uploadSingle(variables, callback),
});
};
export const useMultiUpload = (callback?: (value: number) => void) => {
return useMutation({
mutationFn: (variables: FormData) =>
api.uploadMultiple(variables, callback),
});
};
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 = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.closeTicket(id),
onSuccess: () => {
queryClient.refetchQueries({
queryKey: ["tickets"],
});
queryClient.refetchQueries({
queryKey: ["ticket-messages"],
});
},
});
};
+81
View File
@@ -0,0 +1,81 @@
import axios from "../../../config/axios";
import { type AddMessageTicketType, type CreateTicketType } from "../types/TicketTypes";
export const getTickets = async (status: string) => {
let query = ``;
if (status) {
query = `?status=${status}`;
}
const { data } = await axios.get(`/tickets${query}`);
return data;
};
export const createTicket = async (params: CreateTicketType) => {
const { data } = await axios.post(`/tickets`, params);
return data;
};
export const getCategoriesTicket = async () => {
const { data } = await axios.get(`/tickets/categories`);
return data;
};
export const uploadSingle = async (
params: FormData,
callback?: (value: number) => void
) => {
const { data } = await axios.post(`/uploader/single-file`, params, {
onUploadProgress: (progressEvent: any) => {
if (progressEvent.bytes) {
if (callback) {
callback(
Math.round((progressEvent.loaded / progressEvent.total) * 100)
);
}
}
},
});
if (callback) {
callback(0);
}
return data;
};
export const uploadMultiple = async (
params: FormData,
callback?: (value: number) => void
) => {
const { data } = await axios.post(`/uploader/multi-file`, params, {
onUploadProgress: (progressEvent: any) => {
if (progressEvent.bytes) {
if (callback) {
callback(
Math.round((progressEvent.loaded / progressEvent.total) * 100)
);
}
}
},
});
if (callback) {
callback(0);
}
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;
};
+31
View File
@@ -0,0 +1,31 @@
export type CreateTicketType = {
title: string;
subject: string;
priority: "LOW" | "MEDIUM" | "HIGH";
danakServiceId?: string;
categoryId: string;
message: string;
attachmentUrls?: string[];
};
export type AddMessageTicketType = {
content: string;
attachmentUrls?: string[];
};
export type AttachmentType = {
id: string;
attachmentUrl: string;
};
export type MessageType = {
id: string;
content: string;
createdAt: string;
author: {
firstName: string;
lastName: string;
roles: Array<{ name: string }>;
};
attachments?: AttachmentType[];
};
+6
View File
@@ -8,6 +8,9 @@ import MyOrders from '@/pages/order/MyOrders'
import ProformaInvoice from '@/pages/invoice/ProformaInvoice'
import NewOrder from '@/pages/order/NewOrder'
import OrderDetail from '@/pages/order/OrderDetail'
import TicketList from '@/pages/ticket/TicketList'
import CreateTicket from '@/pages/ticket/CreateTicket'
import TicketDetail from '@/pages/ticket/Detail'
const MainRouter: FC = () => {
return (
@@ -25,6 +28,9 @@ const MainRouter: FC = () => {
<Route path={Paths.proformaInvoice} element={<ProformaInvoice />} />
<Route path={Paths.newOrder} element={<NewOrder />} />
<Route path={`${Paths.orderDetails}:id`} element={<OrderDetail />} />
<Route path={Paths.tickets.list} element={<TicketList />} />
<Route path={Paths.tickets.create} element={<CreateTicket />} />
<Route path={`${Paths.tickets.detail}:id`} element={<TicketDetail />} />
</Routes>
</div>
</div>