diff --git a/src/app/api/monitoring/[[...slug]]/route.ts b/src/app/api/monitoring/[[...slug]]/route.ts index 095a165..6b58c13 100644 --- a/src/app/api/monitoring/[[...slug]]/route.ts +++ b/src/app/api/monitoring/[[...slug]]/route.ts @@ -1871,6 +1871,80 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) detail: { summary: "Export Users CSV", tags: ["user"] }, }) + .get("/village-report", async ({ query, set }) => { + const VALID_RANGES = [7, 30, 90]; + const range = VALID_RANGES.includes(Number(query.range)) ? Number(query.range) : 7; + const doubleRange = range * 2; + + try { + const data = await prisma.$queryRaw` + SELECT + v."id", + v."name", + v."isActive", + COUNT(CASE WHEN ul."createdAt" >= NOW() - (${range} * INTERVAL '1 day') THEN 1 END)::int AS activity_count, + COUNT(CASE WHEN ul."createdAt" < NOW() - (${range} * INTERVAL '1 day') THEN 1 END)::int AS prev_activity_count, + MAX(ul."createdAt") AS last_activity, + ( + SELECT u2."name" FROM "User" u2 + WHERE u2."idVillage" = v."id" AND u2."idUserRole" = 'supadmin' + LIMIT 1 + ) AS perbekel, + ( + SELECT COUNT(*)::int FROM "User" u3 + WHERE u3."idVillage" = v."id" AND u3."isActive" = true AND u3."idUserRole" != 'developer' + ) AS active_users, + ( + SELECT COUNT(*)::int FROM "User" u4 + WHERE u4."idVillage" = v."id" AND u4."isActive" = false AND u4."idUserRole" != 'developer' + ) AS inactive_users + FROM "Village" v + LEFT JOIN "User" u ON u."idVillage" = v."id" AND u."idUserRole" != 'developer' + LEFT JOIN "UserLog" ul ON ul."idUser" = u."id" + AND ul."createdAt" >= NOW() - (${doubleRange} * INTERVAL '1 day') + WHERE v."isDummy" = false + GROUP BY v."id", v."name", v."isActive" + ORDER BY activity_count DESC, v."name" ASC + ` as any[]; + + const result = data.map((v: any) => { + const curr = Number(v.activity_count); + const prev = Number(v.prev_activity_count); + const trend = prev > 0 ? Math.round(((curr - prev) / prev) * 100) : curr > 0 ? 100 : 0; + return { + id: v.id, + name: v.name, + isActive: v.isActive, + perbekel: v.perbekel ?? '-', + activeUsers: Number(v.active_users), + inactiveUsers: Number(v.inactive_users), + activityCount: curr, + prevActivityCount: prev, + trend, + lastActivity: v.last_activity ? moment(v.last_activity).format('DD MMM YYYY HH:mm') : null, + daysSince: v.last_activity + ? Math.floor((Date.now() - new Date(v.last_activity).getTime()) / (1000 * 60 * 60 * 24)) + : null, + }; + }); + + return { + success: true, + message: "Berhasil mendapatkan data", + data: { villages: result, range, generatedAt: moment().format('DD MMM YYYY HH:mm') }, + }; + } catch (error) { + console.error("[village-report] error:", error); + set.status = 500; + return { success: false, message: "Terjadi kesalahan pada server", data: null }; + } + }, { + query: t.Object({ + range: t.Optional(t.String({ description: "Rentang hari: 7, 30, atau 90 (default: 7)" })), + }), + detail: { summary: "Village Report", description: "Data semua desa untuk keperluan laporan PDF.", tags: ["villages"] }, + }) + // ─── API KEY MANAGEMENT ────────────────────────────────────────────────── .get("/api-keys", async ({ set }) => {