feat:detail feedback, onoff userLevel

This commit is contained in:
Rama Priyanto 2025-03-06 17:40:54 +07:00
parent 78be7bc0e1
commit 3817131f52
23 changed files with 1632 additions and 198 deletions

View File

@ -41,7 +41,7 @@ const createArticleSchema = z.object({
}), }),
}); });
export default function BasicPage() { export default function AdvertisePage() {
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure(); const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);

View File

@ -0,0 +1,84 @@
"use client";
import { AddIcon, CloudUploadIcon, TimesIcon } from "@/components/icons";
import AdvertiseTable from "@/components/table/advertise/advertise-table";
import {
Button,
Card,
Chip,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Switch,
Textarea,
useDisclosure,
} from "@heroui/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { Fragment, useEffect, useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useDropzone } from "react-dropzone";
import { close, error, loading } from "@/config/swal";
import Image from "next/image";
import CommentTable from "@/components/table/comment/comment-table";
const createArticleSchema = z.object({
title: z.string().min(2, {
message: "Judul harus diisi",
}),
url: z.string().min(2, {
message: "Link harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
});
export default function AdvertisePage() {
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const MySwal = withReactContent(Swal);
const [refresh, setRefresh] = useState(false);
const [isHeader, setIsHeader] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", url: "" },
};
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
accept: {
"image/*": [],
},
});
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
formState: { errors },
} = useForm<UserSettingSchema>(formOptions);
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<CommentTable triggerRefresh={refresh} />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
import SuggestionsTable from "@/components/table/suggestions/suggestions-table";
export default function SuggestionsPage() {
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<SuggestionsTable />
</div>
</div>
</div>
);
}

View File

