688 lines
21 KiB
TypeScript
688 lines
21 KiB
TypeScript
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";
|
|
|
|
// Gabungkan semua ke dalam satu instance server yang dipasang di /api/noc
|
|
const NocServer = new Elysia({ prefix: "/api/noc" })
|
|
.use(cors({
|
|
origin: "*",
|
|
methods: ["GET", "POST", "OPTIONS"],
|
|
}))
|
|
.use(swagger({
|
|
path: "/docs", // Karena prefix instance adalah /api/noc, maka ini akan diakses di /api/noc/docs
|
|
documentation: {
|
|
info: {
|
|
title: "Sistem Desa Mandiri - NOC API",
|
|
version: "1.0.0",
|
|
description: "API Khusus untuk kebutuhan NOC (Network Operation Center) dan Monitoring Desa",
|
|
},
|
|
tags: [
|
|
{ name: "NOC", description: "Endpoint khusus monitoring" }
|
|
]
|
|
}
|
|
}))
|
|
|
|
// ── GET /api/noc/active-divisions ──────────────────────────────────────────
|
|
.get(
|
|
"/active-divisions",
|
|
async ({ query, set }) => {
|
|
const { idDesa, limit } = query;
|
|
|
|
if (!idDesa) {
|
|
set.status = 400;
|
|
return {
|
|
success: false,
|
|
message: "Parameter idDesa wajib diisi",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
const maxResults = Number(limit ?? 5);
|
|
|
|
try {
|
|
// Cek apakah desa ada
|
|
const village = await prisma.village.findUnique({
|
|
where: { id: idDesa },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (!village) {
|
|
set.status = 404;
|
|
return {
|
|
success: false,
|
|
message: "Desa tidak ditemukan",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
// Ambil semua divisi milik desa ini
|
|
const divisions = await prisma.division.findMany({
|
|
where: {
|
|
idVillage: idDesa,
|
|
isActive: true,
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
idGroup: true,
|
|
Group: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
_count: {
|
|
select: {
|
|
DivisionProject: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Hitung total kegiatan per divisi & urutkan descending, ambil top sesuai limit
|
|
const ranked = divisions
|
|
.map((d: any) => ({
|
|
id: d.id,
|
|
division: d.name,
|
|
group: d.Group.name,
|
|
totalKegiatan: d._count.DivisionProject
|
|
}))
|
|
.sort((a: any, b: any) => b.totalKegiatan - a.totalKegiatan)
|
|
.slice(0, maxResults);
|
|
|
|
return {
|
|
success: true,
|
|
message: "Berhasil mendapatkan divisi teraktif",
|
|
data: {
|
|
idDesa: village.id,
|
|
namaDesa: village.name,
|
|
divisi: ranked,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error("[NOC] active-divisions error:", error);
|
|
set.status = 500;
|
|
return {
|
|
success: false,
|
|
message: "Terjadi kesalahan pada server",
|
|
data: null,
|
|
};
|
|
}
|
|
},
|
|
{
|
|
query: t.Object({
|
|
idDesa: t.String({ description: "ID Desa yang ingin dicari" }),
|
|
limit: t.Optional(t.String({ description: "Jumlah maksimal data (default: 5)" })),
|
|
}),
|
|
detail: {
|
|
summary: "Divisi Teraktif",
|
|
description: "Menu Beranda - Mendapatkan daftar divisi teraktif berdasarkan jumlah proyek pada desa tertentu.",
|
|
tags: ["NOC"],
|
|
},
|
|
}
|
|
)
|
|
|
|
// ── GET /api/noc/latest-projects ──────────────────────────────────────────
|
|
.get(
|
|
"/latest-projects",
|
|
async ({ query, set }) => {
|
|
const { idDesa, limit } = query;
|
|
|
|
if (!idDesa) {
|
|
set.status = 400;
|
|
return {
|
|
success: false,
|
|
message: "Parameter idDesa wajib diisi",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
const maxResults = Math.min(Number(limit ?? 5), 50);
|
|
|
|
try {
|
|
// Cek apakah desa ada
|
|
const village = await prisma.village.findUnique({
|
|
where: { id: idDesa },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (!village) {
|
|
set.status = 404;
|
|
return {
|
|
success: false,
|
|
message: "Desa tidak ditemukan",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
// Ambil proyek umum terbaru dari desa ini
|
|
const projects = await prisma.project.findMany({
|
|
where: {
|
|
idVillage: idDesa,
|
|
isActive: true,
|
|
},
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
status: true,
|
|
desc: true,
|
|
updatedAt: true,
|
|
Group: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
User: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
updatedAt: "desc",
|
|
},
|
|
take: maxResults,
|
|
});
|
|
|
|
const mapped = projects.map((p: any) => ({
|
|
id: p.id,
|
|
title: p.title,
|
|
status: p.status,
|
|
desc: p.desc,
|
|
group: p.Group.name,
|
|
createdBy: p.User.name,
|
|
updatedAt: p.updatedAt,
|
|
}));
|
|
|
|
return {
|
|
success: true,
|
|
message: "Berhasil mendapatkan proyek terbaru",
|
|
data: {
|
|
idDesa: village.id,
|
|
namaDesa: village.name,
|
|
total: mapped.length,
|
|
projects: mapped,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error("[NOC] latest-projects error:", error);
|
|
set.status = 500;
|
|
return {
|
|
success: false,
|
|
message: "Terjadi kesalahan pada server",
|
|
data: null,
|
|
};
|
|
}
|
|
},
|
|
{
|
|
query: t.Object({
|
|
idDesa: t.String({ description: "ID Desa yang ingin dicari" }),
|
|
limit: t.Optional(
|
|
t.String({ description: "Jumlah maksimal proyek (default: 5, maks: 50)" })
|
|
),
|
|
}),
|
|
detail: {
|
|
summary: "Latest Projects General",
|
|
description: "Menu kinerja divisi - Mendapatkan daftar proyek umum terbaru dari berbagai grup pada desa tertentu.",
|
|
tags: ["NOC"],
|
|
},
|
|
}
|
|
)
|
|
|
|
// ── GET /api/noc/upcoming-events ───────────────────────────────────────────
|
|
.get(
|
|
"/upcoming-events",
|
|
async ({ query, set }) => {
|
|
const { idDesa, limit, filter } = query;
|
|
|
|
if (!idDesa) {
|
|
set.status = 400;
|
|
return {
|
|
success: false,
|
|
message: "Parameter idDesa wajib diisi",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
const maxResults = Math.min(Number(limit ?? 10), 50);
|
|
const today = moment().startOf("day").toDate();
|
|
|
|
try {
|
|
const village = await prisma.village.findUnique({
|
|
where: { id: idDesa },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (!village) {
|
|
set.status = 404;
|
|
return {
|
|
success: false,
|
|
message: "Desa tidak ditemukan",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
const events = await prisma.divisionCalendarReminder.findMany({
|
|
where: {
|
|
isActive: true,
|
|
dateStart: {
|
|
gte: today,
|
|
},
|
|
Division: {
|
|
idVillage: idDesa,
|
|
isActive: true,
|
|
},
|
|
DivisionCalendar: {
|
|
isActive: true,
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
idCalendar: true,
|
|
dateStart: true,
|
|
dateEnd: true,
|
|
timeStart: true,
|
|
timeEnd: true,
|
|
status: true,
|
|
Division: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
DivisionCalendar: {
|
|
select: {
|
|
title: true,
|
|
desc: true,
|
|
linkMeet: true,
|
|
repeatEventTyper: true,
|
|
User: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: [
|
|
{ dateStart: "asc" },
|
|
{ timeStart: "asc" },
|
|
],
|
|
take: maxResults,
|
|
});
|
|
|
|
const todayMoment = moment().startOf("day");
|
|
const mapper = (e: any) => ({
|
|
id: e.id,
|
|
idCalendar: e.idCalendar,
|
|
title: e.DivisionCalendar.title,
|
|
desc: e.DivisionCalendar.desc,
|
|
linkMeet: e.DivisionCalendar.linkMeet ?? null,
|
|
repeatEventTyper: e.DivisionCalendar.repeatEventTyper,
|
|
dateStart: moment(e.dateStart).format("YYYY-MM-DD"),
|
|
dateEnd: e.dateEnd
|
|
? moment(e.dateEnd).format("YYYY-MM-DD")
|
|
: null,
|
|
timeStart: moment.utc(e.timeStart).format("HH:mm"),
|
|
timeEnd: moment.utc(e.timeEnd).format("HH:mm"),
|
|
status: e.status,
|
|
createdBy: e.DivisionCalendar.User.name,
|
|
divisi: {
|
|
id: e.Division.id,
|
|
name: e.Division.name,
|
|
},
|
|
});
|
|
|
|
const todayEvents = events.filter((e: any) => moment(e.dateStart).isSame(todayMoment, 'day')).map(mapper);
|
|
const upcomingEvents = events.filter((e: any) => moment(e.dateStart).isAfter(todayMoment, 'day')).map(mapper);
|
|
|
|
let data: any = {
|
|
idDesa: village.id,
|
|
namaDesa: village.name,
|
|
};
|
|
|
|
if (filter === "today") {
|
|
data.events = todayEvents;
|
|
} else if (filter === "upcoming") {
|
|
data.events = upcomingEvents;
|
|
} else {
|
|
data.today = todayEvents;
|
|
data.upcoming = upcomingEvents;
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: "Berhasil mendapatkan events",
|
|
data: data,
|
|
};
|
|
} catch (error) {
|
|
console.error("[NOC] upcoming-events error:", error);
|
|
set.status = 500;
|
|
return {
|
|
success: false,
|
|
message: "Terjadi kesalahan pada server",
|
|
data: null,
|
|
};
|
|
}
|
|
},
|
|
{
|
|
query: t.Object({
|
|
idDesa: t.String({ description: "ID Desa yang ingin dicari" }),
|
|
limit: t.Optional(
|
|
t.String({ description: "Jumlah maksimal event (default: 10, maks: 50)" })
|
|
),
|
|
filter: t.Optional(
|
|
t.String({ description: "Filter event: 'today' atau 'upcoming'" })
|
|
),
|
|
}),
|
|
detail: {
|
|
summary: "Events (Today & Upcoming)",
|
|
description: "Menu beranda dan kinerja divisi - Mendapatkan daftar event pada hari ini dan yang akan datang untuk semua divisi pada desa tertentu.",
|
|
tags: ["NOC"],
|
|
},
|
|
}
|
|
)
|
|
|
|
// ── GET /api/noc/diagram-jumlah-document ───────────────────────────────────────────────
|
|
.get(
|
|
"/diagram-jumlah-document",
|
|
async ({ query, set }) => {
|
|
const { idDesa } = query;
|
|
|
|
if (!idDesa) {
|
|
set.status = 400;
|
|
return {
|
|
success: false,
|
|
message: "Parameter idDesa wajib diisi",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const village = await prisma.village.findUnique({
|
|
where: { id: idDesa },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (!village) {
|
|
set.status = 404;
|
|
return {
|
|
success: false,
|
|
message: "Desa tidak ditemukan",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
const documents = await prisma.divisionDocumentFolderFile.findMany({
|
|
where: {
|
|
isActive: true,
|
|
category: 'FILE',
|
|
Division: {
|
|
isActive: true,
|
|
idVillage: idDesa,
|
|
Group: {
|
|
isActive: true,
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const groupData = _.map(_.groupBy(documents, "extension"), (v: any) => ({
|
|
file: v[0].extension,
|
|
jumlah: v.length,
|
|
}))
|
|
|
|
const image = ['jpg', 'jpeg', 'png', 'heic']
|
|
|
|
|
|
let hasilImage = {
|
|
label: 'Gambar',
|
|
value: 0,
|
|
color: '#fac858'
|
|
}
|
|
|
|
let hasilFile = {
|
|
label: 'Dokumen',
|
|
value: 0,
|
|
color: '#92cc76'
|
|
}
|
|
|
|
groupData.map((v: any) => {
|
|
if (image.some((i: any) => i == v.file)) {
|
|
hasilImage = {
|
|
label: 'Gambar',
|
|
value: hasilImage.value + v.jumlah,
|
|
color: '#fac858'
|
|
}
|
|
} else {
|
|
hasilFile = {
|
|
label: 'Dokumen',
|
|
value: hasilFile.value + v.jumlah,
|
|
color: '#92cc76'
|
|
}
|
|
}
|
|
})
|
|
|
|
const allData = [hasilImage, hasilFile]
|
|
|
|
return {
|
|
success: true,
|
|
message: "Berhasil mendapatkan jumlah document",
|
|
data: allData
|
|
};
|
|
} catch (error) {
|
|
console.error("[NOC] jumlah-document error:", error);
|
|
set.status = 500;
|
|
return {
|
|
success: false,
|
|
message: "Terjadi kesalahan pada server",
|
|
data: null,
|
|
};
|
|
}
|
|
},
|
|
{
|
|
query: t.Object({
|
|
idDesa: t.String({ description: "ID Desa yang ingin dicari" }),
|
|
}),
|
|
detail: {
|
|
summary: "Diagram Jumlah Document",
|
|
description: "Menu kinerja divisi - Mendapatkan diagram jumlah document pada desa tertentu.",
|
|
tags: ["NOC"],
|
|
},
|
|
}
|
|
)
|
|
|
|
// -- GET /api/noc/diagram-progres-kegiatan
|
|
.get(
|
|
"/diagram-progres-kegiatan",
|
|
async ({ query, set }) => {
|
|
const { idDesa } = query;
|
|
|
|
if (!idDesa) {
|
|
set.status = 400;
|
|
return {
|
|
success: false,
|
|
message: "Parameter idDesa wajib diisi",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const village = await prisma.village.findUnique({
|
|
where: { id: idDesa },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (!village) {
|
|
set.status = 404;
|
|
return {
|
|
success: false,
|
|
message: "Desa tidak ditemukan",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
const data = await prisma.project.groupBy({
|
|
where: {
|
|
isActive: true,
|
|
idVillage: idDesa,
|
|
Group: {
|
|
isActive: true,
|
|
}
|
|
},
|
|
by: ["status"],
|
|
_count: true
|
|
})
|
|
|
|
const dataStatus = [{ name: 'Segera dikerjakan', status: 0, color: '#177AD5' }, { name: 'Dikerjakan', status: 1, color: '#fac858' }, { name: 'Selesai dikerjakan', status: 2, color: '#92cc76' }, { name: 'Dibatalkan', status: 3, color: '#ED6665' }]
|
|
const hasil: any[] = []
|
|
let input
|
|
for (let index = 0; index < dataStatus.length; index++) {
|
|
const cek = data.some((i: any) => i.status == dataStatus[index].status)
|
|
if (cek) {
|
|
const find = ((Number(data.find((i: any) => i.status == dataStatus[index].status)?._count) * 100) / data.reduce((n: any, { _count }: any) => n + _count, 0)).toFixed(2)
|
|
const fix = find != "100.00" ? find.substr(-2, 2) == "00" ? find.substr(0, 2) : find : "100"
|
|
input = {
|
|
text: fix + '%',
|
|
value: fix,
|
|
color: dataStatus[index].color
|
|
}
|
|
} else {
|
|
input = {
|
|
text: '0%',
|
|
value: 0,
|
|
color: dataStatus[index].color
|
|
}
|
|
}
|
|
hasil.push(input)
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: "Berhasil mendapatkan progres kegiatan",
|
|
data: hasil
|
|
};
|
|
} catch (error) {
|
|
console.error("[NOC] progres-kegiatan error:", error);
|
|
set.status = 500;
|
|
return {
|
|
success: false,
|
|
message: "Terjadi kesalahan pada server",
|
|
data: null,
|
|
};
|
|
}
|
|
},
|
|
{
|
|
query: t.Object({
|
|
idDesa: t.String({ description: "ID Desa yang ingin dicari" }),
|
|
}),
|
|
detail: {
|
|
summary: "Diagram Progres Kegiatan",
|
|
description: "Menu kinerja divisi - Mendapatkan diagram progres kegiatan pada desa tertentu.",
|
|
tags: ["NOC"],
|
|
},
|
|
}
|
|
)
|
|
|
|
// -- GET /api/noc/latest-discussion
|
|
.get(
|
|
"/latest-discussion",
|
|
async ({ query, set }) => {
|
|
const { idDesa, limit } = query;
|
|
|
|
const maxResults = Math.min(Number(limit ?? 5), 50);
|
|
|
|
if (!idDesa) {
|
|
set.status = 400;
|
|
return {
|
|
success: false,
|
|
message: "Parameter idDesa wajib diisi",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const village = await prisma.village.findUnique({
|
|
where: { id: idDesa },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (!village) {
|
|
set.status = 404;
|
|
return {
|
|
success: false,
|
|
message: "Desa tidak ditemukan",
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
const data = await prisma.discussion.findMany({
|
|
take: maxResults,
|
|
where: {
|
|
idVillage: idDesa,
|
|
isActive: true,
|
|
status: 1,
|
|
},
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
desc: true,
|
|
createdAt: true,
|
|
User: {
|
|
select: {
|
|
name: true
|
|
}
|
|
},
|
|
Group: {
|
|
select: {
|
|
name: true
|
|
}
|
|
}
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc"
|
|
}
|
|
})
|
|
|
|
const allData = data.map((v: any) => ({
|
|
..._.omit(v, ["createdAt", "User", "Group"]),
|
|
date: moment(v.createdAt).format("ll"),
|
|
user: v.User.name,
|
|
group: v.Group.name
|
|
}))
|
|
|
|
return {
|
|
success: true,
|
|
message: "Berhasil mendapatkan latest discussion",
|
|
data: allData
|
|
};
|
|
} catch (error) {
|
|
console.error("[NOC] latest-discussion error:", error);
|
|
set.status = 500;
|
|
return {
|
|
success: false,
|
|
message: "Terjadi kesalahan pada server",
|
|
data: null,
|
|
};
|
|
}
|
|
},
|
|
{
|
|
query: t.Object({
|
|
idDesa: t.String({ description: "ID Desa yang ingin dicari" }),
|
|
limit: t.Optional(t.String({ description: "Limit data" })),
|
|
}),
|
|
detail: {
|
|
summary: "Latest Discussion",
|
|
description: "Menu kinerja divisi - Mendapatkan latest discussion pada desa tertentu.",
|
|
tags: ["NOC"],
|
|
},
|
|
}
|
|
);
|
|
|
|
|
|
export const GET = NocServer.handle;
|
|
export const POST = NocServer.handle;
|