update dashboard admin

Deskripsi:
- list warga
- detail warga

No Issues
This commit is contained in:
2025-11-14 17:19:50 +08:00
parent 6c6ee02cf0
commit 67c066990e
13 changed files with 2014 additions and 1509 deletions

View File

@@ -1,16 +1,16 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Stack,
Table,
Title,
Tooltip
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Stack,
Table,
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit } from "@tabler/icons-react";
@@ -19,139 +19,153 @@ import useSWR from "swr";
import notification from "./notificationGlobal";
export default function DesaSetting() {
const [btnDisable, setBtnDisable] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api["configuration-desa"].list.get(),
);
const list = data?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
value: "",
name: "",
});
const [btnDisable, setBtnDisable] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api["configuration-desa"].list.get(),
);
const list = data?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
value: "",
name: "",
});
useShallowEffect(() => {
mutate();
}, []);
useShallowEffect(() => {
mutate();
}, []);
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api["configuration-desa"].edit.post(dataEdit);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your settings have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to edit configuration",
type: "error",
})
}
} catch (error) {
console.log(error);
notification({
title: "Error",
message: "Failed to edit configuration",
type: "error",
})
} finally {
setBtnLoading(false);
}
}
function chooseEdit({ data }: { data: { id: string, value: string, name: string } }) {
setDataEdit(data);
open();
}
function onValidation({ kat, value }: { kat: 'value', value: string }) {
if (value.length < 1) {
setBtnDisable(true);
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api["configuration-desa"].edit.post(dataEdit);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your settings have been saved",
type: "success",
});
} else {
setBtnDisable(false);
notification({
title: "Error",
message: "Failed to edit configuration",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to edit configuration",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
if (kat === 'value') {
setDataEdit({ ...dataEdit, value: value });
}
}
function chooseEdit({
data,
}: {
data: { id: string; value: string; name: string };
}) {
setDataEdit(data);
open();
}
useShallowEffect(() => {
if (dataEdit.value.length > 0) {
setBtnDisable(false);
}
}, [dataEdit.id]);
function onValidation({ kat, value }: { kat: "value"; value: string }) {
if (value.length < 1) {
setBtnDisable(true);
} else {
setBtnDisable(false);
}
return (
<>
<Modal
opened={opened}
onClose={close}
title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label={dataEdit.name}>
<Input value={dataEdit.value} onChange={(e) => onValidation({ kat: 'value', value: e.target.value })} />
</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>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Pengaturan Desa
</Title>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Nama</Table.Th>
<Table.Th>Value</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>{v.value}</Table.Td>
<Table.Td>
<Tooltip label="Edit Setting">
<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>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
if (kat === "value") {
setDataEdit({ ...dataEdit, value: value });
}
}
useShallowEffect(() => {
if (dataEdit.value.length > 0) {
setBtnDisable(false);
}
}, [dataEdit.id]);
return (
<>
<Modal
opened={opened}
onClose={close}
title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label={dataEdit.name}>
<Input
value={dataEdit.value}
onChange={(e) =>
onValidation({ kat: "value", value: e.target.value })
}
/>
</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>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Pengaturan Desa
</Title>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Nama</Table.Th>
<Table.Th>Value</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>{v.value}</Table.Td>
<Table.Td>
<Tooltip label="Edit Setting">
<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>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Stack,
Table,
Text,
Title,
Tooltip
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Stack,
Table,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
@@ -20,288 +20,336 @@ import useSWR from "swr";
import notification from "./notificationGlobal";
export default function KategoriPengaduan() {
const [openedDelete, { open: openDelete, close: closeDelete }] = useDisclosure(false);
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const [openedTambah, { open: openTambah, close: closeTambah }] = useDisclosure(false);
const [dataDelete, setDataDelete] = useState("")
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.pengaduan.category.get(),
);
const list = data?.data?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
});
const [dataTambah, setDataTambah] = useState("")
const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const [openedTambah, { open: openTambah, close: closeTambah }] =
useDisclosure(false);
const [dataDelete, setDataDelete] = useState("");
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.pengaduan.category.get(),
);
const list = data?.data?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
});
const [dataTambah, setDataTambah] = useState("");
useShallowEffect(() => {
mutate();
}, []);
useShallowEffect(() => {
mutate();
}, []);
async function handleCreate() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.create.post({ name: dataTambah });
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah("");
notification({
title: "Success",
message: "Your category have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to create category",
type: "error",
})
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to create category",
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);
}
}
function chooseEdit({ data }: { data: { id: string, value: string, name: string } }) {
setDataEdit(data);
open();
}
function onValidation({ kat, value, aksi }: { kat: 'name', value: string, aksi: 'edit' | 'tambah' }) {
if (value.length < 1) {
setBtnDisable(true);
async function handleCreate() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.create.post({
name: dataTambah,
});
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah("");
notification({
title: "Success",
message: "Your category have been saved",
type: "success",
});
} else {
setBtnDisable(false);
notification({
title: "Error",
message: "Failed to create category",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to create category",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
if (kat === 'name') {
if (aksi === 'edit') {
setDataEdit({ ...dataEdit, name: value });
} else {
setDataTambah(value);
}
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.pengaduan.category.delete.post({ id: dataDelete });
if (res.status === 200) {
mutate();
closeDelete();
notification({
title: "Success",
message: "Your category have been deleted",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to delete category",
type: "error",
})
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to delete category",
type: "error",
})
} finally {
setBtnLoading(false);
function chooseEdit({
data,
}: {
data: { id: string; value: string; name: string };
}) {
setDataEdit(data);
open();
}
function onValidation({
kat,
value,
aksi,
}: {
kat: "name";
value: string;
aksi: "edit" | "tambah";
}) {
if (value.length < 1) {
setBtnDisable(true);
} else {
setBtnDisable(false);
}
if (kat === "name") {
if (aksi === "edit") {
setDataEdit({ ...dataEdit, name: value });
} else {
setDataTambah(value);
}
}
}
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(false);
async function handleDelete() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.delete.post({
id: dataDelete,
});
if (res.status === 200) {
mutate();
closeDelete();
notification({
title: "Success",
message: "Your category have been deleted",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to delete category",
type: "error",
});
}
}, [dataEdit.id]);
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to delete category",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
useShallowEffect(() => {
if (dataTambah.length > 0) {
setBtnDisable(false);
}
}, [dataTambah]);
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>
useShallowEffect(() => {
if (dataTambah.length > 0) {
setBtnDisable(false);
}
}, [dataTambah]);
{/* Modal Tambah */}
<Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label="Tambah Kategori">
<Input value={dataTambah} onChange={(e) => onValidation({ kat: 'name', 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} loading={btnLoading}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
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"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label="Tambah Kategori">
<Input
value={dataTambah}
onChange={(e) =>
onValidation({
kat: "name",
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}
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 kategori 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>
{/* 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 kategori 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">
Kategori Pengaduan
</Title>
<Tooltip label="Tambah Kategori Pengaduan">
<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>Kategori</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>
<Group>
<Tooltip label="Edit Kategori">
<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 Kategori">
<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.Tbody>
</Table>
</Stack>
</Stack>
</>
);
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
<Tooltip label="Tambah Kategori Pengaduan">
<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>Kategori</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>
<Group>
<Tooltip label="Edit Kategori">
<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 Kategori">
<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.Tbody>
</Table>
</Stack>
</Stack>
</>
);
}

View File

@@ -1,190 +1,246 @@
import apiFetch from "@/lib/apiFetch";
import { Button, Divider, Flex, Group, Input, Modal, Stack, Title } from "@mantine/core";
import {
Button,
Divider,
Flex,
Group,
Input,
Modal,
Stack,
Title,
} from "@mantine/core";
import { useEffect, useState } from "react";
import notification from "./notificationGlobal";
export default function ProfileUser() {
const [opened, setOpened] = useState(false);
const [openedPassword, setOpenedPassword] = useState(false);
const [pwdBaru, setPwdBaru] = useState("");
const [host, setHost] = useState({
id: "",
name: "",
phone: "",
roleId: "",
email: "",
});
const [opened, setOpened] = useState(false);
const [openedPassword, setOpenedPassword] = useState(false);
const [pwdBaru, setPwdBaru] = useState("");
const [host, setHost] = useState({
id: "",
name: "",
phone: "",
roleId: "",
email: "",
});
const [error, setError] = useState({
name: false,
email: false,
phone: false,
});
const [error, setError] = useState({
name: false,
email: false,
phone: false,
});
useEffect(() => {
async function fetchHost() {
const { data } = await apiFetch.api.user.find.get();
setHost({
id: data?.user?.id ?? "",
name: data?.user?.name ?? "",
phone: data?.user?.phone ?? "",
roleId: data?.user?.roleId ?? "",
email: data?.user?.email ?? "",
});
}
fetchHost();
}, []);
useEffect(() => {
async function fetchHost() {
const { data } = await apiFetch.api.user.find.get();
setHost({
id: data?.user?.id ?? "",
name: data?.user?.name ?? "",
phone: data?.user?.phone ?? "",
roleId: data?.user?.roleId ?? "",
email: data?.user?.email ?? "",
});
}
fetchHost();
}, []);
function onValidation({
kat,
value,
}: {
kat: "name" | "email" | "phone";
value: string;
}) {
if (value.length < 1) {
setError({ ...error, [kat]: true });
} else {
setError({ ...error, [kat]: false });
}
function onValidation({ kat, value }: { kat: 'name' | 'email' | 'phone', value: string, }) {
if (value.length < 1) {
setError({ ...error, [kat]: true });
setHost({ ...host, [kat]: value });
}
async function handleUpdate() {
try {
const res = await apiFetch.api.user.update.post(host);
if (res.status === 200) {
setOpened(false);
notification({
title: "Success",
message: "Your profile have been saved",
type: "success",
});
} else {
setError({ ...error, [kat]: false });
notification({
title: "Error",
message: "Failed to update profile",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to update profile",
type: "error",
});
}
}
setHost({ ...host, [kat]: value });
}
async function handleUpdate() {
try {
const res = await apiFetch.api.user.update.post(host);
if (res.status === 200) {
setOpened(false);
notification({
title: "Success",
message: "Your profile have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to update profile",
type: "error",
})
}
} catch (error) {
console.log(error);
notification({
title: "Error",
message: "Failed to update profile",
type: "error",
})
async function handleUpdatePassword() {
try {
const res = await apiFetch.api.user["update-password"].post({
password: pwdBaru,
id: host.id,
});
if (res.status === 200) {
setPwdBaru("");
setOpenedPassword(false);
notification({
title: "Success",
message: "Your password have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to update password",
type: "error",
});
}
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to update password",
type: "error",
});
}
}
async function handleUpdatePassword() {
try {
const res = await apiFetch.api.user["update-password"].post({ password: pwdBaru, id: host.id });
if (res.status === 200) {
setPwdBaru("");
setOpenedPassword(false);
notification({
title: "Success",
message: "Your password have been saved",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to update password",
type: "error",
})
}
} catch (error) {
console.log(error);
notification({
title: "Error",
message: "Failed to update password",
type: "error",
})
}
}
return (
<>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Profile Pengguna
</Title>
<Group gap="md">
<Button variant="light" onClick={() => setOpened(true)}>
Edit
</Button>
<Button variant="light" onClick={() => setOpenedPassword(true)}>
Ubah Password
</Button>
</Group>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Group gap="xl" grow>
<Input.Wrapper label="Nama" description="" error="">
<Input value={host?.name ?? ""} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Phone" description="" error="">
<Input value={host?.phone ?? ""} readOnly />
</Input.Wrapper>
</Group>
<Group gap="xl" grow>
<Input.Wrapper label="Email" description="" error="">
<Input value={host?.email ?? ""} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Role" description="" error="">
<Input value={host?.roleId ?? ""} readOnly />
</Input.Wrapper>
</Group>
</Stack>
</Stack>
<Modal
opened={opened}
onClose={() => setOpened(false)}
title={"Edit Profile"}
size={"lg"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap={"md"}>
<Input.Wrapper
label="Nama"
description=""
error={error.name ? "Field is required" : ""}
>
<Input
value={host?.name ?? ""}
onChange={(e) =>
onValidation({ kat: "name", value: e.target.value })
}
/>
</Input.Wrapper>
<Input.Wrapper
label="Phone"
description=""
error={error.phone ? "Field is required" : ""}
>
<Input
value={host?.phone ?? ""}
onChange={(e) =>
onValidation({ kat: "phone", value: e.target.value })
}
/>
</Input.Wrapper>
<Input.Wrapper
label="Email"
description=""
error={error.email ? "Field is required" : ""}
>
<Input
value={host?.email ?? ""}
onChange={(e) =>
onValidation({ kat: "email", value: e.target.value })
}
/>
</Input.Wrapper>
<Group grow>
<Button variant="light" onClick={() => setOpened(false)}>
Batal
</Button>
<Button
variant="filled"
onClick={() => handleUpdate()}
disabled={error.name || error.phone || error.email}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
return (
<>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Profile Pengguna
</Title>
<Group gap="md">
<Button variant="light" onClick={() => setOpened(true)}>Edit</Button>
<Button variant="light" onClick={() => setOpenedPassword(true)}>Ubah Password</Button>
</Group>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Group gap="xl" grow>
<Input.Wrapper label="Nama" description="" error="">
<Input value={host?.name ?? ""} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Phone" description="" error="">
<Input value={host?.phone ?? ""} readOnly />
</Input.Wrapper>
</Group>
<Group gap="xl" grow>
<Input.Wrapper label="Email" description="" error="">
<Input value={host?.email ?? ""} readOnly />
</Input.Wrapper>
<Input.Wrapper label="Role" description="" error="">
<Input value={host?.roleId ?? ""} readOnly />
</Input.Wrapper>
</Group>
</Stack>
</Stack>
<Modal
opened={opened}
onClose={() => setOpened(false)}
title={"Edit Profile"}
size={"lg"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap={"md"}>
<Input.Wrapper label="Nama" description="" error={error.name ? "Field is required" : ""}>
<Input value={host?.name ?? ""} onChange={(e) => onValidation({ kat: 'name', value: e.target.value })} />
</Input.Wrapper>
<Input.Wrapper label="Phone" description="" error={error.phone ? "Field is required" : ""}>
<Input value={host?.phone ?? ""} onChange={(e) => onValidation({ kat: 'phone', value: e.target.value })} />
</Input.Wrapper>
<Input.Wrapper label="Email" description="" error={error.email ? "Field is required" : ""}>
<Input value={host?.email ?? ""} onChange={(e) => onValidation({ kat: 'email', value: e.target.value })} />
</Input.Wrapper>
<Group grow>
<Button variant="light" onClick={() => setOpened(false)}>
Batal
</Button>
<Button variant="filled" onClick={() => handleUpdate()} disabled={error.name || error.phone || error.email}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<Modal
opened={openedPassword}
onClose={() => setOpenedPassword(false)}
title={"Ubah Password"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap={"md"}>
<Input.Wrapper label="Password Baru" description="">
<Input value={pwdBaru} onChange={(e) => setPwdBaru(e.target.value)} />
</Input.Wrapper>
<Group grow>
<Button variant="light" onClick={() => setOpenedPassword(false)}>
Batal
</Button>
<Button variant="filled" onClick={() => handleUpdatePassword()} disabled={pwdBaru.length < 1}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
</>
)
}
<Modal
opened={openedPassword}
onClose={() => setOpenedPassword(false)}
title={"Ubah Password"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap={"md"}>
<Input.Wrapper label="Password Baru" description="">
<Input
value={pwdBaru}
onChange={(e) => setPwdBaru(e.target.value)}
/>
</Input.Wrapper>
<Group grow>
<Button variant="light" onClick={() => setOpenedPassword(false)}>
Batal
</Button>
<Button
variant="filled"
onClick={() => handleUpdatePassword()}
disabled={pwdBaru.length < 1}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
</>
);
}

View File

@@ -1,18 +1,18 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Select,
Stack,
Table,
Text,
Title,
Tooltip
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";
@@ -21,345 +21,446 @@ 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,
})
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();
}, []);
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 });
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 {
setBtnDisable(false);
setError({ ...error, [kat]: false });
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);
}
}
if (aksi === 'edit') {
setDataEdit({ ...dataEdit, [kat]: value });
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 {
setDataTambah({ ...dataTambah, [kat]: value });
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);
}
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(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",
});
}
}, [dataEdit.id]);
} 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();
}
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>
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 });
}
{/* 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>
if (aksi === "edit") {
setDataEdit({ ...dataEdit, [kat]: value });
} else {
setDataTambah({ ...dataTambah, [kat]: value });
}
}
<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>
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 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>
{/* 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>
<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>
</>
);
{/* 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

@@ -7,6 +7,7 @@ import { apiAuth } from "./server/middlewares/apiAuth";
import AduanRoute from "./server/routes/aduan_route";
import ApiKeyRoute from "./server/routes/apikey_route";
import Auth from "./server/routes/auth_route";
import ConfigurationDesaRoute from "./server/routes/configuration_desa_route";
import CredentialRoute from "./server/routes/credential_route";
import DarmasabaRoute from "./server/routes/darmasaba_route";
import LayananRoute from "./server/routes/layanan_route";
@@ -15,7 +16,7 @@ import PelayananRoute from "./server/routes/pelayanan_surat_route";
import PengaduanRoute from "./server/routes/pengaduan_route";
import TestRoute from "./server/routes/test";
import UserRoute from "./server/routes/user_route";
import ConfigurationDesaRoute from "./server/routes/configuration_desa_route";
import WargaRoute from "./server/routes/warga_route";
const Docs = new Elysia({
tags: ["docs"],
@@ -32,6 +33,7 @@ const Api = new Elysia({
.use(PengaduanRoute)
.use(PelayananRoute)
.use(ConfigurationDesaRoute)
.use(WargaRoute)
.use(TestRoute)
.use(apiAuth)
.use(ApiKeyRoute)

View File

@@ -172,7 +172,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
}
/>
</Group>
{list.length === 0 ? (
{list?.length === 0 ? (
<Flex justify="center" align="center" py={"xl"}>
<Stack gap={4} align="center">
<IconFileSad size={32} color="gray" />
@@ -182,7 +182,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
list.map((v: any) => (
list?.map((v: any) => (
<Card
key={v.id}
radius="lg"

View File

@@ -67,13 +67,12 @@ function DetailDataPengaduan() {
fileName: "57d5ce89-7d18-4244-9f4c-ca21b70adb7e",
},
});
console.error('client',res)
console.error("client", res);
// const blob = await res.data?.blob();
// setImageSrc(URL.createObjectURL(blob!));
// openModalImage();
}
return (
<>
<Modal

View File

@@ -186,7 +186,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
list.map((v: any) => (
list?.map((v: any) => (
<Card
key={v.id}
radius="lg"

View File

@@ -13,14 +13,14 @@ import {
NavLink,
Stack,
Table,
Title
Title,
} from "@mantine/core";
import {
IconBuildingBank,
IconCategory2,
IconMailSpark,
IconUserCog,
IconUsersGroup
IconUsersGroup,
} from "@tabler/icons-react";
import { useLocation } from "react-router-dom";
@@ -104,4 +104,4 @@ export default function DetailSettingPage() {
</Grid>
</Container>
);
}
}

View File

@@ -2,62 +2,66 @@ import apiFetch from "@/lib/apiFetch";
import {
Avatar,
Box,
Button,
Card,
Container,
Divider,
Flex,
Grid,
Group,
LoadingOverlay,
Stack,
Table,
Text,
Title,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconMail, IconMapPin, IconPhone } from "@tabler/icons-react";
import { useState } from "react";
import { useLocation } from "react-router-dom";
import { IconPhone } from "@tabler/icons-react";
import _ from "lodash";
import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
export default function DetailWargaPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.warga.detail.get({
query: {
id: id!,
},
}),
);
useShallowEffect(() => {
mutate();
}, []);
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={4}>
<DetailWarga />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataHistori />
<DetailDataHistori />
</Stack>
</Grid.Col>
</Grid>
</Container>
<>
<LoadingOverlay visible={isLoading} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={4}>
<DetailWarga data={data?.data?.warga} />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataHistori data={data?.data?.pengaduan} kategori="pengaduan" />
<DetailDataHistori data={data?.data?.pelayanan} kategori="pelayanan" />
</Stack>
</Grid.Col>
</Grid>
</Container>
</>
);
}
function DetailDataHistori() {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan' | 'pelayanan' }) {
const navigate = useNavigate();
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return (
<Card
radius="md"
@@ -73,46 +77,59 @@ function DetailDataHistori() {
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
Histori {_.upperFirst(kategori)}
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>No {_.upperFirst(kategori)}</Table.Th>
<Table.Th>{kategori == "pengaduan" ? "Judul" : "Kategori"}</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
<Table.Tbody>
{
data?.length > 0 ? (
data?.map((item: any, index: number) => (
<Table.Tr key={index}>
<Table.Td>{item.noPengaduan}</Table.Td>
<Table.Td>{kategori == "pengaduan" ? item.title : item.category}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
kategori == "pengaduan" ?
navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
) :
navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
)
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))
) : (
<Table.Tr>
<Table.Td colSpan={4} align="center">Tidak ada data</Table.Td>
</Table.Tr>
)
}
</Table.Tbody>
</Table>
</Stack>
</Card>
);
}
function DetailWarga() {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
useShallowEffect(() => {
mutate();
}, [status, value]);
const list = data?.data || [];
function DetailWarga({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -122,38 +139,40 @@ function DetailWarga() {
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
boxShadow: "0 0 20px #00ffc814",
}}
>
<Box
style={{
backgroundColor: "#f7d86c",
background:
"linear-gradient(to left top, #23633a, #00685b, #006984, #0065a5, #0059b1, #114ca3, #193f94, #1d3285, #202864, #1d1f45, #171628, #0b0b0b)",
height: 100,
borderRadius: "12px",
position: "relative",
}}
/>
<Group>
{/* Profile image */}
<Avatar
src="https://i.pravatar.cc/150?img=32"
radius={100}
size={90}
style={{
position: "absolute",
top: 80,
left: 30,
border: "4px solid white",
border: "3x solid white",
backgroundColor: "#099268",
}}
/>
>
A
</Avatar>
{/* Main content */}
<Stack ml={115} gap={4}>
<Text fw={700} fz="lg">
Lizbeth Moore
{data?.name}
</Text>
<Text fz="sm" c="dimmed">
Social Media Strategies
Warga Desa
</Text>
</Stack>
</Group>
@@ -161,19 +180,9 @@ function DetailWarga() {
{/* Contact info */}
<Card radius="md" mt="md" p="md" withBorder={false}>
<Stack gap="xs">
<Group gap="xs">
<IconMail size={18} />
<Text size="sm">lizbeth.moore@email.com</Text>
</Group>
<Group gap="xs">
<IconPhone size={18} />
<Text size="sm">+1 555-7788</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">Greenway Ave, Los Angeles, CA, USA</Text>
<Text size="sm">{data?.phone}</Text>
</Group>
</Stack>
</Card>

View File

@@ -1,3 +1,4 @@
import apiFetch from "@/lib/apiFetch";
import {
Button,
Card,
@@ -10,41 +11,30 @@ import {
Table,
Title,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconSearch } from "@tabler/icons-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import useSWR from "swr";
export default function ListWargaPage() {
const navigate = useNavigate();
const [value, setValue] = useState("");
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.warga.list.get({
query: {
search: value,
},
}),
);
const list = data?.data || [];
const [value, setValue] = useState("");
useShallowEffect(() => {
mutate();
}, [value]);
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${element.position}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
));
return (
<Container size="xl" py="xl" w={"100%"}>
@@ -81,14 +71,39 @@ export default function ListWargaPage() {
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
<Table.Th>Nama</Table.Th>
<Table.Th>No Telepon</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
<Table.Tbody>
{
list?.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
</Table.Tr>
) : (
list?.map((item, i) => (
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td>{item.phone}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))
)
}
</Table.Tbody>
</Table>
</Stack>
</Card>

View File

@@ -0,0 +1,141 @@
import Elysia, { t } from "elysia";
import _ from "lodash";
import { normalizePhoneNumber } from "../lib/normalizePhone";
import { prisma } from "../lib/prisma";
const WargaRoute = new Elysia({
prefix: "warga",
tags: ["warga"],
})
.get("/list", async ({ query }) => {
const { search } = query
const data = await prisma.warga.findMany({
where: {
OR: [
{
name: {
contains: search,
mode: "insensitive"
}
},
{
phone: {
contains: search,
mode: "insensitive"
}
}
]
},
orderBy: {
name: "asc"
}
})
return data
}, {
detail: {
summary: "List Warga",
description: `tool untuk mendapatkan list warga`,
}
})
.post("/edit", async ({ body }) => {
const { id, name, phone } = body
const nomorHP = normalizePhoneNumber({ phone })
await prisma.warga.update({
where: {
id,
},
data: {
name,
phone: nomorHP
}
})
return { success: true, message: 'data warga sudah diperbarui' }
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
name: t.String({ minLength: 1, error: "value harus diisi" }),
phone: t.String({ minLength: 1 })
}),
detail: {
summary: "edit konfigurasi desa",
description: `tool untuk edit konfigurasi desa`
}
})
.get("/detail", async ({ query }) => {
const { id } = query
const dataWarga = await prisma.warga.findUnique({
where: {
id
}
})
const dataPengaduan = await prisma.pengaduan.findMany({
orderBy: {
createdAt: "desc"
},
where: {
isActive: true,
idWarga: id
},
select: {
id: true,
status: true,
noPengaduan: true,
title: true
}
})
const dataPelayanan = await prisma.pelayananAjuan.findMany({
orderBy: {
createdAt: "desc"
},
where: {
isActive: true,
idWarga: id
},
select: {
id: true,
noPengajuan: true,
status: true,
CategoryPelayanan: {
select: {
name: true
}
}
}
})
const dataPelayanFix = dataPelayanan.map((v: any) => ({
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
return {
warga: dataWarga,
pengaduan: dataPengaduan,
pelayanan: dataPelayanFix
}
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" })
}),
detail: {
summary: "Detail Warga",
description: `tool untuk mendapatkan detail warga`,
}
})
;
export default WargaRoute