Compare commits
13 Commits
amalia/11-
...
amalia/13-
| Author | SHA1 | Date | |
|---|---|---|---|
| 039524d092 | |||
| cc293d3bad | |||
| 6e1d3ecb56 | |||
| 5101a21f54 | |||
| 503c3e330d | |||
| 001c3df47d | |||
| a4167cfc8b | |||
| 5b240b782a | |||
| 14e2d711b3 | |||
| dc3ae99c05 | |||
| 63c88161d3 | |||
| eacc8fc220 | |||
| 422ca5a2cc |
@@ -1,7 +1,7 @@
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
import "@mantine/dates/styles.css";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import "@mantine/notifications/styles.css";
|
||||
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import AppRoutes from "./AppRoutes";
|
||||
|
||||
157
src/components/DesaSetting.tsx
Normal file
157
src/components/DesaSetting.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Title,
|
||||
Tooltip
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function DesaSetting() {
|
||||
const [btnDisable, setBtnDisable] = useState(false);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api["configuration-desa"].list.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
value: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api["configuration-desa"].edit.post(dataEdit);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your settings have been saved",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit configuration",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit configuration",
|
||||
type: "error",
|
||||
})
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseEdit({ data }: { data: { id: string, value: string, name: string } }) {
|
||||
setDataEdit(data);
|
||||
open();
|
||||
}
|
||||
|
||||
function onValidation({ kat, value }: { kat: 'value', value: string }) {
|
||||
if (value.length < 1) {
|
||||
setBtnDisable(true);
|
||||
} else {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
|
||||
if (kat === 'value') {
|
||||
setDataEdit({ ...dataEdit, value: value });
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.value.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
centered
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<Input value={dataEdit.value} onChange={(e) => onValidation({ kat: 'value', value: e.target.value })} />
|
||||
</Input.Wrapper>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" onClick={handleEdit} disabled={btnDisable} loading={btnLoading}>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Pengaturan Desa
|
||||
</Title>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>Value</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>{v.value}</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label="Edit Setting">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
234
src/components/KategoriPengaduan.tsx
Normal file
234
src/components/KategoriPengaduan.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Title,
|
||||
Tooltip
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function KategoriPengaduan() {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] = useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api.pengaduan.category.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState("")
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pengaduan.category.create.post({ name: dataTambah });
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah("");
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create category",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.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 {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
|
||||
if (kat === 'name') {
|
||||
if (aksi === 'edit') {
|
||||
setDataEdit({ ...dataEdit, name: value });
|
||||
} else {
|
||||
setDataTambah(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.name.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataTambah.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataTambah]);
|
||||
|
||||
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"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pengaduan
|
||||
</Title>
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Kategori</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
190
src/components/ProfileUser.tsx
Normal file
190
src/components/ProfileUser.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { Button, Divider, Flex, Group, Input, Modal, Stack, Title } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function ProfileUser() {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [openedPassword, setOpenedPassword] = useState(false);
|
||||
const [pwdBaru, setPwdBaru] = useState("");
|
||||
const [host, setHost] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
roleId: "",
|
||||
email: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState({
|
||||
name: false,
|
||||
email: false,
|
||||
phone: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchHost() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
setHost({
|
||||
id: data?.user?.id ?? "",
|
||||
name: data?.user?.name ?? "",
|
||||
phone: data?.user?.phone ?? "",
|
||||
roleId: data?.user?.roleId ?? "",
|
||||
email: data?.user?.email ?? "",
|
||||
});
|
||||
}
|
||||
fetchHost();
|
||||
}, []);
|
||||
|
||||
|
||||
function onValidation({ kat, value }: { kat: 'name' | 'email' | 'phone', value: string, }) {
|
||||
if (value.length < 1) {
|
||||
setError({ ...error, [kat]: true });
|
||||
} else {
|
||||
setError({ ...error, [kat]: false });
|
||||
}
|
||||
|
||||
setHost({ ...host, [kat]: value });
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
try {
|
||||
const res = await apiFetch.api.user.update.post(host);
|
||||
if (res.status === 200) {
|
||||
setOpened(false);
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your profile have been saved",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update profile",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update profile",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdatePassword() {
|
||||
try {
|
||||
const res = await apiFetch.api.user["update-password"].post({ password: pwdBaru, id: host.id });
|
||||
if (res.status === 200) {
|
||||
setPwdBaru("");
|
||||
setOpenedPassword(false);
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your password have been saved",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update password",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update password",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Profile Pengguna
|
||||
</Title>
|
||||
<Group gap="md">
|
||||
<Button variant="light" onClick={() => setOpened(true)}>Edit</Button>
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>Ubah Password</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Group gap="xl" grow>
|
||||
<Input.Wrapper label="Nama" description="" error="">
|
||||
<Input value={host?.name ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Phone" description="" error="">
|
||||
<Input value={host?.phone ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
</Group>
|
||||
<Group gap="xl" grow>
|
||||
<Input.Wrapper label="Email" description="" error="">
|
||||
<Input value={host?.email ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Role" description="" error="">
|
||||
<Input value={host?.roleId ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title={"Edit Profile"}
|
||||
|
||||
size={"lg"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap={"md"}>
|
||||
<Input.Wrapper label="Nama" description="" error={error.name ? "Field is required" : ""}>
|
||||
<Input value={host?.name ?? ""} onChange={(e) => onValidation({ kat: 'name', value: e.target.value })} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Phone" description="" error={error.phone ? "Field is required" : ""}>
|
||||
<Input value={host?.phone ?? ""} onChange={(e) => onValidation({ kat: 'phone', value: e.target.value })} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Email" description="" error={error.email ? "Field is required" : ""}>
|
||||
<Input value={host?.email ?? ""} onChange={(e) => onValidation({ kat: 'email', value: e.target.value })} />
|
||||
</Input.Wrapper>
|
||||
<Group grow>
|
||||
<Button variant="light" onClick={() => setOpened(false)}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" onClick={() => handleUpdate()} disabled={error.name || error.phone || error.email}>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
365
src/components/UserSetting.tsx
Normal file
365
src/components/UserSetting.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function UserSetting() {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] = useDisclosure(false);
|
||||
const [dataDelete, setDataDelete] = useState("")
|
||||
const { data: dataRole, mutate: mutateRole, isLoading: isLoadingRole } = useSWR("user-role", () =>
|
||||
apiFetch.api.user.role.get(),
|
||||
);
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] = useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("user-list", () =>
|
||||
apiFetch.api.user.list.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const listRole = dataRole?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
roleId: "",
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
roleId: "",
|
||||
password: "",
|
||||
phone: "",
|
||||
})
|
||||
const [error, setError] = useState({
|
||||
name: false,
|
||||
email: false,
|
||||
roleId: false,
|
||||
password: false,
|
||||
phone: false,
|
||||
})
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user.create.post(dataTambah);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah({
|
||||
name: "",
|
||||
email: "",
|
||||
roleId: "",
|
||||
password: "",
|
||||
phone: "",
|
||||
});
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your user have been saved",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create user ",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create user",
|
||||
type: "error",
|
||||
})
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
})
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user.delete.post({ id: dataDelete });
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeDelete();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your user have been deleted",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete user",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete user",
|
||||
type: "error",
|
||||
})
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseEdit({ data }: { data: { id: string, name: string, phone: string, email: string, roleId: string } }) {
|
||||
setDataEdit(data);
|
||||
open();
|
||||
}
|
||||
|
||||
function onValidation({ kat, value, aksi }: { kat: 'name' | 'email' | 'roleId' | 'password' | 'phone', value: string | null, aksi: 'edit' | 'tambah' }) {
|
||||
if (value == null || value.length < 1) {
|
||||
setBtnDisable(true);
|
||||
setError({ ...error, [kat]: true });
|
||||
} else {
|
||||
setBtnDisable(false);
|
||||
setError({ ...error, [kat]: false });
|
||||
}
|
||||
|
||||
|
||||
if (aksi === 'edit') {
|
||||
setDataEdit({ ...dataEdit, [kat]: value });
|
||||
} else {
|
||||
setDataTambah({ ...dataTambah, [kat]: value });
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.name.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Edit */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
centered
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label="Edit Kategori">
|
||||
<Input value={dataEdit.name} onChange={(e) => onValidation({ kat: 'name', value: e.target.value, aksi: 'edit' })} />
|
||||
</Input.Wrapper>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" onClick={handleEdit} disabled={btnDisable} loading={btnLoading}>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
opened={openedTambah}
|
||||
onClose={closeTambah}
|
||||
title={"Tambah"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label="Nama" description="" error={error.name ? "Field is required" : ""}>
|
||||
<Input value={dataTambah.name} onChange={(e) => onValidation({ kat: 'name', value: e.target.value, aksi: 'tambah' })} />
|
||||
</Input.Wrapper>
|
||||
<Select
|
||||
label="Role"
|
||||
placeholder="Pilih Role"
|
||||
data={listRole.map((r: any) => ({
|
||||
value: r.id,
|
||||
label: r.name,
|
||||
}))}
|
||||
value={dataTambah.roleId || null}
|
||||
error={error.roleId ? "Field is required" : ""}
|
||||
onChange={(_value, option) => { onValidation({ kat: 'roleId', value: option?.value, aksi: 'tambah' }) }}
|
||||
/>
|
||||
<Input.Wrapper label="Phone" description="">
|
||||
<Input value={dataTambah.phone} onChange={(e) => onValidation({ kat: 'phone', value: e.target.value, aksi: 'tambah' })} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Email" description="" error={error.email ? "Field is required" : ""}>
|
||||
<Input value={dataTambah.email} onChange={(e) => onValidation({ kat: 'email', value: e.target.value, aksi: 'tambah' })} />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Password" description="" error={error.password ? "Field is required" : ""}>
|
||||
<Input value={dataTambah.password} onChange={(e) => onValidation({ kat: 'password', value: e.target.value, aksi: 'tambah' })} />
|
||||
</Input.Wrapper>
|
||||
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeTambah}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" onClick={handleCreate} disabled={btnDisable || dataTambah.name.length < 1 || dataTambah.email.length < 1 || dataTambah.password.length < 1 || dataTambah.roleId.length < 1 || dataTambah.phone.length < 1} loading={btnLoading}>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* Modal Delete */}
|
||||
<Modal
|
||||
opened={openedDelete}
|
||||
onClose={closeDelete}
|
||||
title={"Delete"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="md" color="gray.6">
|
||||
Apakah anda yakin ingin menghapus user ini?
|
||||
</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeDelete}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" color="red" onClick={handleDelete} loading={btnLoading}>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Daftar User
|
||||
</Title>
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>Telepon</Table.Th>
|
||||
<Table.Th>Email</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
list.length > 0 ? (
|
||||
list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>{v.phone}</Table.Td>
|
||||
<Table.Td>{v.email}</Table.Td>
|
||||
<Table.Td>{v.roleId}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="Edit User">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete User">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id)
|
||||
openDelete()
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5} align="center">
|
||||
Data User Tidak Ditemukan
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
src/components/notificationGlobal.ts
Normal file
38
src/components/notificationGlobal.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
|
||||
export default function notification({ title, message, type }: { title: string, message: string, type: "success" | "error" | "warning" | "info" }) {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "green",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
case "error":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "red",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
case "warning":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "orange",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
case "info":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "blue",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import cors from "@elysiajs/cors";
|
||||
import Swagger from "@elysiajs/swagger";
|
||||
import Elysia from "elysia";
|
||||
import html from "./index.html";
|
||||
@@ -14,7 +15,7 @@ import PelayananRoute from "./server/routes/pelayanan_surat_route";
|
||||
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||
import TestRoute from "./server/routes/test";
|
||||
import UserRoute from "./server/routes/user_route";
|
||||
import cors from "@elysiajs/cors";
|
||||
import ConfigurationDesaRoute from "./server/routes/configuration_desa_route";
|
||||
|
||||
const Docs = new Elysia({
|
||||
tags: ["docs"],
|
||||
@@ -30,6 +31,7 @@ const Api = new Elysia({
|
||||
})
|
||||
.use(PengaduanRoute)
|
||||
.use(PelayananRoute)
|
||||
.use(ConfigurationDesaRoute)
|
||||
.use(TestRoute)
|
||||
.use(apiAuth)
|
||||
.use(ApiKeyRoute)
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
import { Button, Card, Container, Divider, Flex, Grid, Group, Input, NavLink, Stack, Table, Title } from "@mantine/core";
|
||||
import { IconCircleOff, IconGauge, IconHome2 } from "@tabler/icons-react";
|
||||
import DesaSetting from "@/components/DesaSetting";
|
||||
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||
import ProfileUser from "@/components/ProfileUser";
|
||||
import UserSetting from "@/components/UserSetting";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
NavLink,
|
||||
Stack,
|
||||
Table,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconBuildingBank,
|
||||
IconCategory2,
|
||||
IconMailSpark,
|
||||
IconUserCog,
|
||||
IconUsersGroup
|
||||
} from "@tabler/icons-react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export default function DetailSettingPage() {
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const type = query.get("type");
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const type = query.get("type");
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={3}>
|
||||
<Card
|
||||
radius="md"
|
||||
p="lg"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
href={`?type=profile`}
|
||||
label="Profile"
|
||||
leftSection={<IconHome2 size={16} stroke={1.5} />}
|
||||
active={type === "profile" || !type}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pengaduan`}
|
||||
label="Kategori Pengaduan"
|
||||
leftSection={<IconGauge size={16} stroke={1.5} />}
|
||||
active={type === "cat-pengaduan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pelayanan`}
|
||||
label="Kategori Pelayanan Surat"
|
||||
leftSection={<IconCircleOff size={16} stroke={1.5} />}
|
||||
active={type === "cat-pelayanan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=desa`}
|
||||
label="Desa"
|
||||
leftSection={<IconCircleOff size={16} stroke={1.5} />}
|
||||
active={type === "desa"}
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<Card
|
||||
radius="md"
|
||||
p="lg"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
{type === "cat-pengaduan"
|
||||
? <KategoriPengaduanPage />
|
||||
: type === "cat-pelayanan"
|
||||
? <KategoriPengaduanPage />
|
||||
: type === "desa"
|
||||
? <KategoriPengaduanPage />
|
||||
: <ProfilePage />}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={3}>
|
||||
<Card
|
||||
radius="md"
|
||||
p="lg"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
href={`?type=profile`}
|
||||
label="Profile"
|
||||
leftSection={<IconUserCog size={16} stroke={1.5} />}
|
||||
active={type === "profile" || !type}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=user`}
|
||||
label="User"
|
||||
leftSection={<IconUsersGroup size={16} stroke={1.5} />}
|
||||
active={type === "user"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pengaduan`}
|
||||
label="Kategori Pengaduan"
|
||||
leftSection={<IconCategory2 size={16} stroke={1.5} />}
|
||||
active={type === "cat-pengaduan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pelayanan`}
|
||||
label="Kategori Pelayanan Surat"
|
||||
leftSection={<IconMailSpark size={16} stroke={1.5} />}
|
||||
active={type === "cat-pelayanan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=desa`}
|
||||
label="Desa"
|
||||
leftSection={<IconBuildingBank size={16} stroke={1.5} />}
|
||||
active={type === "desa"}
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<Card
|
||||
radius="md"
|
||||
p="lg"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
{type === "cat-pengaduan" ? (
|
||||
<KategoriPengaduan />
|
||||
) : type === "cat-pelayanan" ? (
|
||||
<KategoriPengaduanPage />
|
||||
) : type === "desa" ? (
|
||||
<DesaSetting />
|
||||
) : type === "user" ? (
|
||||
<UserSetting />
|
||||
) : (
|
||||
<ProfileUser />
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfilePage() {
|
||||
return (
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Profile Pengguna
|
||||
</Title>
|
||||
<Button variant="light">Edit</Button>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Group gap="xl" grow>
|
||||
<Input.Wrapper label="Nama" description="" error="">
|
||||
<Input value={"Amalia Dwi Yustiani"} readOnly />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Phone" description="" error="">
|
||||
<Input value={"08123456789"} readOnly />
|
||||
</Input.Wrapper>
|
||||
</Group>
|
||||
<Group gap="xl" grow>
|
||||
<Input.Wrapper label="Email" description="" error="">
|
||||
<Input value={"amaliadwiyustiani@gmail.com"} readOnly />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Role" description="" error="">
|
||||
<Input value={"Admin"} readOnly />
|
||||
</Input.Wrapper>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
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 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 (
|
||||
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"}>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
|
||||
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||
return `✅ Uploaded ${file.name} successfully`;
|
||||
}
|
||||
|
||||
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string; }): Promise<string> {
|
||||
const remoteName = path.basename(base64File.name);
|
||||
|
||||
@@ -194,6 +195,38 @@ export async function uploadFileBase64(config: Config, base64File: { name: strin
|
||||
return `✅ Uploaded ${base64File.name} successfully`;
|
||||
}
|
||||
|
||||
export async function uploadFileToFolder(config: Config, base64File: { name: string; data: string; }, folder: 'syarat-dokumen' | 'pengaduan'): Promise<string> {
|
||||
const remoteName = path.basename(base64File.name);
|
||||
|
||||
// 1. Dapatkan upload link (pakai Authorization)
|
||||
const uploadUrlResponse = await fetchWithAuth(
|
||||
config,
|
||||
`${config.URL}/${config.REPO}/upload-link/`
|
||||
);
|
||||
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
|
||||
|
||||
// 2. Konversi base64 ke Blob
|
||||
const binary = Buffer.from(base64File.data, "base64");
|
||||
const blob = new Blob([binary]);
|
||||
|
||||
// 3. Siapkan form-data
|
||||
const formData = new FormData();
|
||||
formData.append("parent_dir", "/");
|
||||
formData.append("relative_path", folder); // tanpa slash di akhir
|
||||
formData.append("file", blob, remoteName);
|
||||
|
||||
// 4. Upload file TANPA Authorization header, token di query param
|
||||
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
|
||||
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||
return `✅ Uploaded ${base64File.name} successfully`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
48
src/server/routes/configuration_desa_route.ts
Normal file
48
src/server/routes/configuration_desa_route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
const ConfigurationDesaRoute = new Elysia({
|
||||
prefix: "configuration-desa",
|
||||
tags: ["configuration-desa"],
|
||||
})
|
||||
|
||||
.get("/list", async () => {
|
||||
const data = await prisma.configuration.findMany({
|
||||
orderBy: {
|
||||
name: "asc"
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Konfigurasi",
|
||||
description: `tool untuk mendapatkan list konfigurasi`,
|
||||
}
|
||||
})
|
||||
.post("/edit", async ({ body }) => {
|
||||
const { id, value } = body
|
||||
|
||||
await prisma.configuration.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
value,
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'konfigurasi sudah diperbarui' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
value: t.String({ minLength: 1, error: "value harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "edit konfigurasi desa",
|
||||
description: `tool untuk edit konfigurasi desa`
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default ConfigurationDesaRoute
|
||||
@@ -100,20 +100,21 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
// --- PENGADUAN ---
|
||||
.post("/create", async ({ body }) => {
|
||||
const { title, detail, location, image, idCategory, idWarga, phone } = body
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
|
||||
let imageFix = namaGambar
|
||||
const noPengaduan = await generateNoPengaduan()
|
||||
let idCategoryFix = idCategory
|
||||
let idWargaFix = idWarga
|
||||
let idCategoryFix = kategoriId
|
||||
let idWargaFix = wargaId
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
where: {
|
||||
id: idCategory,
|
||||
id: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||
where: {
|
||||
name: idCategory,
|
||||
name: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -127,12 +128,12 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id: idWarga,
|
||||
id: wargaId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const nomorHP = normalizePhoneNumber({ phone })
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const cariWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
phone: nomorHP,
|
||||
@@ -142,7 +143,7 @@ const PengaduanRoute = new Elysia({
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: idWarga,
|
||||
name: wargaId,
|
||||
phone: nomorHP,
|
||||
},
|
||||
select: {
|
||||
@@ -158,12 +159,12 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
const pengaduan = await prisma.pengaduan.create({
|
||||
data: {
|
||||
title,
|
||||
detail,
|
||||
title: judulPengaduan,
|
||||
detail: detailPengaduan,
|
||||
idCategory: idCategoryFix,
|
||||
idWarga: idWargaFix,
|
||||
location,
|
||||
image,
|
||||
location: lokasi,
|
||||
image: imageFix,
|
||||
noPengaduan,
|
||||
},
|
||||
select: {
|
||||
@@ -172,7 +173,7 @@ const PengaduanRoute = new Elysia({
|
||||
})
|
||||
|
||||
if (!pengaduan.id) {
|
||||
throw new Error("gagal membuat pengaduan")
|
||||
return { success: false, message: 'gagal membuat pengaduan' }
|
||||
}
|
||||
|
||||
await prisma.historyPengaduan.create({
|
||||
@@ -182,20 +183,77 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'pengaduan sudah dibuat' }
|
||||
return { success: true, message: 'pengaduan sudah dibuat dengan nomer ' + noPengaduan + ', nomer ini akan digunakan untuk mengakses pengaduan ini' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
title: t.String({ minLength: 1, error: "title harus diisi" }),
|
||||
detail: t.String({ minLength: 1, error: "detail harus diisi" }),
|
||||
location: t.String({ minLength: 1, error: "location harus diisi" }),
|
||||
image: t.Any(),
|
||||
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
||||
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
|
||||
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||
judulPengaduan: t.String({
|
||||
minLength: 3,
|
||||
error: "Judul pengaduan harus diisi dan minimal 3 karakter",
|
||||
examples: ["Sampah menumpuk di depan rumah"],
|
||||
description: "Judul singkat dari pengaduan warga"
|
||||
}),
|
||||
|
||||
detailPengaduan: t.String({
|
||||
minLength: 5,
|
||||
error: "Deskripsi pengaduan harus diisi dan minimal 10 karakter",
|
||||
examples: ["Terdapat sampah yang menumpuk selama seminggu di depan rumah saya"],
|
||||
description: "Penjelasan lebih detail mengenai pengaduan"
|
||||
}),
|
||||
|
||||
lokasi: t.String({
|
||||
minLength: 5,
|
||||
error: "Lokasi pengaduan harus diisi",
|
||||
examples: ["Jl. Raya No. 1, RT 01 RW 02, Darmasaba"],
|
||||
description: "Alamat atau titik lokasi pengaduan"
|
||||
}),
|
||||
|
||||
namaGambar: t.String({
|
||||
optional: true,
|
||||
examples: ["sampah.jpg"],
|
||||
description: "Nama file gambar yang telah diupload (opsional)"
|
||||
}),
|
||||
|
||||
kategoriId: t.String({
|
||||
minLength: 1,
|
||||
error: "ID kategori pengaduan harus diisi",
|
||||
examples: ["kebersihan"],
|
||||
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
}),
|
||||
|
||||
wargaId: t.String({
|
||||
minLength: 1,
|
||||
error: "ID warga harus diisi",
|
||||
examples: ["budiman"],
|
||||
description: "ID unik warga yang melapor (jika sudah terdaftar)"
|
||||
}),
|
||||
|
||||
noTelepon: t.String({
|
||||
minLength: 1,
|
||||
error: "Nomor telepon harus diisi",
|
||||
examples: ["08123456789", "+628123456789"],
|
||||
description: "Nomor telepon warga pelapor"
|
||||
}),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "Create Pengaduan Warga",
|
||||
description: `tool untuk membuat pengaduan warga`,
|
||||
summary: "Buat Pengaduan Warga",
|
||||
description: `
|
||||
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.
|
||||
|
||||
Alur proses:
|
||||
1. Sistem memvalidasi kategori pengaduan berdasarkan ID.
|
||||
- Jika ID kategori tidak ditemukan, sistem akan mencari berdasarkan nama kategori.
|
||||
- Jika tetap tidak ditemukan, kategori akan diset menjadi "lainnya".
|
||||
2. Sistem memvalidasi data warga berdasarkan ID.
|
||||
- Jika warga tidak ditemukan, sistem akan mencari berdasarkan nomor telepon.
|
||||
- Jika tetap tidak ditemukan, data warga baru akan dibuat secara otomatis.
|
||||
3. Sistem menghasilkan nomor pengaduan unik (noPengaduan).
|
||||
4. Data pengaduan akan disimpan ke database, termasuk judul, detail, lokasi, gambar (opsional), dan data warga.
|
||||
5. Sistem juga membuat catatan riwayat awal pengaduan dengan deskripsi "Pengaduan dibuat".
|
||||
|
||||
Respon:
|
||||
- success: true jika pengaduan berhasil dibuat.
|
||||
- message: berisi pesan sukses dan nomor pengaduan yang dapat digunakan untuk melacak status pengaduan.`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
@@ -255,6 +313,11 @@ const PengaduanRoute = new Elysia({
|
||||
const data = await prisma.pengaduan.findUnique({
|
||||
where: {
|
||||
id,
|
||||
OR: [
|
||||
{
|
||||
noPengaduan: id
|
||||
}
|
||||
]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -332,7 +395,7 @@ const PengaduanRoute = new Elysia({
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Detail Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan`,
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
@@ -622,7 +685,7 @@ const PengaduanRoute = new Elysia({
|
||||
const { fileName } = query
|
||||
|
||||
const connect = await testConnection(defaultConfigSF)
|
||||
console.log({connect})
|
||||
console.log({ connect })
|
||||
|
||||
const hasil = await catFile(defaultConfigSF, fileName)
|
||||
console.log('hasilnya', hasil)
|
||||
|
||||
@@ -47,5 +47,140 @@ const UserRoute = new Elysia({
|
||||
description: "upsert user",
|
||||
}
|
||||
})
|
||||
.post("/update-password", async ({ body }) => {
|
||||
const { password, id } = body
|
||||
const update = await prisma.user.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
password
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Password updated successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
password: t.String({ minLength: 1, error: "password is required" }),
|
||||
id: t.String({ minLength: 1, error: "id is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "update password",
|
||||
description: "update password user",
|
||||
}
|
||||
})
|
||||
.post("/update", async ({ body }) => {
|
||||
const { name, phone, id, roleId } = body
|
||||
const update = await prisma.user.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
phone,
|
||||
roleId
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User updated successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
phone: t.String({ minLength: 1, error: "phone is required" }),
|
||||
id: t.String({ minLength: 1, error: "id is required" }),
|
||||
roleId: t.String({ minLength: 1, error: "roleId is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "update",
|
||||
description: "update user",
|
||||
}
|
||||
})
|
||||
.post("/create", async ({ body }) => {
|
||||
const { name, phone, roleId, email, password } = body
|
||||
const create = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
phone,
|
||||
roleId,
|
||||
email,
|
||||
password
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User created successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
phone: t.String({ minLength: 1, error: "phone is required" }),
|
||||
roleId: t.String({ minLength: 1, error: "roleId is required" }),
|
||||
email: t.String({ minLength: 1, error: "email is required" }),
|
||||
password: t.String({ minLength: 1, error: "password is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "create",
|
||||
description: "create user",
|
||||
}
|
||||
})
|
||||
.get("/list", async (ctx) => {
|
||||
const { user } = ctx as any
|
||||
|
||||
const data = await prisma.user.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
NOT: {
|
||||
id: user.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "list",
|
||||
description: "list user",
|
||||
}
|
||||
})
|
||||
.get("/role", async () => {
|
||||
const data = await prisma.role.findMany()
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "role",
|
||||
description: "role user",
|
||||
}
|
||||
})
|
||||
.post("/delete", async ({ body }) => {
|
||||
const { id } = body
|
||||
const deleteData = await prisma.user.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
isActive: false
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User deleted successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "delete",
|
||||
description: "delete user",
|
||||
}
|
||||
})
|
||||
|
||||
export default UserRoute
|
||||
Reference in New Issue
Block a user