@ -7,12 +7,6 @@ import React from "react";
const AdminMasterUserLevel = () => { const AdminMasterUserLevel = () => {
return ( return (
// <div>
// <MasterUserLevelTable />
// </div>
// <div>
// <MappingUserLevel />
// </div>
<div className="overflow-x-hidden overflow-y-scroll"> <div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full"> <div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl py-3"> <div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl py-3">

View File

@ -11,7 +11,7 @@ import { useRouter } from "next/navigation";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import { saveActivity } from "@/service/activity-log"; import { saveActivity } from "@/service/activity-log";
import Image from "next/image"; import { Image } from "@heroui/react";
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
@ -165,7 +165,7 @@ export default function Login() {
height={480} height={480}
alt="icon" alt="icon"
src="/divhumas.png" src="/divhumas.png"
className="w-[120px]" className="w-[240px] !h-[240px]"
/> />
</Link> </Link>
</div> </div>

View File

@ -445,3 +445,43 @@ export const AddvertiseIcon = ({
/> />
</svg> </svg>
); );
export const SuggestionsIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M17.175 14H7.5q-1.875 0-3.187-1.312T3 9.5t1.313-3.187T7.5 5q.425 0 .713.288T8.5 6t-.288.713T7.5 7q-1.05 0-1.775.725T5 9.5t.725 1.775T7.5 12h9.675L14.3 9.1q-.275-.275-.288-.687T14.3 7.7q.275-.275.7-.275t.7.275l4.6 4.6q.3.3.3.7t-.3.7l-4.6 4.6q-.3.3-.7.288t-.7-.313q-.275-.3-.288-.7t.288-.7z"
/>
</svg>
);
export const CommentIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M6.5 13.5h11v-1h-11zm0-3h11v-1h-11zm0-3h11v-1h-11zM4.616 17q-.691 0-1.153-.462T3 15.385V4.615q0-.69.463-1.153T4.615 3h14.77q.69 0 1.152.462T21 4.615v15.462L17.923 17z"
/>
</svg>
);

View File

@ -4,9 +4,9 @@ import {
Card, Card,
CardFooter, CardFooter,
CircularProgress, CircularProgress,
Image,
ScrollShadow, ScrollShadow,
} from "@heroui/react"; } from "@heroui/react";
import Image from "next/image";
import { ChevronLeftIcon, ChevronRightIcon, EyeIcon } from "../icons"; import { ChevronLeftIcon, ChevronRightIcon, EyeIcon } from "../icons";
import { Swiper, SwiperSlide, useSwiper } from "swiper/react"; import { Swiper, SwiperSlide, useSwiper } from "swiper/react";
import "swiper/css"; import "swiper/css";
@ -101,8 +101,6 @@ export default function HeaderNews() {
className="border-none rounded-xl shadow-none" className="border-none rounded-xl shadow-none"
> >
<Image <Image
width={720}
height={480}
alt="headernews" alt="headernews"
src={ src={
newsItem?.thumbnailUrl == "" newsItem?.thumbnailUrl == ""
@ -235,8 +233,6 @@ export default function HeaderNews() {
> >
<Image <Image
alt="headernews" alt="headernews"
width={1440}
height={1080}
src={ src={
newsItem?.thumbnailUrl == "" newsItem?.thumbnailUrl == ""
? "/no-image.jpg" ? "/no-image.jpg"

View File

@ -129,7 +129,6 @@ export default function PolriApps(props: {
const { isOpen, onOpen, onOpenChange } = useDisclosure(); const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [scrollBehavior, setScrollBehavior] = const [scrollBehavior, setScrollBehavior] =
React.useState<ModalProps["scrollBehavior"]>("inside"); React.useState<ModalProps["scrollBehavior"]>("inside");
const [modalPlacement, setModalPlacement] = React.useState<any>("auto");
const t = useTranslations("Landing"); const t = useTranslations("Landing");
useEffect(() => { useEffect(() => {
@ -138,24 +137,6 @@ export default function PolriApps(props: {
} }
}, [props.opened]); }, [props.opened]);
// useEffect(() => {
// function updateLimitedData() {
// if (window.matchMedia("(max-width: 767px)").matches) {
// setLimitedData(list.slice(0, 2));
// } else if (window.matchMedia("(min-width: 768px) and (max-width: 1023px)").matches) {
// setLimitedData(list.slice(0, 3));
// } else {
// setLimitedData(list.slice(0, 5));
// }
// }
// updateLimitedData();
// window.addEventListener('resize', updateLimitedData);
// return () => {
// window.removeEventListener('resize', updateLimitedData);
// };
// }, [list]);
const changeNameToSlug = (name: string) => { const changeNameToSlug = (name: string) => {
const cleaned = name.replace("Polda ", "").trim().toLowerCase(); const cleaned = name.replace("Polda ", "").trim().toLowerCase();
const slug = cleaned.replace(/\s+/g, "-"); const slug = cleaned.replace(/\s+/g, "-");
@ -164,16 +145,6 @@ export default function PolriApps(props: {
return ( return (
<> <>
{/* <div className="flex justify-center w-full items-center">
<Button
onPress={onOpen}
className="border-red-700 text-white w-fit"
size="sm"
variant="bordered"
>
{t("lebihBanyak")}
</Button>
</div> */}
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onOpenChange={() => { onOpenChange={() => {

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Image from "next/image";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
ChevronDownIcon, ChevronDownIcon,
@ -35,6 +34,7 @@ import {
PopoverContent, PopoverContent,
Accordion, Accordion,
AccordionItem, AccordionItem,
Image,
} from "@heroui/react"; } from "@heroui/react";
import storedLanguage from "@/store/language-store"; import storedLanguage from "@/store/language-store";
import { ThemeSwitch } from "../theme-switch"; import { ThemeSwitch } from "../theme-switch";
@ -107,8 +107,6 @@ export default function BannerHumasNew() {
> >
{withImage && ( {withImage && (
<Image <Image
width={720}
height={720}
alt="logo" alt="logo"
src={menu.img} src={menu.img}
className="w-[45px] h-[45px]" className="w-[45px] h-[45px]"
@ -167,8 +165,6 @@ export default function BannerHumasNew() {
> >
{withImage && ( {withImage && (
<Image <Image
width={720}
height={720}
alt="logo" alt="logo"
src={menu.img} src={menu.img}
className="w-[35px] h-[35px]" className="w-[35px] h-[35px]"
@ -201,8 +197,6 @@ export default function BannerHumasNew() {
<Link href="" key={index} className="w-full shrink-0"> <Link href="" key={index} className="w-full shrink-0">
<Image <Image
src={img} src={img}
width={1440}
height={1080}
alt={`humasbanner-${index}`} alt={`humasbanner-${index}`}
className="w-full h-full object-cover object-center opacity-[25] dark:opacity-70" className="w-full h-full object-cover object-center opacity-[25] dark:opacity-70"
/> />
@ -221,13 +215,7 @@ export default function BannerHumasNew() {
OBYEKTIF - DIPERCAYA - PARTISIPASI OBYEKTIF - DIPERCAYA - PARTISIPASI
</p> </p>
</div> </div>
<Image <Image src="/divhumas.png" alt="logo-humas" className="w-[200px]" />
src="/divhumas.png"
width={720}
height={720}
alt="logo-humas"
className="w-[200px]"
/>
</div> </div>
<div className="flex flex-col lg:flex-row lg:justify-between gap-4 mt-10 lg:mt-24"> <div className="flex flex-col lg:flex-row lg:justify-between gap-4 mt-10 lg:mt-24">
<div className="flex flex-row gap-6"> <div className="flex flex-row gap-6">

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import Image from "next/image";
import RegionalNews from "./RegionalNews"; import RegionalNews from "./RegionalNews";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import CategorySatker from "./CategorySatker"; import CategorySatker from "./CategorySatker";
import PolriApps from "./PolriApps"; import PolriApps from "./PolriApps";
import Link from "next/link"; import Link from "next/link";
import SuggestionsModal from "./suggestions"; import SuggestionsModal from "./suggestions";
import { Image } from "@heroui/react";
export default function DigitalServices() { export default function DigitalServices() {
const [isPoldaOpen, setIsPoldaOpen] = useState(false); const [isPoldaOpen, setIsPoldaOpen] = useState(false);
@ -27,8 +27,6 @@ export default function DigitalServices() {
> >
<Image <Image
src="/indonesia-map.png" src="/indonesia-map.png"
width={480}
height={480}
alt="indonesia" alt="indonesia"
className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out" className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out"
/> />
@ -45,8 +43,6 @@ export default function DigitalServices() {
> >
<Image <Image
src="/satker.png" src="/satker.png"
width={480}
height={480}
alt="satker" alt="satker"
className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out" className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out"
/> />
@ -63,8 +59,6 @@ export default function DigitalServices() {
> >
<Image <Image
src="/presisi.png" src="/presisi.png"
width={480}
height={480}
alt="presisi" alt="presisi"
className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out" className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out"
/> />
@ -81,8 +75,6 @@ export default function DigitalServices() {
> >
<Image <Image
src="/kritik-saran.png" src="/kritik-saran.png"
width={480}
height={480}
alt="kritik-saran" alt="kritik-saran"
className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out" className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out"
/> />
@ -99,8 +91,6 @@ export default function DigitalServices() {
> >
<Image <Image
src="/survey.png" src="/survey.png"
width={480}
height={480}
alt="survey" alt="survey"
className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out" className="w-[100px] group-hover:scale-125 transition duration-300 ease-in-out"
/> />

View File

@ -25,6 +25,7 @@ import OTPInput from "react-otp-input";
import { otpRequest, otpValidation } from "@/service/master-user"; import { otpRequest, otpValidation } from "@/service/master-user";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import { createFeedback } from "@/service/feedbacks";
const createArticleSchema = z.object({ const createArticleSchema = z.object({
email: z.string().min(2, { email: z.string().min(2, {
@ -71,26 +72,32 @@ export default function SuggestionsModal(props: {
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => { const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
if (!needOtp) { if (!needOtp) {
loading(); loading();
// const res = await otpRequest(values.email, values?.name); const res = await otpRequest(values.email, values?.name);
// if (res?.error) { if (res?.error) {
// error(res.message); error(res.message);
// return false; return false;
// } }
close(); close();
setNeedOtp(true); setNeedOtp(true);
} else { } else {
// const validation = await otpValidation(values.email, otpValue); const validation = await otpValidation(values.email, otpValue);
// if (validation?.error) { if (validation?.error) {
// error("OTP Tidak Sesuai"); error("OTP Tidak Sesuai");
// return false; return false;
// } }
const req = { const req = {
name: values.name, commentFromName: values.name,
description: values.description, message: values.description,
email: values.email, commentFromEmail: values.email,
}; };
const res = await createFeedback(req);
if (res?.error) {
error(res?.message);
return false;
}
MySwal.fire({ MySwal.fire({
title: "Berhasil Kirim", title: "Berhasil Kirim",
text: "", text: "",
@ -108,17 +115,6 @@ export default function SuggestionsModal(props: {
props.modalStatus(!props.opened); props.modalStatus(!props.opened);
onOpenChange(); onOpenChange();
} }
// setRefresh(!refresh);
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then((result) => {
// if (result.isConfirmed) {
// }
// });
}; };
return ( return (

View File

@ -15,6 +15,7 @@ import {
import { import {
AddvertiseIcon, AddvertiseIcon,
ArticleIcon, ArticleIcon,
CommentIcon,
DashboardIcon, DashboardIcon,
HomeIcon, HomeIcon,
InfoCircleIcon, InfoCircleIcon,
@ -25,6 +26,7 @@ import {
MasterUsersIcon, MasterUsersIcon,
MinusCircleIcon, MinusCircleIcon,
StaticPageIcon, StaticPageIcon,
SuggestionsIcon,
TableIcon, TableIcon,
} from "../../icons/sidebar-icon"; } from "../../icons/sidebar-icon";
import { ThemeSwitch } from "../../theme-switch"; import { ThemeSwitch } from "../../theme-switch";
@ -233,7 +235,34 @@ const sideBarDummyData = [
statusName: "Active", statusName: "Active",
childModule: null, childModule: null,
}, },
{
id: 34,
name: "Kritik & Saran",
moduleId: 652,
moduleName: "Apps",
modulePathUrl: "/admin/feedbacks",
parentId: -1,
icon: <SuggestionsIcon size={23} />,
position: 1,
statusId: 1,
childMenu: [],
statusName: "Active",
childModule: null,
},
{
id: 35,
name: "Komentar",
moduleId: 652,
moduleName: "Apps",
modulePathUrl: "/admin/comment",
parentId: -1,
icon: <CommentIcon size={23} />,
position: 1,
statusId: 1,
childMenu: [],
statusName: "Active",
childModule: null,
},
// { // {
// id: 4, // id: 4,
// name: "E-Magazine", // name: "E-Magazine",

View File

@ -15,6 +15,7 @@ import {
import { import {
AddvertiseIcon, AddvertiseIcon,
ArticleIcon, ArticleIcon,
CommentIcon,
DashboardIcon, DashboardIcon,
HomeIcon, HomeIcon,
InfoCircleIcon, InfoCircleIcon,
@ -25,6 +26,7 @@ import {
MasterUsersIcon, MasterUsersIcon,
MinusCircleIcon, MinusCircleIcon,
StaticPageIcon, StaticPageIcon,
SuggestionsIcon,
TableIcon, TableIcon,
} from "../../icons/sidebar-icon"; } from "../../icons/sidebar-icon";
import { ThemeSwitch } from "../../theme-switch"; import { ThemeSwitch } from "../../theme-switch";
@ -234,6 +236,34 @@ const sideBarDummyData = [
statusName: "Active", statusName: "Active",
childModule: null, childModule: null,
}, },
{
id: 34,
name: "Kritik & Saran",
moduleId: 652,
moduleName: "Apps",
modulePathUrl: "/admin/feedbacks",
parentId: -1,
icon: <SuggestionsIcon size={23} />,
position: 1,
statusId: 1,
childMenu: [],
statusName: "Active",
childModule: null,
},
{
id: 35,
name: "Komentar",
moduleId: 652,
moduleName: "Apps",
modulePathUrl: "/admin/comment",
parentId: -1,
icon: <CommentIcon size={23} />,
position: 1,
statusId: 1,
childMenu: [],
statusName: "Active",
childModule: null,
},
// { // {
// id: 4, // id: 4,

View File

@ -0,0 +1,163 @@
"use client";
import React, { Component, useEffect, useState } from "react";
import ReactApexChart from "react-apexcharts";
import dummyData from "../../../../const/dummy.json";
import { getStatisticMonthly } from "@/service/article";
type WeekData = {
week: number;
days: number[];
total: number;
};
type RemainingDays = {
days: number[];
total: number;
};
function processMonthlyData(count: number[]): {
weeks: WeekData[];
remaining_days: RemainingDays;
} {
const weeks: WeekData[] = [];
let weekIndex = 1;
for (let i = 0; i < count.length; i += 7) {
const weekData = count.slice(i, i + 7);
weeks.push({
week: weekIndex,
days: weekData,
total: weekData.reduce((sum, day) => sum + day, 0),
});
weekIndex++;
}
const remainingDays: RemainingDays = {
days: count.length % 7 === 0 ? [] : count.slice(-count.length % 7),
total: count.slice(-count.length % 7).reduce((sum, day) => sum + day, 0),
};
return {
weeks,
remaining_days: remainingDays,
};
}
const SuggestionsChart = (props: { type: string; date: string }) => {
const { date, type } = props;
const [categories, setCategories] = useState<string[]>([]);
const [seriesSuggestions, setSeriesSuggestions] = useState<number[]>([]);
useEffect(() => {
initFetch();
}, [date, type]);
function processYearlyData(data: any) {
const months = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember",
];
const category = [];
const temp = [];
for (let i = 0; i < data.length; i++) {
const total = data[i].suggestions.reduce(
(sum: number, list: number) => sum + list,
0
);
temp.push(total);
category.push(months[data[i].month - 1]);
}
return { categories: category, series: temp };
}
const initFetch = async () => {
const splitDate = date.split(" ");
// const res = await getStatisticMonthly(splitDate[1]);
// const data = res?.data?.data;
const data = dummyData.data;
if (type === "monthly") {
const getDatas = data?.filter(
(a: any) => a.year === Number(splitDate[1])
);
if (getDatas) {
const temp = processYearlyData(getDatas);
console.log("temp", temp);
setSeriesSuggestions(temp.series);
setCategories(temp.categories);
} else {
setSeriesSuggestions([]);
}
} else {
const getDatas = data?.find(
(a: any) =>
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])
);
if (getDatas) {
const temp = processMonthlyData(getDatas?.suggestions);
if (type == "weekly") {
setSeriesSuggestions(
temp.weeks.map((list) => {
return list.total;
})
);
} else {
setSeriesSuggestions(getDatas.suggestions);
}
if (type === "weekly") {
const category = [];
for (let i = 1; i <= temp.weeks.length; i++) {
category.push(`Minggu ke-${i}`);
}
setCategories(category);
}
} else {
setSeriesSuggestions([]);
}
}
};
return (
<div className="h-full">
<div id="chart" className="h-full">
<ReactApexChart
options={{
chart: {
height: "100%",
type: "area",
},
stroke: {
curve: "smooth",
},
dataLabels: {
enabled: false,
},
xaxis: {
categories: type == "daily" ? [] : categories,
},
}}
series={[
{
name: "Kritik & Saran",
data: seriesSuggestions,
},
]}
type="area"
height="100%"
/>
</div>
<div id="html-dist"></div>
</div>
);
};
export default SuggestionsChart;

View File

@ -13,6 +13,7 @@ import {
Calendar, Calendar,
Checkbox, Checkbox,
CheckboxGroup, CheckboxGroup,
Image,
Pagination, Pagination,
Popover, Popover,
PopoverContent, PopoverContent,
@ -138,7 +139,9 @@ export default function DashboardContainer() {
}, [postContentDate]); }, [postContentDate]);
async function fetchPostCount() { async function fetchPostCount() {
const getDate = (data: any) => { const getDate = (data: any) => {
return `${data.year}-${data.month}-${data.day}`; return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${
data.day < 10 ? `0${data.day}` : data.day
}`;
}; };
const res = await getUserLevelDataStat( const res = await getUserLevelDataStat(
getDate(postContentDate.startDate), getDate(postContentDate.startDate),
@ -164,7 +167,6 @@ export default function DashboardContainer() {
return date.month + " " + date.year; return date.month + " " + date.year;
}; };
const getMonthYearName = (date: any) => { const getMonthYearName = (date: any) => {
console.log("dateee", date);
const newDate = new Date(date); const newDate = new Date(date);
const months = [ const months = [
@ -249,14 +251,6 @@ export default function DashboardContainer() {
Rekapitulasi Post Berita Polda/Polres Pada Website Rekapitulasi Post Berita Polda/Polres Pada Website
</p> </p>
<div className="w-[220px] flex flex-row gap-2 justify-between font-semibold"> <div className="w-[220px] flex flex-row gap-2 justify-between font-semibold">
{/* <Datepicker
value={postContentDate}
displayFormat="DD/MM/YYYY"
asSingle={false}
useRange={true}
onChange={(e: any) => setPostContentDate(e)}
inputClassName="z-50 w-full text-xs lg:text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-sm lg:rounded-lg h-[30px] lg:h-[40px] text-gray-600 dark:text-gray-300"
/> */}
<Popover <Popover
placement="bottom" placement="bottom"
classNames={{ content: ["!bg-transparent", "p-0"] }} classNames={{ content: ["!bg-transparent", "p-0"] }}
@ -344,7 +338,8 @@ export default function DashboardContainer() {
key={list?.id} key={list?.id}
className="flex flex-row gap-2 items-center border-b-2 py-2" className="flex flex-row gap-2 items-center border-b-2 py-2"
> >
<img <Image
alt="thumbnail"
src={list?.thumbnailUrl || `/no-image.jpg`} src={list?.thumbnailUrl || `/no-image.jpg`}
className="h-[70px] w-[70px] object-cover rounded-lg" className="h-[70px] w-[70px] object-cover rounded-lg"
/> />
@ -419,14 +414,14 @@ export default function DashboardContainer() {
<Button <Button
color="primary" color="primary"
variant={typeDate === "monthly" ? "solid" : "bordered"} variant={typeDate === "monthly" ? "solid" : "bordered"}
onClick={() => setTypeDate("monthly")} onPress={() => setTypeDate("monthly")}
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg" className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
> >
Bulanan Bulanan
</Button> </Button>
<Button <Button
color="primary" color="primary"
onClick={() => setTypeDate("weekly")} onPress={() => setTypeDate("weekly")}
variant={typeDate === "weekly" ? "solid" : "bordered"} variant={typeDate === "weekly" ? "solid" : "bordered"}
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg" className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
> >

View File

@ -301,6 +301,12 @@ export default function ArticleTable() {
<SelectItem key="10" value="10"> <SelectItem key="10" value="10">
10 10
</SelectItem> </SelectItem>
<SelectItem key="25" value="25">
25
</SelectItem>
<SelectItem key="50" value="50">
50
</SelectItem>
</Select> </Select>
</div> </div>
<div className="flex flex-col gap-1 w-full lg:w-[230px]"> <div className="flex flex-col gap-1 w-full lg:w-[230px]">

View File

@ -0,0 +1,422 @@
"use client";
import {
BannerIcon,
CloudUploadIcon,
CreateIconIon,
DeleteIcon,
DotsYIcon,
EyeIconMdi,
SearchIcon,
TimesIcon,
} from "@/components/icons";
import { close, error, loading, success } from "@/config/swal";
import {
deleteArticle,
getArticleByCategory,
getListArticle,
} from "@/service/article";
import { Article } from "@/types/globals";
import { convertDateFormat } from "@/utils/global";
import { Button } from "@heroui/button";
import {
Chip,
ChipProps,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Pagination,
Select,
SelectItem,
Spinner,
Switch,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
Textarea,
useDisclosure,
} from "@heroui/react";
import Link from "next/link";
import { Fragment, Key, useCallback, useEffect, useState } from "react";
import Cookies from "js-cookie";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useDropzone } from "react-dropzone";
import Image from "next/image";
import { useRouter } from "next/navigation";
const columns = [
{ name: "No", uid: "no" },
{ name: "Nama", uid: "name" },
{ name: "Email", uid: "email" },
{ name: "Komentar", uid: "comment" },
{ name: "Aksi", uid: "actions" },
];
interface Category {
id: number;
title: string;
}
const createArticleSchema = z.object({
id: z.string().optional(),
title: z.string().min(2, {
message: "Judul harus diisi",
}),
url: z.string().min(2, {
message: "Link harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
file: z.string().optional(),
});
export default function CommentTable(props: { triggerRefresh: boolean }) {
const MySwal = withReactContent(Swal);
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const router = useRouter();
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [article, setArticle] = useState<any[]>([]);
const [showData, setShowData] = useState("10");
const [search, setSearch] = useState("");
const [categories, setCategoies] = useState<any>([]);
const [selectedCategories, setSelectedCategories] = useState<any>([]);
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
const [isHeader, setIsHeader] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", url: "", file: "" },
};
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
accept: {
"image/*": [],
},
});
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
initState();
}, [
page,
showData,
startDateValue,
selectedCategories,
props.triggerRefresh,
]);
useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const res = await getArticleByCategory();
const data = res?.data?.data;
setCategoies(data);
}
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
async function initState() {
const req = {
limit: showData,
page: page,
search: search,
startDate:
startDateValue.startDate === null ? "" : startDateValue.startDate,
endDate: startDateValue.endDate === null ? "" : startDateValue.endDate,
category: Array.from(selectedCategories).join(","),
sort: "desc",
sortBy: "created_at",
};
const res = await getListArticle(req);
getTableNumber(parseInt(showData), res.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
}
const getTableNumber = (limit: number, data: Article[]) => {
if (data) {
const startIndex = limit * (page - 1);
let iterate = 0;
const newData = data.map((value: any) => {
iterate++;
value.no = startIndex + iterate;
return value;
});
setArticle(newData);
}
};
async function doDelete(id: any) {
// loading();
const resDelete = await deleteArticle(id);
if (resDelete?.error) {
error(resDelete.message);
return false;
}
close();
success("Berhasil Hapus");
initState();
}
const handleDelete = (id: any) => {
MySwal.fire({
title: "Hapus Data",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData = {
title: values.title,
description: values.description,
isHeader: isHeader,
url: values.url,
};
console.log("dataas", formData);
close();
// setRefresh(!refresh);
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then((result) => {
// if (result.isConfirmed) {
// }
// });
};
const openModal = async (id: number) => {
// const res = await getCategoryById(Number(id));
// const data = res?.data?.data;
// setValue("id", String(data?.id));
// setValue("title", data?.title);
// setValue("description", data?.description);
// setValue("url", data?.url);
// setValue("file", data?.thumbnailUrl);
onOpen();
};
const renderCell = useCallback(
(comment: any, columnKey: Key) => {
const cellValue = comment[columnKey as keyof any];
switch (columnKey) {
case "url":
return (
<Link
href={`https://www.google.com/`}
target="_blank"
className="text-primary hover:underline"
>
https://www.google.com/
</Link>
);
case "actions":
return (
<div className="relative flex justify-star items-center gap-2">
<Dropdown className="lg:min-w-[150px] bg-black text-white shadow border ">
<DropdownTrigger>
<Button isIconOnly size="lg" variant="light">
<DotsYIcon className="text-default-300" />
</Button>
</DropdownTrigger>
<DropdownMenu>
{/* <DropdownItem key="detail">
<Link href={`/admin/comment/detail/${article.id}`}>
<EyeIconMdi className="inline mr-2 mb-1" />
Detail
</Link>
</DropdownItem> */}
<DropdownItem
key="edit"
onPress={() =>
router.push(`/admin/comment/review${comment.id}`)
}
>
<CreateIconIon className="inline mr-2 mb-1" />
Review
</DropdownItem>
<DropdownItem
key="delete"
// onPress={() => handleDelete(article.id)}
>
<DeleteIcon
color="red"
size={18}
className="inline ml-1 mr-2 mb-1"
/>
Delete
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
default:
return cellValue;
}
},
[article]
);
let typingTimer: NodeJS.Timeout;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
initState();
}
return (
<>
<div className="py-3">
<div className="flex flex-col items-start rounded-2xl gap-3">
<div className="flex flex-col md:flex-row gap-3 w-full">
<div className="flex flex-col gap-1 w-full lg:w-1/3">
<p className="font-semibold text-sm">Pencarian</p>
<Input
aria-label="Search"
classNames={{
inputWrapper: "bg-default-100",
input: "text-sm",
}}
labelPlacement="outside"
startContent={
<SearchIcon className="text-base text-default-400 pointer-events-none flex-shrink-0" />
}
type="text"
onChange={(e) => setSearch(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
/>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
<p className="font-semibold text-sm">Data</p>
<Select
label=""
variant="bordered"
labelPlacement="outside"
placeholder="Select"
selectedKeys={[showData]}
className="w-full"
classNames={{ trigger: "border-1" }}
onChange={(e) =>
e.target.value === "" ? "" : setShowData(e.target.value)
}
>
<SelectItem key="5" value="5">
5
</SelectItem>
<SelectItem key="10" value="10">
10
</SelectItem>
</Select>
</div>
</div>
<Table
aria-label="micro issue table"
className="rounded-3xl"
classNames={{
th: "bg-white dark:bg-black text-black dark:text-white border-b-1 text-md",
base: "bg-white dark:bg-black border",
wrapper:
"min-h-[50px] bg-transpararent text-black dark:text-white ",
}}
>
<TableHeader columns={columns}>
{(column) => (
<TableColumn key={column.uid}>{column.name}</TableColumn>
)}
</TableHeader>
<TableBody
items={article}
emptyContent={"No data to display."}
loadingContent={<Spinner label="Loading..." />}
>
{(item: any) => (
<TableRow key={item.id}>
{(columnKey) => (
<TableCell>{renderCell(item, columnKey)}</TableCell>
)}
</TableRow>
)}
</TableBody>
</Table>
<div className="my-2 w-full flex justify-center">
<Pagination
isCompact
showControls
showShadow
color="primary"
classNames={{
base: "bg-transparent",
wrapper: "bg-transparent",
}}
page={page}
total={totalPage}
onChange={(page) => setPage(page)}
/>
</div>
</div>
</div>
</>
);
}

View File

@ -17,6 +17,7 @@ import {
Divider, Divider,
Chip, Chip,
ChipProps, ChipProps,
Checkbox,
} from "@heroui/react"; } from "@heroui/react";
import { Button } from "@heroui/button"; import { Button } from "@heroui/button";
import React, { Key, useCallback, useEffect, useMemo, useState } from "react"; import React, { Key, useCallback, useEffect, useMemo, useState } from "react";
@ -32,6 +33,7 @@ import {
import Link from "next/link"; import Link from "next/link";
import { getAllUserLevels } from "@/services/user-levels/user-levels-service"; import { getAllUserLevels } from "@/services/user-levels/user-levels-service";
import { close, loading } from "@/config/swal"; import { close, loading } from "@/config/swal";
import { stringify } from "querystring";
type UserObject = { type UserObject = {
id: number; id: number;
@ -45,10 +47,12 @@ type UserObject = {
export default function MasterUserLevelTable() { export default function MasterUserLevelTable() {
const [totalPage, setTotalPage] = useState(1); const [totalPage, setTotalPage] = useState(1);
const [masterUserLevelTable, setmasterUserLevel] = useState<UserObject[]>([]); const [masterUserLevelTable, setMasterUserLevel] = useState<UserObject[]>([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [doSetup, setDoSetup] = useState(false);
type TableRow = (typeof masterUserLevelTable)[0]; const [userLevelAll, setUserLevelAll] = useState<UserObject[]>([]);
const [selectAllLevel, setSelectAllLevel] = useState(false);
const [selectedLevel, setSelectedLevel] = useState<string[]>([]);
const columns = [ const columns = [
{ name: "No", uid: "no" }, { name: "No", uid: "no" },
@ -56,8 +60,15 @@ export default function MasterUserLevelTable() {
{ name: "User Name", uid: "aliasName" }, { name: "User Name", uid: "aliasName" },
{ name: "Level Number", uid: "levelNumber" }, { name: "Level Number", uid: "levelNumber" },
{ name: "Parent", uid: "parentLevelId" }, { name: "Parent", uid: "parentLevelId" },
// { name: "Province", uid: "province_id" }, { name: "Action", uid: "actions" },
// { name: "Status", uid: "is_active" }, ];
const columns2 = [
{ name: "No", uid: "no" },
{ name: "Setup", uid: "setup" },
{ name: "Name", uid: "name" },
{ name: "User Name", uid: "aliasName" },
{ name: "Level Number", uid: "levelNumber" },
{ name: "Parent", uid: "parentLevelId" },
{ name: "Action", uid: "actions" }, { name: "Action", uid: "actions" },
]; ];
@ -65,7 +76,11 @@ export default function MasterUserLevelTable() {
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [page]); }, [page, selectedLevel]);
useEffect(() => {
fetchDataAll();
}, []);
async function fetchData() { async function fetchData() {
loading(); loading();
@ -80,6 +95,18 @@ export default function MasterUserLevelTable() {
await initUserData(10, data); await initUserData(10, data);
close(); close();
} }
async function fetchDataAll() {
loading();
const request = {
page: 1,
limit: -1,
search: "",
};
const res = await getAllUserLevels(request);
const data = res?.data?.data;
setUserLevelAll(data);
close();
}
async function initUserData(limit: number, data?: any) { async function initUserData(limit: number, data?: any) {
if (data) { if (data) {
@ -91,24 +118,10 @@ export default function MasterUserLevelTable() {
value.no = startIndex + iterate; value.no = startIndex + iterate;
return value; return value;
}); });
setmasterUserLevel(newData); setMasterUserLevel(newData);
} }
} }
const findParentName = (data: string | number) => {
let name = "-";
for (let i = 9; i < masterUserLevelTable?.length; i++) {
const temp = masterUserLevelTable[i];
if (temp.id === Number(data)) {
name = temp.name;
break;
}
}
return name;
};
let typingTimer: NodeJS.Timeout; let typingTimer: NodeJS.Timeout;
const doneTypingInterval = 1500; const doneTypingInterval = 1500;
@ -125,13 +138,68 @@ export default function MasterUserLevelTable() {
fetchData(); fetchData();
} }
const renderCell = useCallback( const doMapping = (status: boolean) => {
(masterUserLevel: TableRow, columnKey: Key) => { setSelectAllLevel(status);
const cellValue = masterUserLevel[columnKey as keyof UserObject]; if (status) {
const temp = [];
for (const element of userLevelAll) {
temp.push(String(element.id));
}
setSelectedLevel(temp);
} else {
setSelectedLevel([]);
}
};
const handleSelectedLevel = (id: string, checked: boolean) => {
console.log("change", id, checked);
const temp = [...selectedLevel];
if (checked) {
temp.push(id);
setSelectedLevel(temp);
if (temp.length === userLevelAll.length) {
setSelectAllLevel(true);
}
} else {
const newTemp = temp.filter((a) => a !== id);
setSelectedLevel(newTemp);
if (newTemp.length !== userLevelAll.length) {
setSelectAllLevel(false);
}
}
};
const renderCell = useCallback(
(masterUserLevel: UserObject, columnKey: Key) => {
const findParentName = (data: string | number) => {
let name = "-";
for (let i = 9; i < userLevelAll?.length; i++) {
const temp = userLevelAll[i];
if (temp.id === Number(data)) {
name = temp.name;
break;
}
}
return name;
};
const cellValue = masterUserLevel[columnKey as keyof UserObject];
switch (columnKey) { switch (columnKey) {
case "parentLevelId": case "parentLevelId":
return <p className="text-black">{findParentName(cellValue)}</p>; return <p className="text-black">{findParentName(cellValue)}</p>;
case "setup":
return (
<Checkbox
key={masterUserLevel.id}
isSelected={selectedLevel?.includes(String(masterUserLevel.id))}
onValueChange={(e) =>
handleSelectedLevel(String(masterUserLevel.id), e)
}
/>
);
case "actions": case "actions":
return ( return (
<div className="relative flex justify-star items-center gap-2"> <div className="relative flex justify-star items-center gap-2">
@ -173,38 +241,43 @@ export default function MasterUserLevelTable() {
return cellValue; return cellValue;
} }
}, },
[masterUserLevelTable] [selectedLevel, userLevelAll, masterUserLevelTable]
); );
return ( return (
<> <>
<div className="mx-3 my-5"> <div className="mx-3 my-5">
{/* <Link href="/admin/master/master-user-level/create"> <div className="flex flex-col items-start rounded-2xl">
<Button className="my-3 bg-blue-600 text-white"> <div className="flex flex-row items-end gap-3 mb-3">
<CreateIconIon /> <div className="flex flex-col gap-1 w-full justify-start">
Create New User Level <p className="font-semibold text-sm">Pencarian</p>
</Button> <Input
</Link> */} aria-label="Search"
<div className="flex flex-col items-center rounded-2xl"> classNames={{
<div className="flex flex-col gap-1 w-full justify-start mb-3"> inputWrapper: "bg-default-100",
<p className="font-semibold text-sm">Pencarian</p> input: "text-sm",
<Input }}
aria-label="Search" className="max-w-md"
classNames={{ labelPlacement="outside"
inputWrapper: "bg-default-100", startContent={
input: "text-sm", <SearchIcon className="text-base text-default-400 pointer-events-none flex-shrink-0" />
}} }
className="max-w-md" type="text"
labelPlacement="outside" onChange={(e) => setSearch(e.target.value)}
startContent={ onKeyUp={handleKeyUp}
<SearchIcon className="text-base text-default-400 pointer-events-none flex-shrink-0" /> onKeyDown={handleKeyDown}
} />
type="text" </div>
onChange={(e) => setSearch(e.target.value)} <Button color="primary" onPress={() => setDoSetup(!doSetup)}>
onKeyUp={handleKeyUp} Settings
onKeyDown={handleKeyDown} </Button>
/> {doSetup && (
<Button color="success" className="text-white">
Save ({selectedLevel.length})
</Button>
)}
</div> </div>
<Table <Table
// selectionMode="multiple" // selectionMode="multiple"
aria-label="micro issue table" aria-label="micro issue table"
@ -216,9 +289,20 @@ export default function MasterUserLevelTable() {
"min-h-[50px] bg-transpararent text-black dark:text-white ", "min-h-[50px] bg-transpararent text-black dark:text-white ",
}} }}
> >
<TableHeader columns={columns}> <TableHeader columns={doSetup ? columns2 : columns}>
{(column) => ( {(column) => (
<TableColumn key={column.uid}>{column.name}</TableColumn> <TableColumn key={column.uid}>
{column.uid === "setup" ? (
<Checkbox
isSelected={selectAllLevel}
onValueChange={(e) => {
doMapping(e);
}}
></Checkbox>
) : (
column.name
)}
</TableColumn>
)} )}
</TableHeader> </TableHeader>
<TableBody <TableBody
@ -234,7 +318,7 @@ export default function MasterUserLevelTable() {
)} )}
</TableBody> </TableBody>
</Table> </Table>
<div className="mt-2"> <div className="mt-2 justify-center flex w-full">
<Pagination <Pagination
isCompact isCompact
showControls showControls

View File

@ -0,0 +1,607 @@
"use client";
import {
BannerIcon,
CloudUploadIcon,
CreateIconIon,
DeleteIcon,
DotsYIcon,
EyeIconMdi,
SearchIcon,
TimesIcon,
} from "@/components/icons";
import { close, error, loading, success } from "@/config/swal";
import {
deleteArticle,
getArticleByCategory,
getListArticle,
} from "@/service/article";
import { Article } from "@/types/globals";
import { convertDateFormat, convertDateFormatNoTimeV2 } from "@/utils/global";
import { Button } from "@heroui/button";
import {
Calendar,
Chip,
ChipProps,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Pagination,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectItem,
Spinner,
Switch,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
Textarea,
useDisclosure,
} from "@heroui/react";
import Link from "next/link";
import { Fragment, Key, useCallback, useEffect, useState } from "react";
import Cookies from "js-cookie";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useDropzone } from "react-dropzone";
import Image from "next/image";
import SuggestionsChart from "@/components/main/dashboard/chart/suggestions-line-chart";
import { parseDate } from "@internationalized/date";
import { getFeedbacks, getFeedbacksById } from "@/service/feedbacks";
const columns = [
{ name: "No", uid: "no" },
{ name: "Nama", uid: "name" },
{ name: "Email", uid: "email" },
{ name: "Kritik & Saran", uid: "suggestions" },
{ name: "Aksi", uid: "actions" },
];
const createArticleSchema = z.object({
id: z.string().optional(),
commentFromName: z.string().min(2, {
message: "Nama harus diisi",
}),
commentFromEmail: z.string().min(2, {
message: "Email harus diisi",
}),
description: z.string().min(2, {
message: "Pesan harus diisi",
}),
file: z.string().optional(),
});
export default function SuggestionsTable() {
const MySwal = withReactContent(Swal);
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [article, setArticle] = useState<any[]>([]);
const [showData, setShowData] = useState("10");
const [search, setSearch] = useState("");
const [isReply, setIsReply] = useState(false);
const [replyValue, setReplyValue] = useState("");
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: {
commentFromName: "",
description: "",
commentFromEmail: "",
file: "",
},
};
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
initState();
}, [page, showData]);
async function initState() {
const res = await getFeedbacks({ limit: showData, search: search });
getTableNumber(parseInt(showData), res.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
}
const getTableNumber = (limit: number, data: Article[]) => {
if (data) {
const startIndex = limit * (page - 1);
let iterate = 0;
const newData = data.map((value: any) => {
iterate++;
value.no = startIndex + iterate;
return value;
});
setArticle(newData);
}
};
async function doDelete(id: any) {
// loading();
const resDelete = await deleteArticle(id);
if (resDelete?.error) {
error(resDelete.message);
return false;
}
close();
success("Berhasil Hapus");
initState();
}
const handleDelete = (id: any) => {
MySwal.fire({
title: "Hapus Data",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData = {
commentFromName: values.commentFromName,
message: values.description,
commentFromEmail: values.commentFromEmail,
};
console.log("dataas", formData);
close();
// setRefresh(!refresh);
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then((result) => {
// if (result.isConfirmed) {
// }
// });
};
const openModal = async (id: number) => {
const res = await getFeedbacksById(id);
const data = res?.data?.data;
setValue("id", String(data?.id));
setValue("commentFromName", data?.commentFromName);
setValue("commentFromEmail", data?.commentFromEmail);
setValue("description", data?.message);
onOpen();
};
const getMonthYearName = (date: any) => {
const newDate = new Date(date);
const months = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember",
];
const year = newDate.getFullYear();
const month = months[newDate.getMonth()];
return month + " " + year;
};
const renderCell = useCallback(
(suggestion: any, columnKey: Key) => {
const cellValue = suggestion[columnKey as keyof any];
switch (columnKey) {
case "commentFromEmail":
return (
<Link
href={`https://www.google.com/`}
target="_blank"
className="text-primary hover:underline"
>
https://www.google.com/
</Link>
);
case "actions":
return (
<div className="relative flex justify-star items-center gap-2">
<Dropdown className="lg:min-w-[150px] bg-black text-white shadow border ">
<DropdownTrigger>
<Button isIconOnly size="lg" variant="light">
<DotsYIcon className="text-default-300" />
</Button>
</DropdownTrigger>
<DropdownMenu>
{/* <DropdownItem key="detail">
<Link href={`/admin/suggestion/detail/${article.id}`}>
<EyeIconMdi className="inline mr-2 mb-1" />
Detail
</Link>
</DropdownItem> */}
<DropdownItem
key="edit"
onPress={() => openModal(suggestion.id)}
>
<CreateIconIon className="inline mr-2 mb-1" />
Edit
</DropdownItem>
<DropdownItem
key="delete"
// onPress={() => handleDelete(article.id)}
>
<DeleteIcon
color="red"
size={18}
className="inline ml-1 mr-2 mb-1"
/>
Delete
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
default:
return cellValue;
}
},
[article]
);
let typingTimer: NodeJS.Timeout;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
initState();
}
const [startDateValue, setStartDateValue] = useState(
parseDate(convertDateFormatNoTimeV2(new Date()))
);
const [typeDate, setTypeDate] = useState("monthly");
return (
<>
<div className="flex flex-row gap-2">
<Button
color="primary"
variant={typeDate === "monthly" ? "solid" : "bordered"}
onPress={() => setTypeDate("monthly")}
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-md lg:rounded-lg"
>
Bulanan
</Button>
<Button
color="primary"
onPress={() => setTypeDate("weekly")}
variant={typeDate === "weekly" ? "solid" : "bordered"}
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-md lg:rounded-lg"
>
Mingguan
</Button>
<Button
color="primary"
onPress={() => setTypeDate("daily")}
variant={typeDate === "daily" ? "solid" : "bordered"}
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-md lg:rounded-lg"
>
Harian
</Button>
<div className="w-[140px]">
<Popover
placement="bottom"
classNames={{ content: ["!bg-transparent", "p-0"] }}
>
<PopoverTrigger>
<Button className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-md lg:rounded-lg">
{getMonthYearName(startDateValue)}
</Button>
</PopoverTrigger>
<PopoverContent className="bg-transparent">
<Calendar value={startDateValue} onChange={setStartDateValue} />
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex flex-row w-full h-full">
<div className="w-full h-[30vh] lg:h-[300px] text-black">
<SuggestionsChart
type={typeDate}
date={`${startDateValue.month} ${startDateValue.year}`}
/>
</div>
</div>
<div className="py-3">
<div className="flex flex-col items-start rounded-2xl gap-3">
<div className="flex flex-col md:flex-row gap-3 w-full">
<div className="flex flex-col gap-1 w-full lg:w-1/3">
<p className="font-semibold text-sm">Pencarian</p>
<Input
aria-label="Search"
classNames={{
inputWrapper: "bg-default-100",
input: "text-sm",
}}
labelPlacement="outside"
startContent={
<SearchIcon className="text-base text-default-400 pointer-events-none flex-shrink-0" />
}
type="text"
onChange={(e) => setSearch(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
/>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
<p className="font-semibold text-sm">Data</p>
<Select
label=""
variant="bordered"
labelPlacement="outside"
placeholder="Select"
selectedKeys={[showData]}
className="w-full"
classNames={{ trigger: "border-1" }}
onChange={(e) =>
e.target.value === "" ? "" : setShowData(e.target.value)
}
>
<SelectItem key="5" value="5">
5
</SelectItem>
<SelectItem key="10" value="10">
10
</SelectItem>
</Select>
</div>
<Button onPress={() => openModal(4)}>test</Button>
</div>
<Table
aria-label="micro issue table"
className="rounded-3xl"
classNames={{
th: "bg-white dark:bg-black text-black dark:text-white border-b-1 text-md",
base: "bg-white dark:bg-black border",
wrapper:
"min-h-[50px] bg-transpararent text-black dark:text-white ",
}}
>
<TableHeader columns={columns}>
{(column) => (
<TableColumn key={column.uid}>{column.name}</TableColumn>
)}
</TableHeader>
<TableBody
items={article}
emptyContent={"No data to display."}
loadingContent={<Spinner label="Loading..." />}
>
{(item: any) => (
<TableRow key={item.id}>
{(columnKey) => (
<TableCell>{renderCell(item, columnKey)}</TableCell>
)}
</TableRow>
)}
</TableBody>
</Table>
<div className="my-2 w-full flex justify-center">
<Pagination
isCompact
showControls
showShadow
color="primary"
classNames={{
base: "bg-transparent",
wrapper: "bg-transparent",
}}
page={page}
total={totalPage}
onChange={(page) => setPage(page)}
/>
</div>
</div>
</div>
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="3xl">
<ModalContent>
{() => (
<>
<ModalHeader className="flex flex-col gap-1">
Kritik Saran
</ModalHeader>
<ModalBody>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-1">
<p className="text-sm">Nama</p>
<Controller
control={control}
name="commentFromName"
render={({ field: { onChange, value } }) => (
<Input
type="text"
id="commentFromName"
placeholder=""
label=""
value={value}
onChange={onChange}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
/>
{errors?.commentFromName && (
<p className="text-red-400 text-sm">
{errors.commentFromName?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Email</p>
<Controller
control={control}
name="commentFromEmail"
render={({ field: { onChange, value } }) => (
<Input
type="text"
id="commentFromEmail"
placeholder=""
label=""
value={value}
onChange={onChange}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
/>
{errors?.commentFromEmail && (
<p className="text-red-400 text-sm">
{errors.commentFromEmail?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<Textarea
type="text"
id="description"
placeholder=""
label=""
value={value}
onChange={onChange}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm">
{errors.description?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<a
className="text-sm cursor-pointer hover:underline text-primary"
onClick={() => setIsReply(!isReply)}
>
Balas
</a>
{isReply && (
<Textarea
type="text"
id="description"
placeholder=""
label=""
value={replyValue}
onValueChange={setReplyValue}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
</div>
<ModalFooter className="self-end grow items-end">
{isReply && (
<Button
color="primary"
type="submit"
isDisabled={replyValue == ""}
>
Kirim
</Button>
)}
<Button color="danger" variant="light" onPress={onClose}>
Tutup
</Button>
</ModalFooter>
</form>
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</>
);
}

View File

@ -1,55 +1,46 @@
{ {
"data": [ "data": [
{ {
"id": "1", "id": 1,
"year": "2024", "year": 2024,
"month": "november", "month": 11,
"comment": [ "suggestions": [
14, 32, 10, 21, 15, 18, 24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13, 14, 32, 10, 21, 15, 18, 24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13,
20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22 20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22
],
"view": [
20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22, 14, 32, 10, 21, 15, 18,
24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13
],
"share": [
24, 30, 12, 25, 19, 28, 14, 2, 31, 27, 13, 17, 22, 31, 27, 13, 18, 21,
26, 23, 14, 18, 24, 30, 12, 25, 19, 28, 14, 17
] ]
}, },
{ {
"id": "2", "id": 2,
"year": "2024", "year": 2024,
"month": "december", "month": 12,
"comment": [ "suggestions": [
15, 23, 19, 14, 18, 20, 22, 17, 21, 19, 23, 16, 25, 20, 18, 19, 22, 24, 15, 23, 19, 14, 18, 20, 22, 17, 21, 19, 23, 16, 25, 20, 18, 19, 22, 24,
15, 18, 21, 26, 28, 23, 17, 20, 19, 22, 22, 42, 32 15, 18, 21, 26, 28, 23, 17, 20, 19, 22, 22, 42, 32
],
"view": [
20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22, 14, 32, 10, 21, 15, 18,
24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13, 21
],
"share": [
14, 32, 10, 21, 15, 18, 24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13,
20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22, 32
] ]
}, },
{ {
"id": "3", "id": 3,
"year": "2025", "year": 2025,
"month": "january", "month": 1,
"comment": [ "suggestions": [
21, 24, 19, 27, 29, 23, 18, 20, 26, 22, 24, 30, 25, 19, 17, 21, 27, 23, 14, 32, 10, 21, 15, 18, 24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13,
29, 25, 22 20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22, 21
],
"view": [
21, 24, 19, 25, 19, 17, 21, 27, 23, 29, 25, 22, 27, 29, 23, 18, 20, 26,
22, 24, 30
],
"share": [
22, 24, 30, 25, 19, 17, 21, 27, 23, 29, 25, 22, 21, 24, 19, 27, 29, 23,
18, 20, 26
] ]
},
{
"id": 4,
"year": 2025,
"month": 2,
"suggestions": [
15, 23, 19, 14, 18, 20, 22, 17, 21, 19, 23, 16, 25, 20, 18, 19, 22, 24,
15, 18, 21, 26, 28, 23, 17, 20, 19, 22
]
},
{
"id": 5,
"year": 2025,
"month": 3,
"suggestions": [14, 32, 10, 21, 15, 18]
} }
] ]
} }

35
service/feedbacks.ts Normal file
View File

@ -0,0 +1,35 @@
import {
httpDeleteInterceptor,
httpGet,
httpPost,
httpPut,
} from "./http-config/axios-base-service";
import Cookies from "js-cookie";
const token = Cookies.get("access_token");
export async function createFeedback(data: any) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/feedbacks`;
return await httpPost(pathUrl, headers, data);
}
export async function getFeedbacks(data: any) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/feedbacks?limit=${data?.limit || ""}&message=${
data?.search || ""
}`;
return await httpGet(pathUrl, headers);
}
export async function getFeedbacksById(id: number) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/feedbacks/${id}`;
return await httpGet(pathUrl, headers);
}

View File

@ -12,7 +12,7 @@ export async function getAllUserLevels(data?: any) {
return await httpGet( return await httpGet(
`user-levels?limit=${data?.limit || ""}&levelNumber=${ `user-levels?limit=${data?.limit || ""}&levelNumber=${
data?.levelNumber || "" data?.levelNumber || ""
}&name=${data?.search || ""}`, }&name=${data?.search || ""}&page=${data?.page || "1"}`,
headers headers
); );
} }