upd: dashboard admin

Deskripsi:
- list user
- tambah usr
- delete user

- beserta api

NO Issues
This commit is contained in:
2025-11-13 17:43:19 +08:00
parent cc293d3bad
commit 039524d092
3 changed files with 456 additions and 0 deletions

View File

@@ -0,0 +1,365 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Select,
Stack,
Table,
Text,
Title,
Tooltip
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
import { useState } from "react";
import useSWR from "swr";
import notification from "./notificationGlobal";
export default function UserSetting() {
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const [openedDelete, { open: openDelete, close: closeDelete }] = useDisclosure(false);
const [dataDelete, setDataDelete] = useState("")
const { data: dataRole, mutate: mutateRole, isLoading: isLoadingRole } = useSWR("user-role", () =>
apiFetch.api.user.role.get(),
);
const [openedTambah, { open: openTambah, close: closeTambah }] = useDisclosure(false);
const { data, mutate, isLoading } = useSWR("user-list", () =>
apiFetch.api.user.list.get(),
);
const list = data?.data || [];
const listRole = dataRole?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
phone: "",
email: "",
roleId: "",
});
const [dataTambah, setDataTambah] = useState({
name: "",
email: "",
roleId: "",
password: "",
phone: "",
})
const [error, setError] = useState({
name: false,
email: false,
roleId: false,
password: false,
phone: false,
})
useShallowEffect(() => {
mutate();
}, []);
async function handleCreate() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user.create.post(dataTambah);
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah({
name: "",
email: "",
roleId: "",
password: "",
phone: "",
});
notification({
title: "Success",
message: "Your user have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to create user ",
type: "error",
})
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to create user",
type: "error",
})
} finally {
setBtnLoading(false);
}
}
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your category have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to edit category",
type: "error",
})
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to edit category",
type: "error",
})
} finally {
setBtnLoading(false);
}
}
async function handleDelete() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user.delete.post({ id: dataDelete });
if (res.status === 200) {
mutate();
closeDelete();
notification({
title: "Success",
message: "Your user have been deleted",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to delete user",
type: "error",
})
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to delete user",
type: "error",
})
} finally {
setBtnLoading(false);
}
}
function chooseEdit({ data }: { data: { id: string, name: string, phone: string, email: string, roleId: string } }) {
setDataEdit(data);
open();
}
function onValidation({ kat, value, aksi }: { kat: 'name' | 'email' | 'roleId' | 'password' | 'phone', value: string | null, aksi: 'edit' | 'tambah' }) {
if (value == null || value.length < 1) {
setBtnDisable(true);
setError({ ...error, [kat]: true });
} else {
setBtnDisable(false);
setError({ ...error, [kat]: false });
}
if (aksi === 'edit') {
setDataEdit({ ...dataEdit, [kat]: value });
} else {
setDataTambah({ ...dataTambah, [kat]: value });
}
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(false);
}
}, [dataEdit.id]);
return (
<>
{/* Modal Edit */}
<Modal
opened={opened}
onClose={close}
title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label="Edit Kategori">
<Input value={dataEdit.name} onChange={(e) => onValidation({ kat: 'name', value: e.target.value, aksi: 'edit' })} />
</Input.Wrapper>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" onClick={handleEdit} disabled={btnDisable} loading={btnLoading}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Tambah */}
<Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label="Nama" description="" error={error.name ? "Field is required" : ""}>
<Input value={dataTambah.name} onChange={(e) => onValidation({ kat: 'name', value: e.target.value, aksi: 'tambah' })} />
</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' }) }}
/>
<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
</Button>
<Button variant="filled" onClick={handleCreate} disabled={btnDisable || dataTambah.name.length < 1 || dataTambah.email.length < 1 || dataTambah.password.length < 1 || dataTambah.roleId.length < 1 || dataTambah.phone.length < 1} loading={btnLoading}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Delete */}
<Modal
opened={openedDelete}
onClose={closeDelete}
title={"Delete"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Text size="md" color="gray.6">
Apakah anda yakin ingin menghapus user ini?
</Text>
<Group justify="center" grow>
<Button variant="light" onClick={closeDelete}>
Batal
</Button>
<Button variant="filled" color="red" onClick={handleDelete} loading={btnLoading}>
Hapus
</Button>
</Group>
</Stack>
</Modal>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Daftar User
</Title>
<Tooltip label="Tambah User">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Nama</Table.Th>
<Table.Th>Telepon</Table.Th>
<Table.Th>Email</Table.Th>
<Table.Th>Role</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
list.length > 0 ? (
list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>{v.phone}</Table.Td>
<Table.Td>{v.email}</Table.Td>
<Table.Td>{v.roleId}</Table.Td>
<Table.Td>
<Group>
<Tooltip label="Edit User">
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label="Delete User">
<ActionIcon
variant="light"
size="sm"
color="red"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
setDataDelete(v.id)
openDelete()
}}
>
<IconTrash size={20} />
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
</Table.Tr>
))
) : (
<Table.Tr>
<Table.Td colSpan={5} align="center">
Data User Tidak Ditemukan
</Table.Td>
</Table.Tr>
)
}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
}

