diff --git a/package.json b/package.json index 68b6a86..3486826 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sistem-desa-mandiri", - "version": "0.1.16", + "version": "0.1.17", "private": true, "scripts": { "dev": "next dev --experimental-https", diff --git a/prisma/migrations/20260522064632_auto/migration.sql b/prisma/migrations/20260522064632_auto/migration.sql new file mode 100644 index 0000000..af5102c --- /dev/null +++ b/prisma/migrations/20260522064632_auto/migration.sql @@ -0,0 +1 @@ +-- This is an empty migration. \ No newline at end of file diff --git a/src/app/api/monitoring/[[...slug]]/route.ts b/src/app/api/monitoring/[[...slug]]/route.ts index d606510..f8785a6 100644 --- a/src/app/api/monitoring/[[...slug]]/route.ts +++ b/src/app/api/monitoring/[[...slug]]/route.ts @@ -117,50 +117,32 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) ) .get("/daily-activity", async ({ query, set }) => { try { - // const data = await prisma.userLog.findMany({ - // where: { - // User: { - // Village: { - // isDummy: false - // } - // }, - // createdAt: { - // gte: moment().subtract(7, 'days').toDate(), - // lte: moment().toDate(), - // } - // }, - // select: { - // createdAt: true, - // } - // }) + const VALID_RANGES = [7, 30, 90]; + const range = VALID_RANGES.includes(Number(query.range)) ? Number(query.range) : 7; const data = await prisma.$queryRaw` - SELECT + SELECT DATE(ul."createdAt") AS tanggal, COUNT(*) AS total FROM "UserLog" ul JOIN "User" u ON ul."idUser" = u."id" JOIN "Village" v ON u."idVillage" = v."id" WHERE v."isDummy" = false - AND ul."createdAt" >= NOW() - INTERVAL '7 days' + AND ul."createdAt" >= NOW() - (${range} * INTERVAL '1 day') GROUP BY tanggal ORDER BY tanggal;` as any[]; const result = []; - // ubah data ke map biar gampang lookup const map = data.reduce((acc: any, item: any) => { const key = moment(item.tanggal).format('YYYY-MM-DD'); acc[key] = Number(item.total); return acc; }, {}); - // generate 7 hari terakhir - for (let i = 6; i >= 0; i--) { + for (let i = range - 1; i >= 0; i--) { const date = moment().subtract(i, 'days'); - const key = date.format('YYYY-MM-DD'); - result.push({ date: date.format('DD MMM'), logs: map[key] || 0 @@ -183,45 +165,39 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) } }, { + query: t.Object({ + range: t.Optional(t.String({ description: "Rentang hari: 7, 30, atau 90 (default: 7)" })), + }), detail: { summary: "Daily Activity", - description: "Menu Overview - Mendapatkan data grafik aktivitas harian semua desa.", + description: "Menu Overview - Mendapatkan data grafik aktivitas harian semua desa. Gunakan ?range=30 atau ?range=90 untuk rentang lebih panjang.", tags: ["overview"], }, } ) .get("/comparison-activity", async ({ query, set }) => { try { - const villages = await prisma.village.findMany({ - where: { isDummy: false }, - select: { name: true }, - }); + const VALID_RANGES = [7, 30, 90]; + const range = VALID_RANGES.includes(Number(query.range)) ? Number(query.range) : 7; const data = await prisma.$queryRaw` - SELECT + SELECT v."name", COUNT(ul."id") AS total_logs FROM "UserLog" ul JOIN "User" u ON ul."idUser" = u."id" JOIN "Village" v ON u."idVillage" = v."id" WHERE v."isDummy" = false - AND ul."createdAt" >= NOW() - INTERVAL '7 days' + AND ul."createdAt" >= NOW() - (${range} * INTERVAL '1 day') GROUP BY v."id", v."name" ORDER BY total_logs DESC; ` as any[]; - const logMap: Record = {}; - - data.forEach((item) => { - logMap[item.name] = Number(item.total_logs); - }); - - const result = villages.map((v) => ({ - village: v.name, - activity: logMap[v.name] || 0, + const result = data.map((item: any) => ({ + village: item.name, + activity: Number(item.total_logs), })); - return { success: true, message: "Berhasil mendapatkan data", @@ -238,9 +214,12 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) } }, { + query: t.Object({ + range: t.Optional(t.String({ description: "Rentang hari: 7, 30, atau 90 (default: 7)" })), + }), detail: { summary: "Comparison Activity", - description: "Menu Overview - Mendapatkan data grafik perbandingan aktivitas desa selama 7 hari terakhir.", + description: "Menu Overview - Mendapatkan data grafik perbandingan aktivitas desa. Gunakan ?range=30 atau ?range=90 untuk rentang lebih panjang.", tags: ["overview"], }, } @@ -1061,37 +1040,31 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) } ) .get("/log-all-villages", async ({ query, set }) => { - const { page = 1, search } = query; + const { page = 1, search, action, idVillage, dateFrom, dateTo } = query; const pageNum = Number(page) || 1; const take = 15; const skip = (pageNum - 1) * take; + const whereClause = { + ...(action && { action: action.toUpperCase() }), + ...(idVillage && { User: { idVillage } }), + ...(dateFrom || dateTo) && { + createdAt: { + ...(dateFrom && { gte: new Date(dateFrom) }), + ...(dateTo && { lte: new Date(new Date(dateTo).setHours(23, 59, 59, 999)) }), + }, + }, + ...(search && { + OR: [ + { User: { name: { contains: search, mode: "insensitive" as const } } }, + { User: { Village: { name: { contains: search, mode: "insensitive" as const } } } }, + ], + }), + }; + try { const dataLog = await prisma.userLog.findMany({ - where: { - ...(search && { - OR: [ - { - User: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - { - User: { - Village: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - }, - ], - }), - }, + where: whereClause, select: { id: true, createdAt: true, @@ -1115,32 +1088,7 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) take, }); - const total = await prisma.userLog.count({ - where: { - ...(search && { - OR: [ - { - User: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - { - User: { - Village: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - }, - ], - }), - }, - }); + const total = await prisma.userLog.count({ where: whereClause }); const result = dataLog.map((item) => ({ id: item.id, @@ -1175,65 +1123,49 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) { query: t.Object({ page: t.Optional(t.String({ description: "Halaman" })), - search: t.Optional(t.String({ description: "Pencarian" })), + search: t.Optional(t.String({ description: "Pencarian nama user atau desa" })), + action: t.Optional(t.String({ description: "Filter jenis aksi: LOGIN | LOGOUT | CREATE | UPDATE | DELETE" })), + idVillage: t.Optional(t.String({ description: "Filter berdasarkan ID desa" })), + dateFrom: t.Optional(t.String({ description: "Tanggal mulai (ISO 8601, e.g. 2026-05-01)" })), + dateTo: t.Optional(t.String({ description: "Tanggal akhir (ISO 8601, e.g. 2026-05-31)" })), }), detail: { summary: "Log Villages", - description: - "Mendapatkan data log aktivitas desa berdasarkan halaman dan pencarian", + description: "Mendapatkan data log aktivitas desa dengan filter aksi, desa, rentang tanggal, pencarian, dan paginasi", tags: ["log-activity"], }, } ) .get("/user", async ({ query, set }) => { - const { page = 1, search } = query; + const { page = 1, search, isActive, idUserRole, idVillage, orderBy = 'createdAt', orderDir = 'desc' } = query; const pageNum = Number(page) || 1; const take = 15; const skip = (pageNum - 1) * take; + const SORTABLE_FIELDS = ['name', 'email', 'isActive', 'idUserRole', 'createdAt'] as const; + type SortableField = typeof SORTABLE_FIELDS[number]; + const safeOrderBy: SortableField = SORTABLE_FIELDS.includes(orderBy as SortableField) ? (orderBy as SortableField) : 'createdAt'; + const safeOrderDir = orderDir === 'asc' ? 'asc' : 'desc'; + + const whereClause = { + ...(isActive !== undefined && { isActive: isActive === 'true' }), + ...(idUserRole && { idUserRole }), + ...(idVillage && { idVillage }), + ...(search && { + OR: [ + { name: { contains: search, mode: "insensitive" as const } }, + { phone: { contains: search, mode: "insensitive" as const } }, + { email: { contains: search, mode: "insensitive" as const } }, + { nik: { contains: search, mode: "insensitive" as const } }, + { Village: { name: { contains: search, mode: "insensitive" as const } } }, + { idUserRole: search }, + ], + }), + }; + try { const data = await prisma.user.findMany({ - where: { - ...(search && { - OR: [ - { - name: { - contains: search, - mode: "insensitive", - }, - }, - { - phone: { - contains: search, - mode: "insensitive", - }, - }, - { - email: { - contains: search, - mode: "insensitive", - }, - }, - { - nik: { - contains: search, - mode: "insensitive", - }, - }, - { - Village: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - { - idUserRole: search, - }, - ], - }), - }, + where: whereClause, select: { id: true, name: true, @@ -1270,55 +1202,13 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) }, }, orderBy: { - createdAt: "desc", + [safeOrderBy]: safeOrderDir, }, skip, take, }); - const total = await prisma.user.count({ - where: { - ...(search && { - OR: [ - { - name: { - contains: search, - mode: "insensitive", - }, - }, - { - phone: { - contains: search, - mode: "insensitive", - }, - }, - { - email: { - contains: search, - mode: "insensitive", - }, - }, - { - nik: { - contains: search, - mode: "insensitive", - }, - }, - { - Village: { - name: { - contains: search, - mode: "insensitive", - }, - }, - }, - { - idUserRole: search, - }, - ], - }), - }, - }); + const total = await prisma.user.count({ where: whereClause }); const result = data.map((item) => ({ id: item.id, @@ -1363,12 +1253,16 @@ const MonitoringServer = new Elysia({ prefix: "/api/monitoring" }) { query: t.Object({ page: t.Optional(t.String({ description: "Halaman" })), - search: t.Optional(t.String({ description: "Pencarian" })), + search: t.Optional(t.String({ description: "Pencarian nama/NIK/email/telepon" })), + isActive: t.Optional(t.String({ description: "Filter status: 'true' atau 'false'" })), + idUserRole: t.Optional(t.String({ description: "Filter berdasarkan ID role" })), + idVillage: t.Optional(t.String({ description: "Filter berdasarkan ID desa" })), + orderBy: t.Optional(t.String({ description: "Kolom urutan: name | email | isActive | idUserRole | createdAt (default: createdAt)" })), + orderDir: t.Optional(t.String({ description: "Arah urutan: asc | desc (default: desc)" })), }), detail: { summary: "User", - description: - "Mendapatkan data user berdasarkan halaman dan pencarian", + description: "Mendapatkan data user dengan filter status, role, desa, pencarian, dan pengurutan", tags: ["user"], }, }