update data
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Anang Yusman 2026-02-25 17:11:26 +08:00
parent bfa227264b
commit 73e9273bee
18 changed files with 2499 additions and 74 deletions

View File

@ -0,0 +1,341 @@
"use client";
import { AddIcon, CloudUploadIcon, TimesIcon } from "@/components/icons";
import CategoriesTable from "@/components/table/master-categories/categories-table";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { useEffect, useState } from "react";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { useDropzone } from "react-dropzone";
import { close, error, loading } from "@/config/swal";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import Image from "next/image";
import { getArticleByCategory } from "@/service/article";
import {
createCategory,
uploadCategoryThumbnail,
} from "@/service/master-categories";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import useDisclosure from "@/components/useDisclosure";
const createArticleSchema = z.object({
title: z.string().min(2, {
message: "Judul harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
tags: z.array(z.string()),
// parent: z.array(categorySchema).nonempty({
// message: "Kategori harus memiliki setidaknya satu item",
// }),
// tags: z.array(z.string()).nonempty({
// message: "Minimal 1 tag",
// }),
});
interface CategoryType {
id: number;
label: string;
value: number;
}
export default function MasterCategoryTable() {
const MySwal = withReactContent(Swal);
const animatedComponents = makeAnimated();
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [files, setFiles] = useState<File[]>([]);
const [refresh, setRefresh] = useState(false);
const [tag, setTag] = useState("");
const [selectedParent, setSelectedParent] = useState<any>();
const [isDetail] = useState<any>();
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", tags: [] },
};
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
});
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
formState: { errors },
setValue,
getValues,
setError,
clearErrors,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
fetchCategory();
}, []);
const fetchCategory = async () => {
const res = await getArticleByCategory();
if (res?.data?.data) {
setupCategory(res?.data?.data);
}
};
const setupCategory = (data: any) => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.title,
value: element.id,
});
}
setListCategory(temp);
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
console.log("values,", values);
loading();
const formData = {
title: values.title,
statusId: 1,
parentId: selectedParent ? selectedParent.id : 0,
tags: values.tags.join(","),
description: values.description,
};
const response = await createCategory(formData);
if (response?.error) {
error(response.message);
return false;
}
const categoryId = response?.data?.data?.id;
const formFiles = new FormData();
formFiles.append("files", files[0]);
const resFile = await uploadCategoryThumbnail(categoryId, formFiles);
if (resFile?.error) {
error(resFile.message);
return false;
}
close();
setRefresh(!refresh);
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
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">
<Button
size="default"
className="bg-[#F07C00] text-white w-full lg:w-fit flex items-center gap-2"
onClick={onOpen}
>
Tambah Kategori
<AddIcon />
</Button>
<CategoriesTable triggerRefresh={refresh} />
</div>
</div>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl">
<DialogHeader>
<DialogTitle>Kategori Baru</DialogTitle>
</DialogHeader>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-1">
<p className="text-sm">Nama Kategori</p>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<Input
id="title"
value={value}
onChange={onChange}
readOnly={isDetail}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.title && (
<p className="text-red-400 text-sm">{errors.title?.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
id="description"
value={value}
onChange={onChange}
readOnly={isDetail}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm">
{errors.description?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Parent</p>
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: () =>
"!rounded-lg bg-white !border !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
value={selectedParent}
isDisabled={isDetail}
onChange={setSelectedParent}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable
isSearchable
isMulti={false}
placeholder="Kategori..."
name="sub-module"
options={listCategory}
/>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Tag Terkait</p>
<Controller
control={control}
name="tags"
render={({ field: { value } }) => (
<Input
id="tags"
value={tag}
onChange={(e) => setTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (tag.trim() !== "") {
setValue("tags", [...value, tag.trim()]);
setTag("");
e.preventDefault();
}
}
}}
className="w-full border rounded-lg"
/>
)}
/>
<div className="flex flex-wrap gap-1 mt-2">
{getValues("tags")?.map((item, index) => (
<div
key={index}
className="bg-blue-500 text-white px-2 py-1 rounded-full text-sm flex items-center gap-1"
>
{item}
<button
type="button"
onClick={() => {
const filteredTags = getValues("tags").filter(
(tag) => tag !== item,
);
if (filteredTags.length === 0) {
setError("tags", {
type: "manual",
message: "Tags tidak boleh kosong",
});
} else {
clearErrors("tags");
setValue("tags", filteredTags);
}
}}
>
&times;
</button>
</div>
))}
</div>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Thumbnail</p>
{files.length < 1 && (
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border rounded-md py-[52px] flex flex-col items-center">
<CloudUploadIcon />
<h4 className="text-2xl font-medium mb-1 mt-3 text-muted-foreground">
Tarik file disini atau klik untuk upload.
</h4>
<div className="text-xs text-muted-foreground">
( Upload file dengan format .jpg, .jpeg, atau .png. Ukuran
maksimal 100mb.)
</div>
</div>
</div>
)}
{files.length > 0 && (
<div className="flex flex-row gap-2">
<Image
src={URL.createObjectURL(files[0])}
className="w-[30%]"
alt="thumbnail"
width={480}
height={480}
/>
<Button
variant="outline"
onClick={() => handleRemoveFile(files[0])}
>
<TimesIcon />
</Button>
</div>
)}
</div>
<DialogFooter className="self-end">
{!isDetail && <Button type="submit">Simpan</Button>}
<Button variant="outline" onClick={onClose}>
Tutup
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -0,0 +1,10 @@
import FormMasterUser from "@/components/form/form-master-user";
import { Card } from "@/components/ui/card";
export default function CreateMasterUserPage() {
return (
<Card className="h-[96vh] rounded-md bg-transparent">
<FormMasterUser />
</Card>
);
}

View File

@ -0,0 +1,10 @@
import FormMasterUserEdit from "@/components/form/form-master-user-edit";
import { Card } from "@/components/ui/card";
export default function CreateMasterUserPage() {
return (
<Card className="h-[96vh] rounded-md bg-transparent">
<FormMasterUserEdit />
</Card>
);
}

View File

@ -0,0 +1,28 @@
"use client";
import { AddIcon } from "@/components/icons";
import MasterUserTable from "@/components/table/master-user-table";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function MasterUserPage() {
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 py-3">
<Link href="/admin/master-user/create" className="mx-3">
<Button
size="default"
color="primary"
className="bg-[#F07C00] text-white"
>
Pengguna Baru
<AddIcon />
</Button>
</Link>
<MasterUserTable />
</div>
</div>
</div>
);
}

