Merge pull request 'upd: dashboard admin' (#39) from amalia/26-nov-25 into main

Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/39
This commit is contained in:
2025-11-26 12:14:55 +08:00
8 changed files with 170 additions and 37 deletions

View File

@@ -206,9 +206,12 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
{
v.name == "TTD"
?
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always">
Lihat
</Anchor>
v.value ?
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always">
Lihat
</Anchor>
:
"-"
:
v.value
}

View File

@@ -329,7 +329,7 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes("setting.kategori_pengaduan.edit")}
disabled={!permissions.includes("setting.kategori_pengaduan.edit") || v.id == "lainnya"}
>
<IconEdit size={20} />
</ActionIcon>
@@ -344,7 +344,7 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
setDataDelete(v.id);
openDelete();
}}
disabled={!permissions.includes("setting.kategori_pengaduan.delete")}
disabled={!permissions.includes("setting.kategori_pengaduan.delete") || v.id == "lainnya"}
>
<IconTrash size={20} />
</ActionIcon>

View File

@@ -1,5 +1,5 @@
import { groupPermissions } from "@/lib/groupPermission";
import { Button, Stack, Text } from "@mantine/core";
import { Anchor, Flex, Stack, Text } from "@mantine/core";
import { useState } from "react";
interface Node {
@@ -14,7 +14,7 @@ function RenderNode({ node }: { node: Node }) {
return (
<Stack pl="md" gap={6}>
{/* Title */}
<Text fw={600}>- {node.label}</Text>
<Text size="sm">- {node.label}</Text>
{/* Children */}
{sub.map((child: any, i) => (
@@ -24,6 +24,22 @@ function RenderNode({ node }: { node: Node }) {
);
}
function RenderNode2({ node }: { node: Node }) {
const sub = Object.values(node.children || {});
return (
<Flex direction={"row"} wrap={'wrap'} gap={6}>
{/* Title */}
<Text size="sm">{node.label},</Text>
{/* Children */}
{sub.map((child: any, i) => (
<RenderNode2 key={i} node={child} />
))}
</Flex>
);
}
export default function PermissionRole({ permissions }: { permissions: string[] }) {
const [showAll, setShowAll] = useState(false);
if (!permissions?.length) return <Text c="dimmed">-</Text>;
@@ -32,7 +48,7 @@ export default function PermissionRole({ permissions }: { permissions: string[]
const rootNodes = Object.values(groups);
return (
<Stack gap="lg">
<Stack gap="sm">
{
showAll ?
rootNodes.map((node: any, idx) => (
@@ -40,18 +56,12 @@ export default function PermissionRole({ permissions }: { permissions: string[]
))
:
rootNodes.slice(0, 2).map((node: any, idx) => (
<RenderNode key={idx} node={node} />
<RenderNode2 key={idx} node={node} />
))
}
<Button
variant="subtle"
size="xs"
onClick={() => setShowAll(!showAll)}
w="fit-content"
ml="md"
>
<Anchor size="xs" onClick={() => setShowAll(!showAll)} >
{showAll ? "View less" : "View more"}
</Button>
</Anchor>
</Stack>
);
}

View File

@@ -169,7 +169,7 @@ export default function PermissionTree({
return (
<Stack>
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus.map((menu: Node) => (
{permissionConfig.menus.filter((menu: Node) => !menu.key.startsWith("api") && !menu.key.startsWith("credential")).map((menu: Node) => (
<RenderMenu key={menu.key} menu={menu} />
))}
</Stack>

View File

@@ -18,10 +18,18 @@ 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 listMenu from "../lib/listPermission.json";
import notification from "./notificationGlobal";
import PermissionRole from "./PermissionRole";
import PermissionTree from "./PermissionTree";
interface MenuNode {
key: string;
label: string;
default: boolean;
children?: MenuNode[];
}
export default function UserRoleSetting({ permissions }: { permissions: JsonValue[] }) {
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
@@ -179,6 +187,27 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
}
}
function buildOrderList(menus: MenuNode[]): string[] {
const list: string[] = [];
const traverse = (nodes: MenuNode[]) => {
nodes.forEach((node) => {
list.push(node.key);
if (node.children) traverse(node.children);
});
};
traverse(menus);
return list;
}
function sortByJsonOrder(arrayData: string[]): string[] {
const orderList = buildOrderList(listMenu.menus);
return arrayData.sort((a, b) => {
return orderList.indexOf(a) - orderList.indexOf(b);
});
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
@@ -212,7 +241,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
<PermissionTree
selected={dataEdit.permissions}
onChange={(permissions) => {
setDataEdit({ ...dataEdit, permissions: permissions as never[] });
setDataEdit({ ...dataEdit, permissions: sortByJsonOrder(permissions) as never[] });
}}
/>
<Group justify="center" grow>
@@ -263,7 +292,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
<PermissionTree
selected={dataTambah.permissions}
onChange={(permissions) => {
setDataTambah({ ...dataTambah, permissions: permissions as never[] });
setDataTambah({ ...dataTambah, permissions: sortByJsonOrder(permissions) as never[] });
}}
/>
<Group justify="center" grow>
@@ -346,11 +375,11 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
{list.length > 0 ? (
list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td w={"150"}>{v.name}</Table.Td>
<Table.Td>
<PermissionRole permissions={v.permissions} />
</Table.Td>
<Table.Td>
<Table.Td w={"100"}>
<Group>
<Tooltip label={permissions.includes('setting.user_role.edit') ? "Edit Role" : "Edit Role - Anda tidak memiliki akses"}>
<ActionIcon
@@ -358,7 +387,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes('setting.user_role.edit')}
disabled={!permissions.includes('setting.user_role.edit') || v.id == "developer"}
>
<IconEdit size={20} />
</ActionIcon>
@@ -373,7 +402,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
setDataDelete(v.id);
openDelete();
}}
disabled={!permissions.includes('setting.user_role.delete')}
disabled={!permissions.includes('setting.user_role.delete') || v.id == "developer"}
>
<IconTrash size={20} />
</ActionIcon>

View File

@@ -107,19 +107,19 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
const res = await apiFetch.api.user.update.post(dataEdit);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your category have been saved",
message: "Your data have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to edit category",
message: "Failed to edit user",
type: "error",
});
}
@@ -127,7 +127,7 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
console.error(error);
notification({
title: "Error",
message: "Failed to edit category",
message: "Failed to edit user2",
type: "error",
});
} finally {
@@ -222,9 +222,10 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label="Edit Kategori">
<Input.Wrapper label="Nama">
<Input
value={dataEdit.name}
error={error.name ? "Field is required" : ""}
onChange={(e) =>
onValidation({
kat: "name",
@@ -234,6 +235,51 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
}
/>
</Input.Wrapper>
<Select
label="Role"
placeholder="Pilih Role"
data={listRole.map((r: any) => ({
value: r.id,
label: r.name,
}))}
value={dataEdit.roleId || null}
error={error.roleId ? "Field is required" : ""}
onChange={(_value, option) => {
onValidation({
kat: "roleId",
value: option?.value,
aksi: "edit",
});
}}
/>
<Input.Wrapper label="Phone" description="">
<Input
value={dataEdit.phone}
onChange={(e) =>
onValidation({
kat: "phone",
value: e.target.value,
aksi: "edit",
})
}
/>
</Input.Wrapper>
<Input.Wrapper
label="Email"
description=""
error={error.email ? "Field is required" : ""}
>
<Input
value={dataEdit.email}
onChange={(e) =>
onValidation({
kat: "email",
value: e.target.value,
aksi: "edit",
})
}
/>
</Input.Wrapper>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
@@ -434,7 +480,7 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes('setting.user.edit')}
disabled={!permissions.includes('setting.user.edit') || v.roleId == "developer"}
>
<IconEdit size={20} />
</ActionIcon>
@@ -449,7 +495,7 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
setDataDelete(v.id);
openDelete();
}}
disabled={!permissions.includes('setting.user.delete')}
disabled={!permissions.includes('setting.user.delete') || v.roleId == "developer"}
>
<IconTrash size={20} />
</ActionIcon>

View File

@@ -1,20 +1,50 @@
import clientRoutes from "@/clientRoutes";
import {
Button,
Container,
Group,
PasswordInput,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { useState } from "react";
import apiFetch from "../lib/apiFetch";
import clientRoutes from "@/clientRoutes";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
function navigateToRoute(akses: string) {
switch (akses) {
case "dashboard":
window.location.href = clientRoutes["/scr/dashboard/dashboard-home"];
break;
case "pengaduan":
window.location.href = clientRoutes["/scr/dashboard/pengaduan/list"];
break;
case "warga":
window.location.href = clientRoutes["/scr/dashboard/warga/list-warga"];
break;
case "credential":
window.location.href = clientRoutes["/scr/dashboard/credential/credential"];
break;
case "setting":
window.location.href = clientRoutes["/scr/dashboard/setting/detail-setting"];
break;
case "api_key":
window.location.href = clientRoutes["/scr/dashboard/apikey/apikey"];
break;
case "pelayanan":
window.location.href = clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
break;
default:
window.location.href = clientRoutes["/scr/dashboard"];
break;
}
}
const handleSubmit = async () => {
setLoading(true);
try {
@@ -25,7 +55,7 @@ export default function Login() {
if (response.data?.token) {
localStorage.setItem("token", response.data.token);
window.location.href = clientRoutes["/scr/dashboard"];
navigateToRoute(response.data.akses || "dashboard");
return;
}
@@ -48,7 +78,7 @@ export default function Login() {
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<TextInput
<PasswordInput
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}

View File

@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { prisma } from '@/server/lib/prisma'
import { jwt as jwtPlugin, type JWTPayloadSpec } from '@elysiajs/jwt'
import Elysia, { t, type Cookie, type HTTPHeaders, type StatusMap } from 'elysia'
import { type ElysiaCookie } from 'elysia/cookies'
import { prisma } from '@/server/lib/prisma'
const secret = process.env.JWT_SECRET
if (!secret) {
@@ -75,6 +75,15 @@ async function login({
const user = await prisma.user.findUnique({
where: { email },
select: {
id: true,
password: true,
Role: {
select: {
permissions: true
}
}
}
})
if (!user) {
@@ -87,6 +96,12 @@ async function login({
return { message: 'Invalid password' }
}
const rawPermissions = user.Role?.permissions;
const akses = Array.isArray(rawPermissions)
? rawPermissions[0]?.toString()
: undefined;
const token = await issueToken({
jwt,
cookie,
@@ -94,7 +109,7 @@ async function login({
role: 'user',
expiresAt: Math.floor(Date.now() / 1000) + NINETY_YEARS,
})
return { token }
return { token, akses }
} catch (error) {
console.error('Error logging in:', error)
return {
@@ -146,7 +161,7 @@ const Auth = new Elysia({
detail: {
summary: 'logout',
description: 'Logout (clear token cookie)',
},
}
)