upd: role akses

This commit is contained in:
2026-04-16 09:52:17 +08:00
parent 08d67a304a
commit f446aec734
4 changed files with 80 additions and 63 deletions

View File

@@ -1,15 +1,15 @@
import {
Paper,
Stack,
Text,
Group,
ThemeIcon,
Box,
import { BarChart, LineChart } from '@mantine/charts'
import {
Badge,
Box,
Group,
Paper,
Stack,
Text,
ThemeIcon,
useMantineTheme
} from '@mantine/core'
import { LineChart, BarChart } from '@mantine/charts'
import { TbTimeline, TbChartBar, TbArrowUpRight } from 'react-icons/tb'
import { TbArrowUpRight, TbChartBar, TbTimeline } from 'react-icons/tb'
interface ChartProps {
data?: any[]
@@ -18,7 +18,7 @@ interface ChartProps {
export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) {
const theme = useMantineTheme()
return (
<Paper withBorder p="xl" radius="2xl" className="glass h-full">
<Stack gap="md" h="100%">
@@ -32,9 +32,14 @@ export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) {
<Text size="xs" c="dimmed">Trend over the last 7 days</Text>
</Box>
</Group>
<Badge variant="light" color="blue" size="sm" rightSection={<TbArrowUpRight size={12} />}>
{isLoading ? '...' : 'Live'}
</Badge>
{
isLoading && (
<Badge variant="light" color="blue" size="sm" rightSection={<TbArrowUpRight size={12} />}>
...
</Badge>
)
}
</Group>
<Box h={300} mt="lg">
@@ -91,12 +96,12 @@ export function VillageComparisonBarChart({ data = [], isLoading }: ChartProps)
tickLine="none"
gridAxis="y"
barProps={{
radius: [8, 8, 4, 4],
radius: [8, 8, 4, 4],
}}
styles={{
bar: {
fill: 'url(#barGradient)',
}
bar: {
fill: 'url(#barGradient)',
}
}}
>
<defs>

View File

@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
import { SummaryCard } from '@/frontend/components/SummaryCard'
import { useSession } from '@/frontend/hooks/useAuth'
import {
Badge,
Button,
@@ -39,6 +40,8 @@ function AppOverviewPage() {
const navigate = useNavigate()
const isDesaPlus = appId === 'desa-plus'
const [versionModalOpened, { open: openVersionModal, close: closeVersionModal }] = useDisclosure(false)
const { data: session } = useSession()
const isDeveloper = session?.user?.role === 'DEVELOPER'
// Form State
const [latestVersion, setLatestVersion] = useState('')
@@ -177,7 +180,7 @@ function AppOverviewPage() {
value={gridLoading ? '...' : (grid?.version?.mobile_latest_version || 'N/A')}
icon={TbVersions}
color="brand-blue"
onClick={openVersionModal}
onClick={isDeveloper ? openVersionModal : undefined}
>
<Group justify="space-between" mt="md">
<Stack gap={0}>
@@ -220,6 +223,7 @@ function AppOverviewPage() {
icon={TbAlertTriangle}
color="red"
isError={true}
onClick={() => navigate({ to: `/apps/${appId}/errors` })}
/>
</SimpleGrid>

View File

@@ -37,6 +37,7 @@ import {
} from 'react-icons/tb'
import useSWR from 'swr'
import { API_URLS } from '../config/api'
import { useSession } from '../hooks/useAuth'
const fetcher = (url: string) => fetch(url).then((res) => res.json())
@@ -149,6 +150,9 @@ function VillageDetailPage() {
const { appId, villageId } = useParams({ from: '/apps/$appId/villages/$villageId' })
const navigate = useNavigate()
const { data: session } = useSession()
const isDeveloper = session?.user?.role === 'DEVELOPER'
const { data: infoRes, isLoading: infoLoading, mutate } = useSWR(API_URLS.infoVillages(villageId), fetcher)
const { data: gridRes, isLoading: gridLoading } = useSWR(API_URLS.gridVillages(villageId), fetcher)
@@ -323,6 +327,7 @@ function VillageDetailPage() {
onClick={openConfirmModal}
radius="md"
loading={isUpdating}
disabled={!isDeveloper}
>
{village.isActive ? 'Deactivate' : 'Active'}
</Button>

View File

@@ -1,48 +1,47 @@
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { StatsCard } from '@/frontend/components/StatsCard'
import {
ActionIcon,
Avatar,
Badge,
Button,
Card,
Container,
Divider,
Group,
List,
Modal,
Pagination,
Paper,
PasswordInput,
Select,
SimpleGrid,
Stack,
Table,
Tabs,
Text,
TextInput,
Title,
Paper,
Tabs,
Avatar,
SimpleGrid,
ThemeIcon,
List,
Divider,
Pagination,
Modal,
Select,
PasswordInput,
Title,
} from '@mantine/core'
import { createFileRoute } from '@tanstack/react-router'
import { useState, useEffect } from 'react'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import {
TbPlus,
TbSearch,
TbPencil,
TbTrash,
TbUserCheck,
TbShieldCheck,
import { createFileRoute } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import {
TbAccessPoint,
TbCircleCheck,
TbCircleX,
TbClock,
TbApps,
TbPencil,
TbPlus,
TbSearch,
TbShieldCheck,
TbTrash,
TbUserCheck
} from 'react-icons/tb'
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { StatsCard } from '@/frontend/components/StatsCard'
import useSWR from 'swr'
import { API_URLS } from '../config/api'
import { useSession } from '../hooks/useAuth'
export const Route = createFileRoute('/users')({
component: UsersPage,
@@ -59,13 +58,13 @@ const getRoleColor = (role: string) => {
}
const roles = [
{
name: 'DEVELOPER',
{
name: 'DEVELOPER',
color: 'red',
permissions: ['Full Access', 'Error Feedback', 'Error Management', 'App Version Management', 'User Management']
},
{
name: 'ADMIN',
{
name: 'ADMIN',
color: 'orange',
permissions: ['View All Apps', 'View Logs', 'Report Errors']
},
@@ -75,6 +74,8 @@ function UsersPage() {
const [search, setSearch] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [page, setPage] = useState(1)
const { data: session } = useSession()
const isDeveloper = session?.user?.role === 'DEVELOPER'
useEffect(() => {
const timer = setTimeout(() => setDebouncedSearch(search), 300)
@@ -244,15 +245,17 @@ function UsersPage() {
setPage(1)
}}
/>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
radius="md"
onClick={openCreate}
>
Add New User
</Button>
{isDeveloper && (
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />}
radius="md"
onClick={openCreate}
>
Add New User
</Button>
)}
</Group>
<Paper withBorder radius="2xl" className="glass" p={0} style={{ overflow: 'hidden' }}>
@@ -302,12 +305,12 @@ function UsersPage() {
</Table.Td>
<Table.Td>
<Group gap="xs">
<ActionIcon variant="light" size="sm" color="blue" onClick={() => handleOpenEdit(user)}>
<TbPencil size={14} />
</ActionIcon>
<ActionIcon variant="light" size="sm" color="red" onClick={() => handleOpenDelete(user)}>
<TbTrash size={14} />
</ActionIcon>
<ActionIcon disabled={!isDeveloper} variant="light" size="sm" color="blue" onClick={() => handleOpenEdit(user)}>
<TbPencil size={14} />
</ActionIcon>
<ActionIcon disabled={!isDeveloper} variant="light" size="sm" color="red" onClick={() => handleOpenDelete(user)}>
<TbTrash size={14} />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
@@ -340,14 +343,14 @@ function UsersPage() {
<TbShieldCheck size={28} />
</ThemeIcon>
</Group>
<Stack gap={4}>
<Title order={4}>{role.name.replace('_', ' ')}</Title>
<Text size="sm" c="dimmed">Core role for secure app management.</Text>
</Stack>
<Divider />
<Text size="xs" fw={700} c="dimmed" style={{ textTransform: 'uppercase' }}>Key Permissions</Text>
<List
spacing="xs"