View File

@ -98,9 +98,9 @@ const createArticleSchema = z.object({
description: z.string().min(2, { description: z.string().min(2, {
message: "Deskripsi harus diisi", message: "Deskripsi harus diisi",
}), }),
// category: z.array(categorySchema).nonempty({ category: z.array(categorySchema).nonempty({
// message: "Kategori harus memiliki setidaknya satu item", message: "Kategori harus memiliki setidaknya satu item",
// }), }),
tags: z.array(z.string()).nonempty({ tags: z.array(z.string()).nonempty({
message: "Minimal 1 tag", message: "Minimal 1 tag",
}), }),
@ -295,7 +295,7 @@ export default function CreateImageForm() {
slug: values.slug, slug: values.slug,
customCreatorName: values.customCreatorName, customCreatorName: values.customCreatorName,
source: values.source, source: values.source,
categoryIds: "test", categoryIds: values.category.map((a) => a.id).join(","),
tags: values.tags.join(","), tags: values.tags.join(","),
description: htmlToString(removeImgTags(values.description)), description: htmlToString(removeImgTags(values.description)),
htmlDescription: removeImgTags(values.description), htmlDescription: removeImgTags(values.description),
@ -370,7 +370,7 @@ export default function CreateImageForm() {
} }
close(); close();
successSubmit("/admin/article", articleId, values.slug); successSubmit("/admin/news-article/image", articleId, values.slug);
}; };
function successSubmit(redirect: string, id: number, slug: string) { function successSubmit(redirect: string, id: number, slug: string) {
@ -729,7 +729,7 @@ export default function CreateImageForm() {
/> />
</div> </div>
<p className="text-sm mt-3">Kategori</p> <p className="text-sm mt-3">Kategori</p>
{/* <Controller <Controller
control={control} control={control}
name="category" name="category"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
@ -759,7 +759,7 @@ export default function CreateImageForm() {
<p className="text-red-400 text-sm mb-3"> <p className="text-red-400 text-sm mb-3">
{errors.category?.message} {errors.category?.message}
</p> </p>
)} */} )}
<p className="text-sm">Tags</p> <p className="text-sm">Tags</p>

View File

@ -0,0 +1,442 @@
"use client";
import { close, error, loading } from "@/config/swal";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { z } from "zod";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { editMasterUsers, getDetailMasterUsers } from "@/service/master-user";
import { getAllUserLevels } from "@/service/user-levels-service";
import { listUserRole } from "@/service/master-user-role";
import { Card } from "../ui/card";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Button } from "../ui/button";
const userSchema = z.object({
id: z.number(),
label: z.string(),
value: z.string(),
});
const masterUserSchema = z.object({
fullname: z.string().min(1, { message: "Required" }),
username: z.string().min(1, { message: "Required" }),
email: z.string().min(1, { message: "Required" }),
identityNumber: z.string().min(1, { message: "Required" }),
genderType: z.string().min(1, { message: "Required" }),
phoneNumber: z.string().min(1, { message: "Required" }),
address: z.string().min(1, { message: "Required" }),
userLevelType: userSchema,
userRoleType: userSchema,
});
export default function FormMasterUserEdit() {
const router = useRouter();
const MySwal = withReactContent(Swal);
const animatedComponents = makeAnimated();
const params = useParams();
const id = params?.id;
const [parentList, setParentList] = useState<any>([]);
const [listRole, setListRole] = useState<any>([]);
const formOptions = {
resolver: zodResolver(masterUserSchema),
};
type MicroIssueSchema = z.infer<typeof masterUserSchema>;
const {
control,
handleSubmit,
formState: { errors },
setValue,
} = useForm<MicroIssueSchema>(formOptions);
async function save(data: z.infer<typeof masterUserSchema>) {
const formData = {
address: data.address,
email: data.email,
fullname: data.fullname,
genderType: data.genderType,
identityNumber: data.identityNumber,
phoneNumber: data.phoneNumber,
userLevelId: data.userLevelType.id,
userRoleId: data.userRoleType.id,
username: data.username,
};
const response = await editMasterUsers(formData, String(id));
if (response?.error) {
error(response.message);
return false;
}
successSubmit("/admin/master-user");
}
function successSubmit(redirect: any) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
async function onSubmit(data: z.infer<typeof masterUserSchema>) {
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
useEffect(() => {
initFetch();
}, [id]);
const initFetch = async () => {
loading();
const res = await getDetailMasterUsers(String(id));
const profile = res?.data?.data;
const listLevel = await fetchUserLevel();
const listRole = await fetchUserRole();
const findLevel = listLevel?.find((a) => a.id === profile.userLevelId);
const findRole = listRole?.find((a) => a.id === profile.userRoleId);
setValue("fullname", profile?.fullname);
setValue("username", profile?.username);
setValue("email", profile?.email);
setValue("address", profile?.address);
setValue("identityNumber", profile?.identityNumber);
setValue("genderType", profile?.genderType);
setValue("phoneNumber", profile?.phoneNumber);
if (findLevel) {
setValue("userLevelType", findLevel);
}
if (findRole) {
setValue("userRoleType", findRole);
}
close();
};
const fetchUserLevel = async () => {
const res = await getAllUserLevels();
if (res?.data?.data) {
return setupParent(res?.data?.data, "level");
}
};
const fetchUserRole = async () => {
const request = {
limit: 100,
page: 1,
};
const res = await listUserRole(request);
if (res?.data?.data) {
return setupParent(res?.data?.data, "role");
}
};
const setupParent = (data: any, type: "level" | "role") => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.name,
value: element.aliasName || element.code,
});
}
if (type === "level") {
setParentList(temp);
} else {
setListRole(temp);
}
return temp;
};
return (
<div className="mx-5 my-5 overflow-y-auto">
<form
method="POST"
onSubmit={handleSubmit(onSubmit)}
className="w-full lg:w-1/2 lg:ml-4"
>
<Card className="rounded-md p-5 flex flex-col gap-3">
<Controller
control={control}
name="fullname"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="title">Nama Lengkap</Label>
<Input
type="text"
id="title"
placeholder="Nama Lengkap..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.fullname?.message && (
<p className="text-red-400 text-sm">{errors.fullname?.message}</p>
)}
<Controller
control={control}
name="username"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="username">Username</Label>
<Input
type="text"
id="username"
placeholder="Username..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.username?.message && (
<p className="text-red-400 text-sm">{errors.username?.message}</p>
)}
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="email">Email</Label>
<Input
type="email"
id="email"
placeholder="Email..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.email?.message && (
<p className="text-red-400 text-sm">{errors.email?.message}</p>
)}
{/* <Controller
control={control}
name="identityType"
render={({ field: { onChange, value } }) => (
<Select
variant="bordered"
labelPlacement="outside"
label="Identity Type"
placeholder="Select"
className="max-w-xs"
selectedKeys={[value]}
onChange={onChange}
>
{typeIdentity.map((type) => (
<SelectItem key={type.value}>{type.value}</SelectItem>
))}
</Select>
)}
/>
{errors.identityType?.message && (
<p className="text-red-400 text-sm">
{errors.identityType?.message}
</p>
)} */}
<Controller
control={control}
name="identityNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="identityNumber">NRP</Label>
<Input
type="number"
id="identityNumber"
placeholder="NRP..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.identityNumber?.message && (
<p className="text-red-400 text-sm">
{errors.identityNumber?.message}
</p>
)}
<Controller
control={control}
name="address"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="alamat">Alamat</Label>
<Textarea
id="alamat"
placeholder="Alamat..."
value={value ?? ""}
onChange={(e) => onChange(e)}
className="border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.address?.message && (
<p className="text-red-400 text-sm">{errors.address?.message}</p>
)}
<Controller
control={control}
name="genderType"
render={({ field: { onChange, value } }) => (
<div className="space-y-2">
<Label htmlFor="gender">Gender</Label>
<RadioGroup
id="gender"
value={value}
onValueChange={onChange}
className="flex flex-row gap-6"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Male" id="male" />
<Label htmlFor="male">Laki-laki</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Female" id="female" />
<Label htmlFor="female">Perempuan</Label>
</div>
</RadioGroup>
</div>
)}
/>
{errors.genderType?.message && (
<p className="text-red-400 text-sm">{errors.genderType?.message}</p>
)}
<Controller
control={control}
name="userLevelType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Level Pengguna</p>
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
value={value}
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={parentList}
/>
</>
)}
/>
{errors.userLevelType?.message && (
<p className="text-red-400 text-sm">
{errors.userLevelType?.message}
</p>
)}
<Controller
control={control}
name="userRoleType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Peran Pengguna</p>
<ReactSelect
className="basic-single text-black z-49"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
value={value}
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={listRole}
/>
</>
)}
/>
{errors.userRoleType?.message && (
<p className="text-red-400 text-sm">
{errors.userRoleType?.message}
</p>
)}
<Controller
control={control}
name="phoneNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full z-0 space-y-2">
<Label htmlFor="identityNumber">No. Handphone</Label>
<Input
type="number"
id="identityNumber"
placeholder="08*********"
value={value ?? ""}
onChange={onChange}
/>
</div>
)}
/>
{errors.phoneNumber?.message && (
<p className="text-red-400 text-sm">
{errors.phoneNumber?.message}
</p>
)}
<div className="flex justify-end gap-3">
<Link href={`/admin/master-user`}>
<Button color="danger" variant="ghost">
Cancel
</Button>
</Link>
<Button type="submit" color="primary" variant="default">
Save
</Button>
</div>
</Card>
</form>
</div>
);
}

