kontenhumas-fe/components/form/ticketing/ticketing-detail-form.tsx

818 lines
28 KiB
TypeScript
Raw Normal View History

2025-09-25 15:46:47 +00:00
// "use client";
// import React, { useEffect, useState } from "react";
// import { useForm, Controller } from "react-hook-form";
// import { Input } from "@/components/ui/input";
// import { Button } from "@/components/ui/button";
// import { Label } from "@/components/ui/label";
// import { Card } from "@/components/ui/card";
// import { zodResolver } from "@hookform/resolvers/zod";
// import * as z from "zod";
// import Swal from "sweetalert2";
// import withReactContent from "sweetalert2-react-content";
// import { useParams } from "next/navigation";
// import {
// Select,
// SelectContent,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from "@/components/ui/select";
// import { Avatar, AvatarImage } from "@/components/ui/avatar";
// import {
// deleteTicket,
// getTicketingDetail,
// getTicketingInternalDetail,
// getTicketingInternalDiscussion,
// getTicketingReply,
// saveTicketing,
// saveTicketInternalReply,
// saveTicketReply,
// } from "@/service/communication/communication";
// import { Icon } from "@iconify/react/dist/iconify.js";
// import { list, parse } from "postcss";
// import { htmlToString } from "@/utils/globals";
// import { Textarea } from "@/components/ui/textarea";
// import { error } from "@/lib/swal";
// import { useRouter } from "next/navigation";
// import InfoLainnyaModal from "./info-lainnya";
// import { DetailTicket } from "./info-lainnya-types";
// import { useMediaQuery } from "react-responsive";
// import { ArrowLeft, ChevronDownIcon } from "lucide-react";
// import {
// Popover,
// PopoverContent,
// PopoverTrigger,
// } from "@/components/ui/popover";
// import { getOperatorUser } from "@/service/management-user/management-user";
// const taskSchema = z.object({
// title: z.string().min(1, { message: "Judul diperlukan" }),
// naration: z.string().min(2, {
// message: "Narasi Penugasan harus lebih dari 2 karakter.",
// }),
// });
// export type taskDetail = {
// id: number;
// title: string;
// createdAt: string;
// referenceNumber: string | number;
// createdBy: {
// id: number;
// fullname: string;
// };
// sendTo: {
// id: number;
// fullname: string;
// };
// status: {
// id: number;
// name: string;
// };
// priority: {
// id: number;
// name: string;
// };
// broadcastType: string;
// description: string;
// is_active: string;
// };
// export type replyDetail = {
// id: number;
// comments: string;
// createdAt: string;
// user: {
// id: number;
// fullname: string;
// };
// messageTo: {
// id: number;
// fullname: string;
// };
// };
// export type internalDetail = {
// id: number;
// message: string;
// createdAt: string;
// commentFromUserName: string;
// feedTitle: string;
// createdBy: {
// id: number;
// fullname: string;
// };
// sendTo: {
// id: number;
// fullname: string;
// };
// };
// type Props = { id: string };
// export default function FormDetailTicketing({ id }: Props) {
// const MySwal = withReactContent(Swal);
// // const { id } = useParams() as { id: string };
// const router = useRouter();
// const isMobile = useMediaQuery({
// maxWidth: 768,
// });
// const [detail, setDetail] = useState<taskDetail>();
// const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
// const [ticketInternal, setTicketInternal] = useState<internalDetail | null>(
// null
// );
// const [detailTickets, setDetailTickets] = useState<DetailTicket | null>(null);
// const [replyVisible, setReplyVisible] = useState(false);
// const [replyMessage, setReplyMessage] = useState("");
// const [selectedPriority, setSelectedPriority] = useState("");
// const [selectedStatus, setSelectedStatus] = useState("");
// const [openEmergencyModal, setOpenEmergencyModal] = useState(false);
// const [replyValue, setReplyValue] = useState<number>(0);
// const [replyText, setReplyText] = useState<string>("");
// const [operatorOpt, setOperatorOpt] = useState<
// { id: string; label: string; value: string }[]
// >([]);
// const [selectedOperator, setSelectedOperator] = useState<string[]>([]);
// const {
// control,
// handleSubmit,
// formState: { errors },
// } = useForm({
// resolver: zodResolver(taskSchema),
// });
// useEffect(() => {
// async function initState() {
// setReplyValue(0);
// const response = await getTicketingDetail(id);
// setTicketInternal(response?.data?.data || null);
// setDetail(response?.data?.data);
// if (response?.data !== null) {
// setDetailTickets(response?.data?.data);
// }
// }
// initState();
// getTicketReply();
// }, [id]);
// useEffect(() => {
// async function getOperator() {
// const res = await getOperatorUser(detailTickets?.typeId);
// if (res?.data !== null) {
// const rawUser = res?.data?.data;
// const optionArr = rawUser?.map((option: any) => ({
// id: option.id,
// label: option.fullName,
// value: option.id.toString(),
// }));
// setOperatorOpt(optionArr);
// if (detailTickets?.assignedTeams) {
// const assigned = detailTickets.assignedTeams
// .split(":")
// .filter((id: string) => id);
// setSelectedOperator(assigned);
// }
// }
// }
// getOperator();
// }, [detailTickets]);
// const handleReply = () => {
// setReplyValue((prev) => (prev === 1 ? 0 : 1));
// };
// const handleSendReplyData = async () => {
// if (!replyText.trim()) {
// console.warn("Balasan kosong!");
// return;
// }
// try {
// const res = await saveTicketReply({
// ticketId: id,
// comment: replyText,
// parentCommentId: detailTickets?.commentId,
// isFromInternal: true,
// });
// console.log("Berhasil kirim balasan:", res?.data);
// setReplyText("");
// setReplyValue(0);
// getTicketReply();
// } catch (err) {
// console.error("Gagal kirim balasan:", err);
// }
// };
// async function getTicketReply() {
// const res = await getTicketingReply(id);
// if (res?.data !== null) {
// setTicketReply(res?.data?.data);
// }
// }
// const handleSendReply = async () => {
// if (replyMessage.trim() === "") {
// MySwal.fire({
// title: "Error",
// text: "Pesan tidak boleh kosong!",
// icon: "error",
// });
// return;
// }
// const data = {
// ticketId: id,
// comment: replyMessage,
// };
// try {
// const response = await saveTicketReply(data);
// const newReply: replyDetail = {
// id: response?.data?.id,
// comments: replyMessage,
// createdAt: response?.data?.createdAt,
// user: response?.data?.messageFrom,
// messageTo: response?.data?.messageTo,
// };
// setTicketReply((prevReplies) => [newReply, ...prevReplies]);
// MySwal.fire({
// title: "Sukses",
// text: "Pesan berhasil dikirim.",
// icon: "success",
// });
// setReplyMessage("");
// setReplyVisible(false);
// } catch (error) {
// MySwal.fire({
// title: "Error",
// text: "Gagal mengirim balasan.",
// icon: "error",
// });
// console.error("Error sending reply:", error);
// }
// };
// async function doDelete(id: any) {
// const response = await deleteTicket(id);
// if (response?.error) {
// error(response.message);
// return false;
// }
// success("/in/supervisor/ticketing");
// }
// function success(redirect: string) {
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then(() => {
// router.push(redirect);
// });
// }
// const handleDelete = (id: any) => {
// MySwal.fire({
// title: "Hapus Data",
// text: "",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#3085d6",
// confirmButtonColor: "#d33",
// confirmButtonText: "Hapus",
// }).then((result) => {
// if (result.isConfirmed) {
// doDelete(id);
// }
// });
// };
// const openEmergencyIssueDetail = () => {
// setOpenEmergencyModal(true);
// };
// return (
// <div className="py-5">
// <div className="mt-4">
// <div className="flex gap-3">
// <Button onClick={handleReply} variant="outline">
// <ArrowLeft className="mr-2 h-4 w-4" />
// {replyValue === 1 ? "Tutup Balasan" : "Balas"}
// </Button>
// </div>
// {replyValue === 1 && (
// <div className="mt-4 rounded-xl bg-gray-100 p-4">
// <textarea
// value={replyText}
// onChange={(e) => setReplyText(e.target.value)}
// placeholder="Tulis Pesan"
// className="w-full resize-none rounded-md border border-gray-300 bg-white p-4 text-sm text-gray-700 focus:outline-none"
// rows={5}
// />
// <div className="mt-4 flex justify-end gap-3">
// <Button
// onClick={() => setReplyValue(0)}
// variant="outline"
// className="text-blue-600 border-blue-600"
// >
// Batal
// </Button>
// <Button
// onClick={handleSendReplyData}
// className="bg-blue-600 text-white hover:bg-blue-700"
// >
// Kirim
// </Button>
// </div>
// </div>
// )}
// </div>
// <div className="flex flex-col md:flex-row lg:flex-row gap-5 mt-5">
// <div className="flex flex-col w-[100%] lg:w-[70%]">
// {replyVisible && (
// <div className="">
// <textarea
// id="replyMessage"
// className="w-full h-24 border rounded-md p-2"
// value={replyMessage}
// onChange={(e) => setReplyMessage(e.target.value)}
// placeholder="Tulis pesan di sini..."
// />
// <div className="flex justify-end gap-3 my-2">
// <Button
// onClick={() => setReplyVisible(false)}
// color="default"
// variant="outline"
// >
// Batal
// </Button>
// <Button onClick={handleSendReply} color="primary">
// Kirim
// </Button>
// </div>
// </div>
// )}
// <div className="border rounded-t-xl">
// <p className="p-4 bg-slate-300 rounded-t-xl text-lg font-semibold">
// Ticket #{detail?.referenceNumber}
// </p>
// {ticketReply?.map((list) => (
// <div key={list.id} className="flex flex-col mb-4">
// {isMobile ? (
// <div className="flex gap-3 bg-sky-100 p-3 items-center">
// <Icon icon="qlementine-icons:user-16" width={36} />
// <div>
// <p className="text-sm">
// <span className="font-bold">
// {list?.user?.fullname}
// </span>{" "}
// mengirimkan balasan{" "}
// <span className="font-bold">
// {list?.messageTo?.fullname}
// </span>
// </p>
// <p className="text-xs">
// {new Date(list.createdAt).toLocaleString("id-ID", {
// day: "2-digit",
// month: "2-digit",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// })}
// </p>
// </div>
// </div>
// ) : (
// <div className="flex gap-4 bg-sky-100 p-4 items-center">
// <div className="text-center">
// <Icon icon="qlementine-icons:user-16" width={50} />
// </div>
// <div>
// <p className="text-sm">
// <span className="font-bold">
// {list?.user?.fullname}
// </span>{" "}
// mengirimkan balasan{" "}
// <span className="font-bold">
// {list?.messageTo?.fullname}
// </span>
// </p>
// <p className="text-xs">
// {new Date(list.createdAt).toLocaleString("id-ID", {
// day: "2-digit",
// month: "2-digit",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// })}
// </p>
// </div>
// </div>
// )}
// <div className="bg-white text-sm p-4">{list.comments}</div>
// </div>
// ))}
// {ticketInternal && (
// <div key={ticketInternal.id} className="flex flex-col">
// <div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
// <Icon icon="qlementine-icons:user-16" width={36} />
// <div>
// <p>
// <span className="font-bold text-sm">
// {ticketInternal?.commentFromUserName}
// </span>{" "}
// mengirimkan komentar untuk{" "}
// <span className="font-bold text-sm">
// {ticketInternal?.feedTitle}
// </span>
// </p>
// <p className="text-xs">
// {`${new Date(ticketInternal?.createdAt).getDate()}-${
// new Date(ticketInternal?.createdAt).getMonth() + 1
// }-${new Date(
// ticketInternal?.createdAt
// ).getFullYear()} ${new Date(
// ticketInternal?.createdAt
// ).getHours()}:${new Date(
// ticketInternal?.createdAt
// ).getMinutes()}`}
// </p>
// </div>
// </div>
// <div className="p-4 bg-white text-sm">
// <p>{htmlToString(ticketInternal.message)}</p>
// {detailTickets?.typeId === 6 &&
// detailTickets?.emergencyIssue ? (
// <div className="row mx-0 mb-3 emergency-attachments">
// <div className="mt-3 mr-4">
// <Button
// color="primary"
// size="md"
// onClick={openEmergencyIssueDetail}
// >
// Info Lainnya
// </Button>
// </div>
// </div>
// ) : null}
// {detailTickets?.emergencyIssue && (
// <InfoLainnyaModal
// open={openEmergencyModal}
// onClose={() => setOpenEmergencyModal(false)}
// data={detailTickets.emergencyIssue}
// />
// )}
// </div>
// </div>
// )}
// </div>
// </div>
// {detail !== undefined && (
// <div className="gap-5 mb-5 w-[100%] lg:w-[30%] border bg-white rounded-md">
// <p className="mx-3 mt-3">Properties</p>
// <div className="space-y-2 px-3">
// <Label>Judul</Label>
// <Controller
// control={control}
// name="title"
// render={({ field }) => (
// <Input
// size="md"
// type="text"
// value={detail?.title}
// onChange={field.onChange}
// placeholder="Enter Title"
// />
// )}
// />
// {/* {errors.title?.message && (
// <p className="text-red-400 text-sm">
// {errors.title.message}
// </p>
// )} */}
// </div>
// <div className="mt-5 px-3">
// <Label>Prioritas</Label>
// <Select
// onValueChange={setSelectedPriority}
// value={detail?.priority?.name}
// >
// <SelectTrigger size="md">
// <SelectValue placeholder="Pilih" />
// </SelectTrigger>
// <SelectContent>
// <SelectItem value="Low">Low</SelectItem>
// <SelectItem value="Medium">Medium</SelectItem>
// <SelectItem value="High">High</SelectItem>
// </SelectContent>
// </Select>
// </div>
// <div className="mt-5 px-3 mb-3">
// <Label>Status</Label>
// <Select
// onValueChange={setSelectedStatus}
// value={detail?.status?.name}
// >
// <SelectTrigger size="md">
// <SelectValue placeholder="Pilih" />
// </SelectTrigger>
// <SelectContent>
// <SelectItem value="Open">Open</SelectItem>
// <SelectItem value="Close">Close</SelectItem>
// </SelectContent>
// </Select>
// </div>
// <div className="mt-5 px-3 mb-3">
// <Label>Operator</Label>
// {/* Tag yang ditampilkan secara kolom */}
// {selectedOperator.length > 0 && (
// <div className="flex flex-col gap-2 mb-2">
// {selectedOperator.map((id) => {
// const label = operatorOpt.find(
// (op: any) => op.value === id
// )?.label;
// return (
// <div
// key={id}
// className="flex items-center justify-between bg-gray-200 px-3 py-1 rounded"
// >
// <span>{label}</span>
// <button
// type="button"
// onClick={() =>
// setSelectedOperator((prev) =>
// prev.filter((val) => val !== id)
// )
// }
// className="ml-2 text-gray-500 hover:text-red-600"
// >
// ×
// </button>
// </div>
// );
// })}
// </div>
// )}
// {/* Popover Checkbox Dropdown */}
// <Popover>
// <PopoverTrigger asChild>
// <Button variant="outline" className="w-full justify-between">
// Pilih Operator
// <ChevronDownIcon className="ml-2 h-4 w-4" />
// </Button>
// </PopoverTrigger>
// <PopoverContent className="w-full max-h-60 overflow-auto">
// <div className="flex flex-col gap-1">
// {operatorOpt.map((op: any) => (
// <label
// key={op.id}
// className="flex items-center space-x-2 cursor-pointer px-2 py-1 hover:bg-gray-100 rounded"
// >
// <input
// type="checkbox"
// checked={selectedOperator.includes(op.value)}
// disabled
// onChange={(e) => {
// if (e.target.checked) {
// setSelectedOperator((prev) => [
// ...prev,
// op.value,
// ]);
// } else {
// setSelectedOperator((prev) =>
// prev.filter((val) => val !== op.value)
// );
// }
// }}
// />
// <span>{op.label}</span>
// </label>
// ))}
// </div>
// </PopoverContent>
// </Popover>
// </div>
// <div className="space-y-2 px-3 py-3">
// <Label>Description</Label>
// <Controller
// control={control}
// name="title"
// render={({ field }) => (
// <Textarea
// value={detail?.description}
// onChange={field.onChange}
// placeholder="Enter Title"
// />
// )}
// />
// {/* {errors.title?.message && (
// <p className="text-red-400 text-sm">
// {errors.title.message}
// </p>
// )} */}
// </div>
// </div>
// )}
// </div>
// </div>
// );
// }
"use client";
import React, { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { getTicketingDetail, getTicketingReply, saveTicketReply } from "@/service/service/communication/communication";
export type replyDetail = {
id: number;
comments: string;
createdAt: string;
user: { id: number; fullname: string };
messageTo: { id: number; fullname: string };
};
type Props = { id: string };
export default function FormDetailTicketing({ id }: Props) {
const MySwal = withReactContent(Swal);
const [detail, setDetail] = useState<any>(null);
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [replyText, setReplyText] = useState("");
const chatEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
async function init() {
const resDetail = await getTicketingDetail(id);
setDetail(resDetail?.data?.data);
await fetchReplies();
}
init();
}, [id]);
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [ticketReply]);
async function fetchReplies() {
const res = await getTicketingReply(id);
if (res?.data !== null) {
const sortedReplies = res?.data.data.sort(
(a: replyDetail, b: replyDetail) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
setTicketReply(sortedReplies);
}
}
const sendReply = async (resolve: boolean) => {
if (!replyText.trim()) return;
try {
await saveTicketReply({
ticketId: id,
comment: replyText,
resolve,
});
setReplyText("");
await fetchReplies();
} catch (err) {
MySwal.fire("Error", "Gagal kirim balasan", "error");
}
};
const handleTranslate = () => {
MySwal.fire("Info", "Fitur translate belum dihubungkan", "info");
};
return (
<section className="flex flex-col h-full bg-white">
{/* Header */}
<div className="border-b px-4 py-3 shrink-0">
<div className="flex items-center gap-3">
<div className="text-sm font-medium">
{(detail?.title ?? "").split(" ").slice(0, 25).join(" ") +
((detail?.title ?? "").split(" ").length > 25 ? "..." : "")}
</div>
<div className="text-xs text-muted-foreground">
{detail?.source ?? "Ticket"}
</div>
</div>
</div>
{/* Chat Messages (scrollable only this part) */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50">
{!detail ? (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center">
<div className="mb-4">
{/* Icon kosong */}
<svg
width="72"
height="72"
viewBox="0 0 24 24"
className="mx-auto opacity-60"
>
<path
fill="currentColor"
d="M12 3C7 3 3 6.6 3 11c0 1.9.8 3.6 2.2 5v3.1L8 17.9c1 .3 2 .5 4 .5 5 0 9-3.6 9-8.1S17 3 12 3z"
/>
</svg>
</div>
<div className="text-sm">Pilih issue untuk melihat detail</div>
</div>
</div>
) : (
ticketReply.map((m) => {
const isUser = m.user.fullname !== "Agent";
return (
<div
key={m.id}
className={cn(
"flex items-end",
isUser ? "justify-end" : "justify-start"
)}
>
<div
className={cn(
"px-4 py-2 rounded-2xl max-w-[70%] break-words",
isUser
? "bg-green-500 text-white"
: "bg-white text-gray-800",
isUser ? "rounded-br-none" : "rounded-bl-none",
"shadow-sm"
)}
>
<div className="whitespace-pre-wrap text-sm">
{m.comments}
</div>
<div className="text-[10px] text-gray-300 mt-1 text-right">
{new Date(m.createdAt).toLocaleTimeString("id-ID", {
hour: "2-digit",
minute: "2-digit",
})}
</div>
</div>
</div>
);
})
)}
<div ref={chatEndRef} />
</div>
{/* Input Box */}
<div className="border-t px-4 py-3 bg-white shrink-0">
<div className="flex flex-col gap-2 max-w-full mx-auto">
<textarea
placeholder='Enter your reply or type "/" to insert a quick reply'
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
className="w-full border rounded-xl p-3 min-h-[64px] resize-none focus:outline-none focus:ring"
/>
<div className="flex items-center justify-between gap-3">
<Button variant="outline" size="sm" onClick={handleTranslate}>
Translate
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => sendReply(true)}
disabled={!replyText.trim()}
>
Kirim & Resolve
</Button>
<Button
size="sm"
onClick={() => sendReply(false)}
disabled={!replyText.trim()}
>
Kirim
</Button>
</div>
</div>
</div>
</div>
</section>
);
}