Merge pull request 'amalia/24-nov-25' (#35) from amalia/24-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/35
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label="Edit Setting">
|
||||
<Tooltip label={permissions.includes("setting.desa.edit") ? "Edit Setting" : "Edit Setting - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={!permissions.includes("setting.desa.edit")}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -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() {
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pelayanan Surat
|
||||
</Title>
|
||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{
|
||||
permissions.includes("setting.kategori_pelayanan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -572,7 +578,7 @@ export default function KategoriPelayananSurat() {
|
||||
<IconEye size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Edit Kategori">
|
||||
<Tooltip label={permissions.includes("setting.kategori_pelayanan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -581,11 +587,12 @@ export default function KategoriPelayananSurat() {
|
||||
setDataChoose(v);
|
||||
open();
|
||||
}}
|
||||
disabled={!permissions.includes("setting.kategori_pelayanan.edit")}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete Kategori">
|
||||
<Tooltip label={permissions.includes("setting.kategori_pelayanan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -595,6 +602,7 @@ export default function KategoriPelayananSurat() {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes("setting.kategori_pelayanan.delete")}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -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() {
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pengaduan
|
||||
</Title>
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{
|
||||
permissions.includes("setting.kategori_pengaduan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -318,17 +323,18 @@ export default function KategoriPengaduan() {
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="Edit Kategori">
|
||||
<Tooltip label={permissions.includes("setting.kategori_pengaduan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={!permissions.includes("setting.kategori_pengaduan.edit")}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete Kategori">
|
||||
<Tooltip label={permissions.includes("setting.kategori_pengaduan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -338,6 +344,7 @@ export default function KategoriPengaduan() {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes("setting.kategori_pengaduan.delete")}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
|
||||
{viewFile && (
|
||||
<>
|
||||
{typeFile == "pdf" ? (
|
||||
<embed src={viewFile} type="application/pdf" width="100%" height="100%" />
|
||||
<embed src={viewFile} type="application/pdf" width="100%" height="950" />
|
||||
) : (
|
||||
<Image
|
||||
radius="md"
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
144
src/components/PermissionTree.tsx
Normal file
144
src/components/PermissionTree.tsx
Normal file
@@ -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<Record<string, boolean>>({});
|
||||
|
||||
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 (
|
||||
<Stack gap={4} pl="xs">
|
||||
<Group wrap="nowrap">
|
||||
{children.length > 0 ? (
|
||||
<ActionIcon variant="subtle" onClick={() => toggle(node.key)}>
|
||||
{showChildren ? <IconChevronDown size={16} /> : <IconChevronRight size={16} />}
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<div style={{ width: 24 }} />
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
label={node.label}
|
||||
checked={isChecked}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => toggleCheck(e.target.checked)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{children.length > 0 && (
|
||||
<Collapse in={showChildren}>
|
||||
<Stack gap={4} pl="md">
|
||||
{children.map((c) => (
|
||||
<RenderNode key={c.key} node={c} />
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text size="sm">Hak Akses</Text>
|
||||
|
||||
{permissionConfig.menus.map((menu: Node) => (
|
||||
<RenderNode key={menu.key} node={menu} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
</Title>
|
||||
<Group gap="md">
|
||||
<Button variant="light" onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||
Ubah Password
|
||||
</Button>
|
||||
{
|
||||
permissions.includes("setting.profile.edit") && (
|
||||
<Button variant="light" onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
permissions.includes("setting.profile.password") && (
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||
Ubah Password
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</Group>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
|
||||
394
src/components/UserRoleSetting.tsx
Normal file
394
src/components/UserRoleSetting.tsx
Normal file
@@ -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 */}
|
||||
<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>
|
||||
<PermissionTree
|
||||
selected={dataTambah.permissions}
|
||||
onChange={(permissions) => {
|
||||
setDataTambah({ ...dataTambah, permissions: permissions as never[] });
|
||||
}}
|
||||
/>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeTambah}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleCreate}
|
||||
disabled={
|
||||
btnDisable ||
|
||||
dataTambah.name.length < 1 ||
|
||||
dataTambah.permissions.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 role 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>
|
||||
{
|
||||
permissions.includes('setting.user_role.tambah') && (
|
||||
<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={permissions.includes('setting.user_role.edit') ? "Edit Role" : "Edit Role - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={!permissions.includes('setting.user_role.edit')}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={permissions.includes('setting.user_role.delete') ? "Delete Role" : "Delete Role - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes('setting.user_role.delete')}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
<Title order={4} c="gray.2">
|
||||
Daftar User
|
||||
</Title>
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{
|
||||
permissions.includes('setting.user.tambah') && (
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -422,17 +428,18 @@ export default function UserSetting() {
|
||||
<Table.Td>{v.roleId}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="Edit User">
|
||||
<Tooltip label={permissions.includes('setting.user.edit') ? "Edit User" : "Edit User - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={!permissions.includes('setting.user.edit')}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete User">
|
||||
<Tooltip label={permissions.includes('setting.user.delete') ? "Delete User" : "Delete User - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -442,6 +449,7 @@ export default function UserSetting() {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes('setting.user.delete')}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
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;
|
||||
}
|
||||
310
src/lib/listPermission.json
Normal file
310
src/lib/listPermission.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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<JsonValue[]>([]);
|
||||
|
||||
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: <IconDashboard size={20} />,
|
||||
label: "Dashboard Overview",
|
||||
description: "Quick summary and insights",
|
||||
},
|
||||
{
|
||||
key: "pengaduan",
|
||||
path: "/scr/dashboard/pengaduan/list",
|
||||
icon: <IconMessageReport size={20} />,
|
||||
label: "Pengaduan Warga",
|
||||
description: "Manage pengaduan warga",
|
||||
},
|
||||
{
|
||||
key: "pelayanan",
|
||||
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||
icon: <IconFileCertificate size={20} />,
|
||||
label: "Pelayanan Surat",
|
||||
description: "Manage pelayanan surat",
|
||||
},
|
||||
{
|
||||
key: "warga",
|
||||
path: "/scr/dashboard/warga/list-warga",
|
||||
icon: <IconUsersGroup size={20} />,
|
||||
label: "Warga",
|
||||
description: "Manage warga",
|
||||
},
|
||||
{
|
||||
key: "setting",
|
||||
path: "/scr/dashboard/setting/detail-setting",
|
||||
icon: <IconSettings size={20} />,
|
||||
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: <IconKey size={20} />,
|
||||
label: "API Key Manager",
|
||||
description: "Create and manage API keys",
|
||||
},
|
||||
{
|
||||
key: "credential",
|
||||
path: "/scr/dashboard/credential/credential",
|
||||
icon: <IconLock size={20} />,
|
||||
label: "Credentials",
|
||||
@@ -264,7 +285,7 @@ function NavigationDashboard() {
|
||||
|
||||
return (
|
||||
<Stack gap="xs" p="sm">
|
||||
{navItems.map((item) => (
|
||||
{navItems.filter((item) => permissions.includes(item.key)).map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
active={isActive(item.path as keyof typeof clientRoute)}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
IconUser
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
@@ -76,11 +77,17 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
const [host, setHost] = useState<User | null>(null);
|
||||
const [noSurat, setNoSurat] = useState("");
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
|
||||
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" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
@@ -285,6 +293,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.terima")}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
@@ -297,6 +306,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.diterima.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
@@ -306,6 +316,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.diterima.setujui")}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
|
||||
@@ -175,7 +175,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
{list?.length === 0 ? (
|
||||
{Array.isArray(list) && list?.length === 0 ? (
|
||||
<Flex justify="center" align="center" py={"xl"}>
|
||||
<Stack gap={4} align="center">
|
||||
<IconFileSad size={32} color="gray" />
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
@@ -77,11 +78,17 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
useDisclosure(false);
|
||||
const [keterangan, setKeterangan] = useState("");
|
||||
const [host, setHost] = useState<User | null>(null);
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
|
||||
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("pengaduan"));
|
||||
setPermissions(onlySetting);
|
||||
}
|
||||
}
|
||||
fetchHost();
|
||||
}, []);
|
||||
@@ -294,6 +301,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
disabled={!permissions.includes("pengaduan.antrian.tolak")}
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
@@ -303,6 +311,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.antrian.terima")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
@@ -315,6 +324,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.diterima.dikerjakan")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
@@ -327,6 +337,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.dikerjakan.selesai")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
|
||||
@@ -2,7 +2,9 @@ import DesaSetting from "@/components/DesaSetting";
|
||||
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
||||
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||
import ProfileUser from "@/components/ProfileUser";
|
||||
import UserRoleSetting from "@/components/UserRoleSetting";
|
||||
import UserSetting from "@/components/UserSetting";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Card,
|
||||
Container,
|
||||
@@ -14,14 +16,78 @@ import {
|
||||
IconCategory2,
|
||||
IconMailSpark,
|
||||
IconUserCog,
|
||||
IconUsersGroup,
|
||||
IconUserScreen,
|
||||
IconUsersGroup
|
||||
} from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export default function DetailSettingPage() {
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const type = query.get("type");
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPermissions() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
if (Array.isArray(data?.permissions)) {
|
||||
const onlySetting = data.permissions.filter((p: any) => p.startsWith("setting"));
|
||||
setPermissions(onlySetting);
|
||||
} else {
|
||||
setPermissions([]);
|
||||
}
|
||||
}
|
||||
fetchPermissions();
|
||||
}, []);
|
||||
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
key: "setting.profile",
|
||||
path: "profile",
|
||||
icon: <IconUserCog size={20} />,
|
||||
label: "Profile",
|
||||
description: "Manage profile settings",
|
||||
},
|
||||
{
|
||||
key: "setting.user",
|
||||
path: "user",
|
||||
icon: <IconUsersGroup size={20} />,
|
||||
label: "User",
|
||||
description: "Manage user accounts",
|
||||
},
|
||||
{
|
||||
key: "setting.user_role",
|
||||
path: "role",
|
||||
icon: <IconUserScreen size={20} />,
|
||||
label: "Role",
|
||||
description: "Manage user roles",
|
||||
},
|
||||
{
|
||||
key: "setting.kategori_pengaduan",
|
||||
path: "cat-pengaduan",
|
||||
icon: <IconCategory2 size={20} />,
|
||||
label: "Kategori Pengaduan",
|
||||
description: "Manage complaint categories",
|
||||
},
|
||||
{
|
||||
key: "setting.kategori_pelayanan",
|
||||
path: "cat-pelayanan",
|
||||
icon: <IconMailSpark size={20} />,
|
||||
label: "Kategori Pelayanan Surat",
|
||||
description: "Manage letter service categories",
|
||||
},
|
||||
{
|
||||
key: "setting.desa",
|
||||
path: "desa",
|
||||
icon: <IconBuildingBank size={20} />,
|
||||
label: "Desa",
|
||||
description: "Manage desa information",
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
@@ -38,36 +104,17 @@ export default function DetailSettingPage() {
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
href={`?type=profile`}
|
||||
label="Profile"
|
||||
leftSection={<IconUserCog size={16} stroke={1.5} />}
|
||||
active={type === "profile" || !type}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=user`}
|
||||
label="User"
|
||||
leftSection={<IconUsersGroup size={16} stroke={1.5} />}
|
||||
active={type === "user"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pengaduan`}
|
||||
label="Kategori Pengaduan"
|
||||
leftSection={<IconCategory2 size={16} stroke={1.5} />}
|
||||
active={type === "cat-pengaduan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pelayanan`}
|
||||
label="Kategori Pelayanan Surat"
|
||||
leftSection={<IconMailSpark size={16} stroke={1.5} />}
|
||||
active={type === "cat-pelayanan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=desa`}
|
||||
label="Desa"
|
||||
leftSection={<IconBuildingBank size={16} stroke={1.5} />}
|
||||
active={type === "desa"}
|
||||
/>
|
||||
{
|
||||
navItems.filter((item) => permissions.includes(item.key)).map((item) => (
|
||||
<NavLink
|
||||
key={item.key}
|
||||
href={'?type=' + item.path}
|
||||
label={item.label}
|
||||
leftSection={item.icon}
|
||||
active={type === item.path || (!type && item.path === 'profile')}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
@@ -83,15 +130,17 @@ export default function DetailSettingPage() {
|
||||
}}
|
||||
>
|
||||
{type === "cat-pengaduan" ? (
|
||||
<KategoriPengaduan />
|
||||
<KategoriPengaduan permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pengaduan"))} />
|
||||
) : type === "cat-pelayanan" ? (
|
||||
<KategoriPelayananSurat />
|
||||
<KategoriPelayananSurat permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pelayanan"))} />
|
||||
) : type === "desa" ? (
|
||||
<DesaSetting />
|
||||
<DesaSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.desa"))} />
|
||||
) : type === "user" ? (
|
||||
<UserSetting />
|
||||
<UserSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user."))} />
|
||||
) : type === "role" ? (
|
||||
<UserRoleSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user_role"))} />
|
||||
) : (
|
||||
<ProfileUser />
|
||||
<ProfileUser permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.profile"))} />
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
@@ -78,12 +78,12 @@ export default function ListWargaPage() {
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
list?.length === 0 ? (
|
||||
Array.isArray(list) && list?.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
list?.map((item, i) => (
|
||||
Array.isArray(list) && list?.map((item, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>{item.phone}</Table.Td>
|
||||
|
||||
@@ -6,10 +6,16 @@ const UserRoute = new Elysia({
|
||||
prefix: "user",
|
||||
tags: ["user"],
|
||||
})
|
||||
.get('/find', (ctx) => {
|
||||
.get('/find', async (ctx) => {
|
||||
const { user } = ctx as any
|
||||
const permissions = await prisma.role.findFirst({
|
||||
where: { id: user?.roleId },
|
||||
select: { permissions: true }
|
||||
});
|
||||
|
||||
return {
|
||||
user: user as User
|
||||
user: user as User,
|
||||
permissions: permissions?.permissions || []
|
||||
}
|
||||
}, {
|
||||
detail: {
|
||||
@@ -150,7 +156,11 @@ const UserRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
.get("/role", async () => {
|
||||
const data = await prisma.role.findMany()
|
||||
const data = await prisma.role.findMany({
|
||||
where: {
|
||||
isActive: true
|
||||
}
|
||||
})
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
@@ -182,5 +192,80 @@ const UserRoute = new Elysia({
|
||||
description: "delete user",
|
||||
}
|
||||
})
|
||||
.post("role-create", async ({ body }) => {
|
||||
const { name, permission } = body;
|
||||
const create = await prisma.role.create({
|
||||
data: {
|
||||
name,
|
||||
permissions: permission
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Role created successfully",
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
permission: t.Array(t.Any(), { minItems: 1, error: "permission is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "create-role",
|
||||
description: "create role",
|
||||
}
|
||||
})
|
||||
.post("/role-update", async ({ body }) => {
|
||||
const { id, name, permission } = body;
|
||||
const update = await prisma.role.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
permissions: permission
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User role updated successfully",
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id is required" }),
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
permission: t.Array(t.String(), { minItems: 1, error: "permission is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "update-role",
|
||||
description: "update role",
|
||||
}
|
||||
})
|
||||
.post("role-delete", async ({ body }) => {
|
||||
const { id } = body;
|
||||
await prisma.role.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
isActive: false
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Role deleted successfully",
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "delete-role",
|
||||
description: "delete role",
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default UserRoute
|
||||
Reference in New Issue
Block a user