From 78b633d337a477c376f378a32959810943e1e38d Mon Sep 17 00:00:00 2001 From: Anang Yusman Date: Mon, 26 May 2025 18:48:12 +0800 Subject: [PATCH] [QUDO-98] feat:emergency issue --- .../escalation/detail/[id]/page.tsx | 21 + .../escalation/forward/[id]/page.tsx | 4 +- .../forward/components/columns.tsx | 2 +- .../questions/[title]/components/columns.tsx | 4 +- .../form/communication/collaboration-form.tsx | 14 +- .../escalation-detail-new-form.tsx | 647 ++++++++++++++++++ .../communication/escalation-forward-form.tsx | 378 ++++++---- .../communication/questions-reply-form.tsx | 119 +++- .../form/ticketing/info-lainnya-types.ts | 3 + .../form/ticketing/ticketing-detail-form.tsx | 112 ++- .../form/ticketing/ticketing-update-form.tsx | 316 +++++++-- service/management-user/management-user.ts | 5 + 12 files changed, 1393 insertions(+), 232 deletions(-) create mode 100644 app/[locale]/(protected)/supervisor/communications/escalation/detail/[id]/page.tsx create mode 100644 components/form/communication/escalation-detail-new-form.tsx diff --git a/app/[locale]/(protected)/supervisor/communications/escalation/detail/[id]/page.tsx b/app/[locale]/(protected)/supervisor/communications/escalation/detail/[id]/page.tsx new file mode 100644 index 00000000..c62e9bb0 --- /dev/null +++ b/app/[locale]/(protected)/supervisor/communications/escalation/detail/[id]/page.tsx @@ -0,0 +1,21 @@ +import { Card, CardContent } from "@/components/ui/card"; +import SiteBreadcrumb from "@/components/site-breadcrumb"; +import FormTask from "@/components/form/task/task-form"; +import FormTaskDetail from "@/components/form/task/task-detail-form"; +import FormDetailInternal from "@/components/form/communication/internal-detail-form"; +import FormDetailEscalation from "@/components/form/communication/escalation-detail-form"; +import FormQuestionsForward from "@/components/form/communication/escalation-forward-form"; +import FormQuestionsDetail from "@/components/form/communication/escalation-detail-new-form"; + +const EscalationDetailPage = async () => { + return ( +
+ +
+ +
+
+ ); +}; + +export default EscalationDetailPage; diff --git a/app/[locale]/(protected)/supervisor/communications/escalation/forward/[id]/page.tsx b/app/[locale]/(protected)/supervisor/communications/escalation/forward/[id]/page.tsx index ca725ade..bdf4a4f9 100644 --- a/app/[locale]/(protected)/supervisor/communications/escalation/forward/[id]/page.tsx +++ b/app/[locale]/(protected)/supervisor/communications/escalation/forward/[id]/page.tsx @@ -6,7 +6,7 @@ import FormDetailInternal from "@/components/form/communication/internal-detail- import FormDetailEscalation from "@/components/form/communication/escalation-detail-form"; import FormQuestionsForward from "@/components/form/communication/escalation-forward-form"; -const EscalationDetailPage = async () => { +const EscalationForwardPage = async () => { return (
@@ -17,4 +17,4 @@ const EscalationDetailPage = async () => { ); }; -export default EscalationDetailPage; +export default EscalationForwardPage; diff --git a/app/[locale]/(protected)/supervisor/communications/forward/components/columns.tsx b/app/[locale]/(protected)/supervisor/communications/forward/components/columns.tsx index 71f6b839..54e1d2ef 100644 --- a/app/[locale]/(protected)/supervisor/communications/forward/components/columns.tsx +++ b/app/[locale]/(protected)/supervisor/communications/forward/components/columns.tsx @@ -93,7 +93,7 @@ const columns: ColumnDef[] = [ diff --git a/app/[locale]/(protected)/supervisor/communications/questions/[title]/components/columns.tsx b/app/[locale]/(protected)/supervisor/communications/questions/[title]/components/columns.tsx index 231b59cb..d8a284c4 100644 --- a/app/[locale]/(protected)/supervisor/communications/questions/[title]/components/columns.tsx +++ b/app/[locale]/(protected)/supervisor/communications/questions/[title]/components/columns.tsx @@ -143,10 +143,10 @@ const columns: ColumnDef[] = [ - Eskalasi + Bantuan deleteValidation(row?.original?.id)}> diff --git a/components/form/communication/collaboration-form.tsx b/components/form/communication/collaboration-form.tsx index 44aef90b..dcf9d8dd 100644 --- a/components/form/communication/collaboration-form.tsx +++ b/components/form/communication/collaboration-form.tsx @@ -57,7 +57,6 @@ export default function FormCollaboration() { const editor = useRef(null); type TaskSchema = z.infer; - // State for various form fields const [taskOutput, setTaskOutput] = useState({ all: false, video: false, @@ -68,7 +67,7 @@ export default function FormCollaboration() { const [assignmentType, setAssignmentType] = useState("mediahub"); const [assignmentCategory, setAssignmentCategory] = useState("publication"); - const [mainType, setMainType] = useState(1); // untuk Tipe Penugasan + const [mainType, setMainType] = useState(1); const [type, setType] = useState("1"); const [options, setOptions] = useState([]); const [ticketPriority, setTicketPriority] = useState< @@ -81,7 +80,6 @@ export default function FormCollaboration() { const [platformTypeVisible, setPlatformTypeVisible] = useState(false); const [selectedTarget, setSelectedTarget] = useState(null); - // Opsi untuk dropdown const priority = [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, @@ -134,13 +132,12 @@ export default function FormCollaboration() { if (res?.data !== null) { const rawData = res?.data?.data; - // Ubah ke format Select { value: number, label: string } const priorityOptions = rawData.map((item: any) => ({ - value: item.id, // value dikirim ke API (number) - label: item.name, // label yang ditampilkan ke user + value: item.id, + label: item.name, })); - setTicketPriority(priorityOptions); // ← ini akan dipakai di komponen Select + setTicketPriority(priorityOptions); } } @@ -151,7 +148,6 @@ export default function FormCollaboration() { const rawUser = res?.data?.data?.content; console.log("raw user", rawUser); - // Tentukan tipe array sebagai Option[] const optionArr: Option[] = rawUser.map((option: any) => ({ id: option?.id, label: option?.username + option?.fullname + option?.userLevel?.name, @@ -175,7 +171,7 @@ export default function FormCollaboration() { isCollaboration: true, isEscalation: true, isCollaborationWithNoneTicket: true, - operatorTeam: selectedOption?.id, // This should work now without the error + operatorTeam: selectedOption?.id, }; const response = await saveTicketing(requestData); diff --git a/components/form/communication/escalation-detail-new-form.tsx b/components/form/communication/escalation-detail-new-form.tsx new file mode 100644 index 00000000..3b94a704 --- /dev/null +++ b/components/form/communication/escalation-detail-new-form.tsx @@ -0,0 +1,647 @@ +"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 { + getCuratorUser, + getEscalationDiscussion, + getQuestionTicket, + getTicketingDetail, + getTicketingInternalDetail, + getTicketingInternalDiscussion, + saveEscalationDiscussion, + saveTicketInternalReply, + saveTicketsQuestion, +} from "@/service/communication/communication"; +import { Textarea } from "@/components/ui/textarea"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { Link } from "@/i18n/routing"; +import { loading } from "@/lib/swal"; +import { id } from "date-fns/locale"; +import { DetailTicket } from "../ticketing/info-lainnya-types"; +import { Description } from "@radix-ui/react-toast"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ChevronDownIcon } from "lucide-react"; +import { getOperatorUser } from "@/service/management-user/management-user"; +import { getCookiesDecrypt } from "@/lib/utils"; + +const taskSchema = z.object({ + title: z.string().min(1, { message: "Judul diperlukan" }), + description: z.string().min(2, { + message: "Narasi Penugasan harus lebih dari 2 karakter.", + }), +}); + +type DiscussionItem = { + id: string; + username: string; + message: string; + createdAt: string; + parentId?: string | null; +}; + +export type replyDetail = { + id: number; + message: string; + createdAt: string; + messageFrom: { + id: number; + fullname: string; + }; + messageTo: { + id: number; + fullname: string; + }; +}; + +export default function FormQuestionsDetail() { + const MySwal = withReactContent(Swal); + const { id } = useParams() as { id: string }; + const roleId = getCookiesDecrypt("urie"); + + const [detail, setDetail] = useState(); + const [ticketReply, setTicketReply] = useState([]); + const [replyVisible, setReplyVisible] = useState(false); + + const [listDiscussion, setListDiscussion] = useState([]); + const [message, setMessage] = useState(""); + const [replyMessage, setReplyMessage] = useState>({}); + + const [detailTickets, setDetailTickets] = useState(null); + const [selectedPriority, setSelectedPriority] = useState(""); + const [selectedStatus, setSelectedStatus] = useState(""); + const [operatorOpt, setOperatorOpt] = useState< + { id: string; label: string; value: string }[] + >([]); + const [curatorOpt, setCuratorOpt] = useState< + { id: string; label: string; value: string; badge: string }[] + >([]); + const [selectedOperator, setSelectedOperator] = useState([]); + const [selectedOperatorEscalation, setSelectedOperatorEscalation] = useState< + string[] + >([]); + // const [replies, setReplies] = useState([ + // // { + // // id: 1, + // // name: "Mabes Polri - Approver", + // // message: "test", + // // timestamp: "2024-12-20 00:56:10", + // // }, + // // { + // // id: 2, + // // name: "Mabes Polri - Approver", + // // message: "balas", + // // timestamp: "2025-01-18 17:42:48", + // // }, + // ]); + + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(taskSchema), + }); + + // useEffect(() => { + // async function initState() { + // const response = await getQuestionTicket(id); + // setDetail(response?.data?.data); + + // if (response?.data !== null) { + // setDetailTickets(response?.data?.data); + // } + + // if (detailTickets?.emergencyIssue) { + // reset({ + // title: detailTickets.emergencyIssue.title || "", + // description: detailTickets.emergencyIssue.description || "", + // }); + // // setSelectedPriority(String(detailTickets.emergencyIssue.urgencyId)); + // // setSelectedStatus(String(detailTickets.statusId)); // jika ada + // } + // } + // initState(); + // getTicketReply(); + // }, [id, reset]); + + useEffect(() => { + async function initState() { + const response = await getTicketingDetail(id); + const detail = response?.data?.data; + setDetail(detail); + setDetailTickets(detail); + + // Ambil escalationTeams seperti ":891:" + if (detail?.escalationTeams) { + const teamIds = detail.escalationTeams + .split(":") + .filter((id: string) => id); // hapus string kosong + setSelectedOperatorEscalation(teamIds); // set ke state + } + + getTicketReply(); + } + + initState(); + }, [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(), // pastikan string + })); + setOperatorOpt(optionArr); + + // 👇 Parse `assignedTeams` ke dalam array string + if (detailTickets?.assignedTeams) { + const assigned = detailTickets.assignedTeams + .split(":") + .filter((id: string) => id); // hapus string kosong + setSelectedOperator(assigned); + } + } + } + + getOperator(); + }, [detailTickets]); + + useEffect(() => { + async function getCurator() { + const res = await getCuratorUser(); + const rawUser = res?.data?.data?.content ?? []; // ✅ langsung ambil dari content + + const optionArr = rawUser?.map((option: any) => ({ + id: option.id, + label: `${option.username} | ${option.fullname}`, + value: option.id.toString(), + badge: option.userLevel?.name ?? "", // optional jika ingin tampilkan badge/tingkatan + })); + + setCuratorOpt(optionArr); + } + + getCurator(); + }, []); + + async function getTicketReply() { + const res = await getTicketingInternalDiscussion(id); + if (res?.data !== null) { + setTicketReply(res?.data?.data); + } + } + + const onSubmit = async (data: any) => { + try { + const payload = { + id, + title: data.title, + description: data.description, + priorityId: selectedPriority, + statusId: selectedStatus, + typeId: detailTickets?.typeId, + parentCommentId: detailTickets?.feedId, + }; + + const response = await saveTicketsQuestion(payload); + + MySwal.fire({ + title: "Sukses", + text: "Data berhasil diperbarui.", + icon: "success", + }); + + // Refresh data jika perlu + getTicketReply(); + } catch (error) { + console.error("Gagal update:", error); + MySwal.fire({ + title: "Error", + text: "Terjadi kesalahan saat memperbarui.", + icon: "error", + }); + } + }; + + // const handleSendReply = () => { + // if (replyMessage.trim() === "") return; + + // const newReply = { + // id: replies.length + 1, + // name: "Mabes Polri - Approver", // Sesuaikan dengan data dinamis jika ada + // message: replyMessage, + // timestamp: new Date().toISOString().slice(0, 19).replace("T", " "), + // }; + + // setReplies([...replies, newReply]); + // setReplyMessage(""); + // }; + useEffect(() => { + fetchDiscussions(); + }, [id]); + + const fetchDiscussions = async () => { + try { + const response = await getEscalationDiscussion(id); + setListDiscussion(response?.data?.data || []); + } catch (error) { + console.error("Gagal mengambil diskusi", error); + } + }; + + const sendDiscussionParent = async () => { + if (message?.trim().length === 0) return; + + const data = { + ticketId: id, + message, + parentId: null, + }; + + try { + await saveEscalationDiscussion(data); + setMessage(""); + const response = await getEscalationDiscussion(id); + setListDiscussion(response?.data?.data || []); + } catch (error) { + console.error("Gagal kirim tanggapan", error); + } + }; + + const sendDiscussionChild = async (parentId: string) => { + const inputMsg = replyMessage[parentId]; + if (!inputMsg || inputMsg.trim().length === 0) return; + + const data = { + ticketId: id, + message: inputMsg, + parentMessageId: parentId, + }; + + try { + await saveEscalationDiscussion(data); + const response = await getEscalationDiscussion(id); + setListDiscussion(response?.data?.data || []); + setReplyMessage((prev) => ({ ...prev, [parentId]: "" })); + } catch (error) { + console.error("Gagal kirim balasan", error); + } + }; + + return ( +
+
+
+ {detail !== undefined && ( +
+

+ Ticket #{detail.id} +

+
+ + +
+

+ + {detail?.commentFromUserName} + + {` `} + mengirimkan pesan untuk{` `} + + {detail?.message} + +

+

+ {`${new Date(detail?.createdAt).getDate()}-${ + new Date(detail?.createdAt).getMonth() + 1 + }-${new Date(detail?.createdAt).getFullYear()} ${new Date( + detail?.createdAt + ).getHours()}:${new Date(detail?.createdAt).getMinutes()}`} +

+
+
+

{detail.message}

+
+ )} +
+
+ {detail !== undefined && ( +
+
+ + ( + + )} + /> + {/* {errors.title?.message && ( +

+ {errors.title.message} +

+ )} */} +
+
+ + +
+ {(roleId === "9" || roleId === "10") && ( +
+ + + {/* Tag yang ditampilkan secara kolom */} + {selectedOperator.length > 0 && ( +
+ {selectedOperator.map((id) => { + const label = operatorOpt.find( + (op: any) => op.value === id + )?.label; + return ( +
+ {label} + +
+ ); + })} +
+ )} + + {/* Popover Checkbox Dropdown */} + + + + + +
+ {operatorOpt.map((op: any) => ( + + ))} +
+
+
+
+ )} +
+ + ( +