Files
negareh-console/src/pages/ticket/Detail.tsx
T
hamid zarghami 17b577b0d6 close ticket
2026-02-24 16:43:32 +03:30

167 lines
6.0 KiB
TypeScript

import { type FC, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import Input from "@/components/Input";
import { clx } from "@/helpers/utils";
import { t } from "@/locale";
import { getTicketDisplayName } from "./types/TicketTypes";
import type { TicketDetailType } from "./types/TicketTypes";
import { useCloseTicket, useCreateOrReplyTicket, useGetTicketById } from "./hooks/useTicketData";
import { buildThreadItems, getStatusClass } from "./detailUtils";
import MessageBubble from "./MessageBubble";
import ReplyForm from "./ReplyForm";
import TicketInfoSidebar from "./TicketInfoSidebar";
import TicketDetailActions from "./TicketDetailActions";
const STATUS_CLOSED = "closed";
const TicketDetail: FC = () => {
const { id } = useParams<{ id: string }>();
const [content, setContent] = useState("");
const [files, setFiles] = useState<File[]>([]);
const getTicket = useGetTicketById(id ?? "");
const addMessage = useCreateOrReplyTicket();
const closeTicket = useCloseTicket();
const ticket = getTicket.data?.data;
const threadItems = useMemo(
() => (ticket ? buildThreadItems(ticket) : []),
[ticket]
);
const isClosed =
ticket?.status?.toLowerCase() === STATUS_CLOSED ||
ticket?.status?.toUpperCase() === "CLOSED";
const handleCloseTicket = () => {
if (!id) return;
closeTicket.mutate(
{ id, params: { status: "closed" } },
{
onSuccess: () => getTicket.refetch(),
}
);
};
const handleSendReply = () => {
if (!id || !ticket || !content.trim()) return;
addMessage.mutate(
{
parentId: id,
subject: ticket.subject,
content: content.trim(),
attachments: files.length ? files.map(() => ({})) : [],
},
{
onSuccess: () => {
setContent("");
setFiles([]);
getTicket.refetch();
},
}
);
};
const handleScrollToReply = () => {
document.getElementById("content")?.scrollIntoView({ behavior: "smooth" });
document.getElementById("content")?.focus();
};
if (getTicket.isPending) {
return (
<div className="mt-4">
<div className="flex gap-1">{t("ticket.ticket_number")}</div>
<div className="mt-6">Loading...</div>
</div>
);
}
if (!ticket) {
return (
<div className="mt-4">
<div className="flex gap-1">{t("ticket.ticket_number")}</div>
<div className="mt-6 text-description">{t("ticket.info_ticket")}</div>
</div>
);
}
const extendedTicket = ticket as TicketDetailType & {
danakService?: { name?: string };
numericId?: string;
};
return (
<div className="mt-4">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-description">{t("ticket.ticket_number")}:</span>
<span className="font-medium">{extendedTicket.numericId ?? ticket.id}</span>
<span
className={clx(
"text-xs px-3 py-1 rounded-xl",
getStatusClass(ticket.status)
)}
>
{t(`ticket.${ticket.status}`) || t(`ticket.${ticket.status?.toUpperCase()}`)}
</span>
</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={ticket.subject}
readOnly
/>
<div className="gap-6 rowTwoInput mt-5">
{extendedTicket.danakService && (
<Input
label={t("ticket.select_your_service")}
value={extendedTicket.danakService.name}
readOnly
/>
)}
<Input
label={t("ticket.user_name")}
value={getTicketDisplayName(ticket.user)}
readOnly
/>
</div>
<div className={threadItems.length ? "space-y-0" : ""}>
{threadItems.map((item) => (
<div
key={item.id}
className={!item.isFromUser ? "w-full flex justify-end" : ""}
>
<MessageBubble item={item} />
</div>
))}
</div>
{!isClosed && (
<ReplyForm
content={content}
onContentChange={setContent}
onFilesChange={setFiles}
onSubmit={handleSendReply}
isPending={addMessage.isPending}
/>
)}
<div className="h-8" />
</div>
<aside className="h-fit lg:sticky lg:top-0 xl:w-[300px] w-full">
{!isClosed && (
<TicketDetailActions
onScrollToReply={handleScrollToReply}
onCloseTicket={handleCloseTicket}
isClosing={closeTicket.isPending}
/>
)}
<TicketInfoSidebar ticket={ticket} />
</aside>
</div>
</div>
);
};
export default TicketDetail;