amalia/24-nov-25 #35
@@ -11,6 +11,8 @@ datasource db {
|
|||||||
model Role {
|
model Role {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
|
permissions Json?
|
||||||
|
isActive Boolean @default(true)
|
||||||
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`)
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconEdit } from "@tabler/icons-react";
|
import { IconEdit } from "@tabler/icons-react";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ModalFile from "./ModalFile";
|
import ModalFile from "./ModalFile";
|
||||||
import notification from "./notificationGlobal";
|
import notification from "./notificationGlobal";
|
||||||
|
|
||||||
export default function DesaSetting() {
|
export default function DesaSetting({ permissions }: { permissions: JsonValue[] }) {
|
||||||
const [btnDisable, setBtnDisable] = useState(false);
|
const [btnDisable, setBtnDisable] = useState(false);
|
||||||
const [btnLoading, setBtnLoading] = useState(false);
|
const [btnLoading, setBtnLoading] = useState(false);
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
@@ -213,12 +214,13 @@ export default function DesaSetting() {
|
|||||||
}
|
}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Tooltip label="Edit Setting">
|
<Tooltip label={permissions.includes("setting.desa.edit") ? "Edit Setting" : "Edit Setting - Anda tidak memiliki akses"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||||
onClick={() => chooseEdit({ data: v })}
|
onClick={() => chooseEdit({ data: v })}
|
||||||
|
disabled={!permissions.includes("setting.desa.edit")}
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
|
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import notification from "./notificationGlobal";
|
import notification from "./notificationGlobal";
|
||||||
|
|
||||||
export default function KategoriPelayananSurat() {
|
export default function KategoriPelayananSurat({ permissions }: { permissions: JsonValue[] }) {
|
||||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
const [openedDetail, { open: openDetail, close: closeDetail }] =
|
const [openedDetail, { open: openDetail, close: closeDetail }] =
|
||||||
@@ -52,6 +53,7 @@ export default function KategoriPelayananSurat() {
|
|||||||
mutate();
|
mutate();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
try {
|
try {
|
||||||
setBtnLoading(true);
|
setBtnLoading(true);
|
||||||
@@ -533,6 +535,8 @@ export default function KategoriPelayananSurat() {
|
|||||||
<Title order={4} c="gray.2">
|
<Title order={4} c="gray.2">
|
||||||
Kategori Pelayanan Surat
|
Kategori Pelayanan Surat
|
||||||
</Title>
|
</Title>
|
||||||
|
{
|
||||||
|
permissions.includes("setting.kategori_pelayanan.tambah") && (
|
||||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -542,6 +546,8 @@ export default function KategoriPelayananSurat() {
|
|||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider my={0} />
|
<Divider my={0} />
|
||||||
<Stack gap={"md"}>
|
<Stack gap={"md"}>
|
||||||
@@ -572,7 +578,7 @@ export default function KategoriPelayananSurat() {
|
|||||||
<IconEye size={20} />
|
<IconEye size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Edit Kategori">
|
<Tooltip label={permissions.includes("setting.kategori_pelayanan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -581,11 +587,12 @@ export default function KategoriPelayananSurat() {
|
|||||||
setDataChoose(v);
|
setDataChoose(v);
|
||||||
open();
|
open();
|
||||||
}}
|
}}
|
||||||
|
disabled={!permissions.includes("setting.kategori_pelayanan.edit")}
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Delete Kategori">
|
<Tooltip label={permissions.includes("setting.kategori_pelayanan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -595,6 +602,7 @@ export default function KategoriPelayananSurat() {
|
|||||||
setDataDelete(v.id);
|
setDataDelete(v.id);
|
||||||
openDelete();
|
openDelete();
|
||||||
}}
|
}}
|
||||||
|
disabled={!permissions.includes("setting.kategori_pelayanan.delete")}
|
||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import notification from "./notificationGlobal";
|
import notification from "./notificationGlobal";
|
||||||
|
|
||||||
export default function KategoriPengaduan() {
|
export default function KategoriPengaduan({ permissions }: { permissions: JsonValue[] }) {
|
||||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
const [btnDisable, setBtnDisable] = useState(true);
|
const [btnDisable, setBtnDisable] = useState(true);
|
||||||
@@ -293,6 +294,8 @@ export default function KategoriPengaduan() {
|
|||||||
<Title order={4} c="gray.2">
|
<Title order={4} c="gray.2">
|
||||||
Kategori Pengaduan
|
Kategori Pengaduan
|
||||||
</Title>
|
</Title>
|
||||||
|
{
|
||||||
|
permissions.includes("setting.kategori_pengaduan.tambah") && (
|
||||||
<Tooltip label="Tambah Kategori Pengaduan">
|
<Tooltip label="Tambah Kategori Pengaduan">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -302,6 +305,8 @@ export default function KategoriPengaduan() {
|
|||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider my={0} />
|
<Divider my={0} />
|
||||||
<Stack gap={"md"}>
|
<Stack gap={"md"}>
|
||||||
@@ -318,17 +323,18 @@ export default function KategoriPengaduan() {
|
|||||||
<Table.Td>{v.name}</Table.Td>
|
<Table.Td>{v.name}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group>
|
<Group>
|
||||||
<Tooltip label="Edit Kategori">
|
<Tooltip label={permissions.includes("setting.kategori_pengaduan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||||
onClick={() => chooseEdit({ data: v })}
|
onClick={() => chooseEdit({ data: v })}
|
||||||
|
disabled={!permissions.includes("setting.kategori_pengaduan.edit")}
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Delete Kategori">
|
<Tooltip label={permissions.includes("setting.kategori_pengaduan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -338,6 +344,7 @@ export default function KategoriPengaduan() {
|
|||||||
setDataDelete(v.id);
|
setDataDelete(v.id);
|
||||||
openDelete();
|
openDelete();
|
||||||
}}
|
}}
|
||||||
|
disabled={!permissions.includes("setting.kategori_pengaduan.delete")}
|
||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
|
|||||||
{viewFile && (
|
{viewFile && (
|
||||||
<>
|
<>
|
||||||
{typeFile == "pdf" ? (
|
{typeFile == "pdf" ? (
|
||||||
<embed src={viewFile} type="application/pdf" width="100%" height="100%" />
|
<embed src={viewFile} type="application/pdf" width="100%" height="950" />
|
||||||
) : (
|
) : (
|
||||||
<Image
|
<Image
|
||||||
radius="md"
|
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,
|
Stack,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import notification from "./notificationGlobal";
|
import notification from "./notificationGlobal";
|
||||||
|
|
||||||
export default function ProfileUser() {
|
export default function ProfileUser({ permissions }: { permissions: JsonValue[] }) {
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [openedPassword, setOpenedPassword] = useState(false);
|
const [openedPassword, setOpenedPassword] = useState(false);
|
||||||
const [pwdBaru, setPwdBaru] = useState("");
|
const [pwdBaru, setPwdBaru] = useState("");
|
||||||
@@ -126,12 +127,21 @@ export default function ProfileUser() {
|
|||||||
Profile Pengguna
|
Profile Pengguna
|
||||||
</Title>
|
</Title>
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
|
{
|
||||||
|
permissions.includes("setting.profile.edit") && (
|
||||||
<Button variant="light" onClick={() => setOpened(true)}>
|
<Button variant="light" onClick={() => setOpened(true)}>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
permissions.includes("setting.profile.password") && (
|
||||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||||
Ubah Password
|
Ubah Password
|
||||||
</Button>
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Group>
|
</Group>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider my={0} />
|
<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";
|
} from "@mantine/core";
|
||||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import notification from "./notificationGlobal";
|
import notification from "./notificationGlobal";
|
||||||
|
|
||||||
export default function UserSetting() {
|
export default function UserSetting({ permissions }: { permissions: JsonValue[] }) {
|
||||||
const [btnDisable, setBtnDisable] = useState(true);
|
const [btnDisable, setBtnDisable] = useState(true);
|
||||||
const [btnLoading, setBtnLoading] = useState(false);
|
const [btnLoading, setBtnLoading] = useState(false);
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
@@ -390,6 +391,8 @@ export default function UserSetting() {
|
|||||||
<Title order={4} c="gray.2">
|
<Title order={4} c="gray.2">
|
||||||
Daftar User
|
Daftar User
|
||||||
</Title>
|
</Title>
|
||||||
|
{
|
||||||
|
permissions.includes('setting.user.tambah') && (
|
||||||
<Tooltip label="Tambah User">
|
<Tooltip label="Tambah User">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -399,6 +402,9 @@ export default function UserSetting() {
|
|||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider my={0} />
|
<Divider my={0} />
|
||||||
<Stack gap={"md"}>
|
<Stack gap={"md"}>
|
||||||
@@ -422,17 +428,18 @@ export default function UserSetting() {
|
|||||||
<Table.Td>{v.roleId}</Table.Td>
|
<Table.Td>{v.roleId}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group>
|
<Group>
|
||||||
<Tooltip label="Edit User">
|
<Tooltip label={permissions.includes('setting.user.edit') ? "Edit User" : "Edit User - Anda tidak memiliki akses"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||||
onClick={() => chooseEdit({ data: v })}
|
onClick={() => chooseEdit({ data: v })}
|
||||||
|
disabled={!permissions.includes('setting.user.edit')}
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Delete User">
|
<Tooltip label={permissions.includes('setting.user.delete') ? "Delete User" : "Delete User - Anda tidak memiliki akses"}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -442,6 +449,7 @@ export default function UserSetting() {
|
|||||||
setDataDelete(v.id);
|
setDataDelete(v.id);
|
||||||
openDelete();
|
openDelete();
|
||||||
}}
|
}}
|
||||||
|
disabled={!permissions.includes('setting.user.delete')}
|
||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</ActionIcon>
|
</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,
|
IconUsersGroup,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import type { User } from "generated/prisma";
|
import type { User } from "generated/prisma";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@@ -212,36 +213,54 @@ function HostView() {
|
|||||||
function NavigationDashboard() {
|
function NavigationDashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
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) =>
|
const isActive = (path: keyof typeof clientRoute) =>
|
||||||
location.pathname.startsWith(clientRoute[path]);
|
location.pathname.startsWith(clientRoute[path]);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
|
key: "dashboard",
|
||||||
path: "/scr/dashboard/dashboard-home",
|
path: "/scr/dashboard/dashboard-home",
|
||||||
icon: <IconDashboard size={20} />,
|
icon: <IconDashboard size={20} />,
|
||||||
label: "Dashboard Overview",
|
label: "Dashboard Overview",
|
||||||
description: "Quick summary and insights",
|
description: "Quick summary and insights",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "pengaduan",
|
||||||
path: "/scr/dashboard/pengaduan/list",
|
path: "/scr/dashboard/pengaduan/list",
|
||||||
icon: <IconMessageReport size={20} />,
|
icon: <IconMessageReport size={20} />,
|
||||||
label: "Pengaduan Warga",
|
label: "Pengaduan Warga",
|
||||||
description: "Manage pengaduan warga",
|
description: "Manage pengaduan warga",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "pelayanan",
|
||||||
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||||
icon: <IconFileCertificate size={20} />,
|
icon: <IconFileCertificate size={20} />,
|
||||||
label: "Pelayanan Surat",
|
label: "Pelayanan Surat",
|
||||||
description: "Manage pelayanan surat",
|
description: "Manage pelayanan surat",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "warga",
|
||||||
path: "/scr/dashboard/warga/list-warga",
|
path: "/scr/dashboard/warga/list-warga",
|
||||||
icon: <IconUsersGroup size={20} />,
|
icon: <IconUsersGroup size={20} />,
|
||||||
label: "Warga",
|
label: "Warga",
|
||||||
description: "Manage warga",
|
description: "Manage warga",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "setting",
|
||||||
path: "/scr/dashboard/setting/detail-setting",
|
path: "/scr/dashboard/setting/detail-setting",
|
||||||
icon: <IconSettings size={20} />,
|
icon: <IconSettings size={20} />,
|
||||||
label: "Setting",
|
label: "Setting",
|
||||||
@@ -249,12 +268,14 @@ function NavigationDashboard() {
|
|||||||
"Manage setting (category pengaduan dan pelayanan surat, desa, etc)",
|
"Manage setting (category pengaduan dan pelayanan surat, desa, etc)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "api_key",
|
||||||
path: "/scr/dashboard/apikey/apikey",
|
path: "/scr/dashboard/apikey/apikey",
|
||||||
icon: <IconKey size={20} />,
|
icon: <IconKey size={20} />,
|
||||||
label: "API Key Manager",
|
label: "API Key Manager",
|
||||||
description: "Create and manage API keys",
|
description: "Create and manage API keys",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "credential",
|
||||||
path: "/scr/dashboard/credential/credential",
|
path: "/scr/dashboard/credential/credential",
|
||||||
icon: <IconLock size={20} />,
|
icon: <IconLock size={20} />,
|
||||||
label: "Credentials",
|
label: "Credentials",
|
||||||
@@ -264,7 +285,7 @@ function NavigationDashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="xs" p="sm">
|
<Stack gap="xs" p="sm">
|
||||||
{navItems.map((item) => (
|
{navItems.filter((item) => permissions.includes(item.key)).map((item) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.path}
|
key={item.path}
|
||||||
active={isActive(item.path as keyof typeof clientRoute)}
|
active={isActive(item.path as keyof typeof clientRoute)}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
IconUser
|
IconUser
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import type { User } from "generated/prisma";
|
import type { User } from "generated/prisma";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
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 [host, setHost] = useState<User | null>(null);
|
||||||
const [noSurat, setNoSurat] = useState("");
|
const [noSurat, setNoSurat] = useState("");
|
||||||
const [openedPreview, setOpenedPreview] = useState(false);
|
const [openedPreview, setOpenedPreview] = useState(false);
|
||||||
|
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchHost() {
|
async function fetchHost() {
|
||||||
const { data } = await apiFetch.api.user.find.get();
|
const { data } = await apiFetch.api.user.find.get();
|
||||||
setHost(data?.user ?? null);
|
setHost(data?.user ?? null);
|
||||||
|
|
||||||
|
if (data?.permissions && Array.isArray(data.permissions)) {
|
||||||
|
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pelayanan"));
|
||||||
|
setPermissions(onlySetting);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fetchHost();
|
fetchHost();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -276,6 +283,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
|||||||
data?.status === "antrian" ? (
|
data?.status === "antrian" ? (
|
||||||
<Group justify="center" grow>
|
<Group justify="center" grow>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={!permissions.includes("pelayanan.antrian.tolak")}
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("tolak");
|
setCatModal("tolak");
|
||||||
@@ -285,6 +293,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
|||||||
Tolak
|
Tolak
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={!permissions.includes("pelayanan.antrian.terima")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("terima");
|
setCatModal("terima");
|
||||||
@@ -297,6 +306,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
|||||||
) : data?.status === "diterima" ? (
|
) : data?.status === "diterima" ? (
|
||||||
<Group justify="center" grow>
|
<Group justify="center" grow>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={!permissions.includes("pelayanan.diterima.tolak")}
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("tolak");
|
setCatModal("tolak");
|
||||||
@@ -306,6 +316,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
|||||||
Tolak
|
Tolak
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={!permissions.includes("pelayanan.diterima.setujui")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("terima");
|
setCatModal("terima");
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
{list?.length === 0 ? (
|
{Array.isArray(list) && list?.length === 0 ? (
|
||||||
<Flex justify="center" align="center" py={"xl"}>
|
<Flex justify="center" align="center" py={"xl"}>
|
||||||
<Stack gap={4} align="center">
|
<Stack gap={4} align="center">
|
||||||
<IconFileSad size={32} color="gray" />
|
<IconFileSad size={32} color="gray" />
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import type { User } from "generated/prisma";
|
import type { User } from "generated/prisma";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
@@ -77,11 +78,17 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
|||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
const [keterangan, setKeterangan] = useState("");
|
const [keterangan, setKeterangan] = useState("");
|
||||||
const [host, setHost] = useState<User | null>(null);
|
const [host, setHost] = useState<User | null>(null);
|
||||||
|
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchHost() {
|
async function fetchHost() {
|
||||||
const { data } = await apiFetch.api.user.find.get();
|
const { data } = await apiFetch.api.user.find.get();
|
||||||
setHost(data?.user ?? null);
|
setHost(data?.user ?? null);
|
||||||
|
|
||||||
|
if (data?.permissions && Array.isArray(data.permissions)) {
|
||||||
|
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pengaduan"));
|
||||||
|
setPermissions(onlySetting);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fetchHost();
|
fetchHost();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -294,6 +301,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
|||||||
<Group justify="center" grow>
|
<Group justify="center" grow>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
|
disabled={!permissions.includes("pengaduan.antrian.tolak")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("tolak");
|
setCatModal("tolak");
|
||||||
open();
|
open();
|
||||||
@@ -303,6 +311,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
disabled={!permissions.includes("pengaduan.antrian.terima")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("terima");
|
setCatModal("terima");
|
||||||
open();
|
open();
|
||||||
@@ -315,6 +324,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
|||||||
<Group justify="center" grow>
|
<Group justify="center" grow>
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
disabled={!permissions.includes("pengaduan.diterima.dikerjakan")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("terima");
|
setCatModal("terima");
|
||||||
open();
|
open();
|
||||||
@@ -327,6 +337,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
|||||||
<Group justify="center" grow>
|
<Group justify="center" grow>
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
disabled={!permissions.includes("pengaduan.dikerjakan.selesai")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCatModal("terima");
|
setCatModal("terima");
|
||||||
open();
|
open();
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ 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 apiFetch from "@/lib/apiFetch";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Container,
|
Container,
|
||||||
@@ -14,14 +16,78 @@ import {
|
|||||||
IconCategory2,
|
IconCategory2,
|
||||||
IconMailSpark,
|
IconMailSpark,
|
||||||
IconUserCog,
|
IconUserCog,
|
||||||
IconUsersGroup,
|
IconUserScreen,
|
||||||
|
IconUsersGroup
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export default function DetailSettingPage() {
|
export default function DetailSettingPage() {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const type = query.get("type");
|
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 (
|
return (
|
||||||
<Container size="xl" py="xl" w={"100%"}>
|
<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)",
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
navItems.filter((item) => permissions.includes(item.key)).map((item) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
href={`?type=profile`}
|
key={item.key}
|
||||||
label="Profile"
|
href={'?type=' + item.path}
|
||||||
leftSection={<IconUserCog size={16} stroke={1.5} />}
|
label={item.label}
|
||||||
active={type === "profile" || !type}
|
leftSection={item.icon}
|
||||||
/>
|
active={type === item.path || (!type && item.path === 'profile')}
|
||||||
<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"}
|
|
||||||
/>
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={9}>
|
<Grid.Col span={9}>
|
||||||
@@ -83,15 +130,17 @@ export default function DetailSettingPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === "cat-pengaduan" ? (
|
{type === "cat-pengaduan" ? (
|
||||||
<KategoriPengaduan />
|
<KategoriPengaduan permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pengaduan"))} />
|
||||||
) : type === "cat-pelayanan" ? (
|
) : type === "cat-pelayanan" ? (
|
||||||
<KategoriPelayananSurat />
|
<KategoriPelayananSurat permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pelayanan"))} />
|
||||||
) : type === "desa" ? (
|
) : type === "desa" ? (
|
||||||
<DesaSetting />
|
<DesaSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.desa"))} />
|
||||||
) : type === "user" ? (
|
) : 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>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|||||||
@@ -78,12 +78,12 @@ export default function ListWargaPage() {
|
|||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{
|
{
|
||||||
list?.length === 0 ? (
|
Array.isArray(list) && list?.length === 0 ? (
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
|
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
) : (
|
) : (
|
||||||
list?.map((item, i) => (
|
Array.isArray(list) && list?.map((item, i) => (
|
||||||
<Table.Tr key={i}>
|
<Table.Tr key={i}>
|
||||||
<Table.Td>{item.name}</Table.Td>
|
<Table.Td>{item.name}</Table.Td>
|
||||||
<Table.Td>{item.phone}</Table.Td>
|
<Table.Td>{item.phone}</Table.Td>
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ const UserRoute = new Elysia({
|
|||||||
prefix: "user",
|
prefix: "user",
|
||||||
tags: ["user"],
|
tags: ["user"],
|
||||||
})
|
})
|
||||||
.get('/find', (ctx) => {
|
.get('/find', async (ctx) => {
|
||||||
const { user } = ctx as any
|
const { user } = ctx as any
|
||||||
|
const permissions = await prisma.role.findFirst({
|
||||||
|
where: { id: user?.roleId },
|
||||||
|
select: { permissions: true }
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: user as User
|
user: user as User,
|
||||||
|
permissions: permissions?.permissions || []
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -150,7 +156,11 @@ const UserRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get("/role", async () => {
|
.get("/role", async () => {
|
||||||
const data = await prisma.role.findMany()
|
const data = await prisma.role.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
})
|
||||||
return data
|
return data
|
||||||
}, {
|
}, {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -182,5 +192,80 @@ const UserRoute = new Elysia({
|
|||||||
description: "delete user",
|
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
|
export default UserRoute
|
||||||
Reference in New Issue
Block a user