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`,
|
updateBugStatus: (id: string) => `/api/bugs/${id}/status`,
|
||||||
updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`,
|
updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`,
|
||||||
createLog: () => `/api/logs`,
|
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,
|
Anchor,
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
|
Button,
|
||||||
Code,
|
Code,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
@@ -25,6 +26,7 @@ import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
|||||||
import {
|
import {
|
||||||
TbAlertCircle,
|
TbAlertCircle,
|
||||||
TbCalendar,
|
TbCalendar,
|
||||||
|
TbDownload,
|
||||||
TbHistory,
|
TbHistory,
|
||||||
TbHome2,
|
TbHome2,
|
||||||
TbSearch,
|
TbSearch,
|
||||||
@@ -95,6 +97,41 @@ function AppLogsPage() {
|
|||||||
|
|
||||||
const isDesaPlus = appId === 'desa-plus'
|
const isDesaPlus = appId === 'desa-plus'
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)')
|
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 [dateFrom, dateTo] = dateRange
|
||||||
const apiUrl = isDesaPlus
|
const apiUrl = isDesaPlus
|
||||||
@@ -156,6 +193,17 @@ function AppLogsPage() {
|
|||||||
: `${(response?.data?.total ?? 0).toLocaleString()} events across all villages`}
|
: `${(response?.data?.total ?? 0).toLocaleString()} events across all villages`}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="teal"
|
||||||
|
size="sm"
|
||||||
|
leftSection={<TbDownload size={16} />}
|
||||||
|
onClick={handleExportCSV}
|
||||||
|
loading={isExporting}
|
||||||
|
disabled={isLoading || !logs.length}
|
||||||
|
>
|
||||||
|
Export CSV
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Paper withBorder p="md" className="glass">
|
<Paper withBorder p="md" className="glass">
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
TbCircleCheck,
|
TbCircleCheck,
|
||||||
TbCircleX,
|
TbCircleX,
|
||||||
TbClock,
|
TbClock,
|
||||||
|
TbDownload,
|
||||||
TbHome2,
|
TbHome2,
|
||||||
TbId,
|
TbId,
|
||||||
TbMail,
|
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 getRoleColor = (role: string) => {
|
||||||
const r = role.toLowerCase()
|
const r = role.toLowerCase()
|
||||||
if (r.includes('super')) return 'red'
|
if (r.includes('super')) return 'red'
|
||||||
@@ -603,15 +645,28 @@ function UsersIndexPage() {
|
|||||||
: `${totalUsers} users registered in the Desa+ system`}
|
: `${totalUsers} users registered in the Desa+ system`}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button
|
<Group gap="sm">
|
||||||
variant="gradient"
|
<Button
|
||||||
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
variant="light"
|
||||||
leftSection={<TbPlus size={18} />}
|
color="teal"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={open}
|
leftSection={<TbDownload size={16} />}
|
||||||
>
|
onClick={handleExportCSV}
|
||||||
Add User
|
loading={isExporting}
|
||||||
</Button>
|
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>
|
</Group>
|
||||||
|
|
||||||
{/* Filter */}
|
{/* Filter */}
|
||||||
|
|||||||
Reference in New Issue
Block a user