Merge pull request 'amalia/14-nov-25' (#26) from amalia/14-nov-25 into main

Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/26
This commit is contained in:
2025-11-14 17:25:42 +08:00
14 changed files with 2055 additions and 1029 deletions

View File

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

View File

@@ -0,0 +1,612 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Button,
Divider,
Flex,
Grid,
Group,
Input,
List,
Modal,
Stack,
Table,
TagsInput,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
import { useState } from "react";
import useSWR from "swr";
import notification from "./notificationGlobal";
export default function KategoriPelayananSurat() {
const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const [openedDetail, { open: openDetail, close: closeDetail }] =
useDisclosure(false);
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.pelayanan.category.get(),
);
const list = data?.data || [];
const [dataChoose, setDataChoose] = useState({
id: "",
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
});
const [dataTambah, setDataTambah] = useState({
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
});
useShallowEffect(() => {
mutate();
}, []);
async function handleCreate() {
try {
setBtnLoading(true);
const cleanedDataText = dataTambah.dataText
.map((v) => v.trim())
.filter((v) => v !== "");
const cleanedSyarat = dataTambah.syaratDokumen
.map((item) => ({
name: item.name.trim(),
desc: item.desc.trim(),
}))
.filter((item) => item.name !== "" && item.desc !== "");
const cleanedTambah = {
name: dataTambah.name.trim(),
syaratDokumen: cleanedSyarat,
dataText: cleanedDataText,
};
const res =
await apiFetch.api.pelayanan.category.create.post(cleanedTambah);
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah({
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
});
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 cleanedDataText = dataChoose.dataText
.map((v) => v.trim())
.filter((v) => v !== "");
const cleanedSyarat = dataChoose.syaratDokumen
.map((item) => ({
name: item.name.trim(),
desc: item.desc.trim(),
}))
.filter((item) => item.name !== "" && item.desc !== "");
const res = await apiFetch.api.pelayanan.category.update.post({
id: dataChoose.id,
name: dataChoose.name,
syaratDokumen: cleanedSyarat,
dataText: cleanedDataText,
});
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.pelayanan.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 handleAddSyarat() {
setDataChoose({
...dataChoose,
syaratDokumen: [...dataChoose.syaratDokumen, { name: "", desc: "" }],
});
}
function handleDeleteSyarat(index: number) {
setDataChoose({
...dataChoose,
syaratDokumen: dataChoose.syaratDokumen.filter((_, i) => i !== index),
});
}
function handleEditSyarat(
index: number,
data: { name: string; desc: string },
) {
setDataChoose({
...dataChoose,
syaratDokumen: dataChoose.syaratDokumen.map((v, i) =>
i === index ? data : v,
),
});
}
return (
<>
{/* Modal Edit */}
<Modal
opened={opened}
onClose={close}
title={"Edit"}
size="xl"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="lg">
<Input.Wrapper label="Kategori">
<Input
value={dataChoose.name}
onChange={(e) =>
setDataChoose({ ...dataChoose, name: e.target.value })
}
/>
</Input.Wrapper>
<TagsInput
label="Data Pelengkap"
placeholder="Tambah data pelengkap"
splitChars={[","]}
value={dataChoose.dataText}
onChange={(value) =>
setDataChoose({ ...dataChoose, dataText: value })
}
/>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
Syarat dokumen
</Text>
<Tooltip label="Tambah Syarat Dokumen">
<ActionIcon
variant="light"
size="sm"
color="blue"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={handleAddSyarat}
>
<IconPlus size={20} />
</ActionIcon>
</Tooltip>
</Group>
{dataChoose?.syaratDokumen?.map((v: any, i: number) => (
<Grid
key={i}
style={{
borderBottom: "1px solid gray",
paddingBottom: "10px",
}}
>
<Grid.Col
span={1}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Tooltip label="Delete Syarat Dokumen">
<ActionIcon
variant="light"
size="sm"
color="red"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
handleDeleteSyarat(i);
}}
>
<IconTrash size={20} />
</ActionIcon>
</Tooltip>
</Grid.Col>
<Grid.Col span={5}>
<Input.Wrapper label="Nama">
<Input
value={v.name}
onChange={(e) =>
handleEditSyarat(i, {
name: e.target.value,
desc: v.desc,
})
}
/>
</Input.Wrapper>
</Grid.Col>
<Grid.Col span={6}>
<Input.Wrapper label="Deskripsi">
<Input
value={v.desc}
onChange={(e) =>
handleEditSyarat(i, {
name: v.name,
desc: e.target.value,
})
}
/>
</Input.Wrapper>
</Grid.Col>
</Grid>
))}
</Flex>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" onClick={handleEdit} loading={btnLoading}>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Tambah */}
<Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="lg">
<Input.Wrapper label="Tambah Kategori Pelayanan Surat">
<Input
value={dataTambah.name}
onChange={(e) =>
setDataTambah({ ...dataTambah, name: e.target.value })
}
/>
</Input.Wrapper>
<TagsInput
label="Data Pelengkap"
placeholder="Tambah data pelengkap"
splitChars={[","]}
value={dataTambah.dataText}
onChange={(value) =>
setDataTambah({ ...dataTambah, dataText: value })
}
/>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
Syarat dokumen
</Text>
<Tooltip label="Tambah Syarat Dokumen">
<ActionIcon
variant="light"
size="sm"
color="blue"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
setDataTambah({
...dataTambah,
syaratDokumen: [
...dataTambah.syaratDokumen,
{ name: "", desc: "" },
],
});
}}
>
<IconPlus size={20} />
</ActionIcon>
</Tooltip>
</Group>
{dataTambah?.syaratDokumen?.map((v: any, index: number) => (
<Grid
key={index}
style={{
borderBottom: "1px solid gray",
paddingBottom: "10px",
}}
>
<Grid.Col
span={1}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Tooltip label="Delete Syarat Dokumen">
<ActionIcon
variant="light"
size="sm"
color="red"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
setDataTambah({
...dataTambah,
syaratDokumen: dataTambah.syaratDokumen.filter(
(v: any, i: number) => i !== index,
),
});
}}
disabled={dataTambah?.syaratDokumen?.length === 1}
>
<IconTrash size={20} />
</ActionIcon>
</Tooltip>
</Grid.Col>
<Grid.Col span={5}>
<Input.Wrapper label="Nama">
<Input
value={dataTambah?.syaratDokumen[index]?.name}
onChange={(e) =>
setDataTambah({
...dataTambah,
syaratDokumen: dataTambah.syaratDokumen.map(
(v: any, i: number) =>
i === index ? { ...v, name: e.target.value } : v,
),
})
}
/>
</Input.Wrapper>
</Grid.Col>
<Grid.Col span={6}>
<Input.Wrapper label="Deskripsi">
<Input
value={dataTambah?.syaratDokumen[index]?.desc}
onChange={(e) =>
setDataTambah({
...dataTambah,
syaratDokumen: dataTambah.syaratDokumen.map(
(v: any, i: number) =>
i === index ? { ...v, desc: e.target.value } : v,
),
})
}
/>
</Input.Wrapper>
</Grid.Col>
</Grid>
))}
</Flex>
<Group justify="center" grow>
<Button variant="light" onClick={closeTambah}>
Batal
</Button>
<Button
variant="filled"
onClick={handleCreate}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Delete */}
<Modal
opened={openedDelete}
onClose={closeDelete}
title={"Delete Kategori Pelayanan Surat"}
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 Detail */}
<Modal
opened={openedDetail}
onClose={closeDetail}
title={"Detail Kategori"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="sm">
<Flex direction={"column"}>
<Text size="sm" color="white">
Kategori
</Text>
<Text size="md">{dataChoose?.name ?? ""}</Text>
</Flex>
<Flex direction={"column"}>
<Text size="sm" color="white">
Syarat Dokumen
</Text>
<List>
{dataChoose?.syaratDokumen?.map((v: any) => (
<List.Item key={v.id}>{v.desc}</List.Item>
))}
</List>
</Flex>
<Flex direction={"column"}>
<Text size="sm" color="white">
Data Pelengkap
</Text>
<List>
{dataChoose?.dataText?.map((v: any) => (
<List.Item key={v.id}>{v}</List.Item>
))}
</List>
</Flex>
</Stack>
</Modal>
{/* Table */}
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pelayanan Surat
</Title>
<Tooltip label="Tambah Kategori Pelayanan Surat">
<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="View Detail">
<ActionIcon
variant="light"
size="sm"
color="green"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
setDataChoose(v);
openDetail();
}}
>
<IconEye size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label="Edit Kategori">
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
setDataChoose(v);
open();
}}
>
<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,234 +1,355 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import {
ActionIcon, ActionIcon,
Button, Button,
Divider, Divider,
Flex, Flex,
Group, Group,
Input, Input,
Modal, Modal,
Stack, Stack,
Table, Table,
Title, Text,
Tooltip Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconPlus } from "@tabler/icons-react"; import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function KategoriPengaduan() { export default function KategoriPengaduan() {
const [btnDisable, setBtnDisable] = useState(true); const [openedDelete, { open: openDelete, close: closeDelete }] =
const [btnLoading, setBtnLoading] = useState(false); useDisclosure(false);
const [opened, { open, close }] = useDisclosure(false); const [btnDisable, setBtnDisable] = useState(true);
const [openedTambah, { open: openTambah, close: closeTambah }] = useDisclosure(false); const [btnLoading, setBtnLoading] = useState(false);
const { data, mutate, isLoading } = useSWR("/", () => const [opened, { open, close }] = useDisclosure(false);
apiFetch.api.pengaduan.category.get(), const [openedTambah, { open: openTambah, close: closeTambah }] =
); useDisclosure(false);
const list = data?.data || []; const [dataDelete, setDataDelete] = useState("");
const [dataEdit, setDataEdit] = useState({ const { data, mutate, isLoading } = useSWR("/", () =>
id: "", apiFetch.api.pengaduan.category.get(),
name: "", );
}); const list = data?.data?.data || [];
const [dataTambah, setDataTambah] = useState("") const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
});
const [dataTambah, setDataTambah] = useState("");
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, []); }, []);
async function handleCreate() { async function handleCreate() {
try { try {
setBtnLoading(true); setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.create.post({ name: dataTambah }); const res = await apiFetch.api.pengaduan.category.create.post({
if (res.status === 200) { name: dataTambah,
mutate(); });
closeTambah(); if (res.status === 200) {
setDataTambah(""); mutate();
notification({ closeTambah();
title: "Success", setDataTambah("");
message: "Your category have been saved", notification({
type: "success", title: "Success",
}) message: "Your category have been saved",
} else { type: "success",
notification({ });
title: "Error",
message: "Failed to create category",
type: "error",
})
}
} catch (error) {
console.log(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.log(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);
} else { } 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') { async function handleEdit() {
if (aksi === 'edit') { try {
setDataEdit({ ...dataEdit, name: value }); setBtnLoading(true);
} else { const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
setDataTambah(value); 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);
}
}
useShallowEffect(() => { function chooseEdit({
if (dataEdit.name.length > 0) { data,
setBtnDisable(false); }: {
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);
} }
}, [dataEdit.id]); }
}
useShallowEffect(() => { async function handleDelete() {
if (dataTambah.length > 0) { try {
setBtnDisable(false); 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",
});
} }
}, [dataTambah]); } catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to delete category",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
return ( useShallowEffect(() => {
<> if (dataEdit.name.length > 0) {
{/* Modal Edit */} setBtnDisable(false);
<Modal }
opened={opened} }, [dataEdit.id]);
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 */} useShallowEffect(() => {
<Modal if (dataTambah.length > 0) {
opened={openedTambah} setBtnDisable(false);
onClose={closeTambah} }
title={"Tambah"} }, [dataTambah]);
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>
<Stack gap={"md"}> {/* Modal Delete */}
<Flex align="center" justify="space-between"> <Modal
<Title order={4} c="gray.2"> opened={openedDelete}
Kategori Pengaduan onClose={closeDelete}
</Title> title={"Delete"}
<Tooltip label="Tambah Kategori Pengaduan"> overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
<Button >
variant="light" <Stack gap="md">
leftSection={<IconPlus size={20} />} <Text size="md" color="gray.6">
onClick={openTambah} Apakah anda yakin ingin menghapus kategori ini?
> </Text>
Tambah <Group justify="center" grow>
</Button> <Button variant="light" onClick={closeDelete}>
</Tooltip> Batal
</Flex> </Button>
<Divider my={0} /> <Button
<Stack gap={"md"}> variant="filled"
<Table highlightOnHover> color="red"
<Table.Thead> onClick={handleDelete}
<Table.Tr> loading={btnLoading}
<Table.Th>Kategori</Table.Th> >
<Table.Th>Aksi</Table.Th> Hapus
</Table.Tr> </Button>
</Table.Thead> </Group>
<Table.Tbody> </Stack>
{list?.map((v: any) => ( </Modal>
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td> <Stack gap={"md"}>
<Table.Td> <Flex align="center" justify="space-between">
<Tooltip label="Edit Setting"> <Title order={4} c="gray.2">
<ActionIcon Kategori Pengaduan
variant="light" </Title>
size="sm" <Tooltip label="Tambah Kategori Pengaduan">
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }} <Button
onClick={() => chooseEdit({ data: v })} variant="light"
> leftSection={<IconPlus size={20} />}
<IconEdit size={20} /> onClick={openTambah}
</ActionIcon> >
</Tooltip> Tambah
</Table.Td> </Button>
</Table.Tr> </Tooltip>
))} </Flex>
</Table.Tbody> <Divider my={0} />
</Table> <Stack gap={"md"}>
</Stack> <Table highlightOnHover>
</Stack> <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 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 { useEffect, useState } from "react";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function ProfileUser() { export default function ProfileUser() {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [openedPassword, setOpenedPassword] = useState(false); const [openedPassword, setOpenedPassword] = useState(false);
const [pwdBaru, setPwdBaru] = useState(""); const [pwdBaru, setPwdBaru] = useState("");
const [host, setHost] = useState({ const [host, setHost] = useState({
id: "", id: "",
name: "", name: "",
phone: "", phone: "",
roleId: "", roleId: "",
email: "", email: "",
}); });
const [error, setError] = useState({ const [error, setError] = useState({
name: false, name: false,
email: false, email: false,
phone: false, phone: false,
}); });
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
const { data } = await apiFetch.api.user.find.get(); const { data } = await apiFetch.api.user.find.get();
setHost({ setHost({
id: data?.user?.id ?? "", id: data?.user?.id ?? "",
name: data?.user?.name ?? "", name: data?.user?.name ?? "",
phone: data?.user?.phone ?? "", phone: data?.user?.phone ?? "",
roleId: data?.user?.roleId ?? "", roleId: data?.user?.roleId ?? "",
email: data?.user?.email ?? "", email: data?.user?.email ?? "",
}); });
} }
fetchHost(); 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, }) { setHost({ ...host, [kat]: value });
if (value.length < 1) { }
setError({ ...error, [kat]: true });
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 { } 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 handleUpdatePassword() {
} try {
const res = await apiFetch.api.user["update-password"].post({
async function handleUpdate() { password: pwdBaru,
try { id: host.id,
const res = await apiFetch.api.user.update.post(host); });
if (res.status === 200) { if (res.status === 200) {
setOpened(false); setPwdBaru("");
notification({ setOpenedPassword(false);
title: "Success", notification({
message: "Your profile have been saved", title: "Success",
type: "success", message: "Your password have been saved",
}) type: "success",
} else { });
notification({ } else {
title: "Error", notification({
message: "Failed to update profile", title: "Error",
type: "error", message: "Failed to update password",
}) type: "error",
} });
} catch (error) {
console.log(error);
notification({
title: "Error",
message: "Failed to update profile",
type: "error",
})
} }
} } catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to update password",
type: "error",
});
}
}
async function handleUpdatePassword() { return (
try { <>
const res = await apiFetch.api.user["update-password"].post({ password: pwdBaru, id: host.id }); <Stack gap={"md"}>
if (res.status === 200) { <Flex align="center" justify="space-between">
setPwdBaru(""); <Title order={4} c="gray.2">
setOpenedPassword(false); Profile Pengguna
notification({ </Title>
title: "Success", <Group gap="md">
message: "Your password have been saved", <Button variant="light" onClick={() => setOpened(true)}>
type: "success", Edit
}) </Button>
} else { <Button variant="light" onClick={() => setOpenedPassword(true)}>
notification({ Ubah Password
title: "Error", </Button>
message: "Failed to update password", </Group>
type: "error", </Flex>
}) <Divider my={0} />
} <Stack gap={"md"}>
} catch (error) { <Group gap="xl" grow>
console.log(error); <Input.Wrapper label="Nama" description="" error="">
notification({ <Input value={host?.name ?? ""} readOnly />
title: "Error", </Input.Wrapper>
message: "Failed to update password", <Input.Wrapper label="Phone" description="" error="">
type: "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
return ( opened={openedPassword}
<> onClose={() => setOpenedPassword(false)}
<Stack gap={"md"}> title={"Ubah Password"}
<Flex align="center" justify="space-between"> overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
<Title order={4} c="gray.2"> >
Profile Pengguna <Stack gap={"md"}>
</Title> <Input.Wrapper label="Password Baru" description="">
<Group gap="md"> <Input
<Button variant="light" onClick={() => setOpened(true)}>Edit</Button> value={pwdBaru}
<Button variant="light" onClick={() => setOpenedPassword(true)}>Ubah Password</Button> onChange={(e) => setPwdBaru(e.target.value)}
</Group> />
</Flex> </Input.Wrapper>
<Divider my={0} /> <Group grow>
<Stack gap={"md"}> <Button variant="light" onClick={() => setOpenedPassword(false)}>
<Group gap="xl" grow> Batal
<Input.Wrapper label="Nama" description="" error=""> </Button>
<Input value={host?.name ?? ""} readOnly /> <Button
</Input.Wrapper> variant="filled"
<Input.Wrapper label="Phone" description="" error=""> onClick={() => handleUpdatePassword()}
<Input value={host?.phone ?? ""} readOnly /> disabled={pwdBaru.length < 1}
</Input.Wrapper> >
</Group> Simpan
<Group gap="xl" grow> </Button>
<Input.Wrapper label="Email" description="" error=""> </Group>
<Input value={host?.email ?? ""} readOnly /> </Stack>
</Input.Wrapper> </Modal>
<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>
</>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import DesaSetting from "@/components/DesaSetting"; import DesaSetting from "@/components/DesaSetting";
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
import KategoriPengaduan from "@/components/KategoriPengaduan"; import KategoriPengaduan from "@/components/KategoriPengaduan";
import ProfileUser from "@/components/ProfileUser"; import ProfileUser from "@/components/ProfileUser";
import UserSetting from "@/components/UserSetting"; import UserSetting from "@/components/UserSetting";
@@ -12,14 +13,14 @@ import {
NavLink, NavLink,
Stack, Stack,
Table, Table,
Title Title,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconBuildingBank, IconBuildingBank,
IconCategory2, IconCategory2,
IconMailSpark, IconMailSpark,
IconUserCog, IconUserCog,
IconUsersGroup IconUsersGroup,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
@@ -90,7 +91,7 @@ export default function DetailSettingPage() {
{type === "cat-pengaduan" ? ( {type === "cat-pengaduan" ? (
<KategoriPengaduan /> <KategoriPengaduan />
) : type === "cat-pelayanan" ? ( ) : type === "cat-pelayanan" ? (
<KategoriPengaduanPage /> <KategoriPelayananSurat />
) : type === "desa" ? ( ) : type === "desa" ? (
<DesaSetting /> <DesaSetting />
) : type === "user" ? ( ) : type === "user" ? (
@@ -104,47 +105,3 @@ export default function DetailSettingPage() {
</Container> </Container>
); );
} }
function KategoriPengaduanPage() {
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 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 (
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
<Button variant="light">Tambah</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<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.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Stack>
</Stack>
);
}

View File

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

View File

@@ -1,3 +1,4 @@
import apiFetch from "@/lib/apiFetch";
import { import {
Button, Button,
Card, Card,
@@ -10,41 +11,30 @@ import {
Table, Table,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconSearch } from "@tabler/icons-react"; import { IconSearch } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import useSWR from "swr";
export default function ListWargaPage() { export default function ListWargaPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [value, setValue] = useState(""); const { data, mutate, isLoading } = useSWR("/", () =>
const elements = [ apiFetch.api.warga.list.get({
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" }, query: {
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" }, search: value,
{ 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 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 ( return (
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
@@ -81,14 +71,39 @@ export default function ListWargaPage() {
<Table> <Table>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>Tanggal</Table.Th> <Table.Th>Nama</Table.Th>
<Table.Th>Deskripsi</Table.Th> <Table.Th>No Telepon</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
<Table.Th>Aksi</Table.Th> <Table.Th>Aksi</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </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> </Table>
</Stack> </Stack>
</Card> </Card>

View File

@@ -1,8 +1,8 @@
import Elysia, { StatusMap, t } from "elysia" import Elysia, { t } from "elysia"
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
import { prisma } from "../lib/prisma"
import type { StatusPengaduan } from "generated/prisma" import type { StatusPengaduan } from "generated/prisma"
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
import { normalizePhoneNumber } from "../lib/normalizePhone" import { normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
const PelayananRoute = new Elysia({ const PelayananRoute = new Elysia({
prefix: "pelayanan", prefix: "pelayanan",
@@ -15,7 +15,7 @@ const PelayananRoute = new Elysia({
where: { where: {
isActive: true isActive: true
}, },
orderBy:{ orderBy: {
name: "asc" name: "asc"
} }
}) })
@@ -42,8 +42,8 @@ const PelayananRoute = new Elysia({
}, { }, {
body: t.Object({ body: t.Object({
name: t.String({ minLength: 1, error: "name harus diisi" }), name: t.String({ minLength: 1, error: "name harus diisi" }),
syaratDokumen: t.Array(t.String({ minLength: 1, error: "syaratDokumen harus diisi" })), syaratDokumen: t.Any(),
dataText: t.Array(t.String({ minLength: 1, error: "dataText harus diisi" })), dataText: t.Any(),
}), }),
detail: { detail: {
summary: "buat kategori pelayanan surat", summary: "buat kategori pelayanan surat",
@@ -69,8 +69,8 @@ const PelayananRoute = new Elysia({
body: t.Object({ body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }), id: t.String({ minLength: 1, error: "id harus diisi" }),
name: t.String({ minLength: 1, error: "name harus diisi" }), name: t.String({ minLength: 1, error: "name harus diisi" }),
syaratDokumen: t.Array(t.String({ minLength: 1, error: "syaratDokumen harus diisi" })), syaratDokumen: t.Any(),
dataText: t.Array(t.String({ minLength: 1, error: "dataText harus diisi" })), dataText: t.Any(),
}), }),
detail: { detail: {
summary: "update kategori pelayanan surat", summary: "update kategori pelayanan surat",

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