upd: dashboard admin

Deskripsi:
- tambah role user
- edit role user

No Issues
This commit is contained in:
2025-11-25 12:15:29 +08:00
parent ad7b40523c
commit c5b1452955
4 changed files with 160 additions and 120 deletions

View File

@@ -16,112 +16,148 @@ export default function PermissionTree({
selected: string[]; selected: string[];
onChange: (val: string[]) => void; onChange: (val: string[]) => void;
}) { }) {
const [open, setOpen] = useState<Record<string, boolean>>({}); // Ambil semua child dari node
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
const toggle = (key: string) => { function toggleNode(label: string) {
setOpen((prev) => ({ ...prev, [key]: !prev[key] })); setOpenNodes(prev => ({ ...prev, [label]: !prev[label] }));
};
// 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); function getAllChildKeys(node: Node): string[] {
let some = selected.includes(node.key); let result: string[] = [];
for (const c of children) { if (node.children) {
const childState = checkState(c); node.children.forEach((c) => {
if (!childState.all) all = false; result.push(c.key);
if (childState.some) some = true; result = [...result, ...getAllChildKeys(c)];
});
}
return result;
} }
return { all, some };
};
// Untuk ordering sesuai urutan JSON // Dapatkan parentKey, jika ada
const getOrderedKeys = (nodes: Node[]): string[] => function getParentKey(key: string) {
nodes.flatMap((n) => [n.key, ...getOrderedKeys(n.children || [])]); const split = key.split(".");
if (split.length <= 1) return null;
split.pop();
return split.join(".");
}
const RenderNode = ({ node }: { node: Node }) => { // Update parent ke atas secara rekursif
const children = node.children || []; function updateParent(next: string[], parentKey: string | null): string[] {
if (!parentKey) return next;
const state = checkState(node); // ← gunakan recursive evaluator const allChildKeys = findAllChildKeysFromKey(parentKey);
const isChecked = state.all; const selectedChild = allChildKeys.filter((c) => next.includes(c));
const isIndeterminate = !state.all && state.some;
const showChildren = open[node.key] ?? false; if (selectedChild.length === 0) {
// Semua child uncheck → parent uncheck
// Ambil semua key anak + parent next = next.filter((x) => x !== parentKey);
const collectKeys = (n: Node): string[] => { } else if (selectedChild.length === allChildKeys.length) {
if (!n.children) return [n.key]; // Semua child check → parent check
return [n.key, ...n.children.flatMap(collectKeys)]; if (!next.includes(parentKey)) {
}; next.push(parentKey);
}
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 { } else {
// hilangkan parent + semua child // Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
allKeys.forEach((k) => updated.delete(k)); if (!next.includes(parentKey)) {
next.push(parentKey);
}
} }
// ⬇⬇⬇ PERBAIKAN PENTING ⬇⬇⬇ // Rekursif naik ke atas
// return updateParent(next, getParentKey(parentKey));
// Jika node indeterminate → parent harus tetap ada di selected
//
if (isIndeterminate) {
updated.add(node.key);
} }
// Jika semua child tercentang → parent harus checked // dapatkan child dari string key
function findAllChildKeysFromKey(parentKey: string) {
const list: string[] = [];
function traverse(nodes: Node[]) {
nodes.forEach((n) => {
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
list.push(n.key);
}
if (n.children) traverse(n.children);
});
}
traverse(permissionConfig.menus);
return list;
}
const RenderMenu = ({ menu }: { menu: Node }) => {
const hasChild = menu.children && menu.children.length > 0;
const open = openNodes[menu.label] ?? false;
const childKeys = getAllChildKeys(menu);
const isChecked = selected.includes(menu.key);
const isIndeterminate =
!isChecked &&
selected.some(
(x) =>
typeof x === "string" &&
x.startsWith(menu.key + ".")
);
function handleCheck() {
let next = [...selected];
if (childKeys.length > 0) {
// klik parent
if (!isChecked) {
next = [...new Set([...next, menu.key, ...childKeys])];
} else {
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// klik child
if (isChecked) { if (isChecked) {
updated.add(node.key); next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
} }
onChange([...updated]); next = updateParent(next, getParentKey(menu.key));
}; onChange(next);
}
return ( return (
<Stack gap={4} pl="xs"> <Stack gap={4}>
<Group wrap="nowrap"> <Group gap="xs">
{children.length > 0 ? ( {menu.children && menu.children.length > 0 ? (
<ActionIcon variant="subtle" onClick={() => toggle(node.key)}> <ActionIcon
{showChildren ? <IconChevronDown size={16} /> : <IconChevronRight size={16} />} variant="subtle"
onClick={() => toggleNode(menu.label)}
>
{openNodes[menu.label] ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)}
</ActionIcon> </ActionIcon>
) : ( ) : (
<div style={{ width: 24 }} /> <div style={{ width: 28 }} />
)} )}
<Checkbox <Checkbox
label={node.label} label={menu.label}
checked={isChecked} checked={isChecked}
indeterminate={isIndeterminate} indeterminate={isIndeterminate}
onChange={(e) => toggleCheck(e.target.checked)} onChange={handleCheck}
/> />
</Group> </Group>
{children.length > 0 && ( {menu.children && (
<Collapse in={showChildren}> <Collapse in={open}>
<Stack gap={4} pl="md"> <Stack gap={4} pl="md">
{children.map((c) => ( {menu.children.map((child) => (
<RenderNode key={c.key} node={c} /> <RenderMenu key={child.key} menu={child} />
))} ))}
</Stack> </Stack>
</Collapse> </Collapse>
@@ -130,14 +166,11 @@ export default function PermissionTree({
); );
}; };
return ( return (
<Stack> <Stack>
<Text size="sm">Hak Akses</Text> <Text size="sm">Hak Akses</Text>
{permissionConfig.menus.map((menu: Node) => ( {permissionConfig.menus.map((menu: Node) => (
<RenderNode key={menu.key} node={menu} /> <RenderMenu key={menu.key} menu={menu} />
))} ))}
</Stack> </Stack>
); );

View File

@@ -72,13 +72,13 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
}); });
notification({ notification({
title: "Success", title: "Success",
message: "Your user have been saved", message: "Your role have been saved",
type: "success", type: "success",
}); });
} else { } else {
notification({ notification({
title: "Error", title: "Error",
message: "Failed to create user ", message: "Failed to create role",
type: "error", type: "error",
}); });
} }
@@ -86,7 +86,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
console.error(error); console.error(error);
notification({ notification({
title: "Error", title: "Error",
message: "Failed to create user", message: "Failed to create role",
type: "error", type: "error",
}); });
} finally { } finally {
@@ -97,19 +97,19 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
async function handleEdit() { async function handleEdit() {
try { try {
setBtnLoading(true); setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit); const res = await apiFetch.api.user["role-update"].post(dataEdit as any);
if (res.status === 200) { if (res.status === 200) {
mutate(); mutate();
close(); close();
notification({ notification({
title: "Success", title: "Success",
message: "Your category have been saved", message: "Your role have been saved",
type: "success", type: "success",
}); });
} else { } else {
notification({ notification({
title: "Error", title: "Error",
message: "Failed to edit category", message: "Failed to edit role",
type: "error", type: "error",
}); });
} }
@@ -117,7 +117,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
console.error(error); console.error(error);
notification({ notification({
title: "Error", title: "Error",
message: "Failed to edit category", message: "Failed to edit role",
type: "error", type: "error",
}); });
} finally { } finally {
@@ -156,16 +156,10 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
} }
} }
function chooseEdit({ function chooseEdit({ data }: { data: { id: string; name: string; permissions: []; }; }) {
data, setDataEdit({
}: { id: data.id, name: data.name, permissions: data.permissions ? data.permissions : []
data: { });
id: string;
name: string;
permissions: [];
};
}) {
setDataEdit(data);
open(); open();
} }
@@ -185,7 +179,6 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
} }
} }
console.log("dataTambah", dataTambah);
useShallowEffect(() => { useShallowEffect(() => {
if (dataEdit.name.length > 0) { if (dataEdit.name.length > 0) {
@@ -200,8 +193,8 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
opened={opened} opened={opened}
onClose={close} onClose={close}
title={"Edit"} title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
> >
<Stack gap="ld"> <Stack gap="ld">
<Input.Wrapper label="Edit Kategori"> <Input.Wrapper label="Edit Kategori">
@@ -216,6 +209,12 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
} }
/> />
</Input.Wrapper> </Input.Wrapper>
<PermissionTree
selected={dataEdit.permissions}
onChange={(permissions) => {
setDataEdit({ ...dataEdit, permissions: permissions as never[] });
}}
/>
<Group justify="center" grow> <Group justify="center" grow>
<Button variant="light" onClick={close}> <Button variant="light" onClick={close}>
Batal Batal
@@ -223,7 +222,11 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
<Button <Button
variant="filled" variant="filled"
onClick={handleEdit} onClick={handleEdit}
disabled={btnDisable} disabled={
btnDisable ||
dataEdit.name.length < 1 ||
dataEdit.permissions?.length < 1
}
loading={btnLoading} loading={btnLoading}
> >
Simpan Simpan
@@ -238,6 +241,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
onClose={closeTambah} onClose={closeTambah}
title={"Tambah"} title={"Tambah"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
> >
<Stack gap="ld"> <Stack gap="ld">
<Input.Wrapper <Input.Wrapper

View File

@@ -24,41 +24,41 @@
}, },
{ {
"key": "pengaduan.antrian", "key": "pengaduan.antrian",
"label": "Antrian", "label": "Detail pengaduan dengan status antrian",
"default": true, "default": true,
"children": [ "children": [
{ {
"key": "pengaduan.antrian.tolak", "key": "pengaduan.antrian.tolak",
"label": "Menolak", "label": "Menolak pengaduan",
"default": true "default": true
}, },
{ {
"key": "pengaduan.antrian.terima", "key": "pengaduan.antrian.terima",
"label": "Menerima", "label": "Menerima pengaduan",
"default": true "default": true
} }
] ]
}, },
{ {
"key": "pengaduan.diterima", "key": "pengaduan.diterima",
"label": "Diterima", "label": "Detail pengaduan dengan status diterima",
"default": true, "default": true,
"children": [ "children": [
{ {
"key": "pengaduan.diterima.dikerjakan", "key": "pengaduan.diterima.dikerjakan",
"label": "Dikerjakan", "label": "Menegerjakan pengaduan",
"default": true "default": true
} }
] ]
}, },
{ {
"key": "pengaduan.dikerjakan", "key": "pengaduan.dikerjakan",
"label": "Dikerjakan", "label": "Detail pengaduan dengan status dikerjakan",
"default": true, "default": true,
"children": [ "children": [
{ {
"key": "pengaduan.dikerjakan.selesai", "key": "pengaduan.dikerjakan.selesai",
"label": "Diselesaikan", "label": "Menyelesaikan pengaduan",
"default": true "default": true
} }
] ]
@@ -77,34 +77,34 @@
}, },
{ {
"key": "pelayanan.antrian", "key": "pelayanan.antrian",
"label": "Antrian", "label": "Detail pelayanan dengan status antrian",
"default": true, "default": true,
"children": [ "children": [
{ {
"key": "pelayanan.antrian.tolak", "key": "pelayanan.antrian.tolak",
"label": "Menolak", "label": "Menolak pelayanan",
"default": true "default": true
}, },
{ {
"key": "pelayanan.antrian.terima", "key": "pelayanan.antrian.terima",
"label": "Menerima", "label": "Menerima pelayanan",
"default": true "default": true
} }
] ]
}, },
{ {
"key": "pelayanan.diterima", "key": "pelayanan.diterima",
"label": "Diterima", "label": "Detail pelayanan dengan status diterima",
"default": true, "default": true,
"children": [ "children": [
{ {
"key": "pelayanan.diterima.tolak", "key": "pelayanan.diterima.tolak",
"label": "Menolak", "label": "Menolak pelayanan",
"default": true "default": true
}, },
{ {
"key": "pelayanan.diterima.setujui", "key": "pelayanan.diterima.setujui",
"label": "Menyetujui", "label": "Menyetujui pelayanan",
"default": true "default": true
} }
] ]
@@ -300,7 +300,7 @@
"default": true, "default": true,
"children": [ "children": [
{ {
"key": "credential.viewØ", "key": "credential.view",
"label": "View List", "label": "View List",
"default": true "default": true
} }

View File

@@ -159,6 +159,9 @@ const UserRoute = new Elysia({
const data = await prisma.role.findMany({ const data = await prisma.role.findMany({
where: { where: {
isActive: true isActive: true
},
orderBy: {
name: "asc"
} }
}) })
return data return data
@@ -193,11 +196,11 @@ const UserRoute = new Elysia({
} }
}) })
.post("role-create", async ({ body }) => { .post("role-create", async ({ body }) => {
const { name, permission } = body; const { name, permissions } = body;
const create = await prisma.role.create({ const create = await prisma.role.create({
data: { data: {
name, name,
permissions: permission permissions: permissions
} }
}); });
@@ -208,7 +211,7 @@ const UserRoute = new Elysia({
}, { }, {
body: t.Object({ body: t.Object({
name: t.String({ minLength: 1, error: "name is required" }), name: t.String({ minLength: 1, error: "name is required" }),
permission: t.Array(t.Any(), { minItems: 1, error: "permission is required" }) permissions: t.Any(),
}), }),
detail: { detail: {
summary: "create-role", summary: "create-role",
@@ -216,14 +219,14 @@ const UserRoute = new Elysia({
} }
}) })
.post("/role-update", async ({ body }) => { .post("/role-update", async ({ body }) => {
const { id, name, permission } = body; const { id, name, permissions } = body;
const update = await prisma.role.update({ const update = await prisma.role.update({
where: { where: {
id id
}, },
data: { data: {
name, name,
permissions: permission permissions
} }
}); });
@@ -235,7 +238,7 @@ const UserRoute = new Elysia({
body: t.Object({ body: t.Object({
id: t.String({ minLength: 1, error: "id is required" }), id: t.String({ minLength: 1, error: "id is required" }),
name: t.String({ minLength: 1, error: "name is required" }), name: t.String({ minLength: 1, error: "name is required" }),
permission: t.Array(t.String(), { minItems: 1, error: "permission is required" }) permissions: t.Any()
}), }),
detail: { detail: {
summary: "update-role", summary: "update-role",