fix:create edit deatil category

This commit is contained in:
Rama Priyanto 2025-01-07 19:59:32 +07:00
parent e9d22ad3f2
commit 0cd97530ca
6 changed files with 753 additions and 207 deletions

View File

@ -103,7 +103,8 @@ const columns: ColumnDef<any>[] = [
</Button> </Button>
</MenubarTrigger> </MenubarTrigger>
<MenubarContent className="flex flex-col gap-2 justify-center items-start p-4"> <MenubarContent className="flex flex-col gap-2 justify-center items-start p-4">
<EditCategoryModal data={row.original} /> <EditCategoryModal id={row.original.id} isDetail={true} />
<EditCategoryModal id={row.original.id} />
<a <a
onClick={() => categoryDelete(row.original.id)} onClick={() => categoryDelete(row.original.id)}
className="hover:underline cursor-pointer hover:text-destructive" className="hover:underline cursor-pointer hover:text-destructive"

View File

@ -25,11 +25,18 @@ import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { getUserRoles, postCategory } from "@/service/settings/settings"; import { getUserRoles, postCategory } from "@/service/settings/settings";
import { useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js"; import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal"; import { close, error, loading } from "@/config/swal";
import { UnitMapping } from "./unit-mapping";
import { useToast } from "@/components/ui/use-toast";
import { stringify } from "querystring";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import Image from "next/image";
import { Upload } from "tus-js-client";
const FormSchema = z.object({ const FormSchema = z.object({
title: z.string({ title: z.string({
@ -48,17 +55,11 @@ const FormSchema = z.object({
.refine((value) => value.some((item) => item), { .refine((value) => value.some((item) => item), {
message: "Required", message: "Required",
}), }),
publishTo: z.string({ publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
required_error: "Required", message: "Required",
}), }),
file: z
.instanceof(File, { file: z.string().optional(),
message: "Invalid file format",
})
.optional()
.refine((file) => file && file.size > 0, {
message: "File is required",
}),
}); });
const listContent = [ const listContent = [
@ -80,26 +81,62 @@ const listContent = [
}, },
]; ];
const publishToList = [
{
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function CreateCategoryModal() { export default function CreateCategoryModal() {
const router = useRouter(); const router = useRouter();
const { toast } = useToast();
const [files, setFiles] = useState<File[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [satkerData, setSatkerData] = useState<string[]>([]);
const [unitData, setUnitData] = useState<string[]>([]);
const [userList, setUserList] = useState< const [userList, setUserList] = useState<
{ id: string; name: string; isInternal: boolean }[] { id: string; name: string; isInternal: boolean }[]
>([]); >([]);
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { contentType: [], selectedUser: [], publishTo: "national" }, defaultValues: { contentType: [], selectedUser: [], publishTo: [] },
}); });
const contentType = form.watch("contentType"); const contentType = form.watch("contentType");
const isAllContentChecked = listContent.every((item) => const isAllContentChecked = listContent.every((item) =>
contentType?.includes(item.id) contentType?.includes(item.id)
); );
const users = form.watch("selectedUser"); const users = form.watch("selectedUser");
const isAllUserChecked = userList.every((item) => users?.includes(item.id)); const isAllUserChecked = userList.every((item) => users?.includes(item.id));
const target = form.watch("publishTo");
const isAllTargetChecked = publishToList.every((item) =>
target?.includes(item.id)
);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
});
useEffect(() => { useEffect(() => {
getRoles(); getRoles();
}, []); }, []);
@ -111,36 +148,77 @@ export default function CreateCategoryModal() {
dataRoles[i].id = String(dataRoles[i].id); dataRoles[i].id = String(dataRoles[i].id);
} }
setUserList(dataRoles); setUserList(dataRoles);
console.log("dasda", dataRoles);
} }
const onSubmit = async (data: z.infer<typeof FormSchema>) => { const onSubmit = async (data: z.infer<typeof FormSchema>) => {
if (data.file instanceof Blob) { if (data.publishTo.includes("polda") || data.publishTo.includes("satker")) {
const formMedia = new FormData(); if (
(data.publishTo.includes("polda") && unitData?.length < 1) ||
loading(); (data.publishTo.includes("satker") && satkerData?.length < 1)
) {
formMedia.append("name", data.title); const poldaValidation = data.publishTo.includes("polda");
formMedia.append("description", data.description); const satkerValidation = data.publishTo.includes("satker");
formMedia.append("mediaTypes", data.contentType.join(",")); toast({
formMedia.append("file", data.file); title:
formMedia.append("publishedFor", data.selectedUser.sort().join(",")); poldaValidation && satkerValidation
formMedia.append( ? "Pilih Polda dan Satker tujuan"
"isInt", : `Pilih ${poldaValidation ? "Polda" : "Satker"} tujuan`,
data.publishTo === "national" ? "false" : "true" variant: "destructive",
); });
console.log(formMedia); } else {
const response = await postCategory(formMedia); save(data);
close();
if (response?.error == true) {
error(response.message);
return false;
} }
router.push("/admin/settings/category?dataChange=true"); } else {
save(data);
} }
}; };
const save = async (data: z.infer<typeof FormSchema>) => {
const formMedia = new FormData();
loading();
const unit = unitData?.join(",");
const satker = satkerData?.join(",");
const join =
unitData?.length > 0 && satkerData?.length > 0
? unit + "," + satker
: unitData?.length > 0
? unit
: satkerData?.length > 0
? satker
: "";
formMedia.append("name", data.title);
formMedia.append("description", data.description);
formMedia.append("mediaTypes", data.contentType.join(","));
formMedia.append("publishedFor", data.selectedUser.join(","));
formMedia.append("file", files[0]);
formMedia.append("publishedLocation", data.publishTo.sort().join(","));
formMedia.append("publishedLocationLevel", join);
const response = await postCategory(formMedia);
close();
if (response?.error) {
toast({ title: stringify(response.message), variant: "destructive" });
return false;
}
toast({
title: "Succes",
description: "Kategori berhasil dibuat",
});
router.push("/admin/settings/category?dataChange=true");
setIsOpen(false);
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button color="primary" size="md"> <Button color="primary" size="md">
Tambah Kategori Tambah Kategori
@ -299,31 +377,87 @@ export default function CreateCategoryModal() {
<FormField <FormField
control={form.control} control={form.control}
name="publishTo" name="publishTo"
render={({ field }) => ( render={() => (
<FormItem className="space-y-3"> <FormItem>
<FormLabel>Wilayah Publish</FormLabel> <FormLabel>Wilayah Publish</FormLabel>
<FormControl>
<RadioGroup <div className="flex flex-row items-center gap-2">
onValueChange={field.onChange} <div className="flex gap-3 items-center">
value={field.value} <Checkbox
className="flex flex-row gap-2" id="all"
> checked={isAllTargetChecked}
<FormItem className="flex items-center space-x-3 space-y-0"> onCheckedChange={(checked) => {
<FormControl> if (checked) {
<RadioGroupItem value="national" /> form.setValue(
</FormControl> "publishTo",
<FormLabel className="font-normal">Nasional</FormLabel> publishToList.map((item) => item.id)
</FormItem> );
<FormItem className="flex items-center space-x-3 space-y-0"> } else {
<FormControl> form.setValue("publishTo", []);
<RadioGroupItem value="international" /> }
</FormControl> }}
<FormLabel className="font-normal"> />
Internasional <label htmlFor="all" className="text-sm">
</FormLabel> Semua
</FormItem> </label>
</RadioGroup> </div>
</FormControl> {publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
isDetail={false}
sendDataToParent={(data) => setUnitData(data)}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
isDetail={false}
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
/>
)}
</>
))}
</div>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -350,41 +484,36 @@ export default function CreateCategoryModal() {
name="file" name="file"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Nama Kategori</FormLabel> <FormLabel>Thumbnail Category</FormLabel>
{!field.value && ( {files.length < 1 && (
<div className="flex items-center justify-center w-full"> <Fragment>
<label <div {...getRootProps({ className: "dropzone" })}>
htmlFor="dropzone-file" <input {...getInputProps()} />
className="flex flex-col items-center justify-center w-full h-28 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600" <div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
> <CloudUpload className="text-default-300 w-10 h-10" />
<div className="flex flex-col items-center justify-center pt-5 pb-6"> <h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400"> Tarik file disini atau klik untuk upload.
<span className="font-semibold">Unggah Gambar</span> </h4>
</p> <div className=" text-xs text-muted-foreground">
( Upload file dengan format .jpg, .jpeg, atau .png.
Ukuran maksimal 100mb.)
</div>
</div> </div>
<input </div>
id="dropzone-file" </Fragment>
type="file"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
field.onChange(file);
}
}}
/>
</label>
</div>
)} )}
{field.value && ( {files.length > 0 && (
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<img <img
src={URL.createObjectURL(field.value)} src={URL.createObjectURL(files[0])}
className="w-[30%]" className="w-[30%]"
alt="thumbnail" alt="thumbnail"
/> />
<a onClick={() => form.setValue("file", undefined)}> <a
onClick={() => handleRemoveFile(files[0])}
className="cursor-pointer"
>
<Icon icon="fa-solid:times" color="red" /> <Icon icon="fa-solid:times" color="red" />
</a> </a>
</div> </div>
@ -401,11 +530,7 @@ export default function CreateCategoryModal() {
<FormItem> <FormItem>
<FormLabel>Deskripsi</FormLabel> <FormLabel>Deskripsi</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea placeholder="Deskripsi" {...field} />
placeholder="Deskripsi"
// className="resize-none"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@ -24,13 +24,20 @@ import {
import { useRouter } from "@/i18n/routing"; import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { getUserRoles, postCategory } from "@/service/settings/settings"; import {
import { useEffect, useState } from "react"; getCategoryDetail,
getUserRoles,
postCategory,
} from "@/service/settings/settings";
import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js"; import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal"; import { close, error, loading } from "@/config/swal";
import { UnitMapping } from "./unit-mapping";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
const FormSchema = z.object({ const FormSchema = z.object({
title: z.string({ title: z.string({
required_error: "Required", required_error: "Required",
@ -48,20 +55,14 @@ const FormSchema = z.object({
.refine((value) => value.some((item) => item), { .refine((value) => value.some((item) => item), {
message: "Required", message: "Required",
}), }),
publishTo: z.string({ publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
required_error: "Required", message: "Required",
}), }),
file: z
.instanceof(File, {
message: "Invalid file format",
})
.optional()
.refine((file) => file && file.size > 0, {
message: "File is required",
}),
id: z.string({ id: z.string({
required_error: "Required", required_error: "Required",
}), }),
file: z.string().optional(),
}); });
const listContent = [ const listContent = [
@ -83,38 +84,110 @@ const listContent = [
}, },
]; ];
export default function EditCategoryModal(props: { data: any }) { const publishToList = [
const { data } = props; {
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function EditCategoryModal(props: {
id: string;
isDetail?: boolean;
}) {
const { id, isDetail } = props;
const [files, setFiles] = useState<File[]>([]);
const [isOpen, setIsOpen] = useState(false);
const { toast } = useToast();
const router = useRouter(); const router = useRouter();
const [openModal, setOpenModal] = useState(false); const [initDataUnit, setInitDataUnit] = useState<string[]>([]);
const [satkerData, setSatkerData] = useState<string[]>([]);
const [unitData, setUnitData] = useState<string[]>([]);
const [userList, setUserList] = useState< const [userList, setUserList] = useState<
{ id: string; name: string; isInternal: boolean }[] { id: string; name: string; isInternal: boolean }[]
>([]); >([]);
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { contentType: [], selectedUser: [], publishTo: "national" }, defaultValues: { contentType: [], selectedUser: [], publishTo: [] },
});
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
}); });
useEffect(() => { useEffect(() => {
form.setValue("id", String(data?.id)); const initFetch = async () => {
form.setValue("title", String(data?.name)); const req = await getCategoryDetail(id);
form.setValue("description", String(data?.description)); const data = req?.data?.data;
form.setValue("contentType", data?.mediaTypes.split(",")); console.log("dataC", data);
form.setValue("selectedUser", data.publishedFor.split(",")); form.setValue("id", String(data?.id));
form.setValue("publishTo", data?.isInt ? "international" : "national"); form.setValue("title", String(data?.name));
}, [data]); form.setValue("description", String(data?.description));
form.setValue("contentType", data?.mediaTypes?.split(","));
form.setValue(
"selectedUser",
removeAndReturn(data?.publishedFor, [2, 3, 4])
);
form.setValue("publishTo", data?.publishedLocation?.split(","));
setUnitData(filterString(data?.publishedLocationLevel, "under"));
setSatkerData(filterString(data?.publishedLocationLevel, "above"));
};
initFetch();
}, [id]);
function removeAndReturn(inputString: string, toRemove: number[]) {
const numbers = inputString.split(",").map(Number);
const filteredNumbers = numbers.filter((num) => !toRemove.includes(num));
return filteredNumbers.map(String);
}
function filterString(inputString: string, type: string) {
const numbers = inputString.split(",").map(Number);
if (type === "above") {
const above700 = numbers.filter((num) => num > 700);
return above700.map(String);
} else {
const under700 = numbers.filter((num) => num < 700);
return under700.map(String);
}
}
const contentType = form.watch("contentType"); const contentType = form.watch("contentType");
const isAllContentChecked = listContent.every((item) => const isAllContentChecked = listContent.every((item) =>
contentType?.includes(item.id) contentType?.includes(item.id)
); );
const users = form.watch("selectedUser"); const users = form.watch("selectedUser");
const isAllUserChecked = userList.every((item) => users?.includes(item.id)); const isAllUserChecked = userList.every((item) => users?.includes(item.id));
const target = form.watch("publishTo");
const isAllTargetChecked = publishToList.every((item) =>
target?.includes(item.id)
);
useEffect(() => { useEffect(() => {
getRoles(); getRoles();
}, []); }, []);
@ -126,38 +199,63 @@ export default function EditCategoryModal(props: { data: any }) {
dataRoles[i].id = String(dataRoles[i].id); dataRoles[i].id = String(dataRoles[i].id);
} }
setUserList(dataRoles); setUserList(dataRoles);
console.log("dasda", dataRoles);
} }
function removeDuplicates(inputString: string): string {
const numbers = inputString.split(",");
const uniqueNumbers = Array.from(new Set(numbers));
return uniqueNumbers.join(",");
}
const onSubmit = async (data: z.infer<typeof FormSchema>) => { const onSubmit = async (data: z.infer<typeof FormSchema>) => {
if (data.file instanceof Blob) { const formMedia = new FormData();
const formMedia = new FormData();
loading(); loading();
formMedia.append("id", data.id); const unit = unitData?.join(",");
formMedia.append("name", data.title); const satker = satkerData?.join(",");
formMedia.append("description", data.description); const join =
formMedia.append("mediaTypes", data.contentType.join(",")); unitData?.length > 0 && satkerData?.length > 0
formMedia.append("file", data.file); ? unit + "," + satker
formMedia.append("publishedFor", data.selectedUser.sort().join(",")); : unitData?.length > 0
formMedia.append( ? unit
"isInt", : satkerData?.length > 0
data.publishTo === "national" ? "false" : "true" ? satker
); : "";
console.log(formMedia);
const response = await postCategory(formMedia); formMedia.append("id", data.id);
close(); formMedia.append("name", data.title);
if (response?.error == true) { formMedia.append("description", data.description);
error(response.message); formMedia.append("mediaTypes", data.contentType.join(","));
return false; formMedia.append("publishedFor", data.selectedUser.join(","));
} formMedia.append("file", files[0]);
router.push("/admin/settings/category?dataChange=true"); formMedia.append("publishedLocation", data.publishTo.sort().join(","));
formMedia.append("publishedLocationLevel", removeDuplicates(join));
const response = await postCategory(formMedia);
close();
if (response?.error == true) {
error(response.message);
return false;
} }
toast({
title: "Succes",
description: "Kategori berhasil diubah",
});
router.push("/admin/settings/category?dataChange=true");
setIsOpen(false);
}; };
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger> <DialogTrigger>
<a className="hover:underline">Edit Kategori</a> <a onClick={() => setIsOpen(true)} className="hover:underline">
{isDetail ? "Detail" : "Edit"}
</a>
</DialogTrigger> </DialogTrigger>
<DialogContent size="md"> <DialogContent size="md">
<DialogHeader> <DialogHeader>
@ -180,6 +278,7 @@ export default function EditCategoryModal(props: { data: any }) {
<Checkbox <Checkbox
id="all" id="all"
checked={isAllContentChecked} checked={isAllContentChecked}
disabled={isDetail}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
if (checked) { if (checked) {
form.setValue( form.setValue(
@ -209,6 +308,7 @@ export default function EditCategoryModal(props: { data: any }) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FormControl> <FormControl>
<Checkbox <Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)} checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
return checked return checked
@ -250,6 +350,7 @@ export default function EditCategoryModal(props: { data: any }) {
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<Checkbox <Checkbox
id="all" id="all"
disabled={isDetail}
checked={isAllUserChecked} checked={isAllUserChecked}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
if (checked) { if (checked) {
@ -280,6 +381,7 @@ export default function EditCategoryModal(props: { data: any }) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FormControl> <FormControl>
<Checkbox <Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)} checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
return checked return checked
@ -312,31 +414,91 @@ export default function EditCategoryModal(props: { data: any }) {
<FormField <FormField
control={form.control} control={form.control}
name="publishTo" name="publishTo"
render={({ field }) => ( render={() => (
<FormItem className="space-y-3"> <FormItem>
<FormLabel>Wilayah Publish</FormLabel> <FormLabel>Wilayah Publish</FormLabel>
<FormControl>
<RadioGroup <div className="flex flex-row items-center gap-2">
onValueChange={field.onChange} <div className="flex gap-3 items-center">
value={field.value} <Checkbox
className="flex flex-row gap-2" id="all"
> checked={isAllTargetChecked}
<FormItem className="flex items-center space-x-3 space-y-0"> disabled={isDetail}
<FormControl> onCheckedChange={(checked) => {
<RadioGroupItem value="national" /> if (checked) {
</FormControl> form.setValue(
<FormLabel className="font-normal">Nasional</FormLabel> "publishTo",
</FormItem> publishToList.map((item) => item.id)
<FormItem className="flex items-center space-x-3 space-y-0"> );
<FormControl> } else {
<RadioGroupItem value="international" /> form.setValue("publishTo", []);
</FormControl> }
<FormLabel className="font-normal"> }}
Internasional />
</FormLabel> <label htmlFor="all" className="text-sm">
</FormItem> Semua
</RadioGroup> </label>
</FormControl> </div>
{publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
sendDataToParent={(data) => setUnitData(data)}
isDetail={isDetail ? true : false}
initData={unitData}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
isDetail={isDetail ? true : false}
initData={satkerData}
/>
)}
</>
))}
</div>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -349,6 +511,7 @@ export default function EditCategoryModal(props: { data: any }) {
<FormLabel>Nama Kategori</FormLabel> <FormLabel>Nama Kategori</FormLabel>
<Input <Input
value={field.value} value={field.value}
readOnly={isDetail}
placeholder="Masukkan Nama Kategori" placeholder="Masukkan Nama Kategori"
onChange={field.onChange} onChange={field.onChange}
/> />
@ -357,47 +520,41 @@ export default function EditCategoryModal(props: { data: any }) {
</FormItem> </FormItem>
)} )}
/> />
{/* <FormField
<FormField
control={form.control} control={form.control}
name="file" name="file"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Nama Kategori</FormLabel> <FormLabel>Thumbnail Category</FormLabel>
{!field.value && ( {files.length < 1 && (
<div className="flex items-center justify-center w-full"> <Fragment>
<label <div {...getRootProps({ className: "dropzone" })}>
htmlFor="dropzone-file" <input {...getInputProps()} />
className="flex flex-col items-center justify-center w-full h-28 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600" <div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
> <CloudUpload className="text-default-300 w-10 h-10" />
<div className="flex flex-col items-center justify-center pt-5 pb-6"> <h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400"> Tarik file disini atau klik untuk upload.
<span className="font-semibold">Unggah Gambar</span> </h4>
</p> <div className=" text-xs text-muted-foreground">
( Upload file dengan format .jpg, .jpeg, atau .png.
Ukuran maksimal 100mb.)
</div>
</div> </div>
<input </div>
id="dropzone-file" </Fragment>
type="file"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
field.onChange(file);
}
}}
/>
</label>
</div>
)} )}
{field.value && ( {files.length > 0 && (
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<img <img
src={URL.createObjectURL(field.value)} src={URL.createObjectURL(files[0])}
className="w-[30%]" className="w-[30%]"
alt="thumbnail" alt="thumbnail"
/> />
<a onClick={() => form.setValue("file", undefined)}> <a
onClick={() => handleRemoveFile(files[0])}
className="cursor-pointer"
>
<Icon icon="fa-solid:times" color="red" /> <Icon icon="fa-solid:times" color="red" />
</a> </a>
</div> </div>
@ -406,7 +563,8 @@ export default function EditCategoryModal(props: { data: any }) {
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> /> */}
<FormField <FormField
control={form.control} control={form.control}
name="description" name="description"
@ -415,6 +573,7 @@ export default function EditCategoryModal(props: { data: any }) {
<FormLabel>Deskripsi</FormLabel> <FormLabel>Deskripsi</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
readOnly={isDetail}
placeholder="Deskripsi" placeholder="Deskripsi"
// className="resize-none" // className="resize-none"
{...field} {...field}

View File

@ -61,7 +61,7 @@ const AdminCategoryTable = () => {
const [rowSelection, setRowSelection] = React.useState({}); const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({ const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 50, pageSize: 10,
}); });
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
@ -108,16 +108,16 @@ const AdminCategoryTable = () => {
async function fetchData() { async function fetchData() {
try { try {
loading(); loading();
const response = await getCategories(); const response = await getCategories(page - 1);
const data = response?.data?.data; const data = response?.data?.data;
const contentData = data?.content; const contentData = data?.content;
contentData.forEach((item: any, index: number) => { contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * 50 + index + 1; item.no = (page - 1) * 10 + index + 1;
}); });
setDataTable(contentData); setDataTable(contentData);
setTotalData(data?.totalElements); setTotalData(data?.totalElements);
setTotalPage(1); setTotalPage(data?.totalPages);
close(); close();
} catch (error) { } catch (error) {
console.error("Error fetching tasks:", error); console.error("Error fetching tasks:", error);
@ -172,11 +172,11 @@ const AdminCategoryTable = () => {
)} )}
</TableBody> </TableBody>
</Table> </Table>
{/* <TablePagination <TablePagination
table={table} table={table}
totalData={totalData} totalData={totalData}
totalPage={totalPage} totalPage={totalPage}
/> */} />
</div> </div>
); );
}; };

View File

@ -0,0 +1,253 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useEffect, useState } from "react";
import { getUserLevelForAssignments } from "@/service/task";
const FormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "Required",
}),
});
interface UnitType {
id: number;
name: string;
subDestination: { id: number; name: string }[];
}
export function UnitMapping(props: {
unit: string;
sendDataToParent: (data: string[]) => void;
isDetail: boolean;
initData?: string[];
}) {
const { unit, sendDataToParent, isDetail } = props;
const [unitList, setUnitList] = useState<UnitType[]>([]);
const [satkerList, setSatkerList] = useState<{ id: number; name: string }[]>(
[]
);
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
items: props.initData ? props.initData : [],
},
});
useEffect(() => {
async function initState() {
const response = await getUserLevelForAssignments();
setupUnit(response?.data?.data.list);
}
initState();
}, []);
const unitType = form.watch("items");
const isAllUnitChecked = unitList.every((item) =>
unitType?.includes(String(item.id))
);
const isAllSatkerChecked = satkerList.every((item) =>
unitType?.includes(String(item.id))
);
const setupUnit = (data: UnitType[]) => {
const temp = data.filter((a) => a.name.includes("POLDA"));
const temp2 = data.filter((a) => a.name.includes("SATKER"));
setUnitList(temp);
setSatkerList(temp2[0].subDestination);
};
useEffect(() => {
sendDataToParent(form.getValues("items"));
}, [unitType]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<a
onClick={() => setIsOpen(true)}
className="text-primary cursor-pointer text-xs mr-3"
>
Pilih {unit}
</a>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{unit}</DialogTitle>
</DialogHeader>
{unit === "Polda" ? (
<Form {...form}>
<form className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<Checkbox
id={`all-${unit}`}
checked={isAllUnitChecked}
disabled={isDetail}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"items",
unitList.map((item) => String(item.id))
);
} else {
form.setValue("items", []);
}
}}
/>
<label htmlFor="all" className="text-sm text-black uppercase">
SEMUA {unit}
</label>
</div>
<FormField
control={form.control}
name="items"
render={() => (
<FormItem
className={`grid grid-cols-${unit === "Polda" ? "2" : "3"}`}
>
{unitList?.map((item: any) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={String(item.id)}
className="flex flex-row items-center space-x-3 space-y-0"
>
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(
String(item.id)
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
String(item.id),
])
: field.onChange(
field.value?.filter(
(value) => value !== String(item.id)
)
);
}}
/>
</FormControl>
<p className="text-sm text-black">{item.name}</p>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
) : (
<Form {...form}>
<form className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<Checkbox
id={`all-${unit}`}
checked={isAllSatkerChecked}
disabled={isDetail}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"items",
satkerList.map((item) => String(item.id))
);
} else {
form.setValue("items", []);
}
}}
/>
<label htmlFor="all" className="text-sm text-black uppercase">
SEMUA {unit}
</label>
</div>
<FormField
control={form.control}
name="items"
render={() => (
<FormItem
className={`grid grid-cols-${unit === "Polda" ? "2" : "3"}`}
>
{satkerList?.map((item: any) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={String(item.id)}
className="flex flex-row items-center space-x-3 space-y-0"
>
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(
String(item.id)
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
String(item.id),
])
: field.onChange(
field.value?.filter(
(value) => value !== String(item.id)
)
);
}}
/>
</FormControl>
<p className="text-sm text-black">{item.name}</p>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
)}
</DialogContent>
</Dialog>
);
}

View File

@ -4,8 +4,8 @@ import {
httpPostInterceptor, httpPostInterceptor,
} from "../http-config/http-interceptor-service"; } from "../http-config/http-interceptor-service";
export async function getCategories() { export async function getCategories(page: number) {
const url = "media/categories/list?enablePage=1&size=50&sort=desc&sortBy=id"; const url = `media/categories/list?enablePage=1&page=${page}&size=10&sort=desc&sortBy=id`;
return httpGetInterceptor(url); return httpGetInterceptor(url);
} }
@ -24,9 +24,17 @@ export async function getUserRoles() {
return httpGetInterceptor(url); return httpGetInterceptor(url);
} }
export async function getCategoryDetail(id: string) {
const url = `media/categories/${id}`;
return httpGetInterceptor(url);
}
export async function postCategory(data: any) { export async function postCategory(data: any) {
const url = "media/categories"; const url = "media/categories";
return httpPostInterceptor(url, data); const headers = {
"Content-Type": "multipart/form-data",
};
return httpPostInterceptor(url, data, { headers });
} }
export async function getPrivacy(id: string) { export async function getPrivacy(id: string) {