Files
sistem-desa-mandiri/src/app/api/ai/[[...slug]]/route.ts

1440 lines
58 KiB
TypeScript

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<string, number> = { 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<string, number> = { 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;