View File

@ -0,0 +1,579 @@
"use client";
import { close, error, loading } from "@/config/swal";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { z } from "zod";
import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
import ReactPasswordChecklist from "react-password-checklist";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { zodResolver } from "@hookform/resolvers/zod";
import { createMasterUser } from "@/service/master-user";
import { getAllUserLevels } from "@/service/user-levels-service";
import { listUserRole } from "@/service/master-user-role";
import { Card } from "../ui/card";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Button } from "../ui/button";
const userSchema = z.object({
id: z.number(),
label: z.string(),
value: z.string(),
});
const masterUserSchema = z.object({
fullname: z.string().min(1, { message: "Required" }),
username: z.string().min(1, { message: "Required" }),
password: z
.string()
.min(8, "Password harus memiliki minimal 8 karakter.")
.refine((password) => /[A-Z]/.test(password), {
message: "Password harus memiliki minimal satu huruf kapital.",
})
.refine((password) => /[0-9]/.test(password), {
message: "Password harus memiliki minimal satu angka.",
})
.refine((password) => /[!@#$%^&*(),.?":{}|<>]/.test(password), {
message: "Password harus memiliki minimal satu simbol.",
}),
passwordValidate: z.string().min(1, { message: "Required" }),
email: z.string().min(1, { message: "Required" }),
identityNumber: z.string().min(1, { message: "Required" }),
genderType: z.string().min(1, { message: "Required" }),
phoneNumber: z.string().min(1, { message: "Required" }),
address: z.string().min(1, { message: "Required" }),
// userLevelType: userSchema,
userRoleType: userSchema,
});
export default function FormMasterUser() {
const router = useRouter();
const animatedComponents = makeAnimated();
const MySwal = withReactContent(Swal);
const [isVisible, setIsVisible] = useState([false, false]);
const [isValidPassword, setIsValidPassword] = useState(false);
const [parentList, setParentList] = useState<any>([]);
const [listRole, setListRole] = useState<any>([]);
const toggleVisibility = (type: number) => {
setIsVisible(
type === 0
? [!isVisible[0], isVisible[1]]
: [isVisible[0], !isVisible[1]],
);
};
const formOptions = {
resolver: zodResolver(masterUserSchema),
defaultValues: { password: "", passwordValidate: "" },
};
type MicroIssueSchema = z.infer<typeof masterUserSchema>;
const {
control,
handleSubmit,
formState: { errors },
setError,
watch,
setValue,
} = useForm<MicroIssueSchema>(formOptions);
const passwordVal = watch("password");
const passwordConfVal = watch("passwordValidate");
async function save(data: z.infer<typeof masterUserSchema>) {
const formData = {
address: data.address,
password: data.password,
email: data.email,
fullname: data.fullname,
genderType: data.genderType,
identityNumber: data.identityNumber,
identityType: "nrp",
phoneNumber: data.phoneNumber,
userLevelId: 1,
userRoleId: data.userRoleType.id,
username: data.username,
};
const response = await createMasterUser(formData);
if (response?.error) {
error(response.message);
return false;
}
successSubmit("/admin/master-user");
}
function successSubmit(redirect: any) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
async function onSubmit(data: z.infer<typeof masterUserSchema>) {
if (data.password === data.passwordValidate) {
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
} else {
setError("passwordValidate", {
type: "manual",
message: "Password harus sama.",
});
}
}
const generatePassword = () => {
const length = Math.floor(Math.random() * 9) + 8;
const upperCaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const lowerCaseChars = "abcdefghijklmnopqrstuvwxyz";
const numberChars = "0123456789";
const specialChars = "!@#$%^&*";
const allChars =
upperCaseChars + lowerCaseChars + numberChars + specialChars;
let generatedPassword = "";
generatedPassword +=
upperCaseChars[Math.floor(Math.random() * upperCaseChars.length)];
generatedPassword +=
specialChars[Math.floor(Math.random() * specialChars.length)];
generatedPassword +=
numberChars[Math.floor(Math.random() * numberChars.length)];
generatedPassword +=
lowerCaseChars[Math.floor(Math.random() * lowerCaseChars.length)];
for (let i = generatedPassword.length; i < length; i++) {
generatedPassword +=
allChars[Math.floor(Math.random() * allChars.length)];
}
generatedPassword = generatedPassword
.split("")
.sort(() => 0.5 - Math.random())
.join("");
setValue("password", generatedPassword);
};
useEffect(() => {
fetchUserLevel();
fetchUserRole();
}, []);
const fetchUserLevel = async () => {
loading();
const res = await getAllUserLevels();
close();
if (res?.data?.data) {
setupParent(res?.data?.data, "level");
}
};
const fetchUserRole = async () => {
loading();
const request = {
limit: 100,
page: 1,
};
const res = await listUserRole(request);
close();
if (res?.data?.data) {
setupParent(res?.data?.data, "role");
}
};
const setupParent = (data: any, type: "level" | "role") => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.name,
value: element.aliasName || element.code,
});
}
if (type === "level") {
setParentList(temp);
} else {
setListRole(temp);
}
};
return (
<div className="mx-5 my-5 overflow-y-auto">
<form method="POST" onSubmit={handleSubmit(onSubmit)}>
<Card className="rounded-md p-5 flex flex-col gap-3">
<Controller
control={control}
name="fullname"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="title"
className="mb-1 block text-sm font-medium"
>
Nama Lengkap
</Label>
<Input
type="text"
id="title"
placeholder="Nama Lengkap..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.fullname?.message && (
<p className="text-red-400 text-sm">{errors.fullname?.message}</p>
)}
<Controller
control={control}
name="username"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="username"
className="mb-1 block text-sm font-medium"
>
Username
</Label>
<Input
type="text"
id="username"
placeholder="Username..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.username?.message && (
<p className="text-red-400 text-sm">{errors.username?.message}</p>
)}
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="email"
className="mb-1 block text-sm font-medium"
>
Email
</Label>
<Input
type="email"
id="email"
placeholder="Email..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.email?.message && (
<p className="text-red-400 text-sm">{errors.email?.message}</p>
)}
<Controller
control={control}
name="identityNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="identityNumber"
className="mb-1 block text-sm font-medium"
>
NRP
</Label>
<Input
type="number"
id="identityNumber"
placeholder="NRP..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.identityNumber?.message && (
<p className="text-red-400 text-sm">
{errors.identityNumber?.message}
</p>
)}
<Controller
control={control}
name="address"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="alamat"
className="mb-1 block text-sm font-medium"
>
Alamat
</Label>
<Textarea
id="alamat"
placeholder="Alamat..."
value={value ?? ""}
onChange={(e) => onChange(e.target.value)}
className="border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.address?.message && (
<p className="text-red-400 text-sm">{errors.address?.message}</p>
)}
<Controller
control={control}
name="genderType"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label className="mb-2 block text-sm font-medium">Gender</Label>
<RadioGroup
className="flex flex-row gap-4"
value={value}
onValueChange={onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Male" id="male" />
<Label htmlFor="male">Laki-laki</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Female" id="female" />
<Label htmlFor="female">Perempuan</Label>
</div>
</RadioGroup>
</div>
)}
/>
{errors.genderType?.message && (
<p className="text-red-400 text-sm">{errors.genderType?.message}</p>
)}
<Controller
control={control}
name="phoneNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="identityNumber">No. Handphone</Label>
<Input
type="number"
id="identityNumber"
placeholder="08*********"
value={value ?? ""}
onChange={onChange}
className="w-full"
/>
</div>
)}
/>
{errors.phoneNumber?.message && (
<p className="text-red-400 text-sm">
{errors.phoneNumber?.message}
</p>
)}
{/* <Controller
control={control}
name="userLevelType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Level Pengguna</p>
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={parentList}
/>
</>
)}
/>
{errors.userLevelType?.message && (
<p className="text-red-400 text-sm">
{errors.userLevelType?.message}
</p>
)} */}
<Controller
control={control}
name="userRoleType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Peran Pengguna</p>
<ReactSelect
className="basic-single text-black z-49"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={listRole}
/>
</>
)}
/>
{errors.userRoleType?.message && (
<p className="text-red-400 text-sm">
{errors.userRoleType?.message}
</p>
)}
<Controller
control={control}
name="password"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2 relative z-0">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Input
type={isVisible[0] ? "text" : "password"}
id="password"
placeholder="Password..."
value={value ?? ""}
onChange={onChange}
className="w-full pr-10"
/>
<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-2xl text-muted-foreground focus:outline-none"
type="button"
onClick={() => toggleVisibility(0)}
>
{isVisible[0] ? (
<EyeSlashFilledIcon className="pointer-events-none" />
) : (
<EyeFilledIcon className="pointer-events-none" />
)}
</button>
</div>
</div>
)}
/>
{errors.password?.message && (
<p className="text-red-400 text-sm">{errors.password?.message}</p>
)}
<Controller
control={control}
name="passwordValidate"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2 relative z-0">
<Label htmlFor="passwordValidate">Konfirmasi Password</Label>
<div className="relative">
<Input
type={isVisible[1] ? "text" : "password"}
id="passwordValidate"
placeholder="Konfirmasi Password..."
value={value ?? ""}
onChange={onChange}
className="w-full pr-10"
/>
<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-2xl text-muted-foreground focus:outline-none"
type="button"
onClick={() => toggleVisibility(1)}
>
{isVisible[1] ? (
<EyeSlashFilledIcon className="pointer-events-none" />
) : (
<EyeFilledIcon className="pointer-events-none" />
)}
</button>
</div>
</div>
)}
/>
{errors.passwordValidate?.message && (
<p className="text-red-400 text-sm">
{errors.passwordValidate?.message}
</p>
)}
<a
className="cursor-pointer text-[#DD8306]"
onClick={generatePassword}
>
Generate Password
</a>
<ReactPasswordChecklist
rules={["minLength", "specialChar", "number", "capital", "match"]}
minLength={8}
value={passwordVal}
valueAgain={passwordConfVal}
onChange={(isValid) => {
setIsValidPassword(isValid);
}}
className="text-black dark:text-white text-sm my-3"
messages={{
minLength: "Password must be more than 8 characters",
specialChar: "Password must include a special character",
number: "Password must include a number",
capital: "Password must include an uppercase letter",
match: "Passwords match",
}}
/>
<div className="flex justify-end gap-3">
<Link href={`/admin/master-user`}>
<Button color="danger" variant="ghost">
Cancel
</Button>
</Link>
<Button type="submit" color="primary" variant="default">
Save
</Button>
</div>
</Card>
</form>
</div>
);
}

