Merge pull request 'amalia/12-nov-25' (#23) from amalia/12-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/23
This commit is contained in:
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import DesaSetting from "@/components/DesaSetting";
|
import DesaSetting from "@/components/DesaSetting";
|
||||||
|
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -80,7 +81,7 @@ export default function DetailSettingPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === "cat-pengaduan" ? (
|
{type === "cat-pengaduan" ? (
|
||||||
<KategoriPengaduanPage />
|
<KategoriPengaduan />
|
||||||
) : type === "cat-pelayanan" ? (
|
) : type === "cat-pelayanan" ? (
|
||||||
<KategoriPengaduanPage />
|
<KategoriPengaduanPage />
|
||||||
) : type === "desa" ? (
|
) : type === "desa" ? (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { mimeToExtension } from "../lib/mimetypeToExtension"
|
|||||||
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
||||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||||
import { prisma } from "../lib/prisma"
|
import { prisma } from "../lib/prisma"
|
||||||
import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64, uploadFileToFolder } from "../lib/seafile"
|
import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64 } from "../lib/seafile"
|
||||||
|
|
||||||
const PengaduanRoute = new Elysia({
|
const PengaduanRoute = new Elysia({
|
||||||
prefix: "pengaduan",
|
prefix: "pengaduan",
|
||||||
@@ -100,27 +100,21 @@ const PengaduanRoute = new Elysia({
|
|||||||
|
|
||||||
// --- PENGADUAN ---
|
// --- PENGADUAN ---
|
||||||
.post("/create", async ({ body }) => {
|
.post("/create", async ({ body }) => {
|
||||||
const { title, detail, location, imageName, idCategory, idWarga, phone } = body
|
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
|
||||||
let imageFix = imageName
|
let imageFix = namaGambar
|
||||||
const noPengaduan = await generateNoPengaduan()
|
const noPengaduan = await generateNoPengaduan()
|
||||||
let idCategoryFix = idCategory
|
let idCategoryFix = kategoriId
|
||||||
let idWargaFix = idWarga
|
let idWargaFix = wargaId
|
||||||
const category = await prisma.categoryPengaduan.findUnique({
|
const category = await prisma.categoryPengaduan.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: idCategory,
|
id: kategoriId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// if (!imageData && !imageMime) {
|
|
||||||
// const ext = mimeToExtension(imageMime)
|
|
||||||
// imageFix = `${uuidv4()}.${ext}`
|
|
||||||
// await uploadFileToFolder(defaultConfigSF, { name: imageFix, data: imageData }, "pengaduan")
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||||
where: {
|
where: {
|
||||||
name: idCategory,
|
name: kategoriId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -134,12 +128,12 @@ const PengaduanRoute = new Elysia({
|
|||||||
|
|
||||||
const warga = await prisma.warga.findUnique({
|
const warga = await prisma.warga.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: idWarga,
|
id: wargaId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!warga) {
|
if (!warga) {
|
||||||
const nomorHP = normalizePhoneNumber({ phone })
|
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||||
const cariWarga = await prisma.warga.findUnique({
|
const cariWarga = await prisma.warga.findUnique({
|
||||||
where: {
|
where: {
|
||||||
phone: nomorHP,
|
phone: nomorHP,
|
||||||
@@ -149,7 +143,7 @@ const PengaduanRoute = new Elysia({
|
|||||||
if (!cariWarga) {
|
if (!cariWarga) {
|
||||||
const wargaCreate = await prisma.warga.create({
|
const wargaCreate = await prisma.warga.create({
|
||||||
data: {
|
data: {
|
||||||
name: idWarga,
|
name: wargaId,
|
||||||
phone: nomorHP,
|
phone: nomorHP,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -165,11 +159,11 @@ const PengaduanRoute = new Elysia({
|
|||||||
|
|
||||||
const pengaduan = await prisma.pengaduan.create({
|
const pengaduan = await prisma.pengaduan.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title: judulPengaduan,
|
||||||
detail,
|
detail: detailPengaduan,
|
||||||
idCategory: idCategoryFix,
|
idCategory: idCategoryFix,
|
||||||
idWarga: idWargaFix,
|
idWarga: idWargaFix,
|
||||||
location,
|
location: lokasi,
|
||||||
image: imageFix,
|
image: imageFix,
|
||||||
noPengaduan,
|
noPengaduan,
|
||||||
},
|
},
|
||||||
@@ -189,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({
|
body: t.Object({
|
||||||
title: t.String({ minLength: 1, error: "title harus diisi" }),
|
judulPengaduan: t.String({
|
||||||
detail: t.String({ minLength: 1, error: "detail harus diisi" }),
|
minLength: 3,
|
||||||
location: t.String({ minLength: 1, error: "location harus diisi" }),
|
error: "Judul pengaduan harus diisi dan minimal 3 karakter",
|
||||||
imageName: t.String({ optional: true, description: "nama file gambar yg telah diupload" }),
|
examples: ["Sampah menumpuk di depan rumah"],
|
||||||
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
description: "Judul singkat dari pengaduan warga"
|
||||||
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
|
}),
|
||||||
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
|
||||||
|
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: {
|
detail: {
|
||||||
summary: "Create Pengaduan Warga",
|
summary: "Buat Pengaduan Warga",
|
||||||
description: `tool untuk membuat 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"]
|
tags: ["mcp"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -262,6 +313,11 @@ const PengaduanRoute = new Elysia({
|
|||||||
const data = await prisma.pengaduan.findUnique({
|
const data = await prisma.pengaduan.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
noPengaduan: id
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -339,7 +395,7 @@ const PengaduanRoute = new Elysia({
|
|||||||
}, {
|
}, {
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Detail Pengaduan Warga",
|
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"]
|
tags: ["mcp"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user