feat: tambah filter inactive since di halaman user management
This commit is contained in:
@@ -33,6 +33,11 @@ export const API_URLS = {
|
||||
return `${DESA_PLUS_PROXY}/api/monitoring/log-all-villages?${params}`
|
||||
},
|
||||
getStaleVillages: (days: 7 | 14 | 30 = 7) => `${DESA_PLUS_PROXY}/api/monitoring/stale-villages?days=${days}`,
|
||||
getInactiveUsers: (days: 7 | 14 | 30 = 7, idVillage?: string, page = 1) => {
|
||||
const params = new URLSearchParams({ days: String(days), page: String(page) })
|
||||
if (idVillage) params.set('idVillage', idVillage)
|
||||
return `${DESA_PLUS_PROXY}/api/monitoring/inactive-users?${params}`
|
||||
},
|
||||
getGridOverview: () => `${DESA_PLUS_PROXY}/api/monitoring/grid-overview`,
|
||||
getDailyActivity: (range: 7 | 30 | 90 = 7) => `${DESA_PLUS_PROXY}/api/monitoring/daily-activity?range=${range}`,
|
||||
getComparisonActivity: (range: 7 | 30 | 90 = 7) => `${DESA_PLUS_PROXY}/api/monitoring/comparison-activity?range=${range}`,
|
||||
|
||||
@@ -240,6 +240,7 @@ function UsersIndexPage() {
|
||||
const [filterRole, setFilterRole] = useState<string | null>(null)
|
||||
const [filterVillageSearch, setFilterVillageSearch] = useState('')
|
||||
const [filterVillageId, setFilterVillageId] = useState<string | null>(null)
|
||||
const [filterInactiveDays, setFilterInactiveDays] = useState<string | null>(null)
|
||||
const [sortBy, setSortBy] = useState<string | null>(null)
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
@@ -254,22 +255,21 @@ function UsersIndexPage() {
|
||||
}
|
||||
|
||||
const isDesaPlus = appId === 'desa-plus'
|
||||
const isInactiveMode = !!filterInactiveDays
|
||||
|
||||
const filterStatusParam = filterStatus === 'active' ? 'true' : filterStatus === 'inactive' ? 'false' : undefined
|
||||
const apiUrl = isDesaPlus
|
||||
? API_URLS.getUsers(
|
||||
page,
|
||||
searchQuery,
|
||||
filterStatusParam,
|
||||
filterRole ?? undefined,
|
||||
filterVillageId ?? undefined,
|
||||
sortBy ?? undefined,
|
||||
sortBy ? sortDir : undefined,
|
||||
)
|
||||
? isInactiveMode
|
||||
? API_URLS.getInactiveUsers(Number(filterInactiveDays) as 7 | 14 | 30, filterVillageId ?? undefined, page)
|
||||
: API_URLS.getUsers(page, searchQuery, filterStatusParam, filterRole ?? undefined, filterVillageId ?? undefined, sortBy ?? undefined, sortBy ? sortDir : undefined)
|
||||
: null
|
||||
|
||||
const { data: response, error, isLoading, mutate } = useSWR(apiUrl, fetcher)
|
||||
const users: APIUser[] = response?.data?.user || []
|
||||
const users: APIUser[] = isInactiveMode
|
||||
? (response?.data?.users || [])
|
||||
: (response?.data?.user || [])
|
||||
const totalPages = response?.data?.totalPage ?? 0
|
||||
const totalUsers = response?.data?.total ?? 0
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch.length >= 3 || debouncedSearch.length === 0) {
|
||||
@@ -280,7 +280,7 @@ function UsersIndexPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [filterStatus, filterRole, filterVillageId])
|
||||
}, [filterStatus, filterRole, filterVillageId, filterInactiveDays])
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearch('')
|
||||
@@ -524,7 +524,6 @@ function UsersIndexPage() {
|
||||
onChange={(updates) => setForm((f) => ({ ...f, ...updates }))}
|
||||
{...sharedFormProps}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
mt="lg"
|
||||
@@ -597,7 +596,11 @@ function UsersIndexPage() {
|
||||
<Stack gap={4}>
|
||||
<Title order={3}>User Management</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{isLoading ? 'Loading users...' : `${response?.data?.total ?? 0} users registered in the Desa+ system`}
|
||||
{isLoading
|
||||
? 'Loading users...'
|
||||
: isInactiveMode
|
||||
? `${totalUsers} users with no activity in the last ${filterInactiveDays} days`
|
||||
: `${totalUsers} users registered in the Desa+ system`}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Button
|
||||
@@ -611,26 +614,33 @@ function UsersIndexPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Search / Filter */}
|
||||
{/* Filter */}
|
||||
<Paper withBorder p="md" className="glass">
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
placeholder="Search name, NIK, or email... (min. 3 characters)"
|
||||
leftSection={<TbSearch size={16} />}
|
||||
size="sm"
|
||||
rightSection={
|
||||
search ? (
|
||||
<Tooltip label="Clear search" withArrow>
|
||||
<ActionIcon variant="transparent" color="gray" onClick={handleClearSearch} size="sm">
|
||||
<TbX size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
radius="md"
|
||||
/>
|
||||
<Tooltip
|
||||
label="Search is disabled when Inactive filter is active"
|
||||
disabled={!isInactiveMode}
|
||||
withArrow
|
||||
>
|
||||
<TextInput
|
||||
placeholder="Search name, NIK, or email... (min. 3 characters)"
|
||||
leftSection={<TbSearch size={16} />}
|
||||
size="sm"
|
||||
disabled={isInactiveMode}
|
||||
rightSection={
|
||||
search ? (
|
||||
<Tooltip label="Clear search" withArrow>
|
||||
<ActionIcon variant="transparent" color="gray" onClick={handleClearSearch} size="sm">
|
||||
<TbX size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
radius="md"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Select
|
||||
size="sm"
|
||||
@@ -643,6 +653,7 @@ function UsersIndexPage() {
|
||||
onChange={setFilterStatus}
|
||||
radius="md"
|
||||
clearable
|
||||
disabled={isInactiveMode}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Select
|
||||
@@ -653,6 +664,7 @@ function UsersIndexPage() {
|
||||
onChange={setFilterRole}
|
||||
radius="md"
|
||||
clearable
|
||||
disabled={isInactiveMode}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Select
|
||||
@@ -667,6 +679,27 @@ function UsersIndexPage() {
|
||||
clearable
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Select
|
||||
size="sm"
|
||||
placeholder="Inactive since..."
|
||||
data={[
|
||||
{ value: '7', label: 'No activity 7D' },
|
||||
{ value: '14', label: 'No activity 14D' },
|
||||
{ value: '30', label: 'No activity 30D' },
|
||||
]}
|
||||
value={filterInactiveDays}
|
||||
onChange={(v) => {
|
||||
setFilterInactiveDays(v)
|
||||
setFilterStatus(null)
|
||||
setFilterRole(null)
|
||||
setSearch('')
|
||||
setSearchQuery('')
|
||||
setPage(1)
|
||||
}}
|
||||
radius="md"
|
||||
clearable
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -687,7 +720,11 @@ function UsersIndexPage() {
|
||||
<Stack align="center" gap="xs" py="xl">
|
||||
<TbUsers size={32} style={{ opacity: 0.25 }} />
|
||||
<Text size="sm" c="dimmed">
|
||||
{searchQuery || filterStatus || filterRole || filterVillageId ? 'No users match your filters.' : 'No users found.'}
|
||||
{isInactiveMode
|
||||
? `No users with ${filterInactiveDays}+ days of inactivity.`
|
||||
: searchQuery || filterStatus || filterRole || filterVillageId
|
||||
? 'No users match your filters.'
|
||||
: 'No users found.'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -718,12 +755,12 @@ function UsersIndexPage() {
|
||||
].map(({ label, col, width }) => (
|
||||
<Table.Th
|
||||
key={label}
|
||||
style={{ width: isMobile ? undefined : width, cursor: col ? 'pointer' : undefined, userSelect: 'none' }}
|
||||
onClick={col ? () => handleSort(col) : undefined}
|
||||
style={{ width: isMobile ? undefined : width, cursor: col && !isInactiveMode ? 'pointer' : undefined, userSelect: 'none' }}
|
||||
onClick={col && !isInactiveMode ? () => handleSort(col) : undefined}
|
||||
>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<span>{label}</span>
|
||||
{col && (
|
||||
{col && !isInactiveMode && (
|
||||
sortBy === col
|
||||
? sortDir === 'asc'
|
||||
? <TbArrowUp size={13} />
|
||||
@@ -744,13 +781,7 @@ function UsersIndexPage() {
|
||||
>
|
||||
<Table.Td>
|
||||
<Group gap="md" wrap="nowrap">
|
||||
<Avatar
|
||||
size="lg"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color={getRoleColor(user.role)}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Avatar size="lg" radius="md" variant="light" color={getRoleColor(user.role)} style={{ flexShrink: 0 }}>
|
||||
{user.name.charAt(0)}
|
||||
</Avatar>
|
||||
<Stack gap={2} style={{ overflow: 'hidden' }}>
|
||||
@@ -847,12 +878,12 @@ function UsersIndexPage() {
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && response?.data?.totalPage > 1 && (
|
||||
{!isLoading && !error && totalPages > 1 && (
|
||||
<Group justify="center">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
total={response.data.totalPage}
|
||||
total={totalPages}
|
||||
size="sm"
|
||||
radius="md"
|
||||
withEdges={false}
|
||||
|
||||
Reference in New Issue
Block a user