From 733a36bba75e42a117d81a8815284a2360c8b25f Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Thu, 28 May 2026 15:06:18 +0800 Subject: [PATCH] feat: tambah fitur export CSV untuk logs dan users --- src/frontend/config/api.ts | 15 ++++ src/frontend/routes/apps.$appId.logs.tsx | 48 ++++++++++++ .../routes/apps.$appId.users.index.tsx | 73 ++++++++++++++++--- 3 files changed, 127 insertions(+), 9 deletions(-) diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts index f56a9c8..f7d022f 100644 --- a/src/frontend/config/api.ts +++ b/src/frontend/config/api.ts @@ -81,4 +81,19 @@ export const API_URLS = { updateBugStatus: (id: string) => `/api/bugs/${id}/status`, updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`, createLog: () => `/api/logs`, + exportLogs: (search: string, action?: string, idVillage?: string, dateFrom?: string, dateTo?: string) => { + const params = new URLSearchParams({ search }) + if (action) params.set('action', action) + if (idVillage) params.set('idVillage', idVillage) + if (dateFrom) params.set('dateFrom', dateFrom) + if (dateTo) params.set('dateTo', dateTo) + return `${DESA_PLUS_PROXY}/api/monitoring/export-logs?${params}` + }, + exportUsers: (search: string, isActive?: string, idUserRole?: string, idVillage?: string) => { + const params = new URLSearchParams({ search }) + if (isActive) params.set('isActive', isActive) + if (idUserRole) params.set('idUserRole', idUserRole) + if (idVillage) params.set('idVillage', idVillage) + return `${DESA_PLUS_PROXY}/api/monitoring/export-users?${params}` + }, } diff --git a/src/frontend/routes/apps.$appId.logs.tsx b/src/frontend/routes/apps.$appId.logs.tsx index 3bad334..88a41d2 100644 --- a/src/frontend/routes/apps.$appId.logs.tsx +++ b/src/frontend/routes/apps.$appId.logs.tsx @@ -5,6 +5,7 @@ import { Anchor, Avatar, Badge, + Button, Code, Group, Loader, @@ -25,6 +26,7 @@ import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { TbAlertCircle, TbCalendar, + TbDownload, TbHistory, TbHome2, TbSearch, @@ -95,6 +97,41 @@ function AppLogsPage() { const isDesaPlus = appId === 'desa-plus' const isMobile = useMediaQuery('(max-width: 768px)') + const [isExporting, setIsExporting] = useState(false) + + const handleExportCSV = async () => { + setIsExporting(true) + try { + const res = await fetch(API_URLS.exportLogs( + searchQuery, + filterAction ?? undefined, + filterVillageId ?? undefined, + dateFrom ?? undefined, + dateTo ?? undefined, + )) + const json = await res.json() + if (!json.success || !json.data?.length) return + + const headers = ['Timestamp', 'User', 'Village', 'Action', 'Description'] + const rows = json.data.map((r: any) => [ + r.timestamp, + r.username, + r.village, + r.action, + `"${(r.desc ?? '').replace(/"/g, '""')}"`, + ]) + const csv = [headers.join(','), ...rows.map((r: string[]) => r.join(','))].join('\n') + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `activity-logs-${new Date().toISOString().slice(0, 10)}.csv` + a.click() + URL.revokeObjectURL(url) + } finally { + setIsExporting(false) + } + } const [dateFrom, dateTo] = dateRange const apiUrl = isDesaPlus @@ -156,6 +193,17 @@ function AppLogsPage() { : `${(response?.data?.total ?? 0).toLocaleString()} events across all villages`} + diff --git a/src/frontend/routes/apps.$appId.users.index.tsx b/src/frontend/routes/apps.$appId.users.index.tsx index 0e2c1b0..d43310f 100644 --- a/src/frontend/routes/apps.$appId.users.index.tsx +++ b/src/frontend/routes/apps.$appId.users.index.tsx @@ -35,6 +35,7 @@ import { TbCircleCheck, TbCircleX, TbClock, + TbDownload, TbHome2, TbId, TbMail, @@ -476,6 +477,47 @@ function UsersIndexPage() { } } + const [isExporting, setIsExporting] = useState(false) + + const handleExportCSV = async () => { + setIsExporting(true) + try { + const res = await fetch(API_URLS.exportUsers( + searchQuery, + filterStatusParam, + filterRole ?? undefined, + filterVillageId ?? undefined, + )) + const json = await res.json() + if (!json.success || !json.data?.length) return + + const headers = ['Name', 'NIK', 'Email', 'Phone', 'Gender', 'Role', 'Village', 'Group', 'Position', 'Status', 'Last Activity'] + const rows = json.data.map((r: any) => [ + `"${(r.name ?? '').replace(/"/g, '""')}"`, + r.nik, + r.email, + r.phone, + r.gender, + r.role, + `"${(r.village ?? '').replace(/"/g, '""')}"`, + `"${(r.group ?? '').replace(/"/g, '""')}"`, + `"${(r.position ?? '').replace(/"/g, '""')}"`, + r.status, + r.lastActivity, + ]) + const csv = [headers.join(','), ...rows.map((r: string[]) => r.join(','))].join('\n') + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `users-${new Date().toISOString().slice(0, 10)}.csv` + a.click() + URL.revokeObjectURL(url) + } finally { + setIsExporting(false) + } + } + const getRoleColor = (role: string) => { const r = role.toLowerCase() if (r.includes('super')) return 'red' @@ -603,15 +645,28 @@ function UsersIndexPage() { : `${totalUsers} users registered in the Desa+ system`} - + + + + {/* Filter */}