import { isValidApiKey } from "@/lib/apiKey"; import { prisma } from "@/module/_global"; import cors from "@elysiajs/cors"; import { swagger } from "@elysiajs/swagger"; import Elysia, { t } from "elysia"; import _ from "lodash"; import moment from "moment"; import "moment/locale/id"; const AiServer = new Elysia({ prefix: "/api/ai" }) .use(cors({ origin: "*", methods: ["GET", "OPTIONS"], allowedHeaders: ["Content-Type", "x-api-key"], })) .use(swagger({ path: "/docs", documentation: { info: { title: "Desa Plus - Jenna Perangkat Desa API", version: "1.0.0", description: "API untuk kebutuhan integrasi Jenna Perangkat Desa — data desa, divisi, proyek, dll.", }, components: { securitySchemes: { ApiKeyAuth: { type: "apiKey", in: "header", name: "x-api-key", }, }, }, security: [{ ApiKeyAuth: [] }], }, })) .onBeforeHandle(async ({ request, set, path }) => { if (path.startsWith("/api/ai/docs")) return; const incoming = request.headers.get("x-api-key"); if (!incoming || !(await isValidApiKey(incoming))) { set.status = 401; return { success: false, message: "Unauthorized" }; } }) // ─── ANNOUNCEMENT ──────────────────────────────────────────────────────── .get("/announcement", async ({ query, set }) => { try { const { search, page, get, desa, active } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; const data = await prisma.announcement.findMany({ skip: dataSkip, take: getFix, where: { idVillage: String(desa), isActive: active === "false" ? false : true, title: { contains: search ?? "", mode: "insensitive" }, }, orderBy: { createdAt: "desc" }, }); return { success: true, message: "Berhasil mendapatkan pengumuman", data }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan pengumuman", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), search: t.Optional(t.String({ description: "Kata kunci judul" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Pengumuman", tags: ["announcement"] }, }) .get("/announcement/:id", async ({ params, set }) => { try { const { id } = params; const count = await prisma.announcement.count({ where: { id } }); if (count === 0) { set.status = 404; return { success: false, message: "Pengumuman tidak ditemukan" }; } const announcement = await prisma.announcement.findUnique({ where: { id }, select: { id: true, title: true, desc: true }, }); const announcementMember = await prisma.announcementMember.findMany({ where: { idAnnouncement: id }, select: { idGroup: true, idDivision: true, Group: { select: { name: true } }, Division: { select: { name: true } }, }, }); const member = announcementMember.map((v: any) => ({ ..._.omit(v, ["Group", "Division"]), group: v.Group.name, division: v.Division.name, })); return { success: true, message: "Berhasil mendapatkan pengumuman", data: { ...announcement, member } }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan pengumuman", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID pengumuman" }) }), detail: { summary: "Detail Pengumuman", tags: ["announcement"] }, }) // ─── BANNER ────────────────────────────────────────────────────────────── .get("/banner", async ({ query, set }) => { try { const { search, page, get, desa, active } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; const data = await prisma.bannerImage.findMany({ skip: dataSkip, take: getFix, where: { idVillage: String(desa), isActive: active === "false" ? false : true, title: { contains: search ?? "", mode: "insensitive" }, }, orderBy: { createdAt: "desc" }, }); return { success: true, message: "Berhasil mendapatkan banner", data }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan banner", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), search: t.Optional(t.String({ description: "Kata kunci judul" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Banner", tags: ["banner"] }, }) .get("/banner/:id", async ({ params, set }) => { try { const { id } = params; const data = await prisma.bannerImage.findUnique({ where: { id: String(id) } }); return { success: true, message: "Berhasil mendapatkan banner", data }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan banner", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID banner" }) }), detail: { summary: "Detail Banner", tags: ["banner"] }, }) // ─── CALENDAR ──────────────────────────────────────────────────────────── .get("/calendar", async ({ query, set }) => { try { const { division, date, desa, active, search, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = {}; const baseCalendar = { title: { contains: search ?? "", mode: "insensitive" }, isActive: active === "false" ? false : true, Division: { idVillage: String(desa) }, }; if (division) { kondisi = { idDivision: division, ...(date && { dateStart: new Date(date) }), DivisionCalendar: baseCalendar }; } else { kondisi = { ...(date && { dateStart: new Date(date) }), DivisionCalendar: baseCalendar }; } const data = await prisma.divisionCalendarReminder.findMany({ where: kondisi, skip: dataSkip, take: getFix, select: { id: true, dateStart: true, timeStart: true, timeEnd: true, createdAt: true, DivisionCalendar: { select: { isActive: true, title: true, desc: true, User: { select: { name: true } }, }, }, }, orderBy: [{ dateStart: "asc" }, { timeStart: "asc" }, { timeEnd: "asc" }], }); const result = data.map((v: any) => ({ ..._.omit(v, ["DivisionCalendar"]), title: v.DivisionCalendar.title, desc: v.DivisionCalendar.desc, createdBy: v.DivisionCalendar.User.name, isActive: v.DivisionCalendar.isActive, timeStart: moment.utc(v.timeStart).format("HH:mm"), timeEnd: moment.utc(v.timeEnd).format("HH:mm"), })); return { success: true, message: "Berhasil mendapatkan kalender", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan kalender", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), division: t.Optional(t.String({ description: "ID divisi" })), date: t.Optional(t.String({ description: "Tanggal filter (ISO)" })), search: t.Optional(t.String({ description: "Kata kunci judul" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Kalender", tags: ["calendar"] }, }) .get("/calendar/:id", async ({ params, set }) => { try { const { id } = params; const count = await prisma.divisionCalendarReminder.count({ where: { id } }); if (count === 0) { set.status = 404; return { success: false, message: "Acara tidak ditemukan" }; } const data: any = await prisma.divisionCalendarReminder.findUnique({ where: { id }, select: { id: true, timeStart: true, dateStart: true, timeEnd: true, createdAt: true, DivisionCalendar: { select: { id: true, title: true, desc: true, linkMeet: true, repeatEventTyper: true, repeatValue: true, }, }, }, }); const { DivisionCalendar, ...dataCalendar } = data; const result = { ...dataCalendar, timeStart: moment.utc(dataCalendar.timeStart).format("HH:mm"), timeEnd: moment.utc(dataCalendar.timeEnd).format("HH:mm"), title: DivisionCalendar.title, desc: DivisionCalendar.desc, linkMeet: DivisionCalendar.linkMeet, repeatEventTyper: DivisionCalendar.repeatEventTyper, repeatValue: DivisionCalendar.repeatValue, }; const memberRaw = await prisma.divisionCalendarMember.findMany({ where: { idCalendar: DivisionCalendar.id }, select: { id: true, idUser: true, User: { select: { id: true, name: true, email: true, img: true } }, }, }); const member = memberRaw.map((v: any) => ({ ..._.omit(v, ["User"]), name: v.User.name, email: v.User.email, img: v.User.img, })); return { success: true, message: "Berhasil mendapatkan kalender", data: { ...result, member } }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan kalender", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID kalender reminder" }) }), detail: { summary: "Detail Kalender", tags: ["calendar"] }, }) // ─── DISCUSSION GENERAL ────────────────────────────────────────────────── .get("/discussion-general", async ({ query, set }) => { try { const { group, desa, search, page, status, active, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = { isActive: active === "false" ? false : true, status: status === "close" ? 2 : 1, idVillage: String(desa), title: { contains: !search || search === "null" ? "" : search, mode: "insensitive" }, }; if (group && group !== "null") { kondisi.idGroup = String(group); } const data = await prisma.discussion.findMany({ skip: dataSkip, take: getFix, where: kondisi, orderBy: [{ status: "desc" }, { createdAt: "desc" }], select: { id: true, title: true, desc: true, status: true, createdAt: true, DiscussionComment: { select: { id: true } }, Group: { select: { name: true } }, }, }); const result = data.map((v: any) => ({ ..._.omit(v, ["DiscussionComment", "status", "Group"]), totalKomentar: v.DiscussionComment.length, status: v.status === 1 ? "Open" : "Close", group: v.Group.name, })); return { success: true, message: "Berhasil mendapatkan diskusi", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan diskusi", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), group: t.Optional(t.String({ description: "ID group" })), search: t.Optional(t.String({ description: "Kata kunci" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), status: t.Optional(t.String({ description: "Status: open/close" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Diskusi Umum", tags: ["discussion-general"] }, }) .get("/discussion-general/:id", async ({ params, query, set }) => { try { const { id } = params; const { cat, desa } = query; const count = await prisma.discussion.count({ where: { id, idVillage: String(desa) } }); if (count === 0) { set.status = 404; return { success: false, message: "Diskusi tidak ditemukan" }; } if (cat === "comment") { const data = await prisma.discussionComment.findMany({ where: { idDiscussion: id, isActive: true }, select: { id: true, comment: true, createdAt: true, idUser: true, User: { select: { name: true, img: true } }, }, }); const result = data.map((v: any) => ({ ..._.omit(v, ["User"]), username: v.User.name, img: v.User.img, })); return { success: true, message: "Berhasil mendapatkan diskusi", data: result }; } if (cat === "member") { const data = await prisma.discussionMember.findMany({ where: { idDiscussion: id, isActive: true }, select: { idUser: true, User: { select: { name: true, img: true } } }, }); const result = data.map((v: any) => ({ ..._.omit(v, ["User"]), name: v.User.name, img: v.User.img, })); return { success: true, message: "Berhasil mendapatkan diskusi", data: result }; } const data = await prisma.discussion.findUnique({ where: { id, idVillage: String(desa) }, select: { isActive: true, id: true, title: true, idGroup: true, desc: true, status: true, createdAt: true, Group: { select: { name: true } }, }, }); return { success: true, message: "Berhasil mendapatkan diskusi", data: { id: data?.id, isActive: data?.isActive, idGroup: data?.idGroup, group: data?.Group.name, title: data?.title, desc: data?.desc, status: data?.status === 1 ? "Open" : "Close", createdAt: data?.createdAt, }, }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan diskusi", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID diskusi umum" }) }), query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), cat: t.Optional(t.String({ description: "Kategori: comment / member" })), }), detail: { summary: "Detail Diskusi Umum", tags: ["discussion-general"] }, }) // ─── DISCUSSION (DIVISI) ───────────────────────────────────────────────── .get("/discussion", async ({ query, set }) => { try { const { division, search, page, status, active, desa, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = { isActive: active === "false" ? false : true, status: status === "close" ? 2 : 1, Division: { idVillage: String(desa) }, desc: { contains: !search || search === "null" ? "" : search, mode: "insensitive" }, }; if (division && division !== "null") { kondisi.idDivision = division; } const data = await prisma.divisionDisscussion.findMany({ skip: dataSkip, take: getFix, where: kondisi, orderBy: { createdAt: "desc" }, select: { id: true, desc: true, status: true, createdAt: true, idDivision: true, Division: { select: { name: true } }, DivisionDisscussionComment: { select: { id: true } }, }, }); const result = data.map((v: any) => ({ ..._.omit(v, ["DivisionDisscussionComment", "status", "Division"]), totalKomentar: v.DivisionDisscussionComment.length, status: v.status === 1 ? "Open" : "Close", division: v.Division.name, })); return { success: true, message: "Berhasil mendapatkan diskusi", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan diskusi", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), division: t.Optional(t.String({ description: "ID divisi" })), search: t.Optional(t.String({ description: "Kata kunci" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), status: t.Optional(t.String({ description: "Status: open/close" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Diskusi Divisi", tags: ["discussion"] }, }) .get("/discussion/:id", async ({ params, set }) => { try { const { id } = params; const count = await prisma.divisionDisscussion.count({ where: { id } }); if (count === 0) { set.status = 404; return { success: false, message: "Diskusi tidak ditemukan" }; } const data = await prisma.divisionDisscussion.findUnique({ where: { id }, select: { isActive: true, id: true, desc: true, status: true, createdAt: true, idDivision: true, Division: { select: { name: true } }, User: { select: { name: true } }, DivisionDisscussionComment: { select: { id: true, comment: true, createdAt: true, User: { select: { name: true, img: true } }, }, }, }, }); if (!data) { set.status = 404; return { success: false, message: "Diskusi tidak ditemukan" }; } const komentar = data.DivisionDisscussionComment.map((c: any) => ({ id: c.id, comment: c.comment, createdAt: c.createdAt, username: c.User.name, userimg: c.User.img, })); return { success: true, message: "Berhasil mendapatkan diskusi", data: { id: data.id, idDivision: data.idDivision, division: data.Division.name, isActive: data.isActive, desc: data.desc, status: data.status === 1 ? "Open" : "Close", createdAt: data.createdAt, createdBy: data.User.name, komentar, }, }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan diskusi", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID diskusi divisi" }) }), detail: { summary: "Detail Diskusi Divisi", tags: ["discussion"] }, }) // ─── DIVISION ──────────────────────────────────────────────────────────── .get("/division", async ({ query, set }) => { try { const { desa, group, search, page, active, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = { isActive: active === "false" ? false : true, idVillage: String(desa), name: { contains: !search || search === "null" ? "" : search, mode: "insensitive" }, }; if (group && group !== "null") kondisi.idGroup = String(group); const data = await prisma.division.findMany({ skip: dataSkip, take: getFix, where: kondisi, select: { id: true, name: true, desc: true, idGroup: true, Group: { select: { name: true } }, DivisionMember: { where: { isActive: true }, select: { idUser: true } }, }, orderBy: { createdAt: "desc" }, }); const result = data.map((v: any) => ({ ..._.omit(v, ["DivisionMember", "Group"]), group: v.Group.name, jumlahMember: v.DivisionMember.length, })); return { success: true, message: "Berhasil mendapatkan divisi", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan divisi", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), group: t.Optional(t.String({ description: "ID group" })), search: t.Optional(t.String({ description: "Kata kunci nama" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Divisi", tags: ["division"] }, }) .get("/division/report", async ({ query, set }) => { try { const { desa, group, division, "date-start": date, "date-end": dateAkhir, cat } = query; if (cat === "dokumen") { let kondisi: any = { isActive: true, category: "FILE", Division: { idVillage: String(desa) }, createdAt: { gte: new Date(String(date)), lte: new Date(String(dateAkhir)) }, }; if (group) kondisi.Division = { idGroup: String(group) }; if (division) kondisi.idDivision = String(division); const dataDokumen = await prisma.divisionDocumentFolderFile.findMany({ where: kondisi }); const image = ["jpg", "jpeg", "png", "heic"]; let gambar = 0, dokumen = 0; _.map(_.groupBy(dataDokumen, "extension"), (v: any) => { if (image.includes(v[0].extension)) gambar += v.length; else dokumen += v.length; }); return { success: true, message: "Berhasil mendapatkan data", data: { gambar, dokumen } }; } if (cat === "event") { const baseWhere = (dateFilter: any) => group ? { isActive: true, Division: { idGroup: String(group) }, DivisionCalendarReminder: { some: dateFilter } } : { isActive: true, Division: { idVillage: String(desa) }, DivisionCalendarReminder: { some: dateFilter } }; let selesaiWhere = baseWhere({ dateStart: { gte: new Date(String(date)), lte: new Date() } }); let comingWhere = baseWhere({ dateStart: { gt: new Date(), lte: new Date(String(dateAkhir)) } }); if (division) { selesaiWhere = { ...selesaiWhere, idDivision: String(division) } as any; comingWhere = { ...comingWhere, idDivision: String(division) } as any; } const [selesai, akan_datang] = await Promise.all([ prisma.divisionCalendar.count({ where: selesaiWhere }), prisma.divisionCalendar.count({ where: comingWhere }), ]); return { success: true, message: "Berhasil mendapatkan data", data: { selesai, akan_datang } }; } // default: progress let kondisiProgress: any = { isActive: true, Division: group ? { idGroup: String(group) } : { idVillage: String(desa) }, DivisionProjectTask: { some: { dateStart: { gte: new Date(String(date)) }, dateEnd: { lte: new Date(String(dateAkhir)) }, }, }, }; if (division) kondisiProgress.idDivision = String(division); const data = await prisma.divisionProject.groupBy({ where: kondisiProgress, by: ["status"], _count: true }); const dataStatus = [ { name: "Segera", status: 0 }, { name: "Dikerjakan", status: 1 }, { name: "Selesai", status: 2 }, { name: "Dibatalkan", status: 3 }, ]; const total = data.reduce((n, { _count }) => n + _count, 0); const result = dataStatus.reduce((acc: any, ds) => { const found = data.find((i: any) => i.status === ds.status); const raw = found ? ((found._count * 100) / total).toFixed(2) : "0"; const fix = raw !== "100.00" ? (raw.slice(-2) === "00" ? raw.slice(0, 2) : raw) : "100"; acc[ds.name] = fix; return acc; }, {}); return { success: true, message: "Berhasil mendapatkan data", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan data", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), group: t.Optional(t.String({ description: "ID group" })), division: t.Optional(t.String({ description: "ID divisi" })), "date-start": t.Optional(t.String({ description: "Tanggal mulai (ISO)" })), "date-end": t.Optional(t.String({ description: "Tanggal akhir (ISO)" })), cat: t.Optional(t.String({ description: "Kategori: dokumen / event / (kosong = progress)" })), }), detail: { summary: "Laporan Divisi", tags: ["division"] }, }) .get("/division/:id", async ({ params, query, set }) => { try { const { id } = params; const { desa } = query; const data = await prisma.division.findUnique({ where: { id: String(id), idVillage: String(desa) }, }); if (!data) { set.status = 404; return { success: false, message: "Divisi tidak ditemukan" }; } const memberRaw = await prisma.divisionMember.findMany({ where: { idDivision: String(id), isActive: true }, select: { id: true, isAdmin: true, idUser: true, User: { select: { name: true, img: true } }, }, orderBy: { isAdmin: "desc" }, }); const member = memberRaw.map((v: any) => ({ ..._.omit(v, ["User"]), name: v.User.name, img: v.User.img, })); return { success: true, message: "Berhasil mendapatkan divisi", data: { ...data, member } }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan divisi", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID divisi" }) }), query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), }), detail: { summary: "Detail Divisi", tags: ["division"] }, }) // ─── DOCUMENT ──────────────────────────────────────────────────────────── .get("/document", async ({ query, set }) => { try { const { division, desa, path, active, search, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; const pathFix = !path || path === "undefined" || path === "null" || path === "" ? "home" : path; let kondisi: any = { Division: { idVillage: String(desa) }, isActive: active === "false" ? false : true, path: pathFix, name: { contains: !search || search === "null" ? "" : search, mode: "insensitive" }, }; if (division && division !== "null") kondisi.idDivision = String(division); let formatDataShare: any[] = []; if (pathFix === "home") { const dataShare = await prisma.divisionDocumentShare.findMany({ where: { isActive: true, idDivision: String(division), DivisionDocumentFolderFile: { isActive: true } }, select: { DivisionDocumentFolderFile: { select: { idStorage: true, id: true, category: true, name: true, extension: true, path: true, User: { select: { name: true } }, createdAt: true, updatedAt: true, }, }, }, orderBy: { DivisionDocumentFolderFile: { createdAt: "desc" } }, }); formatDataShare = dataShare.map((v: any) => ({ idStorage: v.DivisionDocumentFolderFile.idStorage, id: v.DivisionDocumentFolderFile.id, category: v.DivisionDocumentFolderFile.category, name: v.DivisionDocumentFolderFile.name, extension: v.DivisionDocumentFolderFile.extension, path: v.DivisionDocumentFolderFile.path, createdBy: v.DivisionDocumentFolderFile.User.name, createdAt: v.DivisionDocumentFolderFile.createdAt, updatedAt: v.DivisionDocumentFolderFile.updatedAt, share: true, })); } const data = await prisma.divisionDocumentFolderFile.findMany({ skip: dataSkip, take: getFix, where: kondisi, select: { id: true, category: true, name: true, extension: true, idStorage: true, path: true, User: { select: { name: true } }, createdAt: true, updatedAt: true, }, orderBy: { createdAt: "desc" }, }); const allData = data.map((v: any) => ({ ..._.omit(v, ["User"]), createdBy: v.User.name, share: false, })); if (formatDataShare.length > 0) allData.push(...formatDataShare); return { success: true, message: "Berhasil mendapatkan item", data: _.orderBy(allData, ["category", "createdAt"], ["desc", "desc"]), }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan item", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), division: t.Optional(t.String({ description: "ID divisi" })), path: t.Optional(t.String({ description: "Path folder (default: home)" })), search: t.Optional(t.String({ description: "Kata kunci nama file" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Dokumen", tags: ["document"] }, }) // ─── GROUP ─────────────────────────────────────────────────────────────── .get("/group", async ({ query, set }) => { try { const { desa, active, search, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; const data = await prisma.group.findMany({ skip: dataSkip, take: getFix, where: { isActive: active === "false" ? false : true, idVillage: String(desa), name: { contains: search ?? "", mode: "insensitive" }, }, orderBy: { name: "asc" }, }); return { success: true, message: "Berhasil mendapatkan grup", data }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan grup", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), search: t.Optional(t.String({ description: "Kata kunci nama" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Group", tags: ["group"] }, }) // ─── POSITION ──────────────────────────────────────────────────────────── .get("/position", async ({ query, set }) => { try { const { desa, group, active, search, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = { isActive: active === "false" ? false : true, Group: { idVillage: String(desa) }, name: { contains: search ?? "", mode: "insensitive" }, }; if (group && group !== "null") kondisi.idGroup = String(group); const positions = await prisma.position.findMany({ skip: dataSkip, take: getFix, where: kondisi, select: { id: true, name: true, idGroup: true, isActive: true, createdAt: true, updatedAt: true, Group: { select: { name: true } }, }, orderBy: { name: "asc" }, }); const result = positions.map((v: any) => ({ ..._.omit(v, ["Group"]), group: v.Group.name, })); return { success: true, message: "Berhasil mendapatkan jabatan", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan jabatan", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), group: t.Optional(t.String({ description: "ID group" })), search: t.Optional(t.String({ description: "Kata kunci nama" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Jabatan", tags: ["position"] }, }) // ─── PROJECT ───────────────────────────────────────────────────────────── .get("/project", async ({ query, set }) => { try { const { desa, search, status, group, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = { isActive: true, idVillage: String(desa), title: { contains: !search || search === "null" ? "" : search, mode: "insensitive" }, }; if (status && status !== "null") { const statusMap: Record = { segera: 0, dikerjakan: 1, selesai: 2, batal: 3 }; kondisi.status = statusMap[status] ?? 0; } if (group && group !== "null") kondisi.idGroup = String(group); const data = await prisma.project.findMany({ skip: dataSkip, take: getFix, where: kondisi, select: { id: true, idGroup: true, title: true, desc: true, status: true, ProjectMember: { where: { isActive: true }, select: { idUser: true } }, ProjectTask: { where: { isActive: true }, select: { title: true, status: true } }, Group: { select: { name: true } }, }, orderBy: { createdAt: "desc" }, }); const result = data.map((v: any) => ({ ..._.omit(v, ["ProjectMember", "ProjectTask", "status", "Group"]), group: v.Group.name, status: v.status === 1 ? "dikerjakan" : v.status === 2 ? "selesai" : v.status === 3 ? "batal" : "segera", progress: _.ceil((v.ProjectTask.filter((i: any) => i.status === 1).length * 100) / v.ProjectTask.length), member: v.ProjectMember.length, })); return { success: true, message: "Berhasil mendapatkan kegiatan", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan kegiatan", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), group: t.Optional(t.String({ description: "ID group" })), search: t.Optional(t.String({ description: "Kata kunci judul" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), status: t.Optional(t.String({ description: "Status: segera/dikerjakan/selesai/batal" })), }), detail: { summary: "List Kegiatan", tags: ["project"] }, }) .get("/project/:id", async ({ params, query, set }) => { try { const { id } = params; const { cat } = query; const data = await prisma.project.findUnique({ where: { id: String(id) }, select: { id: true, idVillage: true, idGroup: true, title: true, status: true, desc: true, reason: true, report: true, isActive: true, Group: { select: { name: true } }, }, }); if (!data) { set.status = 404; return { success: false, message: "Kegiatan tidak ditemukan" }; } if (cat === "data") { const tasks = await prisma.projectTask.findMany({ where: { isActive: true, idProject: String(id) }, orderBy: { updatedAt: "desc" } }); const selesai = _.filter(tasks, { status: 1 }).length; const progress = Math.ceil((selesai / tasks.length) * 100); return { success: true, message: "Berhasil mendapatkan kegiatan", data: { id: data.id, idVillage: data.idVillage, idGroup: data.idGroup, group: data.Group.name, title: data.title, status: data.status === 3 ? "batal" : data.status === 2 ? "selesai" : data.status === 1 ? "dikerjakan" : "segera", desc: data.desc, reason: data.reason, report: data.report, isActive: data.isActive, progress: _.isNaN(progress) ? 0 : progress, }, }; } if (cat === "task") { const tasks = await prisma.projectTask.findMany({ where: { isActive: true, idProject: String(id) }, select: { id: true, title: true, desc: true, status: true, dateStart: true, dateEnd: true, createdAt: true }, orderBy: { createdAt: "asc" }, }); return { success: true, message: "Berhasil mendapatkan kegiatan", data: _.orderBy(tasks.map((v: any) => ({ ..._.omit(v, ["dateStart", "dateEnd", "createdAt", "status"]), status: v.status === 1 ? "selesai" : "belum selesai", dateStart: moment(v.dateStart).format("DD-MM-YYYY"), dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"), createdAt: moment(v.createdAt).format("DD-MM-YYYY HH:mm"), })), "createdAt", "asc"), }; } if (cat === "file") { const files = await prisma.projectFile.findMany({ where: { isActive: true, idProject: String(id) }, orderBy: { createdAt: "asc" }, select: { id: true, name: true, extension: true, idStorage: true }, }); return { success: true, message: "Berhasil mendapatkan kegiatan", data: files }; } if (cat === "member") { const members = await prisma.projectMember.findMany({ where: { isActive: true, idProject: String(id) }, select: { id: true, idUser: true, User: { select: { name: true, email: true, img: true, Position: { select: { name: true } } } } }, }); return { success: true, message: "Berhasil mendapatkan kegiatan", data: members.map((v: any) => ({ ..._.omit(v, ["User"]), name: v.User.name, email: v.User.email, img: v.User.img, position: v.User.Position.name, })), }; } if (cat === "link") { const links = await prisma.projectLink.findMany({ where: { isActive: true, idProject: String(id) }, orderBy: { createdAt: "asc" } }); return { success: true, message: "Berhasil mendapatkan kegiatan", data: links }; } return { success: true, message: "Berhasil mendapatkan kegiatan", data: null }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan kegiatan", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID kegiatan" }) }), query: t.Object({ cat: t.Optional(t.String({ description: "Kategori: data / task / file / member / link" })), }), detail: { summary: "Detail Kegiatan", tags: ["project"] }, }) // ─── TASK (DIVISI) ─────────────────────────────────────────────────────── .get("/task", async ({ query, set }) => { try { const { desa, division, search, status, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = { isActive: true, Division: { idVillage: String(desa) }, title: { contains: !search || search === "null" ? "" : search, mode: "insensitive" }, }; if (status && status !== "null") { const statusMap: Record = { segera: 0, dikerjakan: 1, selesai: 2, batal: 3 }; kondisi.status = statusMap[status] ?? 0; } if (division && division !== "null") kondisi.idDivision = String(division); const data = await prisma.divisionProject.findMany({ skip: dataSkip, take: getFix, where: kondisi, select: { id: true, idDivision: true, title: true, desc: true, status: true, DivisionProjectTask: { where: { isActive: true }, select: { title: true, status: true } }, DivisionProjectMember: { where: { isActive: true }, select: { idUser: true } }, Division: { select: { name: true } }, }, orderBy: { createdAt: "desc" }, }); const result = data.map((v: any) => ({ ..._.omit(v, ["DivisionProjectTask", "DivisionProjectMember", "status", "Division"]), division: v.Division.name, status: v.status === 1 ? "dikerjakan" : v.status === 2 ? "selesai" : v.status === 3 ? "batal" : "segera", progress: _.ceil((v.DivisionProjectTask.filter((i: any) => i.status === 1).length * 100) / v.DivisionProjectTask.length), member: v.DivisionProjectMember.length, })); return { success: true, message: "Berhasil mendapatkan tugas divisi", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan tugas divisi", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), division: t.Optional(t.String({ description: "ID divisi" })), search: t.Optional(t.String({ description: "Kata kunci judul" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), status: t.Optional(t.String({ description: "Status: segera/dikerjakan/selesai/batal" })), }), detail: { summary: "List Tugas Divisi", tags: ["task"] }, }) .get("/task/:id", async ({ params, query, set }) => { try { const { id } = params; const { cat } = query; const data = await prisma.divisionProject.findUnique({ where: { id: String(id) }, select: { id: true, idDivision: true, title: true, status: true, desc: true, reason: true, report: true, isActive: true, Division: { select: { name: true } }, }, }); if (!data) { set.status = 404; return { success: false, message: "Tugas tidak ditemukan" }; } if (cat === "data") { const tasks = await prisma.divisionProjectTask.findMany({ where: { isActive: true, idProject: String(id) }, orderBy: { updatedAt: "desc" } }); const selesai = _.filter(tasks, { status: 1 }).length; const progress = Math.ceil((selesai / tasks.length) * 100); return { success: true, message: "Berhasil mendapatkan tugas divisi", data: { id: data.id, idDivision: data.idDivision, division: data.Division.name, title: data.title, status: data.status === 3 ? "batal" : data.status === 2 ? "selesai" : data.status === 1 ? "dikerjakan" : "segera", desc: data.desc, reason: data.reason, report: data.report, isActive: data.isActive, progress, }, }; } if (cat === "task") { const tasks = await prisma.divisionProjectTask.findMany({ where: { isActive: true, idProject: String(id) }, select: { id: true, title: true, status: true, dateStart: true, dateEnd: true }, orderBy: { createdAt: "asc" }, }); return { success: true, message: "Berhasil mendapatkan tugas divisi", data: tasks.map((v: any) => ({ ..._.omit(v, ["dateStart", "dateEnd", "status"]), status: v.status === 1 ? "selesai" : "belum selesai", dateStart: moment(v.dateStart).format("DD-MM-YYYY"), dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"), })), }; } if (cat === "file") { const files = await prisma.divisionProjectFile.findMany({ where: { isActive: true, idProject: String(id) }, select: { id: true, ContainerFileDivision: { select: { id: true, name: true, extension: true, idStorage: true } } }, }); return { success: true, message: "Berhasil mendapatkan tugas divisi", data: files.map((v: any) => ({ ..._.omit(v, ["ContainerFileDivision"]), nameInStorage: v.ContainerFileDivision.id, name: v.ContainerFileDivision.name, extension: v.ContainerFileDivision.extension, idStorage: v.ContainerFileDivision.idStorage, })), }; } if (cat === "member") { const members = await prisma.divisionProjectMember.findMany({ where: { isActive: true, idProject: String(id) }, select: { id: true, idUser: true, User: { select: { name: true, email: true, img: true, Position: { select: { name: true } } } } }, }); return { success: true, message: "Berhasil mendapatkan tugas divisi", data: members.map((v: any) => ({ ..._.omit(v, ["User"]), name: v.User.name, email: v.User.email, img: v.User.img, position: v.User.Position.name, })), }; } if (cat === "link") { const links = await prisma.divisionProjectLink.findMany({ where: { isActive: true, idProject: String(id) }, orderBy: { createdAt: "asc" } }); return { success: true, message: "Berhasil mendapatkan tugas divisi", data: links }; } return { success: true, message: "Berhasil mendapatkan tugas divisi", data: null }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan tugas divisi", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID tugas divisi" }) }), query: t.Object({ cat: t.Optional(t.String({ description: "Kategori: data / task / file / member / link" })), }), detail: { summary: "Detail Tugas Divisi", tags: ["task"] }, }) // ─── USER ──────────────────────────────────────────────────────────────── .get("/user", async ({ query, set }) => { try { const { search, desa, group, active, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; let kondisi: any = { isActive: active === "false" ? false : true, idVillage: String(desa), name: { contains: search ?? "", mode: "insensitive" }, NOT: { idUserRole: "developer" }, }; if (group && group !== "null") kondisi.idGroup = String(group); const users = await prisma.user.findMany({ skip: dataSkip, take: getFix, where: kondisi, select: { id: true, idUserRole: true, isActive: true, nik: true, name: true, phone: true, Position: { select: { name: true } }, Group: { select: { name: true } }, }, orderBy: { name: "asc" }, }); const result = users.map((v: any) => ({ ..._.omit(v, ["phone", "gender", "Group", "Position"]), gender: v.gender === "F" ? "Perempuan" : "Laki-Laki", phone: "+" + v.phone, group: v.Group.name, position: v?.Position?.name, })); return { success: true, message: "Berhasil mendapatkan member", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan anggota", reason: (error as Error).message }; } }, { query: t.Object({ desa: t.Optional(t.String({ description: "ID desa" })), group: t.Optional(t.String({ description: "ID group" })), search: t.Optional(t.String({ description: "Kata kunci nama" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List User", tags: ["user"] }, }) .get("/user/:id", async ({ params, set }) => { try { const { id } = params; const user = await prisma.user.findUnique({ where: { id }, select: { id: true, nik: true, name: true, phone: true, email: true, gender: true, img: true, idGroup: true, isActive: true, idPosition: true, createdAt: true, updatedAt: true, UserRole: { select: { name: true, id: true } }, Position: { select: { name: true, id: true } }, Group: { select: { name: true, id: true } }, }, }); if (!user) { set.status = 404; return { success: false, message: "User tidak ditemukan" }; } const result = _.omit( { ...user, gender: user.gender === "F" ? "Perempuan" : "Laki-Laki", phone: "+62" + user.phone, group: user.Group.name, position: user.Position?.name, idUserRole: user.UserRole.id, role: user.UserRole.name, }, ["Group", "Position", "UserRole"], ); return { success: true, message: "Berhasil mendapatkan anggota", data: result }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan anggota", reason: (error as Error).message }; } }, { params: t.Object({ id: t.String({ description: "ID user" }) }), detail: { summary: "Detail User", tags: ["user"] }, }) // ─── VILLAGE ───────────────────────────────────────────────────────────── .get("/village", async ({ query, set }) => { try { const { active, search, page, get } = query; const getFix = !get || _.isNaN(Number(get)) ? 10 : Number(get); const dataSkip = !page ? 0 : Number(page) * getFix - getFix; const data = await prisma.village.findMany({ skip: dataSkip, take: getFix, where: { isActive: active === "false" ? false : true, name: { contains: search ?? "", mode: "insensitive" }, }, select: { id: true, name: true, isActive: true, createdAt: true, updatedAt: true }, orderBy: { name: "asc" }, }); return { success: true, message: "Berhasil mendapatkan desa", data }; } catch (error) { set.status = 500; return { success: false, message: "Gagal mendapatkan desa", reason: (error as Error).message }; } }, { query: t.Object({ search: t.Optional(t.String({ description: "Kata kunci nama desa" })), page: t.Optional(t.String({ description: "Halaman" })), get: t.Optional(t.String({ description: "Jumlah data per halaman" })), active: t.Optional(t.String({ description: "Filter aktif (true/false)" })), }), detail: { summary: "List Desa", tags: ["village"] }, }); export const GET = AiServer.handle; export const OPTIONS = AiServer.handle;