upd: dahsboar admin

Deskripsi:
- list kategori pelayanan surat
- edit kategori pelayanan surat
- tambah kategori pelayanan surat
- hapush kategori pelayanan surat

No Issues
This commit is contained in:
2025-11-14 14:32:32 +08:00
parent 8d535793b1
commit 6c6ee02cf0
3 changed files with 503 additions and 54 deletions

View File

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

View File

@@ -1,4 +1,5 @@
import DesaSetting from "@/components/DesaSetting";
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
import KategoriPengaduan from "@/components/KategoriPengaduan";
import ProfileUser from "@/components/ProfileUser";
import UserSetting from "@/components/UserSetting";
@@ -90,7 +91,7 @@ export default function DetailSettingPage() {
{type === "cat-pengaduan" ? (
<KategoriPengaduan />
) : type === "cat-pelayanan" ? (
<KategoriPengaduanPage />
<KategoriPelayananSurat />
) : type === "desa" ? (
<DesaSetting />
) : type === "user" ? (
@@ -103,48 +104,4 @@ export default function DetailSettingPage() {
</Grid>
</Container>
);
}
function KategoriPengaduanPage() {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return (
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
<Button variant="light">Tambah</Button>
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Stack>
</Stack>
);
}
}

View File

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