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`}
+ }
+ onClick={handleExportCSV}
+ loading={isExporting}
+ disabled={isLoading || !logs.length}
+ >
+ Export CSV
+
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`}
- }
- size="sm"
- onClick={open}
- >
- Add User
-
+
+ }
+ onClick={handleExportCSV}
+ loading={isExporting}
+ disabled={isLoading || !users.length}
+ >
+ Export CSV
+
+ }
+ size="sm"
+ onClick={open}
+ >
+ Add User
+
+
{/* Filter */}