upd: dashboard admin

Deskripsi:
- tambah role user
- api edit tambah dan delete role user

NO Issues
This commit is contained in:
2025-11-24 17:40:27 +08:00
parent 10db3f922e
commit ad7b40523c
4 changed files with 246 additions and 100 deletions

View File

@@ -12,6 +12,7 @@ model Role {
id String @id @default(cuid())
name String
permissions Json?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
User User[]

View 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>
);
}

View File

@@ -7,12 +7,11 @@ import {
Group,
Input,
Modal,
Select,
Stack,
Table,
Text,
Title,
Tooltip,
Tooltip
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
@@ -21,6 +20,7 @@ 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);
@@ -44,23 +44,15 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
phone: "",
email: "",
roleId: "",
permissions: [],
});
const [dataTambah, setDataTambah] = useState({
name: "",
email: "",
roleId: "",
password: "",
phone: "",
permissions: [],
});
const [error, setError] = useState({
name: false,
email: false,
roleId: false,
password: false,
phone: false,
permissions: false,
});
useShallowEffect(() => {
@@ -70,16 +62,13 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
async function handleCreate() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user.create.post(dataTambah);
const res = await apiFetch.api.user["role-create"].post(dataTambah as any);
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah({
name: "",
email: "",
roleId: "",
password: "",
phone: "",
permissions: [],
});
notification({
title: "Success",
@@ -139,19 +128,19 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
async function handleDelete() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user.delete.post({ id: dataDelete });
const res = await apiFetch.api.user["role-delete"].post({ id: dataDelete });
if (res.status === 200) {
mutate();
closeDelete();
notification({
title: "Success",
message: "Your user have been deleted",
message: "Your role have been deleted",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to delete user",
message: "Failed to delete role",
type: "error",
});
}
@@ -159,7 +148,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
console.error(error);
notification({
title: "Error",
message: "Failed to delete user",
message: "Failed to delete role",
type: "error",
});
} finally {
@@ -173,24 +162,14 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
data: {
id: string;
name: string;
phone: string;
email: string;
roleId: string;
permissions: [];
};
}) {
setDataEdit(data);
open();
}
function onValidation({
kat,
value,
aksi,
}: {
kat: "name" | "email" | "roleId" | "password" | "phone";
value: string | null;
aksi: "edit" | "tambah";
}) {
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 });
@@ -206,6 +185,8 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
}
}
console.log("dataTambah", dataTambah);
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(false);
@@ -275,68 +256,12 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
}
/>
</Input.Wrapper>
<Select
label="Role"
placeholder="Pilih Role"
data={listRole.map((r: any) => ({
value: r.id,
label: r.name,
}))}
value={dataTambah.roleId || null}
error={error.roleId ? "Field is required" : ""}
onChange={(_value, option) => {
onValidation({
kat: "roleId",
value: option?.value,
aksi: "tambah",
});
<PermissionTree
selected={dataTambah.permissions}
onChange={(permissions) => {
setDataTambah({ ...dataTambah, permissions: permissions as never[] });
}}
/>
<Input.Wrapper label="Phone" description="">
<Input
value={dataTambah.phone}
onChange={(e) =>
onValidation({
kat: "phone",
value: e.target.value,
aksi: "tambah",
})
}
/>
</Input.Wrapper>
<Input.Wrapper
label="Email"
description=""
error={error.email ? "Field is required" : ""}
>
<Input
value={dataTambah.email}
onChange={(e) =>
onValidation({
kat: "email",
value: e.target.value,
aksi: "tambah",
})
}
/>
</Input.Wrapper>
<Input.Wrapper
label="Password"
description=""
error={error.password ? "Field is required" : ""}
>
<Input
value={dataTambah.password}
onChange={(e) =>
onValidation({
kat: "password",
value: e.target.value,
aksi: "tambah",
})
}
/>
</Input.Wrapper>
<Group justify="center" grow>
<Button variant="light" onClick={closeTambah}>
Batal
@@ -347,10 +272,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
disabled={
btnDisable ||
dataTambah.name.length < 1 ||
dataTambah.email.length < 1 ||
dataTambah.password.length < 1 ||
dataTambah.roleId.length < 1 ||
dataTambah.phone.length < 1
dataTambah.permissions.length < 1
}
loading={btnLoading}
>
@@ -369,7 +291,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
>
<Stack gap="md">
<Text size="md" color="gray.6">
Apakah anda yakin ingin menghapus user ini?
Apakah anda yakin ingin menghapus role ini?
</Text>
<Group justify="center" grow>
<Button variant="light" onClick={closeDelete}>

View File

@@ -156,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: {
@@ -188,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