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
-
- }
- onClick={openTambah}
- >
- Tambah
-
-
+ {
+ permissions.includes("setting.kategori_pelayanan.tambah") && (
+
+ }
+ onClick={openTambah}
+ >
+ 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
-
- }
- onClick={openTambah}
- >
- Tambah
-
-
+ {
+ permissions.includes("setting.kategori_pengaduan.tambah") && (
+
+ }
+ onClick={openTambah}
+ >
+ 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') && (
+
+ }
+ onClick={openTambah}
+ >
+ 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
-
- }
- onClick={openTambah}
- >
- Tambah
-
-
+ {
+ permissions.includes('setting.user.tambah') && (
+
+ }
+ onClick={openTambah}
+ >
+ 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" ? (