This commit is contained in:
Rama Priyanto 2025-05-20 16:58:05 +07:00
commit 988dc1e3c6
46 changed files with 4519 additions and 1592 deletions

View File

@ -26,6 +26,7 @@ import {
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
@ -40,6 +41,7 @@ import {
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
@ -68,12 +70,16 @@ import SearchDocumentComponent from "@/components/form/media-tracking/search-doc
import SearchAudioComponent from "@/components/form/media-tracking/search-audio-card";
import TrackingMediaModal from "./modal";
import { getMediaTracking } from "@/service/media-tracking/media-tracking";
import { group } from "console";
import router from "next/router";
import { title } from "process";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
const NewsTable = () => {
const router = useRouter();
const asPath = usePathname();
const searchParams = useSearchParams();
const [search, setSearch] = React.useState("");
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
@ -96,10 +102,11 @@ const NewsTable = () => {
const [page, setPage] = React.useState(1);
const [imageData, setImageData] = React.useState<any>();
const [totalData, setTotalData] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState(1);
const [contentAll, setContentAll] = React.useState([]);
const [formatFilter, setFormatFilter] = React.useState<any>([]);
const [totalContent, setTotalContent] = React.useState();
const [search, setSearch] = React.useState<string>("");
const group = searchParams?.get("group");
const title = searchParams?.get("title");
const categorie = searchParams?.get("category");
@ -144,25 +151,22 @@ const NewsTable = () => {
React.useEffect(() => {
getDataTable();
}, []);
}, [page, showData, search]);
const getDataTable = async () => {
const res = await getMediaTracking({ page: page - 1, size: 10 });
const res = await getMediaTracking(page - 1, search, showData);
const data = res?.data?.data;
console.log;
if (data) {
const startIndex = 10 * (page - 1);
let iterate = 0;
const newData = data.content.map((value: any) => {
iterate++;
value.no = startIndex + iterate;
return value;
});
setDataTable(newData);
setTotalData(data.totalElements);
setTotalPage(data?.totalPages);
setTotalContent(data.totalElements);
}
const newData = data?.content;
newData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(newData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
setTotalContent(data.totalElements);
};
async function getCategories() {
@ -203,33 +207,38 @@ const NewsTable = () => {
router.push(`?category=&title=`);
};
async function getData() {
if (asPath?.includes("/polda/") == true) {
if (asPath?.split("/")[2] !== "[polda_name]") {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
// async function getData() {
// if (asPath?.includes("/polda/") == true) {
// if (asPath?.split("/")[2] !== "[polda_name]") {
// const filter =
// categoryFilter?.length > 0
// ? categoryFilter?.sort().join(",")
// : categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
const filterGroup = group == undefined ? asPath.split("/")[2] : group;
loading();
const response = await listDataAll("", name, filter, "");
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setContentAll(response?.data?.data?.content);
setTotalPage(data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
}
}
// const name = title == undefined ? "" : title;
// const format = formatFilter == undefined ? "" : formatFilter?.join(",");
// const filterGroup = group == undefined ? asPath.split("/")[2] : group;
// loading();
// const response = await listDataAll("", name, filter, "");
// close();
// // setGetTotalPage(response?.data?.data?.totalPages);
// // setContentImage(response?.data?.data?.content);
// // setTotalContent(response?.data?.data?.totalElements);
// const data = response?.data?.data;
// const contentData = data?.content;
// setImageData(contentData);
// setTotalData(data?.totalElements);
// setContentAll(response?.data?.data?.content);
// setTotalPage(data?.totalPages);
// setTotalContent(response?.data?.data?.totalElements);
// }
// }
// }
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
table.getColumn("judul")?.setFilterValue(e.target.value);
};
const handleKeyUp = () => {
clearTimeout(typingTimer);
@ -255,50 +264,72 @@ const NewsTable = () => {
Tracking Berita hari ini!
</p>
</div>
<TrackingMediaModal triggerFetch={() => getDataTable()} />
{/* <Dialog>
<DialogTrigger asChild>
<Button className="bg-blue-600" size="md">
Tracking Berita
</Button>
</DialogTrigger>
<DialogContent className="overflow-y-auto h-[500px] min-w-max mx-5">
<DialogHeader>
<DialogTitle>Form Tracking Berita</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4 px-5">
<div className="space-y-2 flex flex-col">
<Label htmlFor="link" className="text-sm font-medium">
Masukkan Link <span className="text-red-500">*</span>
</Label>
<input
value={searchTitle}
onChange={(e) => setSearchTitle(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
type="text"
placeholder="Search..."
className="pl-4 pr-4 py-1 w-full h-10 text-[15px] border focus:outline-none dark:text-white"
/>
</div>
<div className="flex flex-col gap-2 w-full">
<SearchImageComponent categoryFilter={categoryFilter} />
<SearchVideoComponent categoryFilter={categoryFilter} />
<SearchDocumentComponent categoryFilter={categoryFilter} />
<SearchAudioComponent categoryFilter={categoryFilter} />
<div className="flex flex-col sm:flex-row lg:flex-row justify-between sm:items-center md:items-center lg:items-center px-1">
<TrackingMediaModal triggerFetch={() => getDataTable()} />
<div className=" flex flex-row items-center gap-3">
<div className="flex items-center py-2">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
1 - 100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
1 - 250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<DialogFooter className="flex justify-end gap-2">
<Button onClick={cleanCheckbox} variant="outline">
Riset Filter
</Button>
<Button className="bg-blue-600 text-white">Tracking Berita</Button>
</DialogFooter>
</DialogContent>
</Dialog> */}
<Table className="overflow-hidden mt-4">
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">

View File

@ -59,12 +59,12 @@ const columns: ColumnDef<any>[] = [
),
},
{
accessorKey: "static",
accessorKey: "isStaticBanner",
header: "Static Banner",
cell: ({ row }) => (
<StaticToogle
id={row.original.id}
initChecked={row.original.staticPage}
initChecked={row.original.isStaticBanner}
/>
),
},

View File

@ -96,22 +96,9 @@ const BannerListTable = () => {
let temp: any;
const response = await listBanner();
temp = response?.data?.data;
const response2 = await listStaticBanner();
console.log("sadadddd", response2?.data?.data.length);
for (let i = 0; i < response2?.data?.data.length; i++) {
for (let j = 0; j < temp.length; j++) {
console.log("temp", j, temp[j].id);
if (response2?.data?.data[i].mediaUploadId === temp[j].id) {
temp[j].staticPage = true;
} else {
temp[j].staticPage = false;
}
}
}
console.log("tesmasdasdasdasd", temp);
setGetData(temp);
const data = response?.data?.data?.content;
console.log("banner", data);
setGetData(data);
close();
}

View File

@ -45,19 +45,17 @@ const columns: ColumnDef<any>[] = [
),
},
{
accessorKey: "contentType",
accessorKey: "mediaTypesString",
header: "Tipe Konten",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("contentType")}</span>
<span className="normal-case">{row.getValue("mediaTypesString")}</span>
),
},
{
accessorKey: "isInternational",
accessorKey: "publishedLocation",
header: "Wilayah",
cell: ({ row }) => (
<span className="normal-case">
{row.getValue("isInternational") ? "INT" : "ID"}
</span>
<span className="normal-case">{row.getValue("publishedLocation")}</span>
),
},
{

View File

@ -0,0 +1,619 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, 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, CardContent } 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, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import Cookies from "js-cookie";
import { postBlog } from "@/service/blog/blog";
import { Textarea } from "@/components/ui/textarea";
import {
DotSquare,
InboxIcon,
PaperclipIcon,
SmileIcon,
TrashIcon,
} from "lucide-react";
import {
deleteMediaCurationMessage,
detailMedia,
getMediaCurationMessage,
saveMediaCurationMessage,
} from "@/service/curated-content/curated-content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { listData } from "@/service/landing/landing";
import {
createAssignmentResponse,
deleteAssignmentResponse,
getAssignmentResponseList,
} from "@/service/task";
import { getCookiesDecrypt } from "@/lib/utils";
import { close, loading } from "@/lib/swal";
import { Checkbox } from "@/components/ui/checkbox";
import { formatDateToIndonesian, htmlToString } from "@/utils/globals";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import { Icon } from "@/components/ui/icon";
const detailSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
categoryName: z.string().min(1, { message: "Judul diperlukan" }),
meta: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
categoryName: string;
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
// Pastikan validitas tanggal
if (isNaN(date.getTime())) {
throw new Error("Invalid date format");
}
// Format tanggal
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
// const hours = date.getHours().toString().padStart(2, "0");
// Gabungkan hasil format
return `${day}-${month}-${year} `;
};
export type curationDetail = {
id: number;
title: string;
categoryName: string;
htmlDescription: string;
updatedAt: string;
timezone: string;
clickCount: string;
creatorName: string;
uploadedBy: {
id: number;
fullname: string;
username: string | null;
email: string;
isActive: boolean;
isDefault: boolean;
isInternational: boolean;
userLevel: {
id: number;
name: string;
aliasName: string;
userGroupId: number;
};
};
publishedFor: string; // ID for selected radio button
publishedForObject: {
id: number;
name: string;
isInternal: boolean;
code: string;
}[];
tags: string;
provinceId: string;
is_active: string;
};
const initialComments = [
{
id: 1,
username: "Esther Howard",
date: "07-04-2023 20:00 WIB",
text: "Tolong untuk narasinya mengikuti 5W + 1H!",
avatar: "/images/avatar/avatar-3.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 2,
username: "Brooklyn Simmons",
date: "07-04-2023 20:00 WIB",
text: "Ok Baik, Saya segera melakukan perbaikan. Terima kasih atas masukannya. 🙏",
avatar: "/images/avatar/avatar-5.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 3,
username: "Leslie Alexander",
date: "07-04-2023 20:00 WIB",
text: "Sangat berguna. Terima Kasih!",
avatar: "/images/avatar/avatar-7.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
];
export default function DetailAcceptImage() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const t = useTranslations("LandingPage");
console.log(id);
const editor = useRef(null);
type DetailSchema = z.infer<typeof detailSchema>;
const userLevelNumber = getCookiesDecrypt("ulne");
const userId = getCookiesDecrypt("uie");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [selectedTarget, setSelectedTarget] = useState("");
const [detail, setDetail] = useState<curationDetail>();
const [refresh] = useState(false);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [showInput, setShowInput] = useState<boolean>(false);
const [selectedFileId, setSelectedFileId] = useState(null);
const [listData, setListData] = useState([]);
const [message, setMessage] = useState("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<DetailSchema>({
resolver: zodResolver(detailSchema),
});
const [commentsData, setCommentsData] = useState(initialComments);
const [replyText, setReplyText] = useState("");
const [replyingTo, setReplyingTo] = useState<number | null>(null);
const [selectedValue, setSelectedValue] = useState<string>("");
const handleReply = (commentId: number) => {
setReplyingTo(commentId);
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
useEffect(() => {
async function initState() {
// loading();
const response = await getMediaCurationMessage(selectedFileId);
console.log("data", response?.data?.data);
console.log("userLvl", userLevelNumber);
setListData(response?.data?.data);
close();
}
initState();
}, [selectedFileId]);
// const postData = () => {
// sendSuggestionParent();
// };
const postData = async () => {
if (message?.length > 1 && selectedFileId) {
try {
const data = {
mediaUploadFileId: selectedFileId,
message,
parentId: null,
};
const response = await saveMediaCurationMessage(data);
console.log("Komentar terkirim:", response);
const responseGet = await getMediaCurationMessage(selectedFileId);
setListData(responseGet?.data?.data);
setMessage("");
} catch (error) {
console.error("Error posting comment:", error);
}
} else {
console.log("Pesan atau file ID tidak valid.");
}
};
const sendReplyData = async (parentId: number) => {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLTextAreaElement;
if (inputElement?.value?.length > 1 && selectedFileId) {
loading();
const data = {
mediaUploadFileId: selectedFileId,
message: inputElement.value,
parentId,
};
console.log("Sending reply:", data);
const response = await saveMediaCurationMessage(data);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log("Updated comments:", responseGet?.data?.data);
setListData(responseGet?.data?.data);
inputElement.value = "";
close();
setReplyingTo(null);
}
};
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deleteMediaCurationMessage(dataId);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
const deleteData = (dataId: any) => {
deleteDataSuggestion(dataId);
console.log(dataId);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
setDetail(details);
setSelectedValue(details?.publishedFor || "");
setSelectedFileId(details?.files[0]?.id);
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
id: file.id,
thumbnailFileUrl: file.thumbnailFileUrl || "default-image.jpg",
placements: file.placements || "",
}));
setDetailThumb(fileUrls);
}
}
initState();
}, [id, refresh]);
const handleFileClick = async (fileId: any) => {
setSelectedFileId(fileId);
try {
const response = await getMediaCurationMessage(fileId);
console.log("Data komentar:", response?.data?.data);
setListData(response?.data?.data);
} catch (error) {
console.error("Error fetching comments:", error);
}
};
const handleValueChange = (value: string) => {
setSelectedValue(value);
};
return (
<div className="flex gap-10">
{detail !== undefined ? (
<Card className="w-full ">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Kurasi Detail</p>
<div className="flex flex-col lg:flex-row gap-5">
<div className="w-full px-3 mt-3 rounded-md">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<div className="w-full ">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<img
className="object-fill h-full w-full rounded-md"
src={data.thumbnailFileUrl}
alt={`File ID: ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
<div className=" mt-2 ">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={6}
spaceBetween={8}
pagination={{
clickable: true,
}}
modules={[Pagination, Thumbs]}
// className="mySwiper2"
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<img
className="object-fill h-full w-full rounded-md"
src={data.thumbnailFileUrl}
alt={`File ID: ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
<div className="text-gray-500 flex flex-col lg:flex-row justify-between items-center border-t mt-4">
<div className="flex flex-col lg:flex-row items-center mt-3 lg:justify-between">
<p className="text-xs lg:text-sm">
{t("by")}&nbsp;
<span className="font-semibold text-black dark:text-white">
{detail?.uploadedBy?.userLevel?.name}
</span>
</p>
{/* <p className="text-xs lg:text-sm">
&nbsp;|&nbsp;{t("updatedOn")}
{detail?.updatedAt} WIB &nbsp;|&nbsp;
</p> */}
<p className="text-xs lg:text-sm">
&nbsp;|&nbsp;{t("updatedOn")}&nbsp;
{formatDateToIndonesian(new Date(detail?.updatedAt))}{" "}
{detail?.timezone ? detail?.timezone : "WIB"}
&nbsp;
</p>
<p className="text-xs lg:text-sm flex justify-center items-center">
&nbsp;|&nbsp;
<Icon icon="formkit:eye" width="15" height="15" />
&nbsp; {detail?.clickCount} &nbsp;
</p>
</div>
<div className="mt-3">
<p className="flex text-end text-xs lg:text-sm font-semibold">
{t("creator")}
{detail?.creatorName}
</p>
</div>
</div>
{/* Keterangan */}
<div className="w-full">
<h1 className="flex flex-row font-bold text-lg lg:text-2xl my-8">
{detail?.title}
</h1>
<div
className="font-light text-justify mb-5 space-y-4 lg:mb-0"
dangerouslySetInnerHTML={{
__html: detail?.htmlDescription,
}}
/>
</div>
</div>
</div>
<CardContent className="p-1">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">Komentar</Label>
<div className="mt-4 border p-4 rounded bg-gray-50">
<Textarea
placeholder="Tulis tanggapan Anda di sini..."
value={message}
onChange={handleInputChange}
/>
<div className="flex justify-end mt-3">
<Button
color="primary"
onClick={() => postData()}
type="button"
>
Kirim Komentar
</Button>
</div>
</div>
{listData?.map((item: any) => (
<div key={item.id} className="flex flex-col gap-3 mt-2 ">
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${item.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">{item.message}</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(item.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(item.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === item.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${item.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(item.id)}
>
Kirim
</button>
</div>
)}
{item.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col">
{item.children.map((child: any) => (
<div
key={child.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child.message}
</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(child.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(child.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === child.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${child.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(child.id)}
>
Kirim
</button>
</div>
)}
{child.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col mb-3">
{child.children.map((child2: any) => (
<div
key={child2.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3 ">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child2.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child2.message}
</p>
<div className="flex flex-row gap-2">
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() =>
deleteData(child2.id)
}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">
Delete
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</CardContent>
</div>
</Card>
) : (
""
)}
</div>
);
}

View File

@ -63,7 +63,11 @@ const ImageSliderPage = () => {
<div className="p-2">
<Card className="shadow-md rounded-lg overflow-hidden">
<Link
href={`/shared/curated-content/giat-routine/image/detail/${image.id}`}
href={
roleId === 12
? `/shared/curated-content/giat-routine/image/accept-assignment/detail/${image.id}`
: `/shared/curated-content/giat-routine/image/detail/${image.id}`
}
>
<CardContent className="p-0">
<img

View File

@ -0,0 +1,20 @@
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";
const EscalationDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormQuestionsForward />
</div>
</div>
);
};
export default EscalationDetailPage;

View File

@ -136,7 +136,7 @@ const columns: ColumnDef<any>[] = [
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link
href={`/supervisor/communications/internal/update/${row?.original?.id}`}
href={`/supervisor/communications/questions/reply/${row?.original?.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none items-center">
Jawab

View File

@ -0,0 +1,20 @@
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 FormDetailTicketing from "@/components/form/ticketing/ticketing-detail-form";
import FormQuestionsReply from "@/components/form/communication/questions-reply-form";
const QuestionsReplyPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormQuestionsReply />
</div>
</div>
);
};
export default QuestionsReplyPage;

View File

@ -1,4 +1,4 @@
'use client'
"use client";
import {
Accordion,
@ -6,16 +6,25 @@ import {
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {Card, CardContent} from "@/components/ui/card";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import {getKnowledgeBaseCategoryList, getKnowledgeBaseList} from "@/service/master/knowledge-base";
import {
deleteKnowledgeBase,
getKnowledgeBaseCategoryList,
getKnowledgeBaseList,
} from "@/service/master/knowledge-base";
import React from "react";
import {Plus, Trash2} from "lucide-react";
import {Button} from "@/components/ui/button";
import { Plus, Trash, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import CreateCategory from "./create-category";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { deleteMedia } from "@/service/content/content";
import { error } from "@/lib/swal";
const KnowledgeBase = () => {
const MySwal = withReactContent(Swal);
const [categories, setCategories] = React.useState<any>([]);
const [questions, setQuestions] = React.useState<any>([]);
@ -28,7 +37,7 @@ const KnowledgeBase = () => {
const data = response?.data?.data;
if (data) {
setCategories(data);
fetchQuestions(data[0]?.id)
fetchQuestions(data[0]?.id);
}
}
@ -38,84 +47,135 @@ const KnowledgeBase = () => {
if (data) {
setQuestions(data);
}
};
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const response = await deleteKnowledgeBase(id);
if (response?.error) {
error(response.message);
return false;
}
success();
}
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
}
const handleDeleteKnowlagde = (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);
}
});
};
return (
<div>
<SiteBreadcrumb/>
<Tabs defaultValue={`category-0`}>
<div className="grid grid-cols-12 gap-6">
<Card className="lg:col-span-3 md:col-span-5 col-span-12 h-max">
<CardContent className=" p-6">
<TabsList className="md:flex-col gap-2 flex-wrap md:items-start justify-start">
<CreateCategory onSuccess={fetchCategoryList} />
{categories?.map((category: any, index: number) => (
<TabsTrigger
key={index}
value={`category-${index}`}
onClick={() => {
fetchQuestions(category?.id);
}}
className="data-[state=active]:bg-secondary data-[state=active]:text-default rounded-md px-6 py-3 w-full justify-start"
>
{category?.name}
<div
className="absolute right-2 top-2 hidden group-hover:inline-flex"
// onClick={() => deleteCategory(category?.id)}
>
<Trash2 className="w-3.5 h-3.5 me-1" />
<div>
<SiteBreadcrumb />
<Tabs defaultValue={`category-0`}>
<div className="grid grid-cols-12 gap-6">
<Card className="lg:col-span-3 md:col-span-5 col-span-12 h-max">
<CardContent className=" p-6">
<TabsList className="md:flex-col gap-2 flex-wrap md:items-start justify-start">
<CreateCategory onSuccess={fetchCategoryList} />
{categories?.map((category: any, index: number) => (
<TabsTrigger
key={index}
value={`category-${index}`}
onClick={() => {
fetchQuestions(category?.id);
}}
className="group data-[state=active]:bg-secondary data-[state=active]:text-default rounded-md px-6 py-3 w-full justify-between flex items-center"
>
{category?.name}
<div
className="right-2 top-2 hidden group-hover:inline-flex"
// onClick={() => deleteCategory(category?.id)}
>
<Trash size={20} className="text-red-500" />
</div>
</TabsTrigger>
))}
</TabsList>
</CardContent>
</Card>
<div className="lg:col-span-9 md:col-span-7 col-span-12 mt-4 lg:mt-0">
{categories?.map((cateogry: any, index: number) => (
<TabsContent
key={index}
value={`category-${index}`}
className="mt-0"
>
<Accordion type="single" collapsible className="w-full">
{questions?.map((question: any) => (
<AccordionItem
key={question.id}
className="dark:bg-secondary bg-white"
value={question.id}
>
<AccordionTrigger className="flex items-center justify-between gap-4 dark:bg-secondary bg-white data-[state=open]:bg-default-200 data-[state=active]:text-default">
<div className="flex items-center justify-between w-full gap-4">
<span
dangerouslySetInnerHTML={{
__html: question.question,
}}
className="text-left"
/>
<Trash
size={20}
className="text-red-500 hover:cursor-pointer"
onClick={() => handleDeleteKnowlagde(question.id)}
/>
</div>
</TabsTrigger>
</AccordionTrigger>
<AccordionContent className="dark:bg-secondary bg-white">
{question.answer}
</AccordionContent>
</AccordionItem>
))}
</TabsList>
</CardContent>
</Card>
<div className="lg:col-span-9 md:col-span-7 col-span-12 mt-4 lg:mt-0">
{categories?.map((cateogry: any, index: number) => (
<TabsContent key={index} value={`category-${index}`} className="mt-0">
<Accordion type="single" collapsible className="w-full">
{questions?.map((question: any) => (
<AccordionItem
key={question.id}
className="dark:bg-secondary bg-white"
value={question.id}
>
<AccordionTrigger
className="dark:bg-secondary bg-white data-[state=open]:bg-default-200 data-[state=active]:text-default">
{question.question}
</AccordionTrigger>
<AccordionContent className="dark:bg-secondary bg-white">
{question.answer}
</AccordionContent>
</AccordionItem>
))
}
</Accordion>
{questions?.length > 0 &&
<div className="flex gap-3">
<Button
fullWidth
size="md"
variant="outline"
>
<Plus className="w-6 h-6 me-1.5"/>
Import
</Button>
<Button
fullWidth
size="md"
>
<Plus className="w-6 h-6 me-1.5"/>
Add Question
</Button>
</div>
}
</TabsContent>
))}
</div>
</Accordion>
{questions?.length > 0 && (
<div className="flex gap-3">
<Button fullWidth size="md" variant="outline">
<Plus className="w-6 h-6 me-1.5" />
Import
</Button>
<Button fullWidth size="md">
<Plus className="w-6 h-6 me-1.5" />
Add Question
</Button>
</div>
)}
</TabsContent>
))}
</div>
</Tabs>
</div>
</div>
</Tabs>
</div>
);
};

View File

@ -1,7 +1,13 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import {
CheckSquare2,
Eye,
MoreVertical,
SquarePen,
Trash2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
@ -12,6 +18,14 @@ import {
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { formatDateToIndonesian } from "@/utils/globals";
import { Link } from "@/i18n/routing";
import {
closeTicket,
deleteTicket,
} from "@/service/communication/communication";
import withReactContent from "sweetalert2-react-content";
import { error } from "@/lib/swal";
import Swal from "sweetalert2";
const columns: ColumnDef<any>[] = [
{
@ -90,6 +104,73 @@ const columns: ColumnDef<any>[] = [
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
async function doDelete(id: any) {
const response = await deleteTicket(id);
if (response?.error) {
error(response.message);
return false;
}
success();
}
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
}
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);
}
});
};
async function doClose(id: any) {
const response = await closeTicket(id);
if (response?.error) {
error(response.message);
return false;
}
success();
}
const handleClose = (id: any) => {
MySwal.fire({
title: "Ubah status menjadi close?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Iya",
}).then((result) => {
if (result.isConfirmed) {
doClose(id);
}
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -102,15 +183,29 @@ const columns: ColumnDef<any>[] = [
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
<Link href={`/supervisor/ticketing/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
<Link href={`/supervisor/ticketing/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => handleClose(row.original.id)}
className="p-2 border-b text-green-600 bg-green-200 focus:bg-green-400 focus:text-destructive-foreground rounded-none"
>
<CheckSquare2 className="w-4 h-4 me-1.5" />
Close
</DropdownMenuItem>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none">
<DropdownMenuItem
onClick={() => handleDelete(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>

View File

@ -0,0 +1,19 @@
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 FormDetailTicketing from "@/components/form/ticketing/ticketing-detail-form";
const TicketingDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormDetailTicketing />
</div>
</div>
);
};
export default TicketingDetailPage;

View File

@ -0,0 +1,20 @@
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 FormDetailTicketing from "@/components/form/ticketing/ticketing-detail-form";
import FormUpdateTicketing from "@/components/form/ticketing/ticketing-update-form";
const TicketingUpdatePage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormUpdateTicketing />
</div>
</div>
);
};
export default TicketingUpdatePage;

View File

@ -20,7 +20,7 @@ const Home = ({ params: { locale } }: { params: { locale: string } }) => {
<ReactLenis root>
<div className="pb-14">
<Navbar />
<Hero />
<Hero group="mabes" />
<SearchSection />
<NewContent group="mabes" type="latest" />
<NewContent group="mabes" type="popular" />

View File

@ -0,0 +1,291 @@
"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 {
getEscalationDiscussion,
getQuestionTicket,
getTicketingDetail,
getTicketingInternalDetail,
getTicketingInternalDiscussion,
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";
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.",
}),
});
export type replyDetail = {
id: number;
message: string;
createdAt: string;
messageFrom: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
export default function FormQuestionsForward() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const [detail, setDetail] = useState<any>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [replyVisible, setReplyVisible] = useState(false);
const [listDiscussion, setListDiscussion] = useState();
const [message, setMessage] = useState("");
const [detailTickets, setDetailTickets] = useState<DetailTicket | null>(null);
const [selectedPriority, setSelectedPriority] = useState("");
const [replyMessage, setReplyMessage] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
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]);
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("");
};
return (
<div>
<div className="flex">
<div className="flex flex-col mt-6 w-full mb-3">
{detail !== undefined && (
<div key={detail?.id} className="bg-slate-300 rounded-md">
<p className="p-5 bg-slate-300 rounded-md text-lg font-semibold">
Ticket #{detail.id}
</p>
<div className="flex flex-row gap-3 bg-sky-100 p-5 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold">
{detail?.commentFromUserName}
</span>
{` `}
mengirimkan pesan untuk{` `}
<Link
href={
detail?.feed
? detail?.feed?.permalink_url == undefined
? detail?.feedUrl
: detail?.feed?.permalink_url
: ""
}
target="_blank"
className="font-bold"
>
{detail?.message}
</Link>
</p>
<p className="text-xs">
{`${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()}`}
</p>
</div>
</div>
<p className="p-5 bg-white">{detail.message}</p>
</div>
)}
</div>
</div>
{detailTickets && (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5 w-[100%] lg:w-auto 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"
{...field}
placeholder="Masukkan judul"
/>
)}
/>
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={selectedPriority}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih Prioritas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Low</SelectItem>
<SelectItem value="2">Medium</SelectItem>
<SelectItem value="3">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-5 px-3 mb-3">
<Label>Status</Label>
<Select onValueChange={setSelectedStatus} value={selectedStatus}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Open</SelectItem>
<SelectItem value="2">Close</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 px-3 py-3">
<Label>Deskripsi</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea {...field} placeholder="Masukkan description" />
)}
/>
</div>
<div className="flex justify-end mt-3 mr-3 py-3">
<Button type="submit" color="primary">
Simpan
</Button>
</div>
</div>
</form>
)}
</div>
);
}

View File

@ -0,0 +1,553 @@
"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,
getQuestionTicket,
getTicketingDetail,
getTicketingInternalDetail,
getTicketingInternalDiscussion,
getTicketingReply,
saveTicketing,
saveTicketInternalReply,
saveTicketReply,
saveTicketsQuestion,
} 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 { useMediaQuery } from "react-responsive";
import { DetailTicket } from "../ticketing/info-lainnya-types";
import InfoLainnyaModal from "../ticketing/info-lainnya";
import { Description } from "@radix-ui/react-toast";
import { Link } from "@/i18n/routing";
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.",
}),
});
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 FormQuestionsReply() {
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 [detailTicketsQuestions, setDetailTicketsQuestions] =
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); // beri tipe number
const [replyText, setReplyText] = useState<string>(""); // untuk isi balasan
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
async function initState() {
setReplyValue(0);
const response = await getQuestionTicket(id);
setDetailTicketsQuestions(response?.data?.data || null);
setTicketInternal(response?.data?.data || null);
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]);
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); // tutup form setelah kirim
getTicketReply(); // refresh balasan
} 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);
// Tambahkan balasan baru ke daftar balasan
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",
});
// Reset input dan sembunyikan form balasan
setReplyMessage("");
setReplyVisible(false);
} catch (error) {
MySwal.fire({
title: "Error",
text: "Gagal mengirim balasan.",
icon: "error",
});
console.error("Error sending reply:", error);
}
};
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",
});
}
};
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 flex flex-row items-center gap-3">
<div className="mt-4 flex flex-row items-center gap-3">
<Link href={`/supervisor/communications/escalation/forward/${id}`}>
<Button color="default" variant={"outline"}>
Eskalasi
</Button>
</Link>
</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>
))}
{detailTicketsQuestions && (
<div key={detailTicketsQuestions.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">
{detailTicketsQuestions?.commentFromUserName}
</span>{" "}
mengirimkan komentar untuk{" "}
<span className="font-bold text-sm">
{detailTicketsQuestions?.feedTitle}
</span>
</p>
<p className="text-xs">
{`${new Date(
detailTicketsQuestions?.createdAt
).getDate()}-${
new Date(detailTicketsQuestions?.createdAt).getMonth() +
1
}-${new Date(
detailTicketsQuestions?.createdAt
).getFullYear()} ${new Date(
detailTicketsQuestions?.createdAt
).getHours()}:${new Date(
detailTicketsQuestions?.createdAt
).getMinutes()}`}
</p>
</div>
</div>
<div className="p-4 bg-white text-sm">
<p>{htmlToString(detailTicketsQuestions.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>
{detailTickets && (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5 w-[100%] lg:w-auto 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"
{...field}
placeholder="Masukkan judul"
/>
)}
/>
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={selectedPriority}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih Prioritas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Low</SelectItem>
<SelectItem value="2">Medium</SelectItem>
<SelectItem value="3">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-5 px-3 mb-3">
<Label>Status</Label>
<Select
onValueChange={setSelectedStatus}
value={selectedStatus}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Open</SelectItem>
<SelectItem value="2">Close</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 px-3 py-3">
<Label>Deskripsi</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea {...field} placeholder="Masukkan description" />
)}
/>
</div>
<div className="flex justify-end mt-3 mr-3 py-3">
<Button type="submit" color="primary">
Buat Tiket
</Button>
</div>
</div>
</form>
)}
</div>
</div>
);
}

View File

@ -130,7 +130,7 @@ export default function FormAudioUpdate() {
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [selectedOptions, setSelectedOptions] = useState<{
[fileId: number]: string;
[fileId: number]: string[];
}>({});
const options: Option[] = [
@ -243,6 +243,13 @@ export default function FormAudioUpdate() {
if (details?.files) {
setFiles(details.files);
const initialOptions: { [key: number]: string[] } = {};
details.files.forEach((file: any) => {
if (file.placements) {
initialOptions[file.id] = mapPlacementsToOptions(file.placements);
}
});
setSelectedOptions(initialOptions);
}
if (details?.publishedFor) {
@ -274,6 +281,31 @@ export default function FormAudioUpdate() {
initState();
}, [refresh, setValue]);
const mapPlacementsToOptions = (placements: string): string[] => {
const mapping: Record<string, string> = {
all: "all",
mabes: "nasional",
polda: "wilayah",
polres: "internasional",
};
// Jika placements hanya "all", langsung aktifkan semua checkbox
if (placements.trim() === "all") {
return ["all", "nasional", "wilayah", "internasional"];
}
const options = placements
.split(",")
.map((p) => mapping[p.trim()])
.filter(Boolean);
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
options.includes(opt)
);
return allSelected ? ["all", ...options] : options;
};
const handleCheckboxChange = (id: string) => {
if (id === "all") {
// Select all options except "all"

View File

@ -58,6 +58,7 @@ import { error } from "@/lib/swal";
import dynamic from "next/dynamic";
import { useRouter } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import { UnitMapping } from "@/app/[locale]/(protected)/contributor/agenda-setting/unit-mapping";
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
@ -142,6 +143,15 @@ export default function FormImageDetail() {
const [selectedTarget, setSelectedTarget] = useState("");
const [files, setFiles] = useState<FileType[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [wilayahPublish, setWilayahPublish] = React.useState({
semua: false,
nasional: false,
polda: false,
polres: false,
satker: false,
international: false,
});
const [selectedPolda, setSelectedPolda] = React.useState([]);
let fileTypeId = "1";
@ -776,6 +786,15 @@ export default function FormImageDetail() {
>
Wilayah
</label>
{wilayahPublish.polda && (
<UnitMapping
unit="Polda"
isDetail={false}
sendDataToParent={(data: any) =>
setSelectedPolda(data)
}
/>
)}
</div>
<div className="flex items-center space-x-2">

View File

@ -133,7 +133,7 @@ export default function FormImageUpdate() {
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [selectedOptions, setSelectedOptions] = useState<{
[fileId: number]: string;
[fileId: number]: string[];
}>({});
const options: Option[] = [
@ -266,6 +266,14 @@ export default function FormImageUpdate() {
if (details?.files) {
setFiles(details.files);
const initialOptions: { [key: number]: string[] } = {};
details.files.forEach((file: any) => {
if (file.placements) {
initialOptions[file.id] = mapPlacementsToOptions(file.placements);
}
});
setSelectedOptions(initialOptions);
}
if (details?.publishedFor) {
@ -291,11 +299,30 @@ export default function FormImageUpdate() {
initState();
}, [refresh, setValue]);
// const handleCheckboxChange = (id: number) => {
// setSelectedPublishers((prev) =>
// prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
// );
// };
const mapPlacementsToOptions = (placements: string): string[] => {
const mapping: Record<string, string> = {
all: "all",
mabes: "nasional",
polda: "wilayah",
polres: "internasional",
};
// Jika placements hanya "all", langsung aktifkan semua checkbox
if (placements.trim() === "all") {
return ["all", "nasional", "wilayah", "internasional"];
}
const options = placements
.split(",")
.map((p) => mapping[p.trim()])
.filter(Boolean);
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
options.includes(opt)
);
return allSelected ? ["all", ...options] : options;
};
const handleCheckboxChange = (id: string) => {
if (id === "all") {

View File

@ -127,7 +127,7 @@ export default function FormTeksUpdate() {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [selectedOptions, setSelectedOptions] = useState<{
[fileId: number]: string;
[fileId: number]: string[];
}>({});
const [selectedTarget, setSelectedTarget] = useState("");
const [unitSelection, setUnitSelection] = useState({
@ -236,6 +236,13 @@ export default function FormTeksUpdate() {
if (details?.files) {
setFiles(details.files);
const initialOptions: { [key: number]: string[] } = {};
details.files.forEach((file: any) => {
if (file.placements) {
initialOptions[file.id] = mapPlacementsToOptions(file.placements);
}
});
setSelectedOptions(initialOptions);
}
if (details?.publishedFor) {
@ -261,6 +268,31 @@ export default function FormTeksUpdate() {
initState();
}, [refresh, setValue]);
const mapPlacementsToOptions = (placements: string): string[] => {
const mapping: Record<string, string> = {
all: "all",
mabes: "nasional",
polda: "wilayah",
polres: "internasional",
};
// Jika placements hanya "all", langsung aktifkan semua checkbox
if (placements.trim() === "all") {
return ["all", "nasional", "wilayah", "internasional"];
}
const options = placements
.split(",")
.map((p) => mapping[p.trim()])
.filter(Boolean);
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
options.includes(opt)
);
return allSelected ? ["all", ...options] : options;
};
const handleCheckboxChange = (id: string) => {
if (id === "all") {
// Select all options except "all"

View File

@ -145,7 +145,7 @@ export default function FormVideoUpdate() {
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [selectedOptions, setSelectedOptions] = useState<{
[fileId: number]: string;
[fileId: number]: string[];
}>({});
const options: Option[] = [
@ -262,6 +262,13 @@ export default function FormVideoUpdate() {
if (details?.files) {
setFiles(details.files);
const initialOptions: { [key: number]: string[] } = {};
details.files.forEach((file: any) => {
if (file.placements) {
initialOptions[file.id] = mapPlacementsToOptions(file.placements);
}
});
setSelectedOptions(initialOptions);
}
if (details?.publishedFor) {
@ -293,6 +300,31 @@ export default function FormVideoUpdate() {
initState();
}, [refresh, setValue]);
const mapPlacementsToOptions = (placements: string): string[] => {
const mapping: Record<string, string> = {
all: "all",
mabes: "nasional",
polda: "wilayah",
polres: "internasional",
};
// Jika placements hanya "all", langsung aktifkan semua checkbox
if (placements.trim() === "all") {
return ["all", "nasional", "wilayah", "internasional"];
}
const options = placements
.split(",")
.map((p) => mapping[p.trim()])
.filter(Boolean);
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
options.includes(opt)
);
return allSelected ? ["all", ...options] : options;
};
const handleCheckboxChange = (id: string) => {
if (id === "all") {
// Select all options except "all"

View File

@ -25,6 +25,7 @@ import {
createTaskTa,
getTask,
getUserLevelForAssignments,
getUserLevelForExpert,
} from "@/service/task";
import {
Dialog,
@ -55,6 +56,7 @@ import TimePicker from "react-time-picker";
import "react-time-picker/dist/TimePicker.css";
import "react-clock/dist/Clock.css";
import { detailMedia } from "@/service/curated-content/curated-content";
import { getListCompetencies } from "@/service/management-user/management-user";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
@ -130,12 +132,17 @@ export default function FormAskExpert() {
const [detail, setDetail] = useState<taskDetail>();
const [refresh] = useState(false);
const [listDest, setListDest] = useState([]);
const [checkedLevels, setCheckedLevels] = useState(new Set());
const [checkedLevels, setCheckedLevels] = useState<Set<number>>(new Set());
const [expandedPolda, setExpandedPolda] = useState([{}]);
const [isLoading, setIsLoading] = useState(false);
const [audioFile, setAudioFile] = useState<File | null>(null);
const [isRecording, setIsRecording] = useState(false);
const [timer, setTimer] = useState<number>(120);
const [userCompetencies, setUserCompetencies] = useState<any[]>([]);
const [selectedCompetencies, setSelectedCompetencies] = useState<Set<number>>(
new Set()
);
const [listExpert, setListExpert] = useState<any[]>([]);
const t = useTranslations("Form");
const [imageFiles, setImageFiles] = useState<FileWithPreview[]>([]);
@ -151,21 +158,6 @@ export default function FormAskExpert() {
from: new Date(2024, 0, 1),
});
const [platformTypeVisible, setPlatformTypeVisible] = useState(false);
const [unitSelection, setUnitSelection] = useState({
semua: false,
mabes: false,
polda: false,
polres: false,
satker: false,
});
const [taskOutput, setTaskOutput] = useState({
all: false,
video: false,
audio: false,
image: false,
text: false,
});
const [links, setLinks] = useState<string[]>([""]);
const {
@ -179,37 +171,68 @@ export default function FormAskExpert() {
mode: "all",
});
// const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const selectedValue = Number(event.target.value);
// setMainType(selectedValue);
useEffect(() => {
getDataAdditional();
}, []);
// setPlatformTypeVisible(selectedValue === 2);
async function getDataAdditional() {
const resCompetencies = await getListCompetencies();
console.log("competency", resCompetencies);
setUserCompetencies(resCompetencies?.data?.data);
}
useEffect(() => {
async function fetchPoldaPolres() {
async function fetchListExpert() {
setIsLoading(true);
try {
const response = await getUserLevelForAssignments();
setListDest(response?.data?.data.list);
console.log("polda", response?.data?.data?.list);
const initialExpandedState = response?.data?.data.list.reduce(
(acc: any, polda: any) => {
acc[polda.id] = false;
return acc;
},
{}
);
setExpandedPolda(initialExpandedState);
console.log("polres", initialExpandedState);
const response = await getUserLevelForExpert(id);
setListExpert(response?.data?.data);
console.log("tenaga ahli", response?.data?.data);
} catch (error) {
console.error("Error fetching Polda/Polres data:", error);
} finally {
setIsLoading(false);
}
}
fetchPoldaPolres();
fetchListExpert();
}, []);
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
setDetail(details);
}
}
initState();
}, [id, refresh]);
useEffect(() => {
const fetchExpertsForCompetencies = async () => {
const allExperts: any[] = [];
for (const compId of Array.from(selectedCompetencies)) {
const response = await getUserLevelForExpert(compId);
const experts = response?.data?.data || [];
allExperts.push(...experts);
}
const uniqueExperts = Array.from(
new Map(allExperts.map((e) => [e.id, e])).values()
);
setListExpert(uniqueExperts);
};
if (selectedCompetencies.size > 0) {
fetchExpertsForCompetencies();
} else {
setListExpert([]);
}
}, [selectedCompetencies]);
// };
const handleCheckboxChange = (levelId: number) => {
setCheckedLevels((prev) => {
@ -223,217 +246,39 @@ export default function FormAskExpert() {
});
};
const handlePoldaPolresChange = () => {
return Array.from(checkedLevels).join(","); // Mengonversi Set ke string
const handleExpertChange = () => {
return Array.from(checkedLevels).join(",");
};
const handleUnitChange = (
key: keyof typeof unitSelection,
value: boolean
) => {
if (key === "semua") {
const newState = {
semua: value,
mabes: value,
polda: value,
polres: value,
satker: value,
};
setUnitSelection(newState);
} else {
const updatedSelection = {
...unitSelection,
[key]: value,
};
const allChecked = ["mabes", "polda", "polres", "satker"].every(
(k) => updatedSelection[k as keyof typeof unitSelection]
);
updatedSelection.semua = allChecked;
setUnitSelection(updatedSelection);
}
};
const handleExpertiseOutputChange = (
key: keyof typeof expertise,
value: boolean
) => {
if (key === "semua") {
const newState = {
semua: value,
komunikasi: value,
hukum: value,
bahasa: value,
ekonomi: value,
politik: value,
sosiologi: value,
ilmuadministrasipemerintah: value,
ti: value,
};
setExpertiseOutput(newState);
} else {
const updated = {
...expertise,
[key]: value,
};
const allChecked = [
"komunikasi",
"hukum",
"bahasa",
"ekonomi",
"politik",
"sosiologi",
"ilmuadministrasipemerintah",
"ti",
].every((k) => updated[k as keyof typeof expertise]);
updated.semua = allChecked;
setExpertiseOutput(updated);
}
};
const handleExpertOutputChange = (
key: keyof typeof expert,
value: boolean
) => {
if (key === "semua") {
const newState = {
semua: value,
};
setExpertOutput(newState);
} else {
const updated = {
...expert,
[key]: value,
};
const allChecked = ["video", "audio", "image", "text"].every(
(k) => updated[k as keyof typeof expert]
);
updated.semua = allChecked;
setExpertOutput(updated);
}
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
setDetail(details);
const handleCompetencyChange = async (competencyId: number) => {
setSelectedCompetencies((prev) => {
const updated = new Set(prev);
if (updated.has(competencyId)) {
updated.delete(competencyId);
} else {
updated.add(competencyId);
}
}
initState();
}, [id, refresh]);
const handleTaskOutputChange = (
key: keyof typeof taskOutput,
value: boolean
) => {
if (key === "all") {
const newState = {
all: value,
video: value,
audio: value,
image: value,
text: value,
};
setTaskOutput(newState);
} else {
const updated = {
...taskOutput,
[key]: value,
};
const allChecked = ["video", "audio", "image", "text"].every(
(k) => updated[k as keyof typeof taskOutput]
);
updated.all = allChecked;
setTaskOutput(updated);
}
return updated;
});
};
const save = async (data: TaskSchema) => {
const fileTypeMapping = {
all: "1",
video: "2",
audio: "4",
image: "3",
text: "5",
};
const areasMapping = {
semua: "0",
komunikasi: "1",
hukum: "2",
bahasa: "3",
ekonomi: "4",
politik: "5",
sosiologi: "6",
ilmuadministrasipemerintah: "7",
ti: "8",
};
const unitMapping = {
allUnit: "0",
mabes: "1",
polda: "2",
polres: "3",
satker: "4",
};
const assignmentPurposeString = Object.keys(unitSelection)
.filter((key) => unitSelection[key as keyof typeof unitSelection])
.map((key) => unitMapping[key as keyof typeof unitMapping])
.join(",");
const selectedOutputs = Object.keys(taskOutput)
.filter((key) => taskOutput[key as keyof typeof taskOutput])
.map((key) => fileTypeMapping[key as keyof typeof fileTypeMapping])
.join(",");
const selectedAreaExpert = Object.keys(expertise)
.filter((key) => expertise[key as keyof typeof expertise])
.map((key) => areasMapping[key as keyof typeof areasMapping])
.join(",");
const requestData: {
id?: number;
title: string;
assignedToLevel: any;
assignedToUsers: any;
assignmentTypeId: string;
fileTypeOutput: string;
areasExpertise: string;
narration: string;
platformType: string | null;
assignmentMainTypeId: any;
assignmentType: string;
assignedToRole: string;
broadcastType: string;
expertCompetencies: string;
attachmentUrl: string[];
} = {
...data,
// assignmentType,
// assignmentCategory,
assignedToLevel: handlePoldaPolresChange(),
assignedToUsers: assignmentPurposeString,
assignedToRole: selectedTarget,
assignedToUsers: handleExpertChange(),
assignmentType: taskType,
broadcastType: broadcastType,
assignmentMainTypeId: mainType,
areasExpertise: selectedAreaExpert,
assignmentTypeId: type,
fileTypeOutput: selectedOutputs,
narration: data.naration,
platformType: "",
expertCompetencies: "1,2,3",
expertCompetencies: Array.from(selectedCompetencies).join(","),
title: data.title,
attachmentUrl: links,
};
@ -676,7 +521,6 @@ export default function FormAskExpert() {
{detail !== undefined ? (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2">
<Label>{t("title")}</Label>
<Controller
@ -696,53 +540,6 @@ export default function FormAskExpert() {
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
<div className="mt-5">
<Label>Ditujukan Kepada :</Label>
</div>
<div className=" space-y-2">
<Label>{t("areas-expertise")}</Label>
<div className="flex flex-wrap gap-4">
{Object.keys(expertise).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={expertise[key as keyof typeof expertise]}
onCheckedChange={(value) =>
handleExpertiseOutputChange(
key as keyof typeof expertise,
value as boolean
)
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
</div>
</div>
<div className="mt-5 space-y-2">
<Label>{t("choose-expert")}</Label>
<div className="flex flex-wrap gap-4">
{Object.keys(expert).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={expert[key as keyof typeof expert]}
onCheckedChange={(value) =>
handleExpertOutputChange(
key as keyof typeof expert,
value as boolean
)
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
</div>
</div>
<div className="flex flex-col space-y-2 mt-5">
<Label className="mr-3 mb-1">Tanggal</Label>
<Popover>
@ -784,27 +581,54 @@ export default function FormAskExpert() {
</Popover>
</div>
<div className="mt-5 space-y-2">
<Label>{t("output-task")}</Label>
<Label>{t("areas-expertise")}</Label>
<div className="flex flex-wrap gap-4">
{Object.keys(taskOutput).map((key) => (
<div className="flex items-center gap-2" key={key}>
{userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={item.id}>
<Checkbox
id={key}
checked={taskOutput[key as keyof typeof taskOutput]}
onCheckedChange={(value) =>
handleTaskOutputChange(
key as keyof typeof taskOutput,
value as boolean
)
}
id={`comp-${item.id}`}
checked={selectedCompetencies.has(item.id)}
onCheckedChange={() => handleCompetencyChange(item.id)}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Label htmlFor={`comp-${item.id}`}>{item.name}</Label>
</div>
))}
</div>
</div>
<div className="mt-5 space-y-2">
<Label>{t("choose-expert")}</Label>
<div className="flex flex-wrap gap-4">
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[{"Pilih Tenaga Ahli"}]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>Daftar Tenaga Ahli</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listExpert?.map((expert: any) => (
<div key={expert.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(expert.id)}
onCheckedChange={() =>
handleCheckboxChange(expert.id)
}
className="mr-3"
/>
{expert.fullname}
</Label>
</div>
))}
</div>
</DialogContent>
</Dialog>
</div>
</div>
<div className="mt-5 space-y-2">
<Label>{t("description")}</Label>
<Controller

View File

@ -383,32 +383,22 @@ export default function FormDoItYourself() {
const requestData: {
id?: number;
title: string;
assignedToLevel: any;
assignedToUsers: any;
assignmentTypeId: string;
fileTypeOutput: string;
narration: string;
platformType: string | null;
assignmentMainTypeId: any;
assignmentType: string;
assignedToRole: string;
broadcastType: string;
expertCompetencies: string;
attachmentUrl: string[];
} = {
...data,
// assignmentType,
// assignmentCategory,
assignedToLevel: handlePoldaPolresChange(),
assignedToUsers: assignmentPurposeString,
assignedToRole: selectedTarget,
assignmentType: taskType,
broadcastType: broadcastType,
assignmentMainTypeId: mainType,
assignmentTypeId: type,
fileTypeOutput: selectedOutputs,
narration: data.naration,
platformType: "",
expertCompetencies: "1,2,3",
title: data.title,
attachmentUrl: links,
@ -695,116 +685,7 @@ export default function FormDoItYourself() {
))}
</div>
</div>
<div className="mt-5 space-y-2">
<Label>{t("assignment-selection")}</Label>
<div className="flex flex-wrap gap-3 ">
{Object.keys(unitSelection).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={
unitSelection[key as keyof typeof unitSelection]
}
onCheckedChange={(value) =>
handleUnitChange(
key as keyof typeof unitSelection,
value as boolean
)
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
<div className=" lg:pl-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[{t("custom")}]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>
Daftar Wilayah Polda dan Polres
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listDest.map((polda: any) => (
<div key={polda.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(polda.id)}
onCheckedChange={() =>
handleCheckboxChange(polda.id)
}
className="mr-3"
/>
{polda.name}
<button
onClick={() => toggleExpand(polda.id)}
className="ml-2 focus:outline-none"
>
{expandedPolda[polda.id] ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</button>
</Label>
{expandedPolda[polda.id] && (
<div className="ml-6 mt-2">
<Label className="block">
<Checkbox
checked={polda?.subDestination?.every(
(polres: any) =>
checkedLevels.has(polres.id)
)}
onCheckedChange={(isChecked) => {
const updatedLevels = new Set(
checkedLevels
);
polda?.subDestination?.forEach(
(polres: any) => {
if (isChecked) {
updatedLevels.add(polres.id);
} else {
updatedLevels.delete(polres.id);
}
}
);
setCheckedLevels(updatedLevels);
}}
className="mr-2"
/>
Pilih Semua Polres
</Label>
{polda?.subDestination?.map((polres: any) => (
<Label
key={polres.id}
className="block mt-1"
>
<Checkbox
checked={checkedLevels.has(polres.id)}
onCheckedChange={() =>
handleCheckboxChange(polres.id)
}
className="mr-2"
/>
{polres.name}
</Label>
))}
</div>
)}
</div>
))}
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
<div className="mt-5 space-y-2">
<Label>{t("type-of-task")}</Label>
<RadioGroup

View File

@ -594,6 +594,7 @@ export default function FormTaskTaDetail() {
}
initState();
getDataAcceptance();
}, []);
const handleToggleInput = (): void => {
@ -678,10 +679,9 @@ export default function FormTaskTaDetail() {
}
const handleAcceptAcceptance = async () => {
const isAccept = true;
loading();
console.log("Id user :", userId);
const response = await acceptAssignment(id, !isAccept);
const response = await acceptAssignment(id);
if (response?.error) {
error(response?.message);

View File

@ -110,6 +110,7 @@ export type taskDetail = {
broadcastType: string;
narration: string;
is_active: string;
isAssignmentAccepted: boolean;
isDone: any;
};
@ -398,7 +399,7 @@ export default function FormTaskDetail() {
}
}
initState();
// fetchFilteredData();
getDataAcceptance();
}, [id, refresh]);
const handleUrlChange = (index: number, newUrl: string) => {
@ -580,16 +581,17 @@ export default function FormTaskDetail() {
};
async function getDataAcceptance() {
console.log("Get Acceptance Status >> ");
const response = await getAcceptanceAssignmentStatus(id);
setStatusAcceptance(response?.data?.data?.isAccept);
console.log("Status :", response?.data?.data?.isAccept);
console.log("Get Acceptance Status : ", response);
}
const handleAcceptAcceptance = async () => {
const isAccept = true;
loading();
console.log("Id user :", userId);
const response = await acceptAssignment(id, !isAccept);
const response = await acceptAssignment(id);
if (response?.error) {
error(response?.message);
@ -1413,7 +1415,8 @@ export default function FormTaskDetail() {
className="btn btn-primary lg:mx-3"
style={
statusAcceptance ||
detail?.createdBy?.id !== Number(userId)
detail?.isAssignmentAccepted == true ||
detail?.createdBy?.id == Number(userId)
? {
display: "none",
}
@ -1443,7 +1446,7 @@ export default function FormTaskDetail() {
if (!isTableResult) fetchAllData(); // Panggil API saat tombol diklik
}}
>
Hasil Upload {Number(userId)}
Hasil Upload
</Button>
</div>
</div>

View File

@ -0,0 +1,22 @@
export type DetailTicket = {
typeId: number;
feedId: any;
commentId: string;
priority: {
name: string;
};
status: {
name: string;
};
emergencyIssue: {
date: string;
location: string;
title: string;
urgencyName: string;
feedUrl: string;
recommendationName: string;
link: string;
uploadFiles?: string;
description: string;
};
};

View File

@ -0,0 +1,72 @@
// InfoLainnyaModal.tsx
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { DetailTicket } from "./info-lainnya-types";
interface InfoLainnyaModalProps {
open: boolean;
onClose: () => void;
data: DetailTicket["emergencyIssue"];
}
export default function InfoLainnyaModal({
open,
onClose,
data,
}: InfoLainnyaModalProps) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>Info Lainnya</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="font-medium">Tanggal</div>
<div>:{data?.date}</div>
<div className="font-medium">Lokasi Kejadian</div>
<div>:{data?.location}</div>
<div className="font-medium">Isu Menonjol</div>
<div>:{data?.title}</div>
<div className="font-medium">Urgensi</div>
<div>:{data?.urgencyName}</div>
<div className="font-medium">Rekomendasi Tindak Lanjut</div>
<div>:{data?.recommendationName}</div>
<div className="font-medium">Link Pendukung</div>
<div>
:
<a
href={data?.link}
className="text-blue-600"
target="_blank"
rel="noopener noreferrer"
>
{data?.link}
</a>
</div>
{data?.uploadFiles && (
<>
<div className="font-medium">Lampiran</div>
<div>
<img
src={data?.uploadFiles}
alt="Lampiran"
className="max-w-xs"
/>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,532 @@
"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 } from "lucide-react";
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); // beri tipe number
const [replyText, setReplyText] = useState<string>(""); // untuk isi balasan
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]);
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); // tutup form setelah kirim
getTicketReply(); // refresh balasan
} 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);
// Tambahkan balasan baru ke daftar balasan
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",
});
// Reset input dan sembunyikan form balasan
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="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>
);
}

View File

@ -0,0 +1,416 @@
"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 } from "postcss";
import { htmlToString } from "@/utils/globals";
import { Textarea } from "@/components/ui/textarea";
import { error } from "@/lib/swal";
import { useRouter } from "next/navigation";
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;
narration: 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;
createdBy: {
id: number;
fullname: string;
};
sendTo: {
id: number;
fullname: string;
};
};
export default function FormUpdateTicketing() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const router = useRouter();
const [detail, setDetail] = useState<taskDetail>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [ticketInternal, setTicketInternal] = useState<internalDetail | null>(
null
);
const [replyVisible, setReplyVisible] = useState(false);
const [replyMessage, setReplyMessage] = useState("");
const [selectedPriority, setSelectedPriority] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
async function initState() {
if (id) {
const response = await getTicketingDetail(id);
setTicketInternal(response?.data?.data || null);
setDetail(response?.data?.data);
}
}
initState();
getTicketReply();
}, [id]);
async function getTicketReply() {
const res = await getTicketingReply(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
const handleReply = () => {
setReplyVisible((prev) => !prev); // Toggle visibility
};
const handleSendReply = async () => {
if (replyMessage.trim() === "") {
MySwal.fire({
title: "Error",
text: "Pesan tidak boleh kosong!",
icon: "error",
});
return;
}
const data = {
ticketId: id,
message: replyMessage,
};
try {
const response = await saveTicketReply(data);
// Tambahkan balasan baru ke daftar balasan
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",
});
// Reset input dan sembunyikan form balasan
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);
}
});
};
return (
<div className="py-5">
<div className="mt-4 flex flex-row items-center gap-3">
<Button onClick={handleReply} color="default" variant={"outline"}>
Balas
</Button>
{/* <Button onClick={handleDelete} color="default" variant={"outline"}>
Hapus
</Button> */}
</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">
<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">
{list?.user?.fullname}
</span>{" "}
mengirimkan pesan untuk{" "}
<span className="font-bold text-sm">
{list?.messageTo?.fullname}
</span>
</p>
<p className="text-xs">
{`${new Date(list?.createdAt).getDate()}-${
new Date(list?.createdAt).getMonth() + 1
}-${new Date(list?.createdAt).getFullYear()} ${new Date(
list?.createdAt
).getHours()}:${new Date(list?.createdAt).getMinutes()}`}
</p>
</div>
</div>
<p className="p-4 bg-white text-sm">{list.comments}</p>
</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?.createdBy?.fullname}
</span>{" "}
mengirimkan pesan untuk{" "}
<span className="font-bold text-sm">
{ticketInternal?.sendTo?.fullname}
</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>
<p className="p-4 bg-white text-sm">
{htmlToString(ticketInternal.message)}
</p>
</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"
defaultValue={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="space-y-2 px-3 py-3">
<Label>Description</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Textarea
value={detail?.narration}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="flex justify-end mt-3 mr-3 py-3">
<Button type="submit" color="primary">
Update
</Button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -20,13 +20,13 @@ const AreaCoverageWorkUnits = () => {
useEffect(() => {
if (openPolda || openSatker) {
document.body.style.overflow = "hidden";
document.body.classList.add("overflow-hidden");
} else {
document.body.style.overflow = "";
document.body.classList.remove("overflow-hidden");
}
return () => {
document.body.style.overflow = "";
document.body.classList.remove("overflow-hidden");
};
}, [openPolda, openSatker]);
return (
@ -39,17 +39,25 @@ const AreaCoverageWorkUnits = () => {
<div className="flex flex-col justify-center lg:flex-row gap-8 ">
{/* POLDA */}
<Dialog open={openPolda} onOpenChange={setOpenPolda}>
<DialogTrigger className="flex flex-col gap-2 justify-center items-center shadow-lg group rounded-xl py-5 px-24 border-2 border-transparent hover:border-[#bb3523] transition-all duration-300">
<Image
width={1920}
height={1080}
alt="indo"
src="/assets/indo.png"
className="h-32 w-32 group-hover:scale-110 group-hover:border-[#bb3523] "
/>
<p className="text-base font-bold">Polda Jajaran</p>
<DialogTrigger asChild>
<button
onClick={() => setOpenPolda(true)}
className="flex flex-col gap-2 justify-center items-center shadow-lg group rounded-xl py-5 px-24 border-2 border-transparent hover:border-[#bb3523] transition-all duration-300"
>
<Image
width={1920}
height={1080}
alt="indo"
src="/assets/indo.png"
className="h-32 w-32 group-hover:scale-110 group-hover:border-[#bb3523] "
/>
<p className="text-base font-bold">Polda Jajaran</p>
</button>
</DialogTrigger>
<DialogContent size="md" className="h-[90vh] overflow-y-auto ">
<DialogContent
size="md"
className="max-h-[90vh] overflow-hidden flex flex-col "
>
<DialogHeader className="flex flex-col justify-center">
<DialogTitle>
<p className="text-center">Polda Jajaran</p>
@ -58,11 +66,9 @@ const AreaCoverageWorkUnits = () => {
<div className="h-1 w-[150px] bg-[#bb3523] mx-auto mb-6 rounded"></div>
</DialogTitle>
</DialogHeader>
<div className="max-h-[70vh] overflow-y-auto">
<div className="overflow-y-auto px-1 flex-1">
<Coverage />
</div>
<div className="text-right mt-4">
<DialogClose asChild>
<button className="text-[#bb3523] font-bold">Tutup</button>

View File

@ -1,11 +1,20 @@
import { getCategoryData, getPublicCategoryData } from "@/service/landing/landing";
import {
getCategoryData,
getPublicCategoryData,
} from "@/service/landing/landing";
import React, { useEffect, useState } from "react";
import { Reveal } from "./Reveal";
import { useTranslations } from "next-intl";
import { usePathname } from "next/navigation";
import { useParams } from "next/navigation";
import Image from "next/image";
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "../ui/carousel";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "../ui/carousel";
import { useRouter } from "@/i18n/routing";
import { Button } from "../ui/button";
@ -19,14 +28,26 @@ const ContentCategory = (props: { group?: string; type: string }) => {
const satkerName = params?.satker_name;
const router = useRouter();
let prefixPath = poldaName ? `/polda/${poldaName}` : satkerName ? `/satker/${satkerName}` : "/";
let prefixPath = poldaName
? `/polda/${poldaName}`
: satkerName
? `/satker/${satkerName}`
: "/";
useEffect(() => {
initFetch();
}, []);
const initFetch = async () => {
const response = await getPublicCategoryData(
props.group == "mabes" ? "" : props.group == "polda" && poldaName && String(poldaName)?.length > 1 ? poldaName : props.group == "satker" && satkerName && String(satkerName)?.length > 1 ? "satker-" + satkerName : "",
props.group == "mabes"
? ""
: props.group == "polda" && poldaName && String(poldaName)?.length > 1
? poldaName
: props.group == "satker" &&
satkerName &&
String(satkerName)?.length > 1
? "satker-" + satkerName
: "",
"",
locale == "en" ? true : false
);
@ -52,7 +73,10 @@ const ContentCategory = (props: { group?: string; type: string }) => {
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
</svg>`;
const toBase64 = (str: string) => (typeof window === "undefined" ? Buffer.from(str).toString("base64") : window.btoa(str));
const toBase64 = (str: string) =>
typeof window === "undefined"
? Buffer.from(str).toString("base64")
: window.btoa(str);
return (
<div className="px-4 lg:px-24 py-10">
@ -60,12 +84,16 @@ const ContentCategory = (props: { group?: string; type: string }) => {
<h2 className="text-center text-xl lg:text-3xl font-bold text-[#bb3523] mb-4">
{pathname?.split("/")[1] == "in" ? (
<>
<span className="text-black dark:text-white">{t("category")}&nbsp;</span>
<span className="text-black dark:text-white">
{t("category")}&nbsp;
</span>
{t("content")}
</>
) : (
<>
<span className="text-black dark:text-white">{t("content")}&nbsp;</span>
<span className="text-black dark:text-white">
{t("content")}&nbsp;
</span>
{t("category")}
</>
)}
@ -73,35 +101,51 @@ const ContentCategory = (props: { group?: string; type: string }) => {
<div className="h-1 w-52 bg-[#bb3523] mx-auto mb-6 rounded"></div>
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
{(seeAllValue ? categories : categories?.slice(0, 4 ))?.map((category: any) => (
<div key={category?.id}>
<div onClick={() => router.push(`${prefixPath}all/filter?category=${category?.id}`)} className="cursor-pointer relative group rounded-md overflow-hidden shadow-md hover:shadow-lg block">
{/* Gambar */}
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(shimmer(700, 475))}`}
alt="category"
width={2560}
height={1440}
src={category?.thumbnailLink}
className="w-full lg:h-[250px] h-40 object-cover group-hover:scale-110 transition-transform duration-300"
/>
{(seeAllValue ? categories : categories?.slice(0, 4))?.map(
(category: any) => (
<div key={category?.id}>
<div
onClick={() =>
router.push(
`${prefixPath}all/filter?category=${category?.id}`
)
}
className="cursor-pointer relative group rounded-md overflow-hidden shadow-md hover:shadow-lg block"
>
{/* Gambar */}
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
alt="category"
width={2560}
height={1440}
src={category?.thumbnailLink}
className="w-full lg:h-[250px] h-40 object-cover group-hover:scale-110 transition-transform duration-300"
/>
{/* Overlay gelap */}
<div className="absolute inset-0 bg-black bg-opacity-25 group-hover:bg-opacity-35 transition-all duration-300 rounded-md"></div>
{/* Overlay gelap */}
<div className="absolute inset-0 bg-black bg-opacity-25 group-hover:bg-opacity-35 transition-all duration-300 rounded-md"></div>
{/* Judul */}
<div className="absolute bottom-5 left-0 right-16 bg-transparent backdrop-blur-md text-white p-4 border-l-2 border-[#bb3523] z-10 group-hover:scale-x-150 origin-left">
<h3 className="text-sm font-semibold truncate">{category?.name}</h3>
{/* Judul */}
<div className="absolute bottom-5 left-0 right-16 bg-transparent backdrop-blur-md text-white p-4 border-l-2 border-[#bb3523] z-10 group-hover:scale-x-150 origin-left">
<h3 className="text-sm font-semibold truncate">
{category?.name}
</h3>
</div>
</div>
</div>
</div>
))}
)
)}
</div>
{/* Tombol See More / See Less */}
{categories?.length > 8 && (
{categories?.length > 10 && (
<div className="flex items-center flex-row justify-center mt-6">
<Button onClick={() => setSeeAllValue(!seeAllValue)} className="bg-white hover:bg-[#bb3523] text-[#bb3523] hover:text-white border-2 border-[#bb3523]">
<Button
onClick={() => setSeeAllValue(!seeAllValue)}
className="bg-white hover:bg-[#bb3523] text-[#bb3523] hover:text-white border-2 border-[#bb3523]"
>
{seeAllValue ? t("seeLess") : t("seeMore")}
</Button>
</div>

View File

@ -8,55 +8,183 @@ import { usePathname } from "next/navigation";
import Image from "next/image";
const regions = [
{ name: "Polda Metro Jaya", slug: "metro-jaya", logo: "/logo/polda/polda-metro.png" },
{ name: "Polda Jawa Barat", slug: "jawa-barat", logo: "/logo/polda/polda-jawabarat.png" },
{ name: "Polda Banten", slug: "banten", logo: "/logo/polda/polda-banten.png" },
{ name: "Polda Jawa Tengah", slug: "jawa-tengah", logo: "/logo/polda/polda-jawatengah.png" },
{ name: "Polda D.I Yogyakarta", slug: "di-yogyakarta", logo: "/logo/polda/polda-jogja.png" },
{ name: "Polda Jawa Timur", slug: "jawa-timur", logo: "/logo/polda/polda-jawatimur.png" },
{
name: "Polda Metro Jaya",
slug: "metro-jaya",
logo: "/logo/polda/polda-metro.png",
},
{
name: "Polda Jawa Barat",
slug: "jawa-barat",
logo: "/logo/polda/polda-jawabarat.png",
},
{
name: "Polda Banten",
slug: "banten",
logo: "/logo/polda/polda-banten.png",
},
{
name: "Polda Jawa Tengah",
slug: "jawa-tengah",
logo: "/logo/polda/polda-jawatengah.png",
},
{
name: "Polda D.I Yogyakarta",
slug: "di-yogyakarta",
logo: "/logo/polda/polda-jogja.png",
},
{
name: "Polda Jawa Timur",
slug: "jawa-timur",
logo: "/logo/polda/polda-jawatimur.png",
},
{ name: "Polda Aceh", slug: "aceh", logo: "/logo/polda/polda-aceh.png" },
{ name: "Polda Sumatera Utara", slug: "sumatera-utara", logo: "/logo/polda/polda-sumut.png" },
{ name: "Polda Sumatera Barat", slug: "sumatera-barat", logo: "/logo/polda/polda-sumatera-barat.png" },
{
name: "Polda Sumatera Utara",
slug: "sumatera-utara",
logo: "/logo/polda/polda-sumut.png",
},
{
name: "Polda Sumatera Barat",
slug: "sumatera-barat",
logo: "/logo/polda/polda-sumatera-barat.png",
},
{ name: "Polda Riau", slug: "riau", logo: "/logo/polda/polda-riau.png" },
{ name: "Polda Kep. Riau", slug: "kepulauan-riau", logo: "/logo/polda/polda-kepri.png" },
{
name: "Polda Kep. Riau",
slug: "kepulauan-riau",
logo: "/logo/polda/polda-kepri.png",
},
{ name: "Polda Jambi", slug: "jambi", logo: "/logo/polda/polda-jambi.png" },
{ name: "Polda Sumatera Selatan", slug: "sumatera-selatan", logo: "/logo/polda/polda-sumsel.png" },
{ name: "Polda Kep. Bangka Belitung", slug: "bangka-belitung", logo: "/logo/polda/polda-bangkabelitung.png" },
{ name: "Polda Bengkulu", slug: "bengkulu", logo: "/logo/polda/polda-bengkulu.png" },
{ name: "Polda Lampung", slug: "lampung", logo: "/logo/polda/polda-lampung.png" },
{ name: "Polda Nusa Tenggara Barat", slug: "ntb", logo: "/logo/polda/polda-ntb.png" },
{ name: "Polda Nusa Tenggara Timur", slug: "ntt", logo: "/logo/polda/polda-ntt.png" },
{
name: "Polda Sumatera Selatan",
slug: "sumatera-selatan",
logo: "/logo/polda/polda-sumsel.png",
},
{
name: "Polda Kep. Bangka Belitung",
slug: "bangka-belitung",
logo: "/logo/polda/polda-bangkabelitung.png",
},
{
name: "Polda Bengkulu",
slug: "bengkulu",
logo: "/logo/polda/polda-bengkulu.png",
},
{
name: "Polda Lampung",
slug: "lampung",
logo: "/logo/polda/polda-lampung.png",
},
{
name: "Polda Nusa Tenggara Barat",
slug: "ntb",
logo: "/logo/polda/polda-ntb.png",
},
{
name: "Polda Nusa Tenggara Timur",
slug: "ntt",
logo: "/logo/polda/polda-ntt.png",
},
{ name: "Polda Bali", slug: "bali", logo: "/logo/polda/polda-bali.png" },
{ name: "Polda Kalimantan Barat", slug: "kalimantan-barat", logo: "/logo/polda/polda-kalbar.png" },
{ name: "Polda Kalimantan Tengah", slug: "kalimantan-tengah", logo: "/logo/polda/polda-kalteng.png" },
{ name: "Polda Kalimantan Selatan", slug: "kalimantan-selatan", logo: "/logo/polda/polda-kalsel.png" },
{ name: "Polda Kalimantan Timur", slug: "kalimantan-timur", logo: "/logo/polda/polda-kaltim.png" },
{ name: "Polda Kalimantan Utara", slug: "kaltara", logo: "/logo/polda/polda-kaltara.png" },
{ name: "Polda Sulawesi Tengah", slug: "sulawesi-tengah", logo: "/logo/polda/polda-sulawesi-tengah.png" },
{ name: "Polda Sulawesi Utara", slug: "sulawesi-utara", logo: "/logo/polda/polda-sulawesi-utara.png" },
{ name: "Polda Gorontalo", slug: "gorontalo", logo: "/logo/polda/polda-gorontalo.png" },
{ name: "Polda Sulawesi Barat", slug: "sulawesi-barat", logo: "/logo/polda/polda-sulbar.png" },
{ name: "Polda Sulawesi Selatan", slug: "sulawesi-selatan", logo: "/logo/polda/polda-sulsel.png" },
{ name: "Polda Sulawesi Tenggara", slug: "sulawesi-tenggara", logo: "/logo/polda/polda-sulawesi-tenggara.png" },
{ name: "Polda Maluku Utara", slug: "maluku-utara", logo: "/logo/polda/polda-maluku-utara.png" },
{ name: "Polda Maluku", slug: "maluku", logo: "/logo/polda/polda-maluku.png" },
{ name: "Polda Papua Barat", slug: "papua-barat", logo: "/logo/polda/polda-papua-barat.png" },
{
name: "Polda Kalimantan Barat",
slug: "kalimantan-barat",
logo: "/logo/polda/polda-kalbar.png",
},
{
name: "Polda Kalimantan Tengah",
slug: "kalimantan-tengah",
logo: "/logo/polda/polda-kalteng.png",
},
{
name: "Polda Kalimantan Selatan",
slug: "kalimantan-selatan",
logo: "/logo/polda/polda-kalsel.png",
},
{
name: "Polda Kalimantan Timur",
slug: "kalimantan-timur",
logo: "/logo/polda/polda-kaltim.png",
},
{
name: "Polda Kalimantan Utara",
slug: "kaltara",
logo: "/logo/polda/polda-kaltara.png",
},
{
name: "Polda Sulawesi Tengah",
slug: "sulawesi-tengah",
logo: "/logo/polda/polda-sulawesi-tengah.png",
},
{
name: "Polda Sulawesi Utara",
slug: "sulawesi-utara",
logo: "/logo/polda/polda-sulawesi-utara.png",
},
{
name: "Polda Gorontalo",
slug: "gorontalo",
logo: "/logo/polda/polda-gorontalo.png",
},
{
name: "Polda Sulawesi Barat",
slug: "sulawesi-barat",
logo: "/logo/polda/polda-sulbar.png",
},
{
name: "Polda Sulawesi Selatan",
slug: "sulawesi-selatan",
logo: "/logo/polda/polda-sulsel.png",
},
{
name: "Polda Sulawesi Tenggara",
slug: "sulawesi-tenggara",
logo: "/logo/polda/polda-sulawesi-tenggara.png",
},
{
name: "Polda Maluku Utara",
slug: "maluku-utara",
logo: "/logo/polda/polda-maluku-utara.png",
},
{
name: "Polda Maluku",
slug: "maluku",
logo: "/logo/polda/polda-maluku.png",
},
{
name: "Polda Papua Barat",
slug: "papua-barat",
logo: "/logo/polda/polda-papua-barat.png",
},
{ name: "Polda Papua", slug: "papua", logo: "/logo/polda/polda-papua.png" },
{ name: "Satuan Kerja POLRI", slug: "satker-polri", logo: "/logo/satker/SATUAN-KERJA-POLRI.png" },
{ name: "Internasional", slug: "internasional", logo: "/assets/polda/internasional.png" },
{
name: "Satuan Kerja POLRI",
slug: "satker-polri",
logo: "/logo/satker/SATUAN-KERJA-POLRI.png",
},
{
name: "Internasional",
slug: "internasional",
logo: "/assets/polda/internasional.png",
},
];
const Coverage: React.FC = () => {
const [seeAllValue, setSeeAllValue] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [filteredList, setFilteredList] = useState<typeof regions | undefined>(regions);
const [filteredList, setFilteredList] = useState<typeof regions | undefined>(
regions
);
const pathname = usePathname();
const t = useTranslations("LandingPage");
const handleSearch = () => {
const value = searchTerm.toLowerCase();
const filtered = regions.filter((polda) => polda.name.toLowerCase().includes(value));
const filtered = regions.filter((polda) =>
polda.name.toLowerCase().includes(value)
);
setFilteredList(filtered);
};
@ -74,58 +202,38 @@ const Coverage: React.FC = () => {
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
</svg>`;
const toBase64 = (str: string) => (typeof window === "undefined" ? Buffer.from(str).toString("base64") : window.btoa(str));
const toBase64 = (str: string) =>
typeof window === "undefined"
? Buffer.from(str).toString("base64")
: window.btoa(str);
return (
<div className="w-full">
{/* Header */}
{/* <h2 className="text-center text-2xl font-bold text-gray-800 dark:text-white mb-4">
{pathname?.split("/")[1] == "in" ? (
<>
{t("coverageOnly")}&nbsp;<span className="text-[#bb3523]">{t("area")}</span>{" "}
</>
) : (
<>
{t("area")}&nbsp;
<span className="text-[#bb3523]">{t("coverageOnly")}</span>
</>
)}
</h2> */}
{/* <h2 className="text-center text-2xl font-bold text-gray-800 dark:text-white mb-4">
Liputan <span className="text-[#bb3523]">Wilayah</span>
</h2> */}
{/* <div className="h-1 w-48 bg-[#bb3523] mx-auto mb-6 rounded"></div> */}
{/* Pencarian */}
{/* <div className="flex items-center justify-center gap-4 mb-6">
<input onChange={(e) => setSearchTerm(e.target.value)} type="text" placeholder={t("search")} className="w-4/5 px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-[#bb3523] focus:outline-none" />
<button onClick={handleSearch} className="px-2 w-1/5 lg:px-4 py-2 bg-[#bb3523] text-xs lg:text-base text-white flex justify-center items-center gap-2 rounded-md hover:bg-red-700">
{t("searchRegional")}
<Icon icon="ri:arrow-right-s-line" fontSize={20} />
</button>
</div> */}
{/* Grid Wilayah */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-6 overflow-y-auto px-4 max-h-[60vh]">
{filteredList?.map((region: any) => (
<Link key={region.slug} href={`/polda/${region.slug}`} className="flex flex-col items-center text-center p-3 border rounded-lg shadow-md hover:shadow-lg transition-all">
<div className="mb-1 flex items-center justify-center">
<Image placeholder={`data:image/svg+xml;base64,${toBase64(shimmer(700, 475))}`} width={1920} height={1080} src={region.logo} alt={region.name} className="w-14 h-14 object-contain" />
</div>
<p className="text-xs font-semibold">{region.name}</p>
</Link>
))}
</div>
{/* {filteredList && filteredList.length > 9 && (
<div className="flex justify-center py-5">
<Button onClick={() => setSeeAllValue(!seeAllValue)} className="bg-white hover:bg-[#bb3523] text-[#bb3523] hover:text-white border-2 border-[#bb3523]">
{seeAllValue ? t("seeLess") : t("seeMore")}
</Button>
<div className="max-h-[60vh] overflow-y-auto px-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-6 px-4 max-h-[60vh]">
{filteredList?.map((region: any) => (
<Link
key={region.slug}
href={`/polda/${region.slug}`}
className="flex flex-col items-center text-center p-3 border rounded-lg shadow-md hover:shadow-lg transition-all"
>
<div className="mb-1 flex items-center justify-center">
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
width={1920}
height={1080}
src={region.logo}
alt={region.name}
className="w-14 h-14 object-contain"
/>
</div>
<p className="text-xs font-semibold">{region.name}</p>
</Link>
))}
</div>
)} */}
</div>
</div>
);
};

View File

@ -1,5 +1,5 @@
"use client";
import { listData } from "@/service/landing/landing";
import { listData, listStaticBanner } from "@/service/landing/landing";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
@ -28,8 +28,17 @@ const HeaderBannerSatker = () => {
async function fetchData() {
const res = await listData("1", "", "", 5, 0, "createdAt", "", "", "satker-" + satkerName);
let data = res?.data?.data?.content;
setContent(data);
var data = res?.data?.data?.content;
if (data) {
const resStatic = await listStaticBanner(satkerName, false);
for (let i = 0; i < resStatic?.data?.data?.length; i++) {
const media = resStatic?.data.data[i]?.mediaUpload;
media.fileTypeId = media.fileType?.id;
data = data.filter((item: any) => item.id != media.id);
data.splice(0, 0, media);
}
setContent(data);
}
setCenterPadding(`${Math.trunc(Number(window.innerWidth) / 10 + 40)}px`);
setIsBannerLoading(false);
console.log("Done");

View File

@ -1,5 +1,5 @@
"use client";
import { listData } from "@/service/landing/landing";
import { listData, listStaticBanner } from "@/service/landing/landing";
import { useParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
@ -29,8 +29,17 @@ const HeaderBanner = () => {
async function fetchData() {
const res = await listData("1", "", "", 5, 0, "createdAt", "", "", poldaName);
let data = res?.data?.data?.content;
setContent(data);
var data = res?.data?.data?.content;
if (data) {
const resStatic = await listStaticBanner(poldaName, false);
for (let i = 0; i < resStatic?.data?.data?.length; i++) {
const media = resStatic?.data.data[i]?.mediaUpload;
media.fileTypeId = media.fileType?.id;
data = data.filter((item: any) => item.id != media.id);
data.splice(0, 0, media);
}
setContent(data);
}
setCenterPadding(`${Math.trunc(Number(window.innerWidth) / 10 + 40)}px`);
setIsBannerLoading(false);
console.log("Done");

View File

@ -2,7 +2,7 @@ import { formatDateToIndonesian, shimmer, toBase64 } from "@/utils/globals";
import React, { useEffect, useState } from "react";
import "swiper/css/bundle";
import "swiper/css/navigation";
import { getHeroData } from "@/service/landing/landing";
import { getHeroData, listStaticBanner } from "@/service/landing/landing";
import Link from "next/link";
import { useParams, usePathname, useRouter } from "next/navigation";
import {
@ -81,8 +81,20 @@ const HeroModal = ({ onClose }: { onClose: () => void }) => {
const initFetch = async () => {
const response = await getHeroData();
console.log(response);
setHeroData(response?.data?.data?.content);
let data = response?.data?.data?.content;
if (data) {
const resStatic = await listStaticBanner();
for (let i = 0; i < resStatic?.data?.data?.length; i++) {
const media = resStatic?.data.data[i]?.mediaUpload;
media.fileTypeId = media.fileType?.id;
data = data.filter((item: any) => item.id != media.id);
data.splice(0, 0, media);
}
setHeroData(data);
}
};
return (
<div className="fixed inset-0 flex items-center justify-center backdrop-brightness-50 z-50 ">
<div className="relative dark:bg-gray-900 rounded-lg w-[90%] md:w-[600px] p-4 shadow-none">
@ -190,16 +202,19 @@ const SurveyIntroModal = ({ onNext }: { onNext: () => void }) => {
const ONE_MONTH = 30 * 24 * 60 * 60 * 1000;
const Hero: React.FC = () => {
const Hero = (props: { group?: string }) => {
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const locale = params?.locale;
const [isLoading, setIsLoading] = useState<any>(true);
const [heroData, setHeroData] = useState<any>();
const [content, setContent] = useState<any>();
const [showModal, setShowModal] = useState(false);
const [showSurveyModal, setShowSurveyModal] = useState(false);
const [showFormModal, setShowFormModal] = useState(false);
const poldaName = params?.polda_name;
const satkerName = params?.satker_name;
useEffect(() => {
const timer = setTimeout(() => {
@ -274,7 +289,28 @@ const Hero: React.FC = () => {
const initFetch = async () => {
const response = await getHeroData();
console.log(response);
let data = response?.data?.data?.content;
setHeroData(response?.data?.data?.content);
if (data) {
const resStatic = await listStaticBanner(
props.group == "mabes"
? ""
: props.group == "polda" && poldaName && String(poldaName)?.length > 1
? poldaName
: props.group == "satker" &&
satkerName &&
String(satkerName)?.length > 1
? "satker-" + satkerName
: ""
);
for (let i = 0; i < resStatic?.data?.data?.length; i++) {
const media = resStatic?.data.data[i]?.mediaUpload;
media.fileTypeId = media.fileType?.id;
data = data.filter((item: any) => item.id != media.id);
data.splice(0, 0, media);
}
setContent(data);
}
};
const shimmer = (w: number, h: number) => `
@ -322,7 +358,7 @@ const Hero: React.FC = () => {
) : (
<Carousel className="lg:w-2/3 lg:h-full ">
<CarouselContent>
{heroData?.map((list: any) => (
{content?.map((list: any) => (
<CarouselItem key={list?.id}>
<div className="relative h-[310px] lg:h-[460px] mt-1">
<Image
@ -438,7 +474,7 @@ const Hero: React.FC = () => {
className="w-full h-[73px] object-cover rounded-lg"
/>
</div>
<div className="w-[280px] lg:w-auto">
<div className="w-[280px] lg:w-[200px]">
<span className="text-white bg-[#bb3523] px-4 py-1 rounded-lg flex text-[8px] font-bold uppercase w-fit">
{item?.categoryName}
</span>
@ -487,7 +523,7 @@ const Hero: React.FC = () => {
className="w-full h-[73px] object-cover rounded-lg"
/>
</div>
<div className="w-[280px] lg:w-auto">
<div className="w-[280px] lg:w-[200px]">
<span className="text-white bg-[#bb3523] px-4 py-1 rounded-lg flex text-[8px] font-bold uppercase w-fit">
{item?.categoryName}
</span>
@ -534,7 +570,7 @@ const Hero: React.FC = () => {
className="w-full h-[73px] object-cover rounded-lg"
/>
</div>
<div className="w-[280px] lg:w-auto">
<div className="w-[280px] lg:w-[200px]">
<span className="text-white bg-[#bb3523] px-4 py-1 rounded-lg flex text-[8px] font-bold uppercase w-fit">
{item?.categoryName}
</span>

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,10 @@ import { Button } from "./ui/button";
import { Label } from "./ui/label";
import { Input } from "./ui/input";
import pdfGenerator from "@/utils/pdf-generator";
import { tableauSignin, tableauViewImage } from "@/service/tableau/tableau-service";
import {
tableauSignin,
tableauViewImage,
} from "@/service/tableau/tableau-service";
const PageTitle = ({
title,
@ -24,7 +27,6 @@ const PageTitle = ({
title?: string;
className?: string;
}) => {
const [reportDate, setReportDate] = useState<any>();
const pathname = usePathname();
const name = pathname?.split("/").slice(1).join(" ");
@ -113,19 +115,45 @@ const PageTitle = ({
"516c8790-ccd5-44dc-bb66-b0e146d7168b",
reportDate
);
const blobFrontCover = new Blob([resFrontCover.data], { type: "image/png" });
const blobAssignmentDetail = new Blob([resAssignmentDetail.data], { type: "image/png" });
const blobFrontCover = new Blob([resFrontCover.data], {
type: "image/png",
});
const blobAssignmentDetail = new Blob([resAssignmentDetail.data], {
type: "image/png",
});
// const blobMonitoringContent = new Blob([resMonitoringContent.data], { type: "image/png" });
// const blobMonitoringArticle = new Blob([resMonitoringArticle.data], { type: "image/png" });
const blobInteractionContent = new Blob([resInteractionContent.data], { type: "image/png" });
const blobTotalInteractionContent = new Blob([resTotalInteractionContent.data], { type: "image/png" });
const blobInteractionContentCategory1 = new Blob([resInteractionContentCategory1.data], { type: "image/png" });
const blobInteractionContentCategory2 = new Blob([resInteractionContentCategory2.data], { type: "image/png" });
const blobInteractionContentCategory3 = new Blob([resInteractionContentCategory3.data], { type: "image/png" });
const blobInteractionDistribution = new Blob([resInteractionDistribution.data], { type: "image/png" });
const blobAssignments = new Blob([resAssignments.data], { type: "image/png" });
const blobInteractionContent = new Blob([resInteractionContent.data], {
type: "image/png",
});
const blobTotalInteractionContent = new Blob(
[resTotalInteractionContent.data],
{ type: "image/png" }
);
const blobInteractionContentCategory1 = new Blob(
[resInteractionContentCategory1.data],
{ type: "image/png" }
);
const blobInteractionContentCategory2 = new Blob(
[resInteractionContentCategory2.data],
{ type: "image/png" }
);
const blobInteractionContentCategory3 = new Blob(
[resInteractionContentCategory3.data],
{ type: "image/png" }
);
const blobInteractionDistribution = new Blob(
[resInteractionDistribution.data],
{ type: "image/png" }
);
const blobAssignments = new Blob([resAssignments.data], {
type: "image/png",
});
const blobUserCount = new Blob([resUserCount.data], { type: "image/png" });
const blobUserCountDistribution = new Blob([resUserCountDistribution.data], { type: "image/png" });
const blobUserCountDistribution = new Blob(
[resUserCountDistribution.data],
{ type: "image/png" }
);
const blobBackCover = new Blob([resBackCover.data], { type: "image/png" });
console.log(blobFrontCover);
@ -144,14 +172,14 @@ const PageTitle = ({
blobAssignments,
blobUserCount,
blobUserCountDistribution,
blobBackCover
blobBackCover,
]);
};
async function mergeImagesToCanvas(blobs: Blob[]) {
try {
const images = await Promise.all(
blobs.map(blob => {
blobs.map((blob) => {
return new Promise<HTMLImageElement>((resolve) => {
const url = URL.createObjectURL(blob);
const img = new Image();
@ -163,38 +191,37 @@ const PageTitle = ({
});
})
);
// Hitung total tinggi dari semua gambar (stacked vertically)
const width = Math.max(...images.map(img => img.width));
const width = Math.max(...images.map((img) => img.width));
const height = images.reduce((sum, img) => sum + img.height, 0);
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;
const canvas = document.getElementById("pdf-canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
canvas.width = width;
canvas.height = height;
let y = 0;
for (const img of images) {
ctx.drawImage(img, 0, y, img.width, img.height);
y += img.height;
}
// Simpan hasil sebagai gambar (PNG) atau bisa print
const dataURL = canvas.toDataURL('image/png');
const dataURL = canvas.toDataURL("image/png");
// Simpan atau cetak (print sebagai PDF)
const link = document.createElement('a');
const link = document.createElement("a");
link.href = dataURL;
link.download = 'merged-image.png';
link.download = "merged-image.png";
link.click();
// Atau tampilkan dan user bisa "Print to PDF"
window.open(dataURL, '_blank');
window.open(dataURL, "_blank");
} catch (error) {
console.log("Error :::", error)
console.log("Error :::", error);
}
}
return Number(roleId) == 2 || Number(roleId) == 11 || Number(roleId) == 12 ? (
""
@ -208,7 +235,7 @@ const PageTitle = ({
<div className="text-2xl font-medium text-default-800 capitalize">
Dashboard
</div>
<Dialog>
{/* <Dialog>
<DialogTrigger asChild>
<Button variant="outline">Download Report</Button>
</DialogTrigger>
@ -229,15 +256,12 @@ const PageTitle = ({
</div>
</div>
<DialogFooter>
<Button
type="submit"
onClick={downloadReport}
>
<Button type="submit" onClick={downloadReport}>
Download
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog> */}
</div>
);
};

View File

@ -82,7 +82,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
{
href: "/contributor/content/teks",
label: t("text"),
active: pathname.includes("/content/text"),
active: pathname.includes("/content/teks"),
icon: "heroicons:document",
children: [],
},
@ -1492,7 +1492,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
{
href: "/contributor/content/teks",
label: t("text"),
active: pathname.includes("/content/text"),
active: pathname.includes("/content/teks"),
icon: "heroicons:document",
children: [],
},
@ -1719,7 +1719,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
{
href: "/contributor/content/teks",
label: t("text"),
active: pathname.includes("/content/text"),
active: pathname.includes("/content/teks"),
icon: "heroicons:document",
children: [],
},
@ -2421,7 +2421,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
{
href: "/contributor/content/teks",
label: t("text"),
active: pathname.includes("/content/text"),
active: pathname.includes("/content/teks"),
icon: "heroicons:document",
children: [],
},
@ -2703,7 +2703,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
id: "faq",
href: "/supervisor/faq",
label: t("faq"),
active: pathname.includes("/frequently-asked-question"),
active: pathname.includes("/faq"),
icon: "wpf:faq",
submenus: [],
},
@ -3470,70 +3470,70 @@ export function getMenuList(pathname: string, t: any): Group[] {
},
],
},
// {
// groupLabel: "",
// id: "settings",
// menus: [
// {
// id: "settings",
// href: "/admin/settings",
// label: t("settings"),
// active: pathname.includes("/settinng"),
// icon: "material-symbols:settings",
// submenus: [
// {
// href: "/admin/settings/category",
// label: t("category"),
// active: pathname === "/admin/settings/category",
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/admin/settings/tag",
// label: "Tag",
// active: pathname === "/admin/settings/tag",
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/admin/settings/banner",
// label: "Banner",
// active: pathname === "/admin/settings/banner",
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/admin/settings/feedback",
// label: "Feedback",
// active: pathname === "/admin/settings/feedback",
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/admin/settings/faq",
// label: "FAQ",
// active: pathname === "/admin/settings/faq",
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "https://nat-mediahub.polri.go.id/",
// label: "Mediahub 2022",
// active: pathname === "/admin/settings/mediahub-2022",
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/admin/settings/privacy",
// label: t("privacy"),
// active: pathname === "/admin/settings/privacy",
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// ],
// },
// ],
// },
{
groupLabel: "",
id: "settings",
menus: [
{
id: "settings",
href: "/admin/settings",
label: t("settings"),
active: pathname.includes("/settinng"),
icon: "material-symbols:settings",
submenus: [
{
href: "/admin/settings/category",
label: t("category"),
active: pathname === "/admin/settings/category",
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/admin/settings/tag",
label: "Tag",
active: pathname === "/admin/settings/tag",
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/admin/settings/banner",
label: "Banner",
active: pathname === "/admin/settings/banner",
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/admin/settings/feedback",
label: "Feedback",
active: pathname === "/admin/settings/feedback",
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/admin/settings/faq",
label: "FAQ",
active: pathname === "/admin/settings/faq",
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "https://nat-mediahub.polri.go.id/",
label: "Mediahub 2022",
active: pathname === "/admin/settings/mediahub-2022",
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/admin/settings/privacy",
label: t("privacy"),
active: pathname === "/admin/settings/privacy",
icon: "heroicons:arrow-trending-up",
children: [],
},
],
},
],
},
];
} else {
menusSelected = [

8
package-lock.json generated
View File

@ -127,7 +127,7 @@
"react-player": "^2.16.0",
"react-quill": "^0.0.2",
"react-resizable-panels": "^2.0.19",
"react-responsive": "^10.0.0",
"react-responsive": "^10.0.1",
"react-select": "^5.8.3",
"react-slick": "^0.30.2",
"react-syntax-highlighter": "^15.5.0",
@ -15275,9 +15275,9 @@
}
},
"node_modules/react-responsive": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.0.tgz",
"integrity": "sha512-N6/UiRLGQyGUqrarhBZmrSmHi2FXSD++N5VbSKsBBvWfG0ZV7asvUBluSv5lSzdMyEVjzZ6Y8DL4OHABiztDOg==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.1.tgz",
"integrity": "sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==",
"dependencies": {
"hyphenate-style-name": "^1.0.0",
"matchmediaquery": "^0.4.2",

View File

@ -128,7 +128,7 @@
"react-player": "^2.16.0",
"react-quill": "^0.0.2",
"react-resizable-panels": "^2.0.19",
"react-responsive": "^10.0.0",
"react-responsive": "^10.0.1",
"react-select": "^5.8.3",
"react-slick": "^0.30.2",
"react-syntax-highlighter": "^15.5.0",

View File

@ -9,7 +9,7 @@ export async function listTicketingInternal(
page: number,
size: any,
title: string = "",
category: any = "",
category: any = ""
) {
return await httpGetInterceptor(
`ticketing/internal/pagination?enablePage=1&size=${size}&page=${page}&title=${title}&category=${category}`
@ -54,6 +54,21 @@ export async function saveTicketing(data: any) {
return httpPostInterceptor(url, data);
}
export async function saveTicketReply(data: any) {
const url = "ticketing/reply";
return httpPostInterceptor(url, data);
}
export async function deleteTicket(id: any) {
const url = `ticketing?id=${id}`;
return httpDeleteInterceptor(url, id);
}
export async function closeTicket(id: any) {
const url = `ticketing/close?id=${id}`;
return httpPostInterceptor(url, id);
}
export async function getTicketingInternalDetail(id: any) {
const url = `ticketing/internal?id=${id}`;
return httpGetInterceptor(url);
@ -74,6 +89,11 @@ export async function getTicketingDetail(id: any) {
return httpGetInterceptor(url);
}
export async function getTicketingReply(id: any) {
const url = `ticketing/reply?ticketId=${id}`;
return httpGetInterceptor(url);
}
export async function getTicketCollaborationTeams(id: string | number) {
const url = `ticketing/collaboration/teams?ticketId=${id}`;
return httpGetInterceptor(url);
@ -121,3 +141,13 @@ export async function getEscalationDiscussion(id: any) {
const url = `ticketing/escalation/discussion?ticketId=${id}`;
return httpGetInterceptor(url);
}
export async function getQuestionTicket(id: any) {
const url = `question?id=${id}`;
return httpGetInterceptor(url);
}
export async function saveTicketsQuestion(data: any) {
const url = "ticketing/convert-question";
return httpPostInterceptor(url, data);
}

View File

@ -37,6 +37,14 @@ export async function getHeroData() {
);
}
export async function listStaticBanner(
group: any = "",
isInt: Boolean = false
) {
const url = `media/static-banner?group=${group}&isInt=${isInt}`;
return httpGetInterceptor(url);
}
export async function getPublicCategoryData(
group: any = "",
type: string = "",

View File

@ -1,4 +1,5 @@
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
} from "../http-config/http-interceptor-service";
@ -14,6 +15,11 @@ export async function getKnowledgeBaseList(id: number) {
}
export async function saveKnowledgeBaseCategory(data: any) {
const url = 'knowledge-base/category';
const url = "knowledge-base/category";
return httpPostInterceptor(url, data);
}
}
export async function deleteKnowledgeBase(id: any) {
const url = `knowledge-base?id=${id}`;
return httpDeleteInterceptor(url);
}

View File

@ -18,13 +18,24 @@ export async function mediaTrackingSave(data: any) {
return httpPostInterceptor(url, data);
}
export async function getMediaTracking(data: any) {
// const url = `/media/tracking/monitoring/pagination`;
const url = `/media/tracking/monitoring/pagination?enablePagination=1&page=${
data.page || 0
}&size=${data?.size || 10}`;
return httpGetInterceptor(url);
export async function getMediaTracking(
page: any,
title: string = "",
size: any
) {
return httpGetInterceptor(
`/media/tracking/monitoring/pagination?enablePagination=1&size=${size}&page=${page}&title=${title}`
);
}
// export async function getMediaTracking(data: any) {
// // const url = `/media/tracking/monitoring/pagination`;
// const url = `/media/tracking/monitoring/pagination?enablePagination=1&page=${
// data.page || 0
// }&size=${data?.size || 10}`;
// return httpGetInterceptor(url);
// }
export async function getMediaTrackingResult(data: any) {
// const url = `/media/tracking/monitoring/pagination`;
const url = `/media/tracking/monitoring/results/pagination?trackingId=${

View File

@ -103,7 +103,7 @@ export async function getAcceptance(id: any, isAccept: any) {
return httpGetInterceptor(url);
}
export async function acceptAssignment(id: any, isAccept: any) {
export async function acceptAssignment(id: any) {
const url = `assignment/acceptance?id=${id}`;
return httpPostInterceptor(url, id);
}

View File

@ -21,6 +21,7 @@ const config = {
extend: {
screens: {
"custom-lg": "1090px",
"custom-lg-button": "1030px",
},
fontFamily: {
sans: ["DM Sans", "sans-serif"],