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