import Elysia, { t } from "elysia" import type { StatusPengaduan } from "generated/prisma" import _ from "lodash" import { v4 as uuidv4 } from "uuid" import { getLastUpdated } from "../lib/get-last-updated" import { mimeToExtension } from "../lib/mimetypeToExtension" import { generateNoPengaduan } from "../lib/no-pengaduan" import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone" import { prisma } from "../lib/prisma" import { renameFile } from "../lib/rename-file" import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile" const PengaduanRoute = new Elysia({ prefix: "pengaduan", tags: ["pengaduan"], }) // --- KATEGORI PENGADUAN --- .get("/category", async () => { const data = await prisma.categoryPengaduan.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, } }) return { data } }, { detail: { summary: "List Kategori Pengaduan", description: `tool untuk mendapatkan list kategori pengaduan`, tags: ["mcp"] } }) .post("/category/create", async ({ body }) => { const { name } = body await prisma.categoryPengaduan.create({ data: { name, } }) return { success: true, message: 'kategori pengaduan sudah dibuat' } }, { body: t.Object({ name: t.String({ minLength: 1, error: "name harus diisi" }), }), detail: { summary: "buat kategori pengaduan", description: `tool untuk membuat kategori pengaduan` } }) .post("/category/update", async ({ body }) => { const { id, name } = body await prisma.categoryPengaduan.update({ where: { id, }, data: { name } }) return { success: true, message: 'kategori pengaduan sudah diperbarui' } }, { body: t.Object({ id: t.String({ minLength: 1, error: "id harus diisi" }), name: t.String({ minLength: 1, error: "name harus diisi" }), }), detail: { summary: "update kategori pengaduan", description: `tool untuk update kategori pengaduan` } }) .post("/category/delete", async ({ body }) => { const { id } = body await prisma.categoryPengaduan.update({ where: { id, }, data: { isActive: false } }) return { success: true, message: 'kategori pengaduan sudah dihapus' } }, { body: t.Object({ id: t.String({ minLength: 1, error: "id harus diisi" }), }), detail: { summary: "delete kategori pengaduan", description: `tool untuk delete kategori pengaduan` } }) // --- PENGADUAN --- .post("/create", async ({ body, headers }) => { const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId } = body const namaWarga = headers["x-user"] || "" const noTelepon = headers["x-phone"] || "" let imageFix = namaGambar const noPengaduan = await generateNoPengaduan() let idCategoryFix = kategoriId let idWargaFix = "" if (idCategoryFix) { const category = await prisma.categoryPengaduan.findUnique({ where: { id: idCategoryFix, } }) if (!category) { const cariCategory = await prisma.categoryPengaduan.findFirst({ where: { name: kategoriId, } }) if (!cariCategory) { idCategoryFix = "lainnya" } else { idCategoryFix = cariCategory.id } } } else { idCategoryFix = "lainnya" } if (!isValidPhone(noTelepon)) { return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' } } const nomorHP = normalizePhoneNumber({ phone: noTelepon }) const dataWarga = await prisma.warga.upsert({ where: { phone: nomorHP }, create: { name: namaWarga, phone: nomorHP, }, update: { name: namaWarga, }, select: { id: true } }) idWargaFix = dataWarga.id const pengaduan = await prisma.pengaduan.create({ data: { title: judulPengaduan, detail: detailPengaduan, idCategory: idCategoryFix, idWarga: idWargaFix || "", location: lokasi, image: imageFix, noPengaduan, }, select: { id: true, } }) if (!pengaduan.id) { return { success: false, message: 'gagal membuat pengaduan' } } await prisma.historyPengaduan.create({ data: { idPengaduan: pengaduan.id, deskripsi: "Pengaduan dibuat", } }) return { success: true, message: 'pengaduan sudah dibuat dengan nomer ' + noPengaduan + ', nomer ini akan digunakan untuk mengakses pengaduan ini' } }, { body: t.Object({ judulPengaduan: t.String({ error: "Judul pengaduan harus diisi", examples: ["Sampah menumpuk di depan rumah"], description: "Judul singkat dari pengaduan warga" }), detailPengaduan: t.String({ error: "Deskripsi pengaduan harus diisi", examples: ["Terdapat sampah yang menumpuk selama seminggu di depan rumah saya"], description: "Penjelasan lebih detail mengenai pengaduan" }), lokasi: t.String({ error: "Lokasi pengaduan harus diisi", examples: ["Jl. Raya No. 1, RT 01 RW 02, Darmasaba"], description: "Alamat atau titik lokasi pengaduan" }), namaGambar: t.Optional(t.String({ examples: ["sampah.jpg"], description: "Nama file gambar yang telah diupload (opsional)" })), kategoriId: t.Optional(t.String({ examples: ["kebersihan"], description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)" })), // namaWarga: t.String({ // examples: ["budiman"], // description: "Nama warga yang melapor" // }), // noTelepon: t.String({ // error: "Nomor telepon harus diisi", // examples: ["08123456789", "+628123456789"], // description: "Nomor telepon warga pelapor" // }), }), detail: { summary: "Buat Pengaduan Warga", description: `Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga`, tags: ["mcp"] } }) .post("/update-status", async ({ body }) => { const { id, status, keterangan, idUser } = body let deskripsi = "" const pengaduan = await prisma.pengaduan.update({ where: { id, }, data: { status: status as StatusPengaduan, keterangan, } }) if (!pengaduan) { return { success: false, message: 'gagal update status pengaduan' } } if (status === "diterima") { deskripsi = "Pengaduan diterima oleh admin" } else if (status === "dikerjakan") { deskripsi = "Pengaduan dikerjakan oleh petugas" } else if (status === "ditolak") { deskripsi = "Pengaduan ditolak dengan keterangan " + keterangan } else if (status === "selesai") { deskripsi = "Pengaduan selesai" } await prisma.historyPengaduan.create({ data: { idPengaduan: pengaduan.id, deskripsi, status: status as StatusPengaduan, idUser, } }) return { success: true, message: 'status pengaduan sudah diupdate' } }, { body: t.Object({ id: t.String({ minLength: 1, error: "id harus diisi" }), status: t.String({ minLength: 1, error: "status harus diisi" }), keterangan: t.Any(), idUser: t.String({ minLength: 1, error: "idUser harus diisi" }), }), detail: { summary: "Update status pengaduan", description: `tool untuk update status pengaduan` } }) .post("/update", async ({ body }) => { const { noPengaduan, judul, detail, lokasi, namaGambar } = body let dataUpdate = {} const cek = await prisma.pengaduan.findFirst({ where: { noPengaduan, }, select: { id: true } }) if (!cek) { return { success: false, message: 'gagal update status pengaduan, nomer ' + noPengaduan + ' tidak ditemukan' } } if (judul) { dataUpdate = { title: judul } } if (detail) { dataUpdate = { ...dataUpdate, detail } } if (lokasi) { dataUpdate = { ...dataUpdate, location: lokasi } } if (namaGambar) { dataUpdate = { ...dataUpdate, image: namaGambar } } const pengaduan = await prisma.pengaduan.updateMany({ where: { noPengaduan }, data: dataUpdate }) const keys = Object.keys(dataUpdate).join(", "); await prisma.historyPengaduan.create({ data: { idPengaduan: cek.id, deskripsi: `Pengaduan diupdate oleh warga (data yg diupdate: ${keys})`, } }) return { success: true, message: 'pengaduan dengan nomer ' + noPengaduan + ' sudah diupdate' } }, { body: t.Object({ noPengaduan: t.String({ error: "nomer pengaduan harus diisi", description: "Nomer pengaduan yang ingin diupdate" }), judul: t.Optional(t.String({ error: "judul harus diisi", description: "Judul pengaduan yang ingin diupdate" })), detail: t.Optional(t.String({ description: "detail pengaduan yang ingin diupdate" })), lokasi: t.Optional(t.String({ description: "lokasi pengaduan yang ingin diupdate" })), namaGambar: t.Optional(t.String({ description: "Nama file gambar yang telah diupload untuk update data pengaduan" })), }), detail: { summary: "Update Data Pengaduan", description: `tool untuk update data pengaduan`, tags: ["mcp"] } }) .get("/detail", async ({ query }) => { const { id } = query const data = await prisma.pengaduan.findFirst({ where: { OR: [ { noPengaduan: id }, { id: id } ] }, select: { id: true, noPengaduan: true, title: true, detail: true, location: true, image: true, idCategory: true, idWarga: true, status: true, keterangan: true, createdAt: true, updatedAt: true, CategoryPengaduan: { select: { name: true } }, Warga: { select: { name: true, phone: true, _count: { select: { Pengaduan: true, PelayananAjuan: true, } } } } } }) const dataHistory = await prisma.historyPengaduan.findMany({ where: { idPengaduan: data?.id, }, select: { id: true, deskripsi: true, status: true, createdAt: true, idUser: true, User: { select: { name: true, } } } }) const dataHistoryFix = dataHistory.map((item: any) => ({ ..._.omit(item, ["User", "createdAt"]), nameUser: item.User?.name, createdAt: item.createdAt })) const warga = { name: data?.Warga?.name, phone: data?.Warga?.phone, pengaduan: data?.Warga?._count.Pengaduan, pelayanan: data?.Warga?._count.PelayananAjuan, } const dataPengaduan = { id: data?.id, noPengaduan: data?.noPengaduan, title: data?.title, detail: data?.detail, location: data?.location, image: data?.image, category: data?.CategoryPengaduan.name, status: data?.status, keterangan: data?.keterangan, createdAt: data?.createdAt, updatedAt: data?.updatedAt, } const datafix = { pengaduan: dataPengaduan, history: dataHistoryFix, warga: warga, } return datafix }, { detail: { summary: "Detail Pengaduan Warga", description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`, tags: ["mcp"] } }) .get("/", async ({ query }) => { const { take, page, search, phone } = query const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take)) const data = await prisma.pengaduan.findMany({ skip, take: !take ? 10 : Number(take), orderBy: { createdAt: "asc" }, where: { isActive: true, OR: [ { title: { contains: search ?? "", mode: "insensitive" }, }, { noPengaduan: { contains: search ?? "", mode: "insensitive" }, }, { detail: { contains: search ?? "", mode: "insensitive" }, } ], AND: { Warga: { phone: phone } } }, select: { id: true, noPengaduan: true, title: true, detail: true, location: true, status: true, createdAt: true, CategoryPengaduan: { select: { name: true } }, Warga: { select: { name: true, } } } }) const dataFix = data.map((item) => { return { noPengaduan: item.noPengaduan, title: item.title, detail: item.detail, status: item.status, createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }), } }) return dataFix }, { query: t.Object({ take: t.String({ optional: true }), page: t.String({ optional: true }), search: t.String({ optional: true }), phone: t.String({ minLength: 11, error: "phone harus diisi" }), }), detail: { summary: "List Pengaduan Warga By Phone", description: `tool untuk mendapatkan list pengaduan warga by phone`, tags: ["mcp"] } }) .post("/upload", async ({ body }) => { const { file, folder } = body; // Validasi file if (!file) { return { success: false, message: "File tidak ditemukan" }; } // Rename file const renamedFile = renameFile({ oldFile: file, newName: 'random' }); // Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer) // const buffer = await file.arrayBuffer(); const result = await uploadFile(defaultConfigSF, renamedFile, folder); if (result == 'gagal') { return { success: false, message: "Upload gagal" }; } return { success: true, message: "Upload berhasil", filename: renamedFile.name, size: renamedFile.size, seafileResult: result }; }, { body: t.Object({ file: t.Any(), folder: t.String(), }), detail: { summary: "Upload File (FormData)", description: "Tool untuk upload file ke folder tujuan dengan memakai FormData", // tags: ["mcp"], consumes: ["multipart/form-data"] }, }) .post("/upload-file-form-data", async ({ body }) => { const { file } = body; // // Validasi file // if (!file) { // return { success: false, message: "File tidak ditemukan" }; // } // // Rename file // const renamedFile = renameFile({ oldFile: file, newName: 'random' }); // // Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer) // // const buffer = await file.arrayBuffer(); // const result = await uploadFile(defaultConfigSF, renamedFile, 'pengaduan'); // if (result == 'gagal') { // return { success: false, message: "Upload gagal" }; // } return { success: true, file: JSON.stringify(file), fileInfo: { name: file.name || 'kosong', size: file.size || 0, type: file.type || 'kosong' } // message: "Upload berhasil", // filename: renamedFile.name, // size: renamedFile.size, // seafileResult: result }; }, { body: t.Object({ file: t.Any(), // folder: t.String(), }), detail: { summary: "Upload File (FormData)", description: "Tool untuk upload file ke folder tujuan dengan memakai FormData", // tags: ["mcp"], consumes: ["multipart/form-data"] }, }) .post("/upload-base64", async ({ body }) => { const { data, mimetype, kategori } = body; const ext = mimeToExtension(mimetype) const name = `${uuidv4()}.${ext}` const kategoriFix = kategori === 'pengaduan' ? 'pengaduan' : 'syarat-dokumen'; // Validasi file if (!data) { return { success: false, message: "File tidak ditemukan" }; } // Konversi file ke base64 // const buffer = await file.arrayBuffer(); // const base64String = Buffer.from(buffer).toString("base64"); // (Opsional) jika perlu dikirim ke Seafile sebagai base64 // const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data }); const result = await uploadFileToFolder(defaultConfigSF, { name: name, data: data }, kategoriFix); return { success: true, message: "Upload berhasil", data: { name, mimetype, ext, kategori, } }; }, { body: t.Object({ data: t.String(), mimetype: t.String(), kategori: t.String() }), detail: { summary: "Upload File (Base64)", description: "Tool untuk upload file ke Seafile dalam format Base64", // tags: ["mcp"], consumes: ["multipart/form-data"] }, }) .get("/list", async ({ query }) => { const { take, page, search, status } = query const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take)) let where: any = { isActive: true, OR: [ { title: { contains: search ?? "", mode: "insensitive" }, }, { noPengaduan: { contains: search ?? "", mode: "insensitive" }, }, { detail: { contains: search ?? "", mode: "insensitive" }, }, { Warga: { phone: { contains: search ?? "", mode: "insensitive" }, }, } ] } if (status && status !== "semua") { where = { ...where, status: status } } const totalData = await prisma.pengaduan.count({ where }); const data = await prisma.pengaduan.findMany({ skip, take: !take ? 10 : Number(take), orderBy: { createdAt: "desc" }, where, select: { id: true, noPengaduan: true, title: true, detail: true, location: true, status: true, createdAt: true, updatedAt: true, CategoryPengaduan: { select: { name: true } }, Warga: { select: { name: true, } } } }) const dataFix = data.map((item) => { return { noPengaduan: item.noPengaduan, id: item.id, title: item.title, detail: item.detail, status: item.status, location: item.location, createdAt: item.createdAt.toISOString(), updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt), } }) const dataReturn = { data: dataFix, total: totalData, page: Number(page) || 1, pageSize: !take ? 10 : Number(take), totalPages: Math.ceil(totalData / (!take ? 10 : Number(take))) } return dataReturn }, { query: t.Object({ take: t.String({ optional: true }), page: t.String({ optional: true }), search: t.String({ optional: true }), status: t.String({ optional: true }), }), detail: { summary: "List Pengaduan Warga", description: `tool untuk mendapatkan list pengaduan warga`, } }) .get("/count", async ({ query }) => { const counts = await prisma.pengaduan.groupBy({ by: ['status'], where: { isActive: true, }, _count: { status: true, }, }); const grouped = Object.fromEntries( counts.map(c => [c.status, c._count.status]) ); const total = await prisma.pengaduan.count({ where: { isActive: true }, }); return { antrian: grouped?.antrian || 0, diterima: grouped?.diterima || 0, dikerjakan: grouped?.dikerjakan || 0, ditolak: grouped?.ditolak || 0, selesai: grouped?.selesai || 0, semua: total, }; }, { detail: { summary: "Jumlah Pengaduan Warga", description: `tool untuk mendapatkan jumlah pengaduan warga`, } }) .get("/image", async ({ query, set }) => { const { fileName, folder } = query; const hasil = await catFile(defaultConfigSF, folder, fileName); const ext = fileName.split(".").pop()?.toLowerCase(); let mime = "application/octet-stream"; // default if (["jpg", "jpeg"].includes(ext!)) mime = "image/jpeg"; if (["png"].includes(ext!)) mime = "image/png"; if (["gif"].includes(ext!)) mime = "image/gif"; if (["webp"].includes(ext!)) mime = "image/webp"; if (["svg"].includes(ext!)) mime = "image/svg+xml"; if (["pdf"].includes(ext!)) mime = "application/pdf"; set.headers["Content-Type"] = mime; set.headers["Content-Length"] = hasil.byteLength.toString(); return new Response(hasil); }, { query: t.Object({ fileName: t.String(), folder: t.String() }), detail: { summary: "View Gambar", description: "tool untuk mendapatkan gambar", } }) .post("/delete-image", async ({ body }) => { const { file, folder } = body; // Validasi file if (!file) { return { success: false, message: "File tidak ditemukan" }; } const result = await removeFile(defaultConfigSF, file, folder); if (result == 'gagal') { return { success: false, message: "Delete gagal" }; } return { success: true, message: "Delete berhasil", }; }, { body: t.Object({ file: t.String(), folder: t.String(), }), detail: { summary: "Delete File", description: "Tool untuk delete file Seafile", }, }) ; export default PengaduanRoute