Complete TicketMessages page implementation
- Add ticket details section with seller info, category, status, etc. - Implement messages display with chat-like UI (admin vs seller messages) - Add send message form with textarea and file upload - Connect to API for fetching ticket data and sending messages - Use existing components: PageLoading, Error, StatusWithText, Button, Textarea, UploadBox - Fix CreateTicketMessageType to allow dynamic content strings
This commit is contained in:
@@ -0,0 +1,228 @@
|
|||||||
|
import { type FC, useState } from 'react'
|
||||||
|
import { useGetTicket, useAddTicketMessage } from './hooks/useTicketData'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { type IMessage, type TicketStatus } from './types/Types'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import Error from '../../components/Error'
|
||||||
|
import StatusWithText from '../../components/StatusWithText'
|
||||||
|
import { formatDate } from '../../helpers/func'
|
||||||
|
import { MessageQuestion, User as UserIcon, Category, Calendar } from 'iconsax-react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
import UploadBox from '@/components/UploadBox'
|
||||||
|
|
||||||
|
const TicketMessages: FC = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { data, isLoading, refetch } = useGetTicket(id as string)
|
||||||
|
const [messageContent, setMessageContent] = useState('')
|
||||||
|
const [attachments, setAttachments] = useState<File[]>([])
|
||||||
|
|
||||||
|
const addMessageMutation = useAddTicketMessage()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<Error errorText="تیکت یافت نشد" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticket = data.results.ticket
|
||||||
|
const messages = data.results.messages
|
||||||
|
|
||||||
|
const getStatusVariant = (status: TicketStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'closed': return 'error'
|
||||||
|
case 'answered': return 'success'
|
||||||
|
case 'pending': return 'warning'
|
||||||
|
default: return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: TicketStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'closed': return 'بسته شده'
|
||||||
|
case 'answered': return 'پاسخ داده شده'
|
||||||
|
case 'pending': return 'در انتظار'
|
||||||
|
default: return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendMessage = async () => {
|
||||||
|
if (!messageContent.trim()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addMessageMutation.mutateAsync({
|
||||||
|
ticketId: ticket._id,
|
||||||
|
params: {
|
||||||
|
content: messageContent,
|
||||||
|
attachments: [] // TODO: handle file uploads
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setMessageContent('')
|
||||||
|
setAttachments([])
|
||||||
|
refetch()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending message:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (files: File[]) => {
|
||||||
|
setAttachments(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Ticket Info */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<MessageQuestion size={24} color="#2563eb" />
|
||||||
|
<h1 className="text-xl font-semibold">جزئیات تیکت</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm text-gray-500">شماره تیکت</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MessageQuestion size={16} color="#8C90A3" />
|
||||||
|
<span className="font-medium">{ticket.numericId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm text-gray-500">فروشنده</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<UserIcon size={16} color="#8C90A3" />
|
||||||
|
<span>{ticket.seller?.fullName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm text-gray-500">موضوع</div>
|
||||||
|
<div className="font-medium">{ticket.subject}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm text-gray-500">دستهبندی</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Category size={16} color="#8C90A3" />
|
||||||
|
<span>{ticket.category?.title_fa || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm text-gray-500">وضعیت</div>
|
||||||
|
<StatusWithText
|
||||||
|
variant={getStatusVariant(ticket.status)}
|
||||||
|
text={getStatusText(ticket.status)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm text-gray-500">تاریخ بروزرسانی</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar size={16} color="#8C90A3" />
|
||||||
|
<span>{formatDate(ticket.updatedAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages Section */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">پیامها</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4 max-h-96 overflow-y-auto">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
هیچ پیامی یافت نشد
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map((message: IMessage) => (
|
||||||
|
<div
|
||||||
|
key={message._id}
|
||||||
|
className={`flex ${message.sender === 'admin' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[70%] p-4 rounded-2xl ${
|
||||||
|
message.sender === 'admin'
|
||||||
|
? 'bg-blue-500 text-white rounded-br-md'
|
||||||
|
: 'bg-gray-100 text-gray-900 rounded-bl-md'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-sm mb-2">
|
||||||
|
{message.sender === 'admin' ? 'ادمین' : 'فروشنده'}
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-pre-wrap">
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
{message.attachments && message.attachments.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{message.attachments.map((attachment, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`text-xs underline ${
|
||||||
|
message.sender === 'admin' ? 'text-blue-100' : 'text-blue-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
فایل ضمیمه: {attachment}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`text-xs mt-2 ${
|
||||||
|
message.sender === 'admin' ? 'text-blue-100' : 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{message.createdAt && formatDate(message.createdAt)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Send Message Form */}
|
||||||
|
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">ارسال پیام جدید</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Textarea
|
||||||
|
label="متن پیام"
|
||||||
|
placeholder="پیام خود را وارد کنید..."
|
||||||
|
value={messageContent}
|
||||||
|
onChange={(e) => setMessageContent(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadBox
|
||||||
|
label="فایلهای ضمیمه (اختیاری)"
|
||||||
|
isMultiple={true}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
isReset={attachments.length === 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
label="ارسال پیام"
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
isLoading={addMessageMutation.isPending}
|
||||||
|
disabled={!messageContent.trim()}
|
||||||
|
className="w-fit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TicketMessages
|
||||||
@@ -27,13 +27,6 @@ export const useGetTicketCategory = (categoryId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetTicketMessages = (ticketId: string) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["ticket-messages", ticketId],
|
|
||||||
queryFn: () => api.getTicketMessages(ticketId),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAddTicketMessage = () => {
|
export const useAddTicketMessage = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
|
|||||||
@@ -52,11 +52,6 @@ export const createTicketCategory = async (
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTicketMessages = async (ticketId: string) => {
|
|
||||||
const { data } = await axios.get(`/ticket-messages/${ticketId}`);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateTicketCategory = async (
|
export const updateTicketCategory = async (
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
params: CreateTicketCategoryType
|
params: CreateTicketCategoryType
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export type CreateTicketCategoryType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CreateTicketMessageType = {
|
export type CreateTicketMessageType = {
|
||||||
content: "مشکل در اضافه کردن محصول";
|
content: string;
|
||||||
attachments: string[];
|
attachments: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,6 +59,18 @@ export interface ITicketsResponse extends IBaseResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITicketResponse extends IBaseResponse {
|
export interface IMessage {
|
||||||
results: ITicket;
|
_id?: string;
|
||||||
|
content: string;
|
||||||
|
attachments?: string[];
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
sender?: "admin" | "seller";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITicketResponse extends IBaseResponse {
|
||||||
|
results: {
|
||||||
|
ticket: ITicket;
|
||||||
|
messages: IMessage[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import ContractPage from '@/pages/seller/Contract'
|
|||||||
import Category from '@/pages/ticket/Category'
|
import Category from '@/pages/ticket/Category'
|
||||||
import CreateCategory from '@/pages/ticket/Create'
|
import CreateCategory from '@/pages/ticket/Create'
|
||||||
import UpdateCategory from '@/pages/ticket/Update'
|
import UpdateCategory from '@/pages/ticket/Update'
|
||||||
|
import TicketMessages from '@/pages/ticket/TicketMessages'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
const { hasSubMenu } = useSharedStore()
|
const { hasSubMenu } = useSharedStore()
|
||||||
@@ -139,6 +140,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.financial.fines} element={<Fines />} />
|
<Route path={Pages.financial.fines} element={<Fines />} />
|
||||||
|
|
||||||
<Route path={Pages.ticket.list} element={<TicketsList />} />
|
<Route path={Pages.ticket.list} element={<TicketsList />} />
|
||||||
|
<Route path={`${Pages.ticket.detail}:id`} element={<TicketMessages />} />
|
||||||
<Route path={Pages.ticket.category.list} element={<Category />} />
|
<Route path={Pages.ticket.category.list} element={<Category />} />
|
||||||
<Route path={Pages.ticket.category.create} element={<CreateCategory />} />
|
<Route path={Pages.ticket.category.create} element={<CreateCategory />} />
|
||||||
<Route path={`${Pages.ticket.category.update}:id`} element={<UpdateCategory />} />
|
<Route path={`${Pages.ticket.category.update}:id`} element={<UpdateCategory />} />
|
||||||
|
|||||||
Reference in New Issue
Block a user