View File

@ -12,6 +12,8 @@ import { Label } from "../ui/label";
import { EyeSlashFilledIcon, EyeFilledIcon } from "../icons"; import { EyeSlashFilledIcon, EyeFilledIcon } from "../icons";
import Image from "next/image"; import Image from "next/image";
import { EyeOff, Eye } from "lucide-react"; import { EyeOff, Eye } from "lucide-react";
import { saveActivity } from "@/service/activity-log";
import { postSignIn, getProfile } from "@/service/master-user";
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
@ -34,66 +36,89 @@ export default function Login() {
}; };
const onSubmit = async () => { const onSubmit = async () => {
const data = {
username: username,
password: password,
};
if (!username || !password) { if (!username || !password) {
error("Username & Password Wajib Diisi !"); error("Username & Password Wajib Diisi !");
return; } else {
} loading();
const response = await postSignIn(data);
loading(); if (response?.error) {
setTimeout(() => {
const users = [
{
username: "admin",
password: "admin123",
role: "Admin",
redirect: "/admin/dashboard",
},
{
username: "approver",
password: "approver123",
role: "Approver",
redirect: "/admin/dashboard",
},
{
username: "kontributor",
password: "kontributor123",
role: "Kontributor",
redirect: "/admin/dashboard",
},
];
const foundUser = users.find(
(u) => u.username === username && u.password === password,
);
if (!foundUser) {
close();
error("Username / Password Tidak Sesuai"); error("Username / Password Tidak Sesuai");
return; } else {
const profile = await getProfile(response?.data?.data?.access_token);
const dateTime: any = new Date();
const newTime: any = dateTime.getTime() + 10 * 60 * 1000;
Cookies.set("access_token", response?.data?.data?.access_token, {
expires: 1,
});
Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
expires: 1,
});
Cookies.set("time_refresh", newTime, {
expires: 1,
});
Cookies.set("is_first_login", "true", {
secure: true,
sameSite: "strict",
});
await saveActivity(
{
activityTypeId: 1,
url: "https://dev.mikulnews.com/auth",
userId: profile?.data?.data?.id,
},
response?.data?.data?.access_token,
);
Cookies.set("profile_picture", profile?.data?.data?.profilePictureUrl, {
expires: 1,
});
Cookies.set("uie", profile?.data?.data?.id, {
expires: 1,
});
Cookies.set("ufne", profile?.data?.data?.fullname, {
expires: 1,
});
Cookies.set("ulie", profile?.data?.data?.userLevelGroup, {
expires: 1,
});
Cookies.set("username", profile?.data?.data?.username, {
expires: 1,
});
Cookies.set("fullname", profile?.data?.data?.fullname, {
expires: 1,
});
Cookies.set("urie", profile?.data?.data?.userRoleId, {
expires: 1,
});
Cookies.set("roleName", profile?.data?.data?.roleName, {
expires: 1,
});
Cookies.set("masterPoldaId", profile?.data?.data?.masterPoldaId, {
expires: 1,
});
Cookies.set("ulne", profile?.data?.data?.userLevelId, {
expires: 1,
});
Cookies.set("urce", profile?.data?.data?.roleCode, {
expires: 1,
});
Cookies.set("email", profile?.data?.data?.email, {
expires: 1,
});
router.push("/admin/dashboard");
Cookies.set("status", "login", {
expires: 1,
});
close();
} }
}
// Dummy Token
const fakeToken = `dummy-token-${foundUser.role}`;
const fakeRefresh = `dummy-refresh-${foundUser.role}`;
const newTime = (new Date().getTime() + 10 * 60 * 1000).toString();
Cookies.set("time_refresh", newTime, { expires: 1 });
Cookies.set("access_token", fakeToken, { expires: 1 });
Cookies.set("refresh_token", fakeRefresh, { expires: 1 });
Cookies.set("time_refresh", newTime, { expires: 1 });
Cookies.set("username", foundUser.username);
Cookies.set("fullname", foundUser.role);
Cookies.set("roleName", foundUser.role);
Cookies.set("status", "login");
close();
router.push(foundUser.redirect);
}, 1000);
}; };
return ( return (

View File

@ -29,8 +29,8 @@ interface SidebarSection {
children?: SidebarItem[]; children?: SidebarItem[];
} }
const getSidebarByRole = (role: string) => { const getSidebarByRole = (roleId: string | null) => {
if (role === "Admin") { if (roleId === "1") {
return [ return [
{ {
title: "Dashboard", title: "Dashboard",
@ -44,10 +44,27 @@ const getSidebarByRole = (role: string) => {
}, },
], ],
}, },
{
title: "System",
items: [
{
title: "Categories",
icon: () => (
<Icon icon="famicons:list-outline" className="text-lg" />
),
link: "/admin/master-category",
},
{
title: "User Management",
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
link: "/admin/master-user",
},
],
},
]; ];
} }
if (role === "Approver" || role === "Kontributor") { if (roleId === "2" || roleId === "3") {
return [ return [
{ {
title: "Dashboard", title: "Dashboard",
@ -243,7 +260,7 @@ const SidebarContent = ({
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
const [username, setUsername] = useState<string>("Guest"); const [username, setUsername] = useState<string>("Guest");
const [roleName, setRoleName] = useState<string>(""); const [roleId, setRoleId] = useState<string | null>(null);
const [openMenus, setOpenMenus] = useState<string[]>([]); const [openMenus, setOpenMenus] = useState<string[]>([]);
// =============================== // ===============================
@ -258,10 +275,10 @@ const SidebarContent = ({
}; };
const cookieUsername = getCookie("username"); const cookieUsername = getCookie("username");
const cookieRole = getCookie("roleName"); const cookieRoleId = getCookie("urie");
if (cookieUsername) setUsername(cookieUsername); if (cookieUsername) setUsername(cookieUsername);
if (cookieRole) setRoleName(cookieRole); if (cookieRoleId) setRoleId(cookieRoleId);
}, []); }, []);
// =============================== // ===============================
@ -281,7 +298,7 @@ const SidebarContent = ({
); );
}; };
const sidebarSections = getSidebarByRole(roleName); const sidebarSections = getSidebarByRole(roleId);
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">

View File

@ -0,0 +1,713 @@
"use client";
import {
CloudUploadIcon,
CreateIconIon,
DeleteIcon,
DotsYIcon,
EyeIconMdi,
SearchIcon,
} from "@/components/icons";
import { Article } from "@/types/globals";
import { convertDateFormat } from "@/utils/global";
import { Key, useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { useDropzone } from "react-dropzone";
import { close, error, loading, success } from "@/config/swal";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import Image from "next/image";
import { getArticleByCategory, getCategoryPagination } from "@/service/article";
import {
deleteCategory,
getCategoryById,
updateCategory,
uploadCategoryThumbnail,
} from "@/service/master-categories";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
} from "@/components/ui/table";
import CustomPagination from "@/components/layout/custom-pagination";
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";
import useDisclosure from "@/components/useDisclosure";
const columns = [
{ name: "No", uid: "no" },
{ name: "Kategori", uid: "title" },
{ name: "Deskripsi", uid: "description" },
{ name: "Tag Terkait", uid: "tags" },
{ name: "Dibuat ", uid: "createdAt" },
{ name: "Aksi", uid: "actions" },
];
interface CategoryType {
id: number;
label: string;
value: number;
}
type ArticleData = Article & {
no: number;
createdAt: string;
tags: string[];
};
// const categorySchema = z.object({
// id: z.number(),
// label: z.string(),
// value: z.number(),
// });
const createArticleSchema = z.object({
id: z.string().min(1, {
message: "Id harus valid",
}),
title: z.string().min(2, {
message: "Judul harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
tags: z.array(z.string()),
file: z.string(),
});
export default function CategoriesTable(props: { triggerRefresh: boolean }) {
const MySwal = withReactContent(Swal);
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const animatedComponents = makeAnimated();
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [categories, setCategories] = useState<ArticleData[]>([]);
const [showData, setShowData] = useState("10");
const [search, setSearch] = useState("");
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [files, setFiles] = useState<File[]>([]);
const [isDetail, setIsDetail] = useState(false);
const [tag, setTag] = useState("");
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", category: [], tags: [] },
};
const [selectedParent, setSelectedParent] = useState<any>();
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 },
setValue,
getValues,
setError,
clearErrors,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
initState();
}, [page, showData, props.triggerRefresh]);
async function initState() {
const req = {
limit: showData,
page: page,
search: search,
};
const res = await getCategoryPagination(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;
});
setCategories(newData);
} else {
setCategories([]);
}
};
async function doDelete(id: number) {
// loading();
const resDelete = await deleteCategory(id);
if (resDelete?.error) {
error(resDelete.message);
return false;
}
close();
success("Berhasil Hapus");
initState();
}
const handleDelete = (id: number) => {
MySwal.fire({
title: "Hapus Data",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
useEffect(() => {
fetchCategory();
}, []);
const fetchCategory = async () => {
const res = await getArticleByCategory();
if (res?.data?.data) {
setupCategory(res?.data?.data);
}
};
const setupCategory = (data: any) => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.title,
value: element.id,
});
}
setListCategory(temp);
};
const openModal = async (id: number | string, detail: boolean) => {
setIsDetail(detail);
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("tags", data?.tags);
setValue("file", data?.thumbnailUrl);
findParent(data?.parentId);
onOpen();
};
const findParent = (parent: number | undefined) => {
const finded = listCategory?.find((a: any) => a.id === parent);
if (finded) {
setSelectedParent(finded);
}
};
const renderCell = useCallback(
(category: ArticleData, columnKey: Key) => {
const cellValue = category[columnKey as keyof ArticleData];
// const statusColorMap: Record<string, ChipProps["color"]> = {
// active: "primary",
// cancel: "danger",
// pending: "success",
// };
// const findRelated = (parent: number | string) => {
// const filter = listCategory?.filter((a) => a.id == parent);
// return filter[0]?.label;
// };
switch (columnKey) {
case "tags":
return (
<div className="flex flex-row gap-1">
{category.tags
? category.tags.map((value) => value).join(", ")
: "-"}
</div>
);
case "createdAt":
return <p>{convertDateFormat(category.createdAt)}</p>;
case "actions":
return (
<div className="relative flex justify-star items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<DotsYIcon className="text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="lg:min-w-[150px] bg-black text-white shadow border">
<DropdownMenuItem
onClick={() => openModal(category.id, true)}
>
<EyeIconMdi className="inline mr-2 mb-1" />
Detail
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => openModal(category.id, false)}
>
<CreateIconIon className="inline mr-2 mb-1" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(category.id)}>
<DeleteIcon
color="red"
size={20}
className="inline mr-3 mb-1"
/>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
default:
return cellValue;
}
},
[listCategory],
);
let typingTimer: NodeJS.Timeout;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
initState();
}
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData = {
id: Number(values.id),
title: values.title,
statusId: 1,
parentId: selectedParent ? selectedParent.id : 0,
tags: values.tags.join(","),
description: values.description,
};
const response = await updateCategory(values.id, formData);
if (response?.error) {
error(response.message);
return false;
}
if (files?.length > 0) {
const formFiles = new FormData();
formFiles.append("files", files[0]);
const resFile = await uploadCategoryThumbnail(values.id, formFiles);
if (resFile?.error) {
error(resFile.message);
return false;
}
}
setFiles([]);
close();
initState();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return (
<>
<div className="py-3 w-full">
<div className="flex flex-col items-start rounded-2xl gap-3 w-full">
<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>
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-base pointer-events-none" />
<Input
type="text"
placeholder="Cari..."
aria-label="Search"
className="pl-10 text-sm bg-muted"
onChange={(e) => setSearch(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
/>
</div>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
<p className="font-semibold text-sm">Data</p>
<Select
value={showData}
onValueChange={(value) =>
value === "" ? "" : setShowData(value)
}
>
<SelectTrigger className="w-full border">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</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={categories} emptyContent={"No data to display."} loadingContent={<Spinner label="Loading..." />}>
{(item) => <TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>}
</TableBody>
</Table> */}
<div className="rounded-3xl border bg-white dark:bg-black text-black dark:text-white w-full">
<Table className="min-h-[50px] w-full">
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={column.uid}
className="text-md border-b bg-white dark:bg-black text-black dark:text-white"
>
{column.name}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{categories.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-4"
>
No data to display.
</TableCell>
</TableRow>
) : (
categories.map((item) => (
<TableRow key={item.id}>
{columns.map((column) => (
<TableCell key={column.uid}>
{renderCell(item, column.uid)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<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)}
/> */}
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
</div>
</div>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Kategori Baru</DialogTitle>
</DialogHeader>
<ScrollArea className="h-[70vh] pr-4">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-1">
<p className="text-sm">Nama Kategori</p>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<Input
id="title"
type="text"
value={value}
onChange={onChange}
readOnly={isDetail}
className="rounded-lg border dark:border-gray-400"
/>
)}
/>
{errors?.title && (
<p className="text-red-400 text-sm">
{errors.title?.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
id="description"
value={value}
onChange={onChange}
readOnly={isDetail}
className="rounded-lg border dark:border-gray-400"
/>
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm">
{errors.description?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Parent</p>
<ReactSelect
className="text-black z-50"
classNames={{
control: () =>
"rounded-lg bg-white border border-gray-200 dark:border-stone-500",
}}
classNamePrefix="select"
value={selectedParent}
isDisabled={isDetail}
onChange={setSelectedParent}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable
isSearchable
isMulti={false}
placeholder="Kategori..."
name="sub-module"
options={listCategory}
/>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Tag Terkait</p>
<Controller
control={control}
name="tags"
render={({ field: { value } }) => (
<div className="relative">
<Input
id="tags"
type="text"
value={tag}
onChange={(e) => setTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (tag.trim() !== "") {
setValue("tags", [...value, tag.trim()]);
setTag("");
e.preventDefault();
}
}
}}
readOnly={isDetail}
className="rounded-lg border dark:border-gray-400 h-[45px]"
/>
<div className="absolute top-2 left-3 flex gap-1">
{value?.map((item, index) => (
<div
key={index}
className="bg-blue-100 text-blue-700 px-2 py-1 text-xs rounded flex items-center gap-1"
>
{item}
<button
type="button"
onClick={() => {
const filtered = value.filter(
(tag) => tag !== item,
);
if (filtered.length === 0) {
setError("tags", {
type: "manual",
message: "Tags tidak boleh kosong",
});
} else {
clearErrors("tags");
setValue(
"tags",
filtered as [string, ...string[]],
);
}
}}
>
×
</button>
</div>
))}
</div>
</div>
)}
/>
</div>
{isDetail ? (
<img
src={getValues("file")}
className="w-[30%]"
alt="thumbnail"
width={480}
height={480}
/>
) : (
<Controller
control={control}
name="file"
render={({ field: { value } }) => (
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Thumbnail</p>
{files.length < 1 && value === "" && (
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border rounded-md py-[52px] flex items-center flex-col">
<CloudUploadIcon />
<h4 className="text-2xl font-medium mb-1 mt-3">
Tarik file disini atau klik untuk upload.
</h4>
<div className="text-xs text-muted-foreground">
( Upload file .jpg, .jpeg, .png. Maks 100mb )
</div>
</div>
</div>
)}
{value !== "" && (
<div className="flex gap-2">
<img
src={value}
className="w-[30%]"
alt="thumbnail"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setValue("file", "")}
>
×
</Button>
</div>
)}
{files.length > 0 && (
<div className="flex gap-2">
<Image
src={URL.createObjectURL(files[0])}
className="w-[30%]"
alt="thumbnail"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleRemoveFile(files[0])}
>
×
</Button>
</div>
)}
</div>
)}
/>
)}
<DialogFooter className="gap-2 mt-4">
{!isDetail && <Button type="submit">Simpan</Button>}
<Button type="button" variant="outline" onClick={onClose}>
Tutup
</Button>
</DialogFooter>
</form>
</ScrollArea>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -0,0 +1,237 @@
"use client";
import { CreateIconIon, DeleteIcon, DotsYIcon } from "@/components/icons";
import { close, error } from "@/config/swal";
import { deleteMasterUser, listMasterUsers } from "@/service/master-user";
import { MasterUser } from "@/types/globals";
import Link from "next/link";
import { Key, useCallback, useEffect, useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "../ui/button";
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
const columns = [
{ name: "No", uid: "no" },
{ name: "Username", uid: "username" },
{ name: "Fullname", uid: "fullname" },
{ name: "Email", uid: "email" },
{ name: "Identity Type", uid: "identityType" },
{ name: "Identity Number", uid: "identityNumber" },
// { name: "Users", uid: "users" },
// { name: "Status", uid: "status" },
{ name: "Aksi", uid: "actions" },
];
export default function MasterUserTable() {
const MySwal = withReactContent(Swal);
const [user, setUser] = useState<MasterUser[]>([]);
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
useEffect(() => {
initState();
}, [page]);
async function initState() {
const res = await listMasterUsers({ page: page, limit: 10 });
getTableNumber(10, res?.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
}
const getTableNumber = (limit: number, data?: any) => {
if (data) {
const startIndex = limit * (page - 1);
let iterate = 0;
const newData = data.map((value: any) => {
iterate++;
value.no = startIndex + iterate;
return value;
});
setUser(newData);
}
};
async function doDelete(id: string) {
// loading();
const resDelete = await deleteMasterUser(id);
if (resDelete?.error) {
error(resDelete.message);
return false;
}
close();
successSubmit();
}
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);
}
});
};
function successSubmit() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
initState();
}
});
}
const renderCell = useCallback((user: MasterUser, columnKey: Key) => {
const cellValue = user[columnKey as keyof MasterUser];
// const statusColorMap: Record<string, ChipProps["color"]> = {
// active: "primary",
// cancel: "danger",
// pending: "success",
// };
switch (columnKey) {
case "id":
return <div>{user.id}</div>;
case "status":
return (
<div></div>
// <Chip
// className="capitalize "
// // color={statusColorMap[user.status]}
// size="lg"
// variant="flat"
// >
// <div className="flex flex-row items-center gap-2 justify-center">{cellValue}</div>
// </Chip>
);
case "actions":
return (
<div className="relative flex justify-start items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
>
<DotsYIcon className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem asChild>
<Link
href={`/admin/master-user/edit/${user.id}`}
className="flex items-center"
>
<CreateIconIon className="inline mr-2 mb-1" />
Edit
</Link>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDelete(user.id)}
className="text-red-600"
>
<DeleteIcon
width={20}
height={16}
className="inline mr-2 mb-1"
/>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
default:
return cellValue;
}
}, []);
return (
<>
<div className="mx-3 my-5">
<div className="flex flex-col items-center rounded-2xl">
<Table className="rounded-2xl text-black dark:text-white bg-white dark:bg-black min-h-[50px]">
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={column.uid}
className="bg-white dark:bg-black text-black dark:text-white border-b text-md"
>
{column.name}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{user.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="text-center">
No data to display.
</TableCell>
</TableRow>
) : (
user.map((item) => (
<TableRow key={item.id}>
{columns.map((column) => (
<TableCell key={column.uid}>
{renderCell(item, column.uid)}
</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)}
/> */}
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,13 @@
import { useState, useCallback } from "react";
function useDisclosure(initial = false) {
const [isOpen, setIsOpen] = useState(initial);
const onOpen = useCallback(() => setIsOpen(true), []);
const onClose = useCallback(() => setIsOpen(false), []);
const onOpenChange = useCallback(() => setIsOpen((prev) => !prev), []);
return { isOpen, onOpen, onClose, onOpenChange };
}
export default useDisclosure;

9
package-lock.json generated
View File

@ -46,6 +46,7 @@
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hook-form": "^7.71.2", "react-hook-form": "^7.71.2",
"react-password-checklist": "^1.8.1",
"react-select": "^5.10.2", "react-select": "^5.10.2",
"sweetalert2": "^11.26.18", "sweetalert2": "^11.26.18",
"sweetalert2-react-content": "^5.1.1", "sweetalert2-react-content": "^5.1.1",
@ -9686,6 +9687,14 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}, },
"node_modules/react-password-checklist": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/react-password-checklist/-/react-password-checklist-1.8.1.tgz",
"integrity": "sha512-QHIU/OejxoH4/cIfYLHaHLb+yYc8mtL0Vr4HTmULxQg3ZNdI9Ni/yYf7pwLBgsUh4sseKCV/GzzYHWpHqejTGw==",
"peerDependencies": {
"react": ">16.0.0-alpha || >17.0.0-alpha || >18.0.0-alpha"
}
},
"node_modules/react-remove-scroll": { "node_modules/react-remove-scroll": {
"version": "2.7.2", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",

View File

@ -47,6 +47,7 @@
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hook-form": "^7.71.2", "react-hook-form": "^7.71.2",
"react-password-checklist": "^1.8.1",
"react-select": "^5.10.2", "react-select": "^5.10.2",
"sweetalert2": "^11.26.18", "sweetalert2": "^11.26.18",
"sweetalert2-react-content": "^5.1.1", "sweetalert2-react-content": "^5.1.1",

View File

@ -6,7 +6,7 @@ const axiosBaseInstance = axios.create({
baseURL, baseURL,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9", "X-Client-Key": "9ca7f706-a8b0-4520-b467-5e8321df36fb",
}, },
}); });

View File

@ -10,7 +10,7 @@ const axiosInterceptorInstance = axios.create({
baseURL, baseURL,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9", "X-Client-Key": "9ca7f706-a8b0-4520-b467-5e8321df36fb",
}, },
withCredentials: true, withCredentials: true,
}); });

View File

@ -2,7 +2,7 @@ import axiosBaseInstance from "./axios-base-instance";
const defaultHeaders = { const defaultHeaders = {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9", "X-Client-Key": "9ca7f706-a8b0-4520-b467-5e8321df36fb",
}; };
export async function httpGet(pathUrl: any, headers?: any) { export async function httpGet(pathUrl: any, headers?: any) {

View File

@ -5,7 +5,7 @@ import { getCsrfToken } from "../master-user";
const defaultHeaders = { const defaultHeaders = {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9", "X-Client-Key": "9ca7f706-a8b0-4520-b467-5e8321df36fb",
}; };
export async function httpGetInterceptor(pathUrl: any) { export async function httpGetInterceptor(pathUrl: any) {