diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1e7f556..3783364 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,11 +9,13 @@ datasource db { } model Role { - id String @id @default(cuid()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - User User[] + id String @id @default(cuid()) + name String + permissions Json? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + User User[] } model User { diff --git a/prisma/seed.ts b/prisma/seed.ts index 56b5941..a20e41f 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,5 +1,6 @@ import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat"; import { confDesa } from "@/lib/configurationDesa"; +import permissionConfig from "@/lib/listPermission.json"; // JSON yang kita buat import { prisma } from "@/server/lib/prisma"; const category = [ @@ -29,14 +30,6 @@ const role = [ { id: "developer", name: "developer" - }, - { - id: "admin", - name: "admin" - }, - { - id: "pelaksana", - name: "pelaksana" } ] @@ -51,11 +44,30 @@ const user = [ ]; (async () => { + const allKeys: string[] = []; + + function collectKeys(items: any[]) { + items.forEach((item) => { + allKeys.push(item.key); + if (item.children) collectKeys(item.children); + }); + } + + collectKeys(permissionConfig.menus); + + for (const r of role) { await prisma.role.upsert({ where: { id: r.id }, - create: r, - update: r + create: { + id: r.id, + name: r.name, + permissions: allKeys as any, + }, + update: { + name: r.name, + permissions: allKeys as any, + } }) console.log(`✅ Role ${r.name} seeded successfully`) diff --git a/src/components/DesaSetting.tsx b/src/components/DesaSetting.tsx index 9bb41ce..938cd96 100644 --- a/src/components/DesaSetting.tsx +++ b/src/components/DesaSetting.tsx @@ -16,13 +16,14 @@ import { } from "@mantine/core"; import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconEdit } from "@tabler/icons-react"; +import type { JsonValue } from "generated/prisma/runtime/library"; import _ from "lodash"; import { useState } from "react"; import useSWR from "swr"; import ModalFile from "./ModalFile"; import notification from "./notificationGlobal"; -export default function DesaSetting() { +export default function DesaSetting({ permissions }: { permissions: JsonValue[] }) { const [btnDisable, setBtnDisable] = useState(false); const [btnLoading, setBtnLoading] = useState(false); const [opened, { open, close }] = useDisclosure(false); @@ -213,12 +214,13 @@ export default function DesaSetting() { } - + chooseEdit({ data: v })} + disabled={!permissions.includes("setting.desa.edit")} > diff --git a/src/components/KategoriPelayananSurat.tsx b/src/components/KategoriPelayananSurat.tsx index 1982bf8..0121ea2 100644 --- a/src/components/KategoriPelayananSurat.tsx +++ b/src/components/KategoriPelayananSurat.tsx @@ -18,11 +18,12 @@ import { } from "@mantine/core"; import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react"; +import type { JsonValue } from "generated/prisma/runtime/library"; import { useState } from "react"; import useSWR from "swr"; import notification from "./notificationGlobal"; -export default function KategoriPelayananSurat() { +export default function KategoriPelayananSurat({ permissions }: { permissions: JsonValue[] }) { const [openedDelete, { open: openDelete, close: closeDelete }] = useDisclosure(false); const [openedDetail, { open: openDetail, close: closeDetail }] = @@ -52,6 +53,7 @@ export default function KategoriPelayananSurat() { mutate(); }, []); + async function handleCreate() { try { setBtnLoading(true); @@ -533,15 +535,19 @@ export default function KategoriPelayananSurat() { Kategori Pelayanan Surat - - - + { + permissions.includes("setting.kategori_pelayanan.tambah") && ( + + + + ) + } @@ -572,7 +578,7 @@ export default function KategoriPelayananSurat() { - + - + diff --git a/src/components/KategoriPengaduan.tsx b/src/components/KategoriPengaduan.tsx index fd2ea73..0f85343 100644 --- a/src/components/KategoriPengaduan.tsx +++ b/src/components/KategoriPengaduan.tsx @@ -15,11 +15,12 @@ import { } from "@mantine/core"; import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react"; +import type { JsonValue } from "generated/prisma/runtime/library"; import { useState } from "react"; import useSWR from "swr"; import notification from "./notificationGlobal"; -export default function KategoriPengaduan() { +export default function KategoriPengaduan({ permissions }: { permissions: JsonValue[] }) { const [openedDelete, { open: openDelete, close: closeDelete }] = useDisclosure(false); const [btnDisable, setBtnDisable] = useState(true); @@ -293,15 +294,19 @@ export default function KategoriPengaduan() { Kategori Pengaduan - - - + { + permissions.includes("setting.kategori_pengaduan.tambah") && ( + + + + ) + } @@ -318,17 +323,18 @@ export default function KategoriPengaduan() { {v.name} - + chooseEdit({ data: v })} + disabled={!permissions.includes("setting.kategori_pengaduan.edit")} > - + diff --git a/src/components/ModalFile.tsx b/src/components/ModalFile.tsx index 71766c8..f77759f 100644 --- a/src/components/ModalFile.tsx +++ b/src/components/ModalFile.tsx @@ -64,7 +64,7 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b {viewFile && ( <> {typeFile == "pdf" ? ( - + ) : ( + {/* Title */} + - {node.label} + + {/* Children */} + {sub.map((child: any, i) => ( + + ))} + + ); +} + +export default function PermissionRole({ permissions }: { permissions: string[] }) { + const [showAll, setShowAll] = useState(false); + if (!permissions?.length) return -; + + const groups = groupPermissions(permissions); + const rootNodes = Object.values(groups); + + return ( + + { + showAll ? + rootNodes.map((node: any, idx) => ( + + )) + : + rootNodes.slice(0, 2).map((node: any, idx) => ( + + )) + } + + + ); +} 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/ProfileUser.tsx b/src/components/ProfileUser.tsx index d622062..9f0250f 100644 --- a/src/components/ProfileUser.tsx +++ b/src/components/ProfileUser.tsx @@ -9,10 +9,11 @@ import { Stack, Title, } from "@mantine/core"; +import type { JsonValue } from "generated/prisma/runtime/library"; import { useEffect, useState } from "react"; import notification from "./notificationGlobal"; -export default function ProfileUser() { +export default function ProfileUser({ permissions }: { permissions: JsonValue[] }) { const [opened, setOpened] = useState(false); const [openedPassword, setOpenedPassword] = useState(false); const [pwdBaru, setPwdBaru] = useState(""); @@ -126,12 +127,21 @@ export default function ProfileUser() { Profile Pengguna - - + { + permissions.includes("setting.profile.edit") && ( + + ) + } + + { + permissions.includes("setting.profile.password") && ( + + ) + } diff --git a/src/components/UserRoleSetting.tsx b/src/components/UserRoleSetting.tsx new file mode 100644 index 0000000..b565285 --- /dev/null +++ b/src/components/UserRoleSetting.tsx @@ -0,0 +1,394 @@ +import apiFetch from "@/lib/apiFetch"; +import { + ActionIcon, + Button, + Divider, + Flex, + Group, + Input, + Modal, + Stack, + Table, + Text, + Title, + Tooltip +} from "@mantine/core"; +import { useDisclosure, useShallowEffect } from "@mantine/hooks"; +import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react"; +import type { JsonValue } from "generated/prisma/runtime/library"; +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); + const [btnLoading, setBtnLoading] = useState(false); + const [opened, { open, close }] = useDisclosure(false); + const [openedDelete, { open: openDelete, close: closeDelete }] = + useDisclosure(false); + const [dataDelete, setDataDelete] = useState(""); + const { + data: dataRole, + mutate: mutateRole, + isLoading: isLoadingRole, + } = useSWR("user-role", () => apiFetch.api.user.role.get()); + const [openedTambah, { open: openTambah, close: closeTambah }] = + useDisclosure(false); + const { data, mutate, isLoading } = useSWR("role-list", () => + apiFetch.api.user.role.get(), + ); + const list = data?.data || []; + const listRole = dataRole?.data || []; + const [dataEdit, setDataEdit] = useState({ + id: "", + name: "", + permissions: [], + }); + const [dataTambah, setDataTambah] = useState({ + name: "", + permissions: [], + }); + const [error, setError] = useState({ + name: false, + permissions: false, + }); + + useShallowEffect(() => { + mutate(); + }, []); + + async function handleCreate() { + try { + setBtnLoading(true); + const res = await apiFetch.api.user["role-create"].post(dataTambah as any); + if (res.status === 200) { + mutate(); + closeTambah(); + setDataTambah({ + name: "", + permissions: [], + }); + notification({ + title: "Success", + message: "Your user have been saved", + type: "success", + }); + } else { + notification({ + title: "Error", + message: "Failed to create user ", + type: "error", + }); + } + } catch (error) { + console.error(error); + notification({ + title: "Error", + message: "Failed to create user", + type: "error", + }); + } finally { + setBtnLoading(false); + } + } + + async function handleEdit() { + try { + setBtnLoading(true); + const res = await apiFetch.api.pengaduan.category.update.post(dataEdit); + if (res.status === 200) { + mutate(); + close(); + notification({ + title: "Success", + message: "Your category have been saved", + type: "success", + }); + } else { + notification({ + title: "Error", + message: "Failed to edit category", + type: "error", + }); + } + } catch (error) { + console.error(error); + notification({ + title: "Error", + message: "Failed to edit category", + type: "error", + }); + } finally { + setBtnLoading(false); + } + } + + async function handleDelete() { + try { + setBtnLoading(true); + const res = await apiFetch.api.user["role-delete"].post({ id: dataDelete }); + if (res.status === 200) { + mutate(); + closeDelete(); + notification({ + title: "Success", + message: "Your role have been deleted", + type: "success", + }); + } else { + notification({ + title: "Error", + message: "Failed to delete role", + type: "error", + }); + } + } catch (error) { + console.error(error); + notification({ + title: "Error", + message: "Failed to delete role", + type: "error", + }); + } finally { + setBtnLoading(false); + } + } + + function chooseEdit({ + data, + }: { + data: { + id: string; + name: string; + permissions: []; + }; + }) { + setDataEdit(data); + open(); + } + + 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 }); + } else { + setBtnDisable(false); + setError({ ...error, [kat]: false }); + } + + if (aksi === "edit") { + setDataEdit({ ...dataEdit, [kat]: value }); + } else { + setDataTambah({ ...dataTambah, [kat]: value }); + } + } + + console.log("dataTambah", dataTambah); + + useShallowEffect(() => { + if (dataEdit.name.length > 0) { + setBtnDisable(false); + } + }, [dataEdit.id]); + + return ( + <> + {/* Modal Edit */} + + + + + onValidation({ + kat: "name", + value: e.target.value, + aksi: "edit", + }) + } + /> + + + + + + + + + {/* Modal Tambah */} + + + + + onValidation({ + kat: "name", + value: e.target.value, + aksi: "tambah", + }) + } + /> + + { + setDataTambah({ ...dataTambah, permissions: permissions as never[] }); + }} + /> + + + + + + + + {/* Modal Delete */} + + + + Apakah anda yakin ingin menghapus role ini? + + + + + + + + + + + + Daftar Role + + { + permissions.includes('setting.user_role.tambah') && ( + + + + ) + } + + + + + + + Role + Permission + Aksi + + + + {list.length > 0 ? ( + list?.map((v: any) => ( + + {v.name} + + + + + + + chooseEdit({ data: v })} + disabled={!permissions.includes('setting.user_role.edit')} + > + + + + + { + setDataDelete(v.id); + openDelete(); + }} + disabled={!permissions.includes('setting.user_role.delete')} + > + + + + + + + )) + ) : ( + + + Data Role Tidak Ditemukan + + + )} + +
+
+
+ + ); +} diff --git a/src/components/UserSetting.tsx b/src/components/UserSetting.tsx index 1861a15..65e7acc 100644 --- a/src/components/UserSetting.tsx +++ b/src/components/UserSetting.tsx @@ -16,11 +16,12 @@ import { } from "@mantine/core"; import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react"; +import type { JsonValue } from "generated/prisma/runtime/library"; import { useState } from "react"; import useSWR from "swr"; import notification from "./notificationGlobal"; -export default function UserSetting() { +export default function UserSetting({ permissions }: { permissions: JsonValue[] }) { const [btnDisable, setBtnDisable] = useState(true); const [btnLoading, setBtnLoading] = useState(false); const [opened, { open, close }] = useDisclosure(false); @@ -390,15 +391,20 @@ export default function UserSetting() { Daftar User - - - + { + permissions.includes('setting.user.tambah') && ( + + + + ) + } + @@ -422,17 +428,18 @@ export default function UserSetting() { {v.roleId} - + chooseEdit({ data: v })} + disabled={!permissions.includes('setting.user.edit')} > - + diff --git a/src/lib/groupPermission.ts b/src/lib/groupPermission.ts new file mode 100644 index 0000000..f9500f7 --- /dev/null +++ b/src/lib/groupPermission.ts @@ -0,0 +1,59 @@ +import config from "@/lib/listPermission.json"; + +export interface PermissionNode { + key: string; + label: string; + children?: PermissionNode[]; +} + +interface Grouped { + [key: string]: { + label: string; + children: Grouped; + actions: string[]; + }; +} + +/* --- Build lookup table --- */ +const permissionMap: Record = {}; + +function walk(nodes: PermissionNode[], path: string[] = []) { + nodes.forEach((n) => { + const full = [...path, n.label]; + permissionMap[n.key] = full; + if (n.children) walk(n.children, full); + }); +} + +walk(config.menus); + +/* --- Convert keys → hierarchical grouped --- */ +export function groupPermissions(keys: string[]) { + const tree: Grouped = {}; + + keys.forEach((key) => { + const path = permissionMap[key]; + if (!path) return; + + let pointer = tree; + + path.forEach((label, idx) => { + if (!pointer[label]) { + pointer[label] = { + label, + children: {}, + actions: [] + }; + } + + // last item = actual permission action + if (idx === path.length - 1) { + pointer[label].actions.push(label); + } + + pointer = pointer[label].children; + }); + }); + + return tree; +} diff --git a/src/lib/listPermission.json b/src/lib/listPermission.json new file mode 100644 index 0000000..148ed4d --- /dev/null +++ b/src/lib/listPermission.json @@ -0,0 +1,310 @@ +{ + "menus": [ + { + "key": "dashboard", + "label": "Dashboard", + "default": true, + "children": [ + { + "key": "dashboard.view", + "label": "Melihat Dashboard", + "default": true + } + ] + }, + { + "key": "pengaduan", + "label": "Pengaduan", + "default": true, + "children": [ + { + "key": "pengaduan.view", + "label": "Melihat List & Detail", + "default": true + }, + { + "key": "pengaduan.antrian", + "label": "Antrian", + "default": true, + "children": [ + { + "key": "pengaduan.antrian.tolak", + "label": "Menolak", + "default": true + }, + { + "key": "pengaduan.antrian.terima", + "label": "Menerima", + "default": true + } + ] + }, + { + "key": "pengaduan.diterima", + "label": "Diterima", + "default": true, + "children": [ + { + "key": "pengaduan.diterima.dikerjakan", + "label": "Dikerjakan", + "default": true + } + ] + }, + { + "key": "pengaduan.dikerjakan", + "label": "Dikerjakan", + "default": true, + "children": [ + { + "key": "pengaduan.dikerjakan.selesai", + "label": "Diselesaikan", + "default": true + } + ] + } + ] + }, + { + "key": "pelayanan", + "label": "Pelayanan", + "default": true, + "children": [ + { + "key": "pelayanan.view", + "label": "Melihat List & Detail", + "default": true + }, + { + "key": "pelayanan.antrian", + "label": "Antrian", + "default": true, + "children": [ + { + "key": "pelayanan.antrian.tolak", + "label": "Menolak", + "default": true + }, + { + "key": "pelayanan.antrian.terima", + "label": "Menerima", + "default": true + } + ] + }, + { + "key": "pelayanan.diterima", + "label": "Diterima", + "default": true, + "children": [ + { + "key": "pelayanan.diterima.tolak", + "label": "Menolak", + "default": true + }, + { + "key": "pelayanan.diterima.setujui", + "label": "Menyetujui", + "default": true + } + ] + } + ] + }, + { + "key": "warga", + "label": "Warga", + "default": true, + "children": [ + { + "key": "warga.view", + "label": "Melihat List & Detail", + "default": true + } + ] + }, + { + "key": "setting", + "label": "Setting", + "default": true, + "children": [ + { + "key": "setting.profile", + "label": "Profile", + "default": true, + "children": [ + { + "key": "setting.profile.view", + "label": "View", + "default": true + }, + { + "key": "setting.profile.edit", + "label": "Edit", + "default": true + }, + { + "key": "setting.profile.password", + "label": "Ubah Password", + "default": true + } + ] + }, + { + "key": "setting.user", + "label": "User", + "default": true, + "children": [ + { + "key": "setting.user.view", + "label": "View List", + "default": true + }, + { + "key": "setting.user.tambah", + "label": "Tambah", + "default": true + }, + { + "key": "setting.user.edit", + "label": "Edit", + "default": true + }, + { + "key": "setting.user.delete", + "label": "Delete", + "default": true + } + ] + }, + { + "key": "setting.user_role", + "label": "User Role", + "default": true, + "children": [ + { + "key": "setting.user_role.view", + "label": "View List", + "default": true + }, + { + "key": "setting.user_role.tambah", + "label": "Tambah", + "default": true + }, + { + "key": "setting.user_role.edit", + "label": "Edit", + "default": true + }, + { + "key": "setting.user_role.delete", + "label": "Delete", + "default": true + } + ] + }, + { + "key": "setting.kategori_pengaduan", + "label": "Kategori Pengaduan", + "default": true, + "children": [ + { + "key": "setting.kategori_pengaduan.view", + "label": "View List", + "default": true + }, + { + "key": "setting.kategori_pengaduan.tambah", + "label": "Tambah", + "default": true + }, + { + "key": "setting.kategori_pengaduan.edit", + "label": "Edit", + "default": true + }, + { + "key": "setting.kategori_pengaduan.delete", + "label": "Delete", + "default": true + } + ] + }, + { + "key": "setting.kategori_pelayanan", + "label": "Kategori Pelayanan Surat", + "default": true, + "children": [ + { + "key": "setting.kategori_pelayanan.view", + "label": "View List", + "default": true + }, + { + "key": "setting.kategori_pelayanan.detail", + "label": "View Detail", + "default": true + }, + { + "key": "setting.kategori_pelayanan.tambah", + "label": "Tambah", + "default": true + }, + { + "key": "setting.kategori_pelayanan.edit", + "label": "Edit", + "default": true + }, + { + "key": "setting.kategori_pelayanan.delete", + "label": "Delete", + "default": true + } + ] + }, + { + "key": "setting.desa", + "label": "Desa", + "default": true, + "children": [ + { + "key": "setting.desa.view", + "label": "View List", + "default": true + }, + { + "key": "setting.desa.edit", + "label": "Edit", + "default": true + } + ] + } + ] + }, + { + "key": "api_key", + "label": "API Key", + "default": true, + "children": [ + { + "key": "api_key.view", + "label": "View List", + "default": true + } + ] + }, + { + "key": "credential", + "label": "Credential", + "default": true, + "children": [ + { + "key": "credential.viewØ", + "label": "View List", + "default": true + } + ] + } + ] +} \ No newline at end of file diff --git a/src/pages/scr/dashboard/dashboard_layout.tsx b/src/pages/scr/dashboard/dashboard_layout.tsx index 2fc3115..ca3f687 100644 --- a/src/pages/scr/dashboard/dashboard_layout.tsx +++ b/src/pages/scr/dashboard/dashboard_layout.tsx @@ -35,6 +35,7 @@ import { IconUsersGroup, } from "@tabler/icons-react"; import type { User } from "generated/prisma"; +import type { JsonValue } from "generated/prisma/runtime/library"; import { useEffect, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; @@ -212,36 +213,54 @@ function HostView() { function NavigationDashboard() { const navigate = useNavigate(); const location = useLocation(); + const [permissions, setPermissions] = useState([]); + + useEffect(() => { + async function fetchPermissions() { + const { data } = await apiFetch.api.user.find.get(); + if (Array.isArray(data?.permissions)) { + setPermissions(data.permissions); + } else { + setPermissions([]); + } + } + fetchPermissions(); + }, []); const isActive = (path: keyof typeof clientRoute) => location.pathname.startsWith(clientRoute[path]); const navItems = [ { + key: "dashboard", path: "/scr/dashboard/dashboard-home", icon: , label: "Dashboard Overview", description: "Quick summary and insights", }, { + key: "pengaduan", path: "/scr/dashboard/pengaduan/list", icon: , label: "Pengaduan Warga", description: "Manage pengaduan warga", }, { + key: "pelayanan", path: "/scr/dashboard/pelayanan-surat/list-pelayanan", icon: , label: "Pelayanan Surat", description: "Manage pelayanan surat", }, { + key: "warga", path: "/scr/dashboard/warga/list-warga", icon: , label: "Warga", description: "Manage warga", }, { + key: "setting", path: "/scr/dashboard/setting/detail-setting", icon: , label: "Setting", @@ -249,12 +268,14 @@ function NavigationDashboard() { "Manage setting (category pengaduan dan pelayanan surat, desa, etc)", }, { + key: "api_key", path: "/scr/dashboard/apikey/apikey", icon: , label: "API Key Manager", description: "Create and manage API keys", }, { + key: "credential", path: "/scr/dashboard/credential/credential", icon: , label: "Credentials", @@ -264,7 +285,7 @@ function NavigationDashboard() { return ( - {navItems.map((item) => ( + {navItems.filter((item) => permissions.includes(item.key)).map((item) => ( (null); const [noSurat, setNoSurat] = useState(""); const [openedPreview, setOpenedPreview] = useState(false); + const [permissions, setPermissions] = useState([]); useEffect(() => { async function fetchHost() { const { data } = await apiFetch.api.user.find.get(); setHost(data?.user ?? null); + + if (data?.permissions && Array.isArray(data.permissions)) { + const onlySetting = data.permissions.filter((p: any) => p.startsWith("pelayanan")); + setPermissions(onlySetting); + } } fetchHost(); }, []); @@ -276,6 +283,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data data?.status === "antrian" ? (