821 lines
28 KiB
TypeScript
821 lines
28 KiB
TypeScript
// "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 {
|
||
getTicketingDetail,
|
||
getTicketingReply,
|
||
saveTicketReply,
|
||
} from "@/service/communication/communication";
|
||
import { cn } from "@/lib/utils";
|
||
import Swal from "sweetalert2";
|
||
import withReactContent from "sweetalert2-react-content";
|
||
|
||
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-1 flex flex-col bg-white">
|
||
{/* Header */}
|
||
<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"}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Chat Messages */}
|
||
<div className="flex-1 overflow-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">
|
||
<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">
|
||
<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>
|
||
);
|
||
}
|