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

637 lines
20 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";
"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;
};
};
export default function FormDetailTicketing() {
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>
);
}