feat: tambah fitur export CSV untuk logs dan users

This commit is contained in:
2026-05-28 15:06:18 +08:00
parent 1f18001c86
commit 733a36bba7
3 changed files with 127 additions and 9 deletions

View File

@@ -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}`
},
}

View File

@@ -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`}
</Text>
</Stack>
<Button
variant="light"
color="teal"
size="sm"
leftSection={<TbDownload size={16} />}
onClick={handleExportCSV}
loading={isExporting}
disabled={isLoading || !logs.length}
>
Export CSV
</Button>
</Group>
<Paper withBorder p="md" className="glass">

View File

@@ -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`}
</Text>
</Stack>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
size="sm"
onClick={open}
>
Add User
</Button>
<Group gap="sm">
<Button
variant="light"
color="teal"
size="sm"
leftSection={<TbDownload size={16} />}
onClick={handleExportCSV}
loading={isExporting}
disabled={isLoading || !users.length}
>
Export CSV
</Button>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
size="sm"
onClick={open}
>
Add User
</Button>
</Group>
</Group>
{/* Filter */}