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