import Elysia, { t } from "elysia" import type { StatusPengaduan } from "generated/prisma" import { createSurat } from "../lib/create-surat" import { getLastUpdated } from "../lib/get-last-updated" import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat" import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone" import { prisma } from "../lib/prisma" const PelayananRoute = new Elysia({ prefix: "pelayanan", tags: ["pelayanan"], }) // --- KATEGORI PELAYANAN --- .get("/category", async () => { const data = await prisma.categoryPelayanan.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }) return data }, { detail: { summary: "List Kategori Pelayanan Surat", description: `tool untuk mendapatkan list kategori pelayanan surat beserta syaratnya untuk memenuhi syarat dokumen sesuai kategori yg dipilih saat melakukan pengajuan surat`, tags: ["mcp"] } }) .post("/category/create", async ({ body }) => { const { name, syaratDokumen, dataText } = body await prisma.categoryPelayanan.create({ data: { name, syaratDokumen, dataText, } }) return { success: true, message: 'kategori pelayanan surat sudah dibuat' } }, { body: t.Object({ name: t.String({ minLength: 1, error: "name harus diisi" }), syaratDokumen: t.Any(), dataText: t.Any(), }), detail: { summary: "buat kategori pelayanan surat", description: `tool untuk membuat kategori pelayanan surat` } }) .post("/category/update", async ({ body }) => { const { id, name, syaratDokumen, dataText } = body await prisma.categoryPelayanan.update({ where: { id, }, data: { name, syaratDokumen, dataText, } }) return { success: true, message: 'kategori pelayanan surat sudah diperbarui' } }, { body: t.Object({ id: t.String({ minLength: 1, error: "id harus diisi" }), name: t.String({ minLength: 1, error: "name harus diisi" }), syaratDokumen: t.Any(), dataText: t.Any(), }), detail: { summary: "update kategori pelayanan surat", description: `tool untuk update kategori pelayanan surat` } }) .post("/category/delete", async ({ body }) => { const { id } = body await prisma.categoryPelayanan.update({ where: { id, }, data: { isActive: false } }) return { success: true, message: 'kategori pelayanan surat sudah dihapus' } }, { body: t.Object({ id: t.String({ minLength: 1, error: "id harus diisi" }), }), detail: { summary: "delete kategori pelayanan surat", description: `tool untuk delete kategori pelayanan surat` } }) // --- PELAYANAN SURAT --- .get("/", async ({ query, headers }) => { // const { phone } = query const phone = headers['x-phone'] || "" const data = await prisma.pelayananAjuan.findMany({ orderBy: { createdAt: "asc" }, where: { isActive: true, Warga: { phone } }, select: { noPengajuan: true, status: true, createdAt: true, CategoryPelayanan: { select: { name: true } } } }) const dataFix = data.map((item) => { return { noPengajuan: item.noPengajuan, status: item.status, category: item.CategoryPelayanan.name, createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }), } }) return dataFix }, { // query: t.Object({ // phone: t.String({ minLength: 1, error: "phone harus diisi" }), // }), detail: { summary: "List Ajuan Pelayanan Surat by Phone", description: `tool untuk mendapatkan list ajuan pelayanan surat`, tags: ["mcp"] } }) .get("/detail", async ({ query }) => { const { id } = query const data = await prisma.pelayananAjuan.findFirst({ where: { id: id }, select: { id: true, noPengajuan: true, status: true, createdAt: true, updatedAt: true, CategoryPelayanan: { select: { name: true, dataText: true, syaratDokumen: true, } }, Warga: { select: { name: true, phone: true, _count: { select: { Pengaduan: true, PelayananAjuan: true, } } } }, } }) if (!data) { const datafix = { pengajuan: {}, history: [], warga: {}, syaratDokumen: [], dataText: [], } return datafix } const dataSurat = await prisma.suratPelayanan.findFirst({ where: { idPengajuanLayanan: data?.id, isActive: true }, select: { id: true, idCategory: true, } }) const dataSyarat = await prisma.syaratDokumenPelayanan.findMany({ where: { idPengajuanLayanan: data?.id, isActive: true }, select: { id: true, jenis: true, value: true, } }) const dataText = await prisma.dataTextPelayanan.findMany({ where: { idPengajuanLayanan: data?.id, isActive: true }, select: { id: true, value: true, jenis: true, } }) const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as { name: string; desc: string; }[]; const dataSyaratFix = dataSyarat.map((item) => { const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc return { id: item.id, jenis: desc, value: item.value, } }) const dataTextFix = dataText.map((item) => { const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis) return { id: item.id, jenis: item.jenis, value: item.value, } }) const dataHistory = await prisma.historyPelayanan.findMany({ where: { idPengajuanLayanan: data?.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 warga = { name: data?.Warga?.name, phone: data?.Warga?.phone, pengaduan: data?.Warga?._count.Pengaduan, pelayanan: data?.Warga?._count.PelayananAjuan, } const dataPengajuan = { id: data?.id, noPengajuan: data?.noPengajuan, category: data?.CategoryPelayanan.name, status: data?.status, createdAt: data?.createdAt, updatedAt: data?.updatedAt, idSurat: dataSurat?.id, } const datafix = { pengajuan: dataPengajuan, history: dataHistoryFix, warga: warga, syaratDokumen: dataSyaratFix, dataText: dataTextFix, } return datafix }, { query: t.Object({ id: t.String({ minLength: 1, error: "id harus diisi" }), }), detail: { summary: "Detail Ajuan Pelayanan Surat by ID", description: `tool untuk mendapatkan detail ajuan pelayanan surat berdasarkan id`, } }) .post("/create", async ({ body, headers }) => { const { kategoriId, dataText, syaratDokumen } = body const namaWarga = headers['x-user'] || "" const noTelepon = headers['x-phone'] || "" const noPengajuan = await generateNoPengajuanSurat() let idCategoryFix = kategoriId let idWargaFix = "" const category = await prisma.categoryPelayanan.findUnique({ where: { id: kategoriId, } }) if (!category) { const cariCategory = await prisma.categoryPelayanan.findFirst({ where: { name: kategoriId, } }) if (!cariCategory) { return { success: false, message: 'kategori pelayanan surat tidak ditemukan' } } else { idCategoryFix = cariCategory.id } } 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.pelayananAjuan.create({ data: { noPengajuan, idCategory: idCategoryFix, idWarga: idWargaFix, }, select: { id: true, } }) if (!pengaduan.id) { return { success: false, message: 'gagal membuat pengajuan surat' } } let dataInsertSyaratDokumen = [] let dataInsertDataText = [] for (const item of syaratDokumen) { dataInsertSyaratDokumen.push({ idPengajuanLayanan: pengaduan.id, idCategory: idCategoryFix, jenis: item.jenis, value: item.value, }) } for (const item of dataText) { dataInsertDataText.push({ idPengajuanLayanan: pengaduan.id, idCategory: idCategoryFix, jenis: item.jenis, value: item.value, }) } await prisma.syaratDokumenPelayanan.createMany({ data: dataInsertSyaratDokumen, }) await prisma.dataTextPelayanan.createMany({ data: dataInsertDataText, }) await prisma.historyPelayanan.create({ data: { idPengajuanLayanan: pengaduan.id, deskripsi: "Pengajuan surat dibuat", } }) return { success: true, message: 'pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini' } }, { body: t.Object({ kategoriId: t.String({ description: "ID atau nama kategori pelayanan surat yang dipilih. Jika berupa nama, sistem akan mencocokkan secara otomatis.", examples: ["skusaha"], error: "ID kategori harus diisi" }), // namaWarga: t.String({ // description: "Nama warga", // examples: ["Budi Santoso"], // error: "Nama warga harus diisi" // }), // noTelepon: t.String({ // error: "Nomor telepon harus diisi", // examples: ["08123456789", "+628123456789"], // description: "Nomor telepon warga pelapor" // }), dataText: t.Array( t.Object({ jenis: t.String({ description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.", examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"], error: "jenis harus diisi" }), value: t.String({ description: "Isi atau nilai dari jenis field terkait.", examples: ["Budi Santoso", "Laki-laki", "Denpasar, 28 Februari 1990", "Indonesia", "Islam", "Belum menikah", "Jl. Mawar No. 10", "Karyawan Swasta", "usaha makanan", "Jl. Melati No. 21"], error: "value harus diisi" }), }), { description: "Kumpulan data text dinamis sesuai kategori layanan.", examples: [ [ { jenis: "nama", value: "Budi Santoso" }, { jenis: "jenis kelamin", value: "Laki-laki" }, { jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" }, { jenis: "negara", value: "Indonesia" }, { jenis: "agama", value: "Islam" }, { jenis: "status perkawinan", value: "Belum menikah" }, { jenis: "alamat", value: "Jl. Mawar No. 10" }, { jenis: "pekerjaan", value: "Karyawan Swasta" }, { jenis: "jenis usaha", value: "usaha makanan" }, { jenis: "alamat usaha", value: "Jl. Melati No. 21" }, ] ], error: "dataText harus berupa array" } ), syaratDokumen: t.Array( t.Object({ jenis: t.String({ description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.", examples: ["ktp", "kk", "surat_pengantar_rt"], error: "jenis harus diisi" }), value: t.String({ description: "Nama file atau identifier file dokumen yang diupload.", examples: ["ktp_budi.png", "kk_budi.png"], error: "value harus diisi" }), }), { description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.", examples: [ [ { jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" }, { jenis: "ktp/kk", value: "kk_budi.png" }, { jenis: "foto lokasi", value: "foto_lokasi_budi.png" } ] ], error: "syaratDokumen harus berupa array" } ), }), detail: { summary: "Buat Pengajuan Pelayanan Surat", description: `tool untuk membuat pengajuan pelayanan surat dengan syarat dokumen serta data text sesuai kategori pelayanan surat yang dipilih`, tags: ["mcp"] } }) .post("/detail-data", async ({ body }) => { const { nomerPengajuan } = body const data = await prisma.pelayananAjuan.findFirst({ where: { noPengajuan: nomerPengajuan }, select: { id: true, noPengajuan: true, status: true, createdAt: true, updatedAt: true, CategoryPelayanan: { select: { name: true, dataText: true, syaratDokumen: true, } }, Warga: { select: { name: true, phone: true, _count: { select: { Pengaduan: true, PelayananAjuan: true, } } } }, } }) if (!data) { return { success: false, message: "Data tidak ditemukan" } } const dataSurat = await prisma.suratPelayanan.findFirst({ where: { idPengajuanLayanan: data?.id, isActive: true }, select: { id: true, idCategory: true, } }) const dataSyarat = await prisma.syaratDokumenPelayanan.findMany({ where: { idPengajuanLayanan: data?.id, isActive: true }, select: { id: true, jenis: true, value: true, } }) const dataText = await prisma.dataTextPelayanan.findMany({ where: { idPengajuanLayanan: data?.id, isActive: true }, select: { id: true, value: true, jenis: true, } }) const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as { name: string; desc: string; }[]; const dataSyaratFix = dataSyarat.map((item) => { const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc return { id: item.id, jenis: desc, value: item.value, } }) const dataTextFix = dataText.map((item) => { const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis) return { id: item.id, jenis: item.jenis, value: item.value, } }) const dataHistory = await prisma.historyPelayanan.findMany({ where: { idPengajuanLayanan: data?.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 warga = { name: data?.Warga?.name, phone: data?.Warga?.phone, pengaduan: data?.Warga?._count.Pengaduan, pelayanan: data?.Warga?._count.PelayananAjuan, } const dataPengajuan = { id: data?.id, noPengajuan: data?.noPengajuan, category: data?.CategoryPelayanan.name, status: data?.status, createdAt: data?.createdAt, updatedAt: data?.updatedAt, idSurat: dataSurat?.id, } const datafix = { pengajuan: dataPengajuan, history: dataHistoryFix, warga: warga, syaratDokumen: dataSyaratFix, dataText: dataTextFix, } return datafix }, { body: t.Object({ nomerPengajuan: t.String({ description: "Nomor pengajuan pelayanan surat yang ingin diakses.", examples: ["PS-101225-001", "PS-101225-002"], error: "Nomor pengajuan harus diisi" }) }), detail: { summary: "Detail Pengajuan Pelayanan Surat By Nomor Pengajuan", description: `tool untuk mendapatkan detail pengajuan pelayanan surat berdasarkan nomor pengajuan`, tags: ["mcp"] } }) .post("/update-status", async ({ body }) => { const { id, status, keterangan, idUser, noSurat } = body let deskripsi = "" const pengajuan = await prisma.pelayananAjuan.update({ where: { id, }, data: { status: status as StatusPengaduan, }, select: { id: true, idCategory: true, idWarga: true, } }) if (!pengajuan) { return { success: false, message: 'gagal update status pengajuan surat' } } if (status === "diterima") { deskripsi = "Pengajuan surat diterima" } else if (status === "ditolak") { deskripsi = "Pengajuan surat ditolak dengan keterangan " + keterangan } else if (status === "selesai") { deskripsi = "Pengajuan surat disetujui" } await prisma.historyPelayanan.create({ data: { idPengajuanLayanan: pengajuan.id, deskripsi: deskripsi, status: status as StatusPengaduan, idUser, keteranganAlasan: keterangan, } }) if (status === "selesai") { await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat }) } return { success: true, message: 'pengajuan surat sudah diperbarui' } }, { body: t.Object({ id: t.String({ minLength: 1, error: "id harus diisi" }), status: t.String({ minLength: 1, error: "status harus diisi" }), keterangan: t.String({ optional: true }), idUser: t.String({ optional: true }), noSurat: t.String({ optional: true }), }), detail: { summary: "Update Status Pengajuan Pelayanan Surat", description: `tool untuk update status pengajuan pelayanan surat`, } }) .post("/update", async ({ body }) => { const { nomerPengajuan, syaratDokumen, dataText } = body let dataUpdate = [] console.log(body) const pengajuan = await prisma.pelayananAjuan.findFirst({ where: { noPengajuan: nomerPengajuan, } }) if (!pengajuan) { console.log("data pengajuan surat tidak ditemukan") return { success: false, message: 'data pengajuan surat tidak ditemukan' } } if (pengajuan.status != "ditolak" && pengajuan.status != "antrian") { console.log("pengajuan surat tidak dapat diupdate karena status " + pengajuan.status) return { success: false, message: 'pengajuan surat tidak dapat diupdate karena status ' + pengajuan.status } } if (dataText && dataText.length > 0) { console.log("dataText") for (const item of dataText) { dataUpdate.push(item.jenis) const hasil = await prisma.dataTextPelayanan.findFirst({ where: { idPengajuanLayanan: pengajuan.id, jenis: item.jenis, } }) const upd = await prisma.dataTextPelayanan.upsert({ where: { id: hasil?.id }, update: { value: item.value, }, create: { value: item.value, jenis: item.jenis, idPengajuanLayanan: pengajuan.id, idCategory: pengajuan.idCategory, } }) } } if (syaratDokumen && syaratDokumen.length > 0) { console.log("syaratDokumen") for (const item of syaratDokumen) { dataUpdate.push(item.jenis) const hasil = await prisma.syaratDokumenPelayanan.findFirst({ where: { idPengajuanLayanan: pengajuan.id, jenis: item.jenis, } }) if (hasil && hasil.id) { const upd = await prisma.syaratDokumenPelayanan.upsert({ where: { id: hasil.id }, update: { value: item.value, }, create: { value: item.value, jenis: item.jenis, idPengajuanLayanan: pengajuan.id, idCategory: pengajuan.idCategory, } }) } else { const newData = await prisma.syaratDokumenPelayanan.create({ data: { value: item.value, jenis: item.jenis, idPengajuanLayanan: pengajuan.id, idCategory: pengajuan.idCategory, } }) } } } const keys = dataUpdate.join(", "); if (pengajuan.status == "ditolak") { const updStatus = await prisma.pelayananAjuan.update({ where: { id: pengajuan.id, }, data: { status: "antrian", } }) } const history = await prisma.historyPelayanan.create({ data: { idPengajuanLayanan: pengajuan.id, deskripsi: `Pengajuan surat diupdate oleh warga (data yg diupdate: ${keys})`, status: "antrian", } }) console.log("pengajuan surat sudah diperbarui") return { success: true, message: 'pengajuan surat sudah diperbarui' } }, { body: t.Object({ nomerPengajuan: t.String({ error: "nomer pengajuan harus diisi", description: "Nomer pengajuan yang ingin diupdate" }), dataText: t.Optional(t.Array( t.Object({ jenis: t.String({ description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.", examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"], error: "jenis harus diisi" }), value: t.String({ description: "Isi atau nilai dari jenis field terkait.", examples: ["Budi Santoso", "Laki-laki", "Denpasar, 28 Februari 1990", "Indonesia", "Islam", "Belum menikah", "Jl. Mawar No. 10", "Karyawan Swasta", "usaha makanan", "Jl. Melati No. 21"], error: "value harus diisi" }), }), { description: "Kumpulan data text dinamis sesuai kategori layanan.", examples: [ [ { jenis: "nama", value: "Budi Santoso" }, { jenis: "jenis kelamin", value: "Laki-laki" }, { jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" }, { jenis: "negara", value: "Indonesia" }, { jenis: "agama", value: "Islam" }, { jenis: "status perkawinan", value: "Belum menikah" }, { jenis: "alamat", value: "Jl. Mawar No. 10" }, { jenis: "pekerjaan", value: "Karyawan Swasta" }, { jenis: "jenis usaha", value: "usaha makanan" }, { jenis: "alamat usaha", value: "Jl. Melati No. 21" }, ] ], } )), syaratDokumen: t.Optional(t.Array( t.Object({ jenis: t.String({ description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.", examples: ["ktp", "kk", "surat_pengantar_rt"], error: "jenis harus diisi" }), value: t.String({ description: "Nama file atau identifier file dokumen yang diupload.", examples: ["ktp_budi.png", "kk_budi.png"], error: "value harus diisi" }), }), { description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.", examples: [ [ { jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" }, { jenis: "ktp/kk", value: "kk_budi.png" }, { jenis: "foto lokasi", value: "foto_lokasi_budi.png" } ] ], } )), }), detail: { summary: "Update Data Pengajuan Pelayanan Surat", description: `tool untuk update data pengajuan pelayanan surat`, tags: ["mcp"] } }) .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: [ { CategoryPelayanan: { name: { contains: search ?? "", mode: "insensitive" }, }, }, { noPengajuan: { contains: search ?? "", mode: "insensitive" }, }, { Warga: { phone: { contains: search ?? "", mode: "insensitive" }, }, }, { Warga: { name: { contains: search ?? "", mode: "insensitive" }, }, } ] } if (status && status !== "semua") { where = { ...where, status: status } } const totalData = await prisma.pelayananAjuan.count({ where }); const data = await prisma.pelayananAjuan.findMany({ skip, take: !take ? 10 : Number(take), orderBy: { createdAt: "desc" }, where, select: { id: true, noPengajuan: true, status: true, createdAt: true, updatedAt: true, CategoryPelayanan: { select: { name: true } }, Warga: { select: { name: true, } } } }) const dataFix = data.map((item) => { return { noPengajuan: item.noPengajuan, id: item.id, category: item.CategoryPelayanan.name, warga: item.Warga.name, status: item.status, 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 Pengajuan Pelayanan Surat Warga", description: `tool untuk mendapatkan list pengajuan pelayanan surat warga`, } }) .get("/count", async ({ query }) => { const counts = await prisma.pelayananAjuan.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.pelayananAjuan.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 Pengajuan Pelayanan Surat Warga", description: `tool untuk mendapatkan jumlah pengajuan pelayanan surat warga`, } }) export default PelayananRoute