upd: dashboard admin #39

Merged
amaliadwiy merged 1 commits from amalia/26-nov-25 into main 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" v.name == "TTD"
? ?
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always"> v.value ?
Lihat <Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always">
</Anchor> Lihat
</Anchor>
:
"-"
: :
v.value v.value
} }

View File

@@ -329,7 +329,7 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
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")} disabled={!permissions.includes("setting.kategori_pengaduan.edit") || v.id == "lainnya"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</ActionIcon> </ActionIcon>
@@ -344,7 +344,7 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
setDataDelete(v.id); setDataDelete(v.id);
openDelete(); openDelete();
}} }}
disabled={!permissions.includes("setting.kategori_pengaduan.delete")} disabled={!permissions.includes("setting.kategori_pengaduan.delete") || v.id == "lainnya"}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</ActionIcon> </ActionIcon>

View File

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

View File

@@ -169,7 +169,7 @@ 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.filter((menu: Node) => !menu.key.startsWith("api") && !menu.key.startsWith("credential")).map((menu: Node) => (
<RenderMenu key={menu.key} menu={menu} /> <RenderMenu key={menu.key} menu={menu} />
))} ))}
</Stack> </Stack>

View File

@@ -18,10 +18,18 @@ import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
import type { JsonValue } from "generated/prisma/runtime/library"; 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 listMenu from "../lib/listPermission.json";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
import PermissionRole from "./PermissionRole"; import PermissionRole from "./PermissionRole";
import PermissionTree from "./PermissionTree"; import PermissionTree from "./PermissionTree";
interface MenuNode {
key: string;
label: string;
default: boolean;
children?: MenuNode[];
}
export default function UserRoleSetting({ permissions }: { permissions: JsonValue[] }) { export default function UserRoleSetting({ permissions }: { permissions: JsonValue[] }) {
const [btnDisable, setBtnDisable] = useState(true); const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false); 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(() => { useShallowEffect(() => {
if (dataEdit.name.length > 0) { if (dataEdit.name.length > 0) {
@@ -212,7 +241,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
<PermissionTree <PermissionTree
selected={dataEdit.permissions} selected={dataEdit.permissions}
onChange={(permissions) => { onChange={(permissions) => {
setDataEdit({ ...dataEdit, permissions: permissions as never[] }); setDataEdit({ ...dataEdit, permissions: sortByJsonOrder(permissions) as never[] });
}} }}
/> />
<Group justify="center" grow> <Group justify="center" grow>
@@ -263,7 +292,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
<PermissionTree <PermissionTree
selected={dataTambah.permissions} selected={dataTambah.permissions}
onChange={(permissions) => { onChange={(permissions) => {
setDataTambah({ ...dataTambah, permissions: permissions as never[] }); setDataTambah({ ...dataTambah, permissions: sortByJsonOrder(permissions) as never[] });
}} }}
/> />
<Group justify="center" grow> <Group justify="center" grow>
@@ -346,11 +375,11 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
{list.length > 0 ? ( {list.length > 0 ? (
list?.map((v: any) => ( list?.map((v: any) => (
<Table.Tr key={v.id}> <Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td> <Table.Td w={"150"}>{v.name}</Table.Td>
<Table.Td> <Table.Td>
<PermissionRole permissions={v.permissions} /> <PermissionRole permissions={v.permissions} />
</Table.Td> </Table.Td>
<Table.Td> <Table.Td w={"100"}>
<Group> <Group>
<Tooltip label={permissions.includes('setting.user_role.edit') ? "Edit Role" : "Edit Role - Anda tidak memiliki akses"}> <Tooltip label={permissions.includes('setting.user_role.edit') ? "Edit Role" : "Edit Role - Anda tidak memiliki akses"}>
<ActionIcon <ActionIcon
@@ -358,7 +387,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
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_role.edit')} disabled={!permissions.includes('setting.user_role.edit') || v.id == "developer"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</ActionIcon> </ActionIcon>
@@ -373,7 +402,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
setDataDelete(v.id); setDataDelete(v.id);
openDelete(); openDelete();
}} }}
disabled={!permissions.includes('setting.user_role.delete')} disabled={!permissions.includes('setting.user_role.delete') || v.id == "developer"}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</ActionIcon> </ActionIcon>

View File

@@ -107,19 +107,19 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
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.update.post(dataEdit);
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 data have been saved",
type: "success", type: "success",
}); });
} else { } else {
notification({ notification({
title: "Error", title: "Error",
message: "Failed to edit category", message: "Failed to edit user",
type: "error", type: "error",
}); });
} }
@@ -127,7 +127,7 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
console.error(error); console.error(error);
notification({ notification({
title: "Error", title: "Error",
message: "Failed to edit category", message: "Failed to edit user2",
type: "error", type: "error",
}); });
} finally { } finally {
@@ -222,9 +222,10 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
> >
<Stack gap="ld"> <Stack gap="ld">
<Input.Wrapper label="Edit Kategori"> <Input.Wrapper label="Nama">
<Input <Input
value={dataEdit.name} value={dataEdit.name}
error={error.name ? "Field is required" : ""}
onChange={(e) => onChange={(e) =>
onValidation({ onValidation({
kat: "name", kat: "name",
@@ -234,6 +235,51 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
} }
/> />
</Input.Wrapper> </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> <Group justify="center" grow>
<Button variant="light" onClick={close}> <Button variant="light" onClick={close}>
Batal Batal
@@ -434,7 +480,7 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
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')} disabled={!permissions.includes('setting.user.edit') || v.roleId == "developer"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</ActionIcon> </ActionIcon>
@@ -449,7 +495,7 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
setDataDelete(v.id); setDataDelete(v.id);
openDelete(); openDelete();
}} }}
disabled={!permissions.includes('setting.user.delete')} disabled={!permissions.includes('setting.user.delete') || v.roleId == "developer"}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</ActionIcon> </ActionIcon>

View File

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

View File

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