diff --git a/src/components/KategoriPengaduan.tsx b/src/components/KategoriPengaduan.tsx new file mode 100644 index 0000000..ffd3f54 --- /dev/null +++ b/src/components/KategoriPengaduan.tsx @@ -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 */} + + + + onValidation({ kat: 'name', value: e.target.value, aksi: 'edit' })} /> + + + + + + + + + {/* Modal Tambah */} + + + + onValidation({ kat: 'name', value: e.target.value, aksi: 'tambah' })} /> + + + + + + + + + + + + + + Kategori Pengaduan + + + + + + + + + + + Kategori + Aksi + + + + {list?.map((v: any) => ( + + {v.name} + + + chooseEdit({ data: v })} + > + + + + + + ))} + +
+
+
+ + ); +} diff --git a/src/pages/scr/dashboard/setting/detail_setting_page.tsx b/src/pages/scr/dashboard/setting/detail_setting_page.tsx index 230e5ae..767f4cb 100644 --- a/src/pages/scr/dashboard/setting/detail_setting_page.tsx +++ b/src/pages/scr/dashboard/setting/detail_setting_page.tsx @@ -1,4 +1,5 @@ import DesaSetting from "@/components/DesaSetting"; +import KategoriPengaduan from "@/components/KategoriPengaduan"; import { Button, Card, @@ -80,7 +81,7 @@ export default function DetailSettingPage() { }} > {type === "cat-pengaduan" ? ( - + ) : type === "cat-pelayanan" ? ( ) : type === "desa" ? ( diff --git a/src/server/routes/pengaduan_route.ts b/src/server/routes/pengaduan_route.ts index 97c6021..f4543e5 100644 --- a/src/server/routes/pengaduan_route.ts +++ b/src/server/routes/pengaduan_route.ts @@ -6,7 +6,7 @@ import { mimeToExtension } from "../lib/mimetypeToExtension" import { generateNoPengaduan } from "../lib/no-pengaduan" import { normalizePhoneNumber } from "../lib/normalizePhone" 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({ prefix: "pengaduan", @@ -100,27 +100,21 @@ const PengaduanRoute = new Elysia({ // --- PENGADUAN --- .post("/create", async ({ body }) => { - const { title, detail, location, imageName, idCategory, idWarga, phone } = body - let imageFix = imageName + 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 (!imageData && !imageMime) { - // const ext = mimeToExtension(imageMime) - // imageFix = `${uuidv4()}.${ext}` - // await uploadFileToFolder(defaultConfigSF, { name: imageFix, data: imageData }, "pengaduan") - // } - if (!category) { const cariCategory = await prisma.categoryPengaduan.findFirst({ where: { - name: idCategory, + name: kategoriId, } }) @@ -134,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, @@ -149,7 +143,7 @@ const PengaduanRoute = new Elysia({ if (!cariWarga) { const wargaCreate = await prisma.warga.create({ data: { - name: idWarga, + name: wargaId, phone: nomorHP, }, select: { @@ -165,11 +159,11 @@ const PengaduanRoute = new Elysia({ const pengaduan = await prisma.pengaduan.create({ data: { - title, - detail, + title: judulPengaduan, + detail: detailPengaduan, idCategory: idCategoryFix, idWarga: idWargaFix, - location, + location: lokasi, image: imageFix, 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({ - 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" }), - imageName: t.String({ optional: true, description: "nama file gambar yg telah diupload" }), - 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"] } }) @@ -262,6 +313,11 @@ const PengaduanRoute = new Elysia({ const data = await prisma.pengaduan.findUnique({ where: { id, + OR: [ + { + noPengaduan: id + } + ] }, select: { id: true, @@ -339,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"] } })