ticket list + create ticket

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