From ad7b40523cb2ce46942bbdffff8355d7318dfb0f Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 24 Nov 2025 17:40:27 +0800 Subject: [PATCH] upd: dashboard admin Deskripsi: - tambah role user - api edit tambah dan delete role user NO Issues --- prisma/schema.prisma | 1 + src/components/PermissionTree.tsx | 144 +++++++++++++++++++++++++++++ src/components/UserRoleSetting.tsx | 120 +++++------------------- src/server/routes/user_route.ts | 81 +++++++++++++++- 4 files changed, 246 insertions(+), 100 deletions(-) create mode 100644 src/components/PermissionTree.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ee65913..3783364 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,7 @@ model Role { id String @id @default(cuid()) name String permissions Json? + isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt User User[] diff --git a/src/components/PermissionTree.tsx b/src/components/PermissionTree.tsx new file mode 100644 index 0000000..f73222c --- /dev/null +++ b/src/components/PermissionTree.tsx @@ -0,0 +1,144 @@ +import permissionConfig from "@/lib/listPermission.json"; +import { ActionIcon, Checkbox, Collapse, Group, Stack, Text } from "@mantine/core"; +import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"; +import { useState } from "react"; + +interface Node { + label: string; + key: string; + children?: Node[]; +} + +export default function PermissionTree({ + selected, + onChange, +}: { + selected: string[]; + onChange: (val: string[]) => void; +}) { + const [open, setOpen] = useState>({}); + + const toggle = (key: string) => { + setOpen((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + + + // Ambil semua key dari node termasuk semua keturunannya + const collectKeys = (n: Node): string[] => { + if (!n.children) return [n.key]; + return [n.key, ...n.children.flatMap(collectKeys)]; + }; + + const checkState = (node: Node): { all: boolean; some: boolean } => { + const children = node.children || []; + // Jika tidak ada anak → nilai hanya berdasarkan dirinya sendiri + if (children.length === 0) { + const checked = selected.includes(node.key); + return { all: checked, some: checked }; + } + // Rekursif ke anak + let all = selected.includes(node.key); + let some = selected.includes(node.key); + for (const c of children) { + const childState = checkState(c); + if (!childState.all) all = false; + if (childState.some) some = true; + } + return { all, some }; + }; + + // Untuk ordering sesuai urutan JSON + const getOrderedKeys = (nodes: Node[]): string[] => + nodes.flatMap((n) => [n.key, ...getOrderedKeys(n.children || [])]); + + + const RenderNode = ({ node }: { node: Node }) => { + const children = node.children || []; + + const state = checkState(node); // ← gunakan recursive evaluator + + const isChecked = state.all; + const isIndeterminate = !state.all && state.some; + + const showChildren = open[node.key] ?? false; + + // Ambil semua key anak + parent + const collectKeys = (n: Node): string[] => { + if (!n.children) return [n.key]; + return [n.key, ...n.children.flatMap(collectKeys)]; + }; + + const allKeys = collectKeys(node); + + const toggleCheck = (checked: boolean) => { + let updated = new Set(selected); + + if (checked) { + // parent + semua child + allKeys.forEach((k) => updated.add(k)); + } else { + // hilangkan parent + semua child + allKeys.forEach((k) => updated.delete(k)); + } + + // ⬇⬇⬇ PERBAIKAN PENTING ⬇⬇⬇ + // + // Jika node indeterminate → parent harus tetap ada di selected + // + if (isIndeterminate) { + updated.add(node.key); + } + + // Jika semua child tercentang → parent harus checked + if (isChecked) { + updated.add(node.key); + } + + onChange([...updated]); + }; + + return ( + + + {children.length > 0 ? ( + toggle(node.key)}> + {showChildren ? : } + + ) : ( +
+ )} + + toggleCheck(e.target.checked)} + /> + + + {children.length > 0 && ( + + + {children.map((c) => ( + + ))} + + + )} + + ); + }; + + + + return ( + + Hak Akses + + {permissionConfig.menus.map((menu: Node) => ( + + ))} + + ); +} diff --git a/src/components/UserRoleSetting.tsx b/src/components/UserRoleSetting.tsx index 8c9624b..b565285 100644 --- a/src/components/UserRoleSetting.tsx +++ b/src/components/UserRoleSetting.tsx @@ -7,12 +7,11 @@ import { Group, Input, Modal, - Select, Stack, Table, Text, Title, - Tooltip, + Tooltip } from "@mantine/core"; import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react"; @@ -21,6 +20,7 @@ import { useState } from "react"; import useSWR from "swr"; import notification from "./notificationGlobal"; import PermissionRole from "./PermissionRole"; +import PermissionTree from "./PermissionTree"; export default function UserRoleSetting({ permissions }: { permissions: JsonValue[] }) { const [btnDisable, setBtnDisable] = useState(true); @@ -44,23 +44,15 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu const [dataEdit, setDataEdit] = useState({ id: "", name: "", - phone: "", - email: "", - roleId: "", + permissions: [], }); const [dataTambah, setDataTambah] = useState({ name: "", - email: "", - roleId: "", - password: "", - phone: "", + permissions: [], }); const [error, setError] = useState({ name: false, - email: false, - roleId: false, - password: false, - phone: false, + permissions: false, }); useShallowEffect(() => { @@ -70,16 +62,13 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu async function handleCreate() { try { setBtnLoading(true); - const res = await apiFetch.api.user.create.post(dataTambah); + const res = await apiFetch.api.user["role-create"].post(dataTambah as any); if (res.status === 200) { mutate(); closeTambah(); setDataTambah({ name: "", - email: "", - roleId: "", - password: "", - phone: "", + permissions: [], }); notification({ title: "Success", @@ -139,19 +128,19 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu async function handleDelete() { try { setBtnLoading(true); - const res = await apiFetch.api.user.delete.post({ id: dataDelete }); + const res = await apiFetch.api.user["role-delete"].post({ id: dataDelete }); if (res.status === 200) { mutate(); closeDelete(); notification({ title: "Success", - message: "Your user have been deleted", + message: "Your role have been deleted", type: "success", }); } else { notification({ title: "Error", - message: "Failed to delete user", + message: "Failed to delete role", type: "error", }); } @@ -159,7 +148,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu console.error(error); notification({ title: "Error", - message: "Failed to delete user", + message: "Failed to delete role", type: "error", }); } finally { @@ -173,24 +162,14 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu data: { id: string; name: string; - phone: string; - email: string; - roleId: string; + permissions: []; }; }) { setDataEdit(data); open(); } - function onValidation({ - kat, - value, - aksi, - }: { - kat: "name" | "email" | "roleId" | "password" | "phone"; - value: string | null; - aksi: "edit" | "tambah"; - }) { + function onValidation({ kat, value, aksi, }: { kat: "name" | "permission"; value: string | null; aksi: "edit" | "tambah"; }) { if (value == null || value.length < 1) { setBtnDisable(true); setError({ ...error, [kat]: true }); @@ -206,6 +185,8 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu } } + console.log("dataTambah", dataTambah); + useShallowEffect(() => { if (dataEdit.name.length > 0) { setBtnDisable(false); @@ -275,68 +256,12 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu } /> - - onValidation({ - kat: "phone", - value: e.target.value, - aksi: "tambah", - }) - } - /> - - - - onValidation({ - kat: "email", - value: e.target.value, - aksi: "tambah", - }) - } - /> - - - - onValidation({ - kat: "password", - value: e.target.value, - aksi: "tambah", - }) - } - /> - -