167 lines
6.0 KiB
TypeScript
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;
|