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:
hamid zarghami
2025-10-12 17:03:49 +03:30
parent baa57bc7d5
commit 28e59d6805
5 changed files with 245 additions and 15 deletions
+228
View File
@@ -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
-7
View File
@@ -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
+15 -3
View File
@@ -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[];
};
}
+2
View File
@@ -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 />} />