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 = () => {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
|
||||
@@ -52,11 +52,6 @@ export const createTicketCategory = async (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getTicketMessages = async (ticketId: string) => {
|
||||
const { data } = await axios.get(`/ticket-messages/${ticketId}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateTicketCategory = async (
|
||||
categoryId: string,
|
||||
params: CreateTicketCategoryType
|
||||
|
||||
@@ -29,7 +29,7 @@ export type CreateTicketCategoryType = {
|
||||
};
|
||||
|
||||
export type CreateTicketMessageType = {
|
||||
content: "مشکل در اضافه کردن محصول";
|
||||
content: string;
|
||||
attachments: string[];
|
||||
};
|
||||
|
||||
@@ -59,6 +59,18 @@ export interface ITicketsResponse extends IBaseResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITicketResponse extends IBaseResponse {
|
||||
results: ITicket;
|
||||
export interface IMessage {
|
||||
_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 CreateCategory from '@/pages/ticket/Create'
|
||||
import UpdateCategory from '@/pages/ticket/Update'
|
||||
import TicketMessages from '@/pages/ticket/TicketMessages'
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
const { hasSubMenu } = useSharedStore()
|
||||
@@ -139,6 +140,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.financial.fines} element={<Fines />} />
|
||||
|
||||
<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.create} element={<CreateCategory />} />
|
||||
<Route path={`${Pages.ticket.category.update}:id`} element={<UpdateCategory />} />
|
||||
|
||||
Reference in New Issue
Block a user