View File

@@ -1,6 +1,7 @@
import DesaSetting from "@/components/DesaSetting";
import KategoriPengaduan from "@/components/KategoriPengaduan";
import ProfileUser from "@/components/ProfileUser";
import UserSetting from "@/components/UserSetting";
import {
Button,
Card,
@@ -18,6 +19,7 @@ import {
IconCategory2,
IconMailSpark,
IconUserCog,
IconUsersGroup
} from "@tabler/icons-react";
import { useLocation } from "react-router-dom";
@@ -47,6 +49,12 @@ export default function DetailSettingPage() {
leftSection={<IconUserCog size={16} stroke={1.5} />}
active={type === "profile" || !type}
/>
<NavLink
href={`?type=user`}
label="User"
leftSection={<IconUsersGroup size={16} stroke={1.5} />}
active={type === "user"}
/>
<NavLink
href={`?type=cat-pengaduan`}
label="Kategori Pengaduan"
@@ -85,6 +93,8 @@ export default function DetailSettingPage() {
<KategoriPengaduanPage />
) : type === "desa" ? (
<DesaSetting />
) : type === "user" ? (
<UserSetting />
) : (
<ProfileUser />
)}

View File

@@ -101,5 +101,86 @@ const UserRoute = new Elysia({
description: "update user",
}
})
.post("/create", async ({ body }) => {
const { name, phone, roleId, email, password } = body
const create = await prisma.user.create({
data: {
name,
phone,
roleId,
email,
password
}
})
return {
success: true,
message: "User created successfully",
}
}, {
body: t.Object({
name: t.String({ minLength: 1, error: "name is required" }),
phone: t.String({ minLength: 1, error: "phone is required" }),
roleId: t.String({ minLength: 1, error: "roleId is required" }),
email: t.String({ minLength: 1, error: "email is required" }),
password: t.String({ minLength: 1, error: "password is required" })
}),
detail: {
summary: "create",
description: "create user",
}
})
.get("/list", async (ctx) => {
const { user } = ctx as any
const data = await prisma.user.findMany({
where: {
isActive: true,
NOT: {
id: user.id
}
}
})
return data
}, {
detail: {
summary: "list",
description: "list user",
}
})
.get("/role", async () => {
const data = await prisma.role.findMany()
return data
}, {
detail: {
summary: "role",
description: "role user",
}
})
.post("/delete", async ({ body }) => {
const { id } = body
const deleteData = await prisma.user.update({
where: {
id
},
data: {
isActive: false
}
})
return {
success: true,
message: "User deleted successfully",
}
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id is required" })
}),
detail: {
summary: "delete",
description: "delete user",
}
})
export default UserRoute