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:
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
612
src/components/KategoriPelayananSurat.tsx
Normal file
612
src/components/KategoriPelayananSurat.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
141
src/server/routes/warga_route.ts
Normal file
141
src/server/routes/warga_route.ts
Normal 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
|
||||||
Reference in New Issue
Block a user