feat: tambah fitur export CSV untuk logs dan users
This commit is contained in:
@@ -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}`
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user