upd: dashboard admin
Deskripsi: - databse - seeder - list user role NO Issues
This commit is contained in:
@@ -11,6 +11,7 @@ datasource db {
|
|||||||
model Role {
|
model Role {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
|
permissions Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
User User[]
|
User User[]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat";
|
import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat";
|
||||||
import { confDesa } from "@/lib/configurationDesa";
|
import { confDesa } from "@/lib/configurationDesa";
|
||||||
|
import permissionConfig from "@/lib/listPermission.json"; // JSON yang kita buat
|
||||||
import { prisma } from "@/server/lib/prisma";
|
import { prisma } from "@/server/lib/prisma";
|
||||||
|
|
||||||
const category = [
|
const category = [
|
||||||
@@ -29,14 +30,6 @@ const role = [
|
|||||||
{
|
{
|
||||||
id: "developer",
|
id: "developer",
|
||||||
name: "developer"
|
name: "developer"
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "admin",
|
|
||||||
name: "admin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pelaksana",
|
|
||||||
name: "pelaksana"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -51,11 +44,30 @@ const user = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
(async () => {
|
(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) {
|
for (const r of role) {
|
||||||
await prisma.role.upsert({
|
await prisma.role.upsert({
|
||||||
where: { id: r.id },
|
where: { id: r.id },
|
||||||
create: r,
|
create: {
|
||||||
update: r
|
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`)
|
console.log(`✅ Role ${r.name} seeded successfully`)
|
||||||
|
|||||||
57
src/components/PermissionRole.tsx
Normal file
57
src/components/PermissionRole.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { groupPermissions } from "@/lib/groupPermission";
|
||||||
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
label: string;
|
||||||
|
children: any;
|
||||||
|
actions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function RenderNode({ node }: { node: Node }) {
|
||||||
|
const sub = Object.values(node.children || {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack pl="md" gap={6}>
|
||||||
|
{/* Title */}
|
||||||
|
<Text fw={600}>- {node.label}</Text>
|
||||||
|
|
||||||
|
{/* Children */}
|
||||||
|
{sub.map((child: any, i) => (
|
||||||
|
<RenderNode key={i} node={child} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PermissionRole({ permissions }: { permissions: string[] }) {
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
if (!permissions?.length) return <Text c="dimmed">-</Text>;
|
||||||
|
|
||||||
|
const groups = groupPermissions(permissions);
|
||||||
|
const rootNodes = Object.values(groups);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg">
|
||||||
|
{
|
||||||
|
showAll ?
|
||||||
|
rootNodes.map((node: any, idx) => (
|
||||||
|
<RenderNode key={idx} node={node} />
|
||||||
|
))
|
||||||
|
:
|
||||||
|
rootNodes.slice(0, 2).map((node: any, idx) => (
|
||||||
|
<RenderNode key={idx} node={node} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => setShowAll(!showAll)}
|
||||||
|
w="fit-content"
|
||||||
|
ml="md"
|
||||||
|
>
|
||||||
|
{showAll ? "View less" : "View more"}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
465
src/components/UserRoleSetting.tsx
Normal file
465
src/components/UserRoleSetting.tsx
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
|
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import notification from "./notificationGlobal";
|
||||||
|
import PermissionRole from "./PermissionRole";
|
||||||
|
|
||||||
|
export default function UserRoleSetting() {
|
||||||
|
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: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
roleId: "",
|
||||||
|
});
|
||||||
|
const [dataTambah, setDataTambah] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
roleId: "",
|
||||||
|
password: "",
|
||||||
|
phone: "",
|
||||||
|
});
|
||||||
|
const [error, setError] = useState({
|
||||||
|
name: false,
|
||||||
|
email: false,
|
||||||
|
roleId: false,
|
||||||
|
password: false,
|
||||||
|
phone: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
try {
|
||||||
|
setBtnLoading(true);
|
||||||
|
const res = await apiFetch.api.user.create.post(dataTambah);
|
||||||
|
if (res.status === 200) {
|
||||||
|
mutate();
|
||||||
|
closeTambah();
|
||||||
|
setDataTambah({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
roleId: "",
|
||||||
|
password: "",
|
||||||
|
phone: "",
|
||||||
|
});
|
||||||
|
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.delete.post({ id: dataDelete });
|
||||||
|
if (res.status === 200) {
|
||||||
|
mutate();
|
||||||
|
closeDelete();
|
||||||
|
notification({
|
||||||
|
title: "Success",
|
||||||
|
message: "Your user have been deleted",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification({
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to delete user",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notification({
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to delete user",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setBtnLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseEdit({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
roleId: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
setDataEdit(data);
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onValidation({
|
||||||
|
kat,
|
||||||
|
value,
|
||||||
|
aksi,
|
||||||
|
}: {
|
||||||
|
kat: "name" | "email" | "roleId" | "password" | "phone";
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
if (dataEdit.name.length > 0) {
|
||||||
|
setBtnDisable(false);
|
||||||
|
}
|
||||||
|
}, [dataEdit.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Modal Edit */}
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={close}
|
||||||
|
title={"Edit"}
|
||||||
|
centered
|
||||||
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||||
|
>
|
||||||
|
<Stack gap="ld">
|
||||||
|
<Input.Wrapper label="Edit Kategori">
|
||||||
|
<Input
|
||||||
|
value={dataEdit.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
onValidation({
|
||||||
|
kat: "name",
|
||||||
|
value: e.target.value,
|
||||||
|
aksi: "edit",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button variant="light" onClick={close}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
onClick={handleEdit}
|
||||||
|
disabled={btnDisable}
|
||||||
|
loading={btnLoading}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Modal Tambah */}
|
||||||
|
<Modal
|
||||||
|
opened={openedTambah}
|
||||||
|
onClose={closeTambah}
|
||||||
|
title={"Tambah"}
|
||||||
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||||
|
>
|
||||||
|
<Stack gap="ld">
|
||||||
|
<Input.Wrapper
|
||||||
|
label="Nama"
|
||||||
|
description=""
|
||||||
|
error={error.name ? "Field is required" : ""}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={dataTambah.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
onValidation({
|
||||||
|
kat: "name",
|
||||||
|
value: e.target.value,
|
||||||
|
aksi: "tambah",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Select
|
||||||
|
label="Role"
|
||||||
|
placeholder="Pilih Role"
|
||||||
|
data={listRole.map((r: any) => ({
|
||||||
|
value: r.id,
|
||||||
|
label: r.name,
|
||||||
|
}))}
|
||||||
|
value={dataTambah.roleId || null}
|
||||||
|
error={error.roleId ? "Field is required" : ""}
|
||||||
|
onChange={(_value, option) => {
|
||||||
|
onValidation({
|
||||||
|
kat: "roleId",
|
||||||
|
value: option?.value,
|
||||||
|
aksi: "tambah",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input.Wrapper label="Phone" description="">
|
||||||
|
<Input
|
||||||
|
value={dataTambah.phone}
|
||||||
|
onChange={(e) =>
|
||||||
|
onValidation({
|
||||||
|
kat: "phone",
|
||||||
|
value: e.target.value,
|
||||||
|
aksi: "tambah",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Input.Wrapper
|
||||||
|
label="Email"
|
||||||
|
description=""
|
||||||
|
error={error.email ? "Field is required" : ""}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={dataTambah.email}
|
||||||
|
onChange={(e) =>
|
||||||
|
onValidation({
|
||||||
|
kat: "email",
|
||||||
|
value: e.target.value,
|
||||||
|
aksi: "tambah",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Input.Wrapper
|
||||||
|
label="Password"
|
||||||
|
description=""
|
||||||
|
error={error.password ? "Field is required" : ""}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={dataTambah.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
onValidation({
|
||||||
|
kat: "password",
|
||||||
|
value: e.target.value,
|
||||||
|
aksi: "tambah",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Input.Wrapper>
|
||||||
|
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button variant="light" onClick={closeTambah}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
onClick={handleCreate}
|
||||||
|
disabled={
|
||||||
|
btnDisable ||
|
||||||
|
dataTambah.name.length < 1 ||
|
||||||
|
dataTambah.email.length < 1 ||
|
||||||
|
dataTambah.password.length < 1 ||
|
||||||
|
dataTambah.roleId.length < 1 ||
|
||||||
|
dataTambah.phone.length < 1
|
||||||
|
}
|
||||||
|
loading={btnLoading}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Modal Delete */}
|
||||||
|
<Modal
|
||||||
|
opened={openedDelete}
|
||||||
|
onClose={closeDelete}
|
||||||
|
title={"Delete"}
|
||||||
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="md" color="gray.6">
|
||||||
|
Apakah anda yakin ingin menghapus user ini?
|
||||||
|
</Text>
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button variant="light" onClick={closeDelete}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
onClick={handleDelete}
|
||||||
|
loading={btnLoading}
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Daftar Role
|
||||||
|
</Title>
|
||||||
|
<Tooltip label="Tambah Role">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconPlus size={20} />}
|
||||||
|
onClick={openTambah}
|
||||||
|
>
|
||||||
|
Tambah
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Role</Table.Th>
|
||||||
|
<Table.Th>Permission</Table.Th>
|
||||||
|
<Table.Th>Aksi</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{list.length > 0 ? (
|
||||||
|
list?.map((v: any) => (
|
||||||
|
<Table.Tr key={v.id}>
|
||||||
|
<Table.Td>{v.name}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<PermissionRole permissions={v.permissions} />
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group>
|
||||||
|
<Tooltip label="Edit User">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||||
|
onClick={() => chooseEdit({ data: v })}
|
||||||
|
>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Delete User">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
color="red"
|
||||||
|
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||||
|
onClick={() => {
|
||||||
|
setDataDelete(v.id);
|
||||||
|
openDelete();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={5} align="center">
|
||||||
|
Data Role Tidak Ditemukan
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/lib/groupPermission.ts
Normal file
59
src/lib/groupPermission.ts
Normal file
@@ -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<string, string[]> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
178
src/lib/listPermission.json
Normal file
178
src/lib/listPermission.json
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"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": "credential",
|
||||||
|
"label": "Credential",
|
||||||
|
"default": true,
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import DesaSetting from "@/components/DesaSetting";
|
|||||||
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
||||||
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||||
import ProfileUser from "@/components/ProfileUser";
|
import ProfileUser from "@/components/ProfileUser";
|
||||||
|
import UserRoleSetting from "@/components/UserRoleSetting";
|
||||||
import UserSetting from "@/components/UserSetting";
|
import UserSetting from "@/components/UserSetting";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -14,7 +15,8 @@ import {
|
|||||||
IconCategory2,
|
IconCategory2,
|
||||||
IconMailSpark,
|
IconMailSpark,
|
||||||
IconUserCog,
|
IconUserCog,
|
||||||
IconUsersGroup,
|
IconUserScreen,
|
||||||
|
IconUsersGroup
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
@@ -50,6 +52,12 @@ export default function DetailSettingPage() {
|
|||||||
leftSection={<IconUsersGroup size={16} stroke={1.5} />}
|
leftSection={<IconUsersGroup size={16} stroke={1.5} />}
|
||||||
active={type === "user"}
|
active={type === "user"}
|
||||||
/>
|
/>
|
||||||
|
<NavLink
|
||||||
|
href={`?type=role`}
|
||||||
|
label="Role"
|
||||||
|
leftSection={<IconUserScreen size={16} stroke={1.5} />}
|
||||||
|
active={type === "role"}
|
||||||
|
/>
|
||||||
<NavLink
|
<NavLink
|
||||||
href={`?type=cat-pengaduan`}
|
href={`?type=cat-pengaduan`}
|
||||||
label="Kategori Pengaduan"
|
label="Kategori Pengaduan"
|
||||||
@@ -90,6 +98,8 @@ export default function DetailSettingPage() {
|
|||||||
<DesaSetting />
|
<DesaSetting />
|
||||||
) : type === "user" ? (
|
) : type === "user" ? (
|
||||||
<UserSetting />
|
<UserSetting />
|
||||||
|
) : type === "role" ? (
|
||||||
|
<UserRoleSetting />
|
||||||
) : (
|
) : (
|
||||||
<ProfileUser />
|
<ProfileUser />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user