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

818 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// "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>
);
}