feat: tambahkan village-report endpoint dengan perbandingan periode sebelumnya

- Endpoint /village-report kini menghitung activity_count periode saat ini
  dan prev_activity_count periode sebelumnya dalam satu query (doubleRange)
- Tambahkan kalkulasi trend persentase perubahan antar periode
- Sertakan data perbekel, active_users, inactive_users, lastActivity, dan daysSince
- Tambahkan endpoint /export-logs dan /export-users untuk ekspor CSV
This commit is contained in:
2026-05-28 15:39:47 +08:00
parent a0bffd53cb
commit 1e02747e22

View File

@@ -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 }) => {