721 lines
20 KiB
TypeScript
721 lines
20 KiB
TypeScript
import Elysia, { t } from "elysia"
|
|
import type { StatusPengaduan } from "generated/prisma"
|
|
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 { normalizePhoneNumber } from "../lib/normalizePhone"
|
|
import { prisma } from "../lib/prisma"
|
|
import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64 } 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 }) => {
|
|
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
|
|
let imageFix = namaGambar
|
|
const noPengaduan = await generateNoPengaduan()
|
|
let idCategoryFix = kategoriId
|
|
let idWargaFix = wargaId
|
|
const category = await prisma.categoryPengaduan.findUnique({
|
|
where: {
|
|
id: kategoriId,
|
|
}
|
|
})
|
|
|
|
if (!category) {
|
|
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
|
where: {
|
|
name: kategoriId,
|
|
}
|
|
})
|
|
|
|
if (!cariCategory) {
|
|
idCategoryFix = "lainnya"
|
|
} else {
|
|
idCategoryFix = cariCategory.id
|
|
}
|
|
|
|
}
|
|
|
|
const warga = await prisma.warga.findUnique({
|
|
where: {
|
|
id: wargaId,
|
|
}
|
|
})
|
|
|
|
if (!warga) {
|
|
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
|
const cariWarga = await prisma.warga.findUnique({
|
|
where: {
|
|
phone: nomorHP,
|
|
}
|
|
})
|
|
|
|
if (!cariWarga) {
|
|
const wargaCreate = await prisma.warga.create({
|
|
data: {
|
|
name: wargaId,
|
|
phone: nomorHP,
|
|
},
|
|
select: {
|
|
id: true
|
|
}
|
|
})
|
|
idWargaFix = wargaCreate.id
|
|
} else {
|
|
idWargaFix = cariWarga.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({
|
|
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: "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"]
|
|
}
|
|
})
|
|
.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) {
|
|
throw new Error("gagal membuat 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`
|
|
}
|
|
})
|
|
.get("/detail", async ({ query }) => {
|
|
const { id } = query
|
|
const data = await prisma.pengaduan.findUnique({
|
|
where: {
|
|
id,
|
|
OR: [
|
|
{
|
|
noPengaduan: 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,
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const dataHistory = await prisma.historyPengaduan.findMany({
|
|
where: {
|
|
idPengaduan: id,
|
|
},
|
|
select: {
|
|
id: true,
|
|
deskripsi: true,
|
|
status: true,
|
|
createdAt: true,
|
|
idUser: true,
|
|
User: {
|
|
select: {
|
|
name: true,
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const dataHistoryFix = dataHistory.map((item) => {
|
|
return {
|
|
id: item.id,
|
|
deskripsi: item.deskripsi,
|
|
status: item.status,
|
|
createdAt: item.createdAt,
|
|
idUser: item.idUser,
|
|
nameUser: item.User?.name,
|
|
}
|
|
})
|
|
|
|
const datafix = {
|
|
id: data?.id,
|
|
noPengaduan: data?.noPengaduan,
|
|
title: data?.title,
|
|
detail: data?.detail,
|
|
location: data?.location,
|
|
image: data?.image,
|
|
CategoryPengaduan: data?.CategoryPengaduan.name,
|
|
idWarga: data?.idWarga,
|
|
nameWarga: data?.Warga?.name,
|
|
status: data?.status,
|
|
keterangan: data?.keterangan,
|
|
createdAt: data?.createdAt,
|
|
updatedAt: data?.updatedAt,
|
|
history: dataHistoryFix,
|
|
}
|
|
|
|
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 } = body;
|
|
|
|
// Validasi file
|
|
if (!file) {
|
|
return { success: false, message: "File tidak ditemukan" };
|
|
}
|
|
|
|
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
|
// const buffer = await file.arrayBuffer();
|
|
const result = await uploadFile(defaultConfigSF, file);
|
|
|
|
return {
|
|
success: true,
|
|
message: "Upload berhasil",
|
|
filename: file.name,
|
|
size: file.size,
|
|
seafileResult: result
|
|
};
|
|
}, {
|
|
body: t.Object({
|
|
file: t.File({ format: "binary" })
|
|
}),
|
|
detail: {
|
|
summary: "Upload File",
|
|
description: "Tool untuk upload file ke Seafile",
|
|
tags: ["mcp"],
|
|
consumes: ["multipart/form-data"]
|
|
},
|
|
})
|
|
.post("/upload-base64", async ({ body }) => {
|
|
const { data, mimetype } = body;
|
|
const ext = mimeToExtension(mimetype)
|
|
const name = `${uuidv4()}.${ext}`
|
|
|
|
// 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 });
|
|
|
|
return {
|
|
success: true,
|
|
message: "Upload berhasil",
|
|
data: {
|
|
name,
|
|
mimetype,
|
|
ext,
|
|
}
|
|
};
|
|
}, {
|
|
body: t.Object({
|
|
data: t.String(),
|
|
mimetype: 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 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.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
|
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
|
}
|
|
})
|
|
|
|
return dataFix
|
|
}, {
|
|
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 } = query
|
|
|
|
const connect = await testConnection(defaultConfigSF)
|
|
console.log({ connect })
|
|
|
|
const hasil = await catFile(defaultConfigSF, fileName)
|
|
console.log('hasilnya', hasil)
|
|
// Tentukan tipe MIME berdasarkan ekstensi
|
|
const ext = fileName.split(".").pop()?.toLowerCase();
|
|
const mime =
|
|
ext === "jpg" || ext === "jpeg"
|
|
? "image/jpeg"
|
|
: ext === "png"
|
|
? "image/png"
|
|
: "application/octet-stream";
|
|
|
|
set.headers["Content-Type"] = mime;
|
|
return new Response(hasil);
|
|
}, {
|
|
query: t.Object({
|
|
fileName: t.String(),
|
|
}),
|
|
detail: {
|
|
summary: "Gambar Pengaduan Warga",
|
|
description: `tool untuk mendapatkan gambar pengaduan warga`,
|
|
}
|
|
})
|
|
;
|
|
|
|
export default PengaduanRoute
|