2025-09-08 17:26:30 +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>
|
|
|
|
|
|
// );
|
|
|
|
|
|
// }
|
2025-05-19 01:24:48 +00:00
|
|
|
|
"use client";
|
2025-09-08 17:26:30 +00:00
|
|
|
|
|
2025-09-09 15:33:29 +00:00
|
|
|
|
import React, { useEffect, useRef, useState } from "react";
|
2025-05-19 01:24:48 +00:00
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import {
|
|
|
|
|
|
getTicketingDetail,
|
|
|
|
|
|
getTicketingReply,
|
|
|
|
|
|
saveTicketReply,
|
|
|
|
|
|
} from "@/service/communication/communication";
|
2025-09-08 17:26:30 +00:00
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
import Swal from "sweetalert2";
|
|
|
|
|
|
import withReactContent from "sweetalert2-react-content";
|
2025-05-19 01:24:48 +00:00
|
|
|
|
|
|
|
|
|
|
export type replyDetail = {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
comments: string;
|
|
|
|
|
|
createdAt: string;
|
2025-09-08 17:26:30 +00:00
|
|
|
|
user: { id: number; fullname: string };
|
|
|
|
|
|
messageTo: { id: number; fullname: string };
|
2025-05-19 01:24:48 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-08 17:26:30 +00:00
|
|
|
|
type Props = { id: string };
|
|
|
|
|
|
|
|
|
|
|
|
export default function FormDetailTicketing({ id }: Props) {
|
2025-05-19 01:24:48 +00:00
|
|
|
|
const MySwal = withReactContent(Swal);
|
2025-09-08 17:26:30 +00:00
|
|
|
|
const [detail, setDetail] = useState<any>(null);
|
2025-05-19 01:24:48 +00:00
|
|
|
|
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
|
2025-09-08 17:26:30 +00:00
|
|
|
|
const [replyText, setReplyText] = useState("");
|
2025-09-09 15:33:29 +00:00
|
|
|
|
const chatEndRef = useRef<HTMLDivElement>(null);
|
2025-05-19 01:24:48 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-09-08 17:26:30 +00:00
|
|
|
|
async function init() {
|
|
|
|
|
|
const resDetail = await getTicketingDetail(id);
|
|
|
|
|
|
setDetail(resDetail?.data?.data);
|
|
|
|
|
|
await fetchReplies();
|
2025-05-26 10:48:12 +00:00
|
|
|
|
}
|
2025-09-08 17:26:30 +00:00
|
|
|
|
init();
|
|
|
|
|
|
}, [id]);
|
2025-05-26 10:48:12 +00:00
|
|
|
|
|
2025-09-09 15:33:29 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
|
|
}, [ticketReply]);
|
|
|
|
|
|
|
2025-09-08 17:26:30 +00:00
|
|
|
|
async function fetchReplies() {
|
2025-05-19 01:24:48 +00:00
|
|
|
|
const res = await getTicketingReply(id);
|
|
|
|
|
|
if (res?.data !== null) {
|
2025-09-09 15:33:29 +00:00
|
|
|
|
const sortedReplies = res?.data.data.sort(
|
|
|
|
|
|
(a: replyDetail, b: replyDetail) =>
|
|
|
|
|
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
|
|
|
|
);
|
|
|
|
|
|
setTicketReply(sortedReplies);
|
2025-05-19 01:24:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 17:26:30 +00:00
|
|
|
|
const sendReply = async (resolve: boolean) => {
|
|
|
|
|
|
if (!replyText.trim()) return;
|
2025-05-19 01:24:48 +00:00
|
|
|
|
try {
|
2025-09-08 17:26:30 +00:00
|
|
|
|
await saveTicketReply({
|
|
|
|
|
|
ticketId: id,
|
|
|
|
|
|
comment: replyText,
|
2025-09-09 15:33:29 +00:00
|
|
|
|
resolve,
|
2025-05-19 01:24:48 +00:00
|
|
|
|
});
|
2025-09-08 17:26:30 +00:00
|
|
|
|
setReplyText("");
|
|
|
|
|
|
await fetchReplies();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
MySwal.fire("Error", "Gagal kirim balasan", "error");
|
2025-05-19 01:24:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-08 17:26:30 +00:00
|
|
|
|
const handleTranslate = () => {
|
|
|
|
|
|
MySwal.fire("Info", "Fitur translate belum dihubungkan", "info");
|
2025-05-19 19:39:21 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-05-19 01:24:48 +00:00
|
|
|
|
return (
|
2025-09-08 17:26:30 +00:00
|
|
|
|
<section className="flex-1 flex flex-col bg-white">
|
|
|
|
|
|
{/* Header */}
|
2025-09-09 15:33:29 +00:00
|
|
|
|
<div className="border-b px-4 py-3">
|
|
|
|
|
|
<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"}
|
2025-09-08 17:26:30 +00:00
|
|
|
|
</div>
|
2025-05-19 19:39:21 +00:00
|
|
|
|
</div>
|
2025-09-08 17:26:30 +00:00
|
|
|
|
</div>
|
2025-05-19 01:24:48 +00:00
|
|
|
|
|
2025-09-08 17:26:30 +00:00
|
|
|
|
{/* Chat Messages */}
|
2025-09-09 15:33:29 +00:00
|
|
|
|
<div className="flex-1 overflow-auto p-4 space-y-3 bg-gray-50">
|
2025-09-08 17:26:30 +00:00
|
|
|
|
{!detail ? (
|
|
|
|
|
|
<div className="h-full flex items-center justify-center text-muted-foreground">
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
|
<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>
|
2025-05-19 19:39:21 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-08 17:26:30 +00:00
|
|
|
|
) : (
|
2025-09-09 15:33:29 +00:00
|
|
|
|
ticketReply.map((m) => {
|
|
|
|
|
|
const isUser = m.user.fullname !== "Agent";
|
|
|
|
|
|
return (
|
2025-09-08 17:26:30 +00:00
|
|
|
|
<div
|
|
|
|
|
|
key={m.id}
|
|
|
|
|
|
className={cn(
|
2025-09-09 15:33:29 +00:00
|
|
|
|
"flex items-end",
|
|
|
|
|
|
isUser ? "justify-end" : "justify-start"
|
2025-09-08 17:26:30 +00:00
|
|
|
|
)}
|
|
|
|
|
|
>
|
2025-09-09 15:33:29 +00:00
|
|
|
|
<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">
|
2025-09-08 17:26:30 +00:00
|
|
|
|
{new Date(m.createdAt).toLocaleTimeString("id-ID", {
|
|
|
|
|
|
hour: "2-digit",
|
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
|
})}
|
2025-09-09 15:33:29 +00:00
|
|
|
|
</div>
|
2025-09-08 17:26:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-09 15:33:29 +00:00
|
|
|
|
);
|
|
|
|
|
|
})
|
2025-05-19 19:39:21 +00:00
|
|
|
|
)}
|
2025-09-09 15:33:29 +00:00
|
|
|
|
<div ref={chatEndRef} />
|
2025-05-19 01:24:48 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-08 17:26:30 +00:00
|
|
|
|
{/* Input Box */}
|
2025-09-09 15:33:29 +00:00
|
|
|
|
<div className="border-t px-4 py-3 bg-white">
|
|
|
|
|
|
<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>
|
2025-05-19 01:24:48 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-08 17:26:30 +00:00
|
|
|
|
</section>
|
2025-05-19 01:24:48 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|