Merge pull request 'amalia/02-apr-26' (#2) from amalia/02-apr-26 into main
Reviewed-on: #2
This commit is contained in:
@@ -4,38 +4,38 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|||||||
import { createRouter, RouterProvider } from '@tanstack/react-router'
|
import { createRouter, RouterProvider } from '@tanstack/react-router'
|
||||||
import { routeTree } from './routeTree.gen'
|
import { routeTree } from './routeTree.gen'
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
primaryColor: 'brand-blue',
|
primaryColor: 'brand-blue',
|
||||||
colors: {
|
colors: {
|
||||||
'brand-blue': [
|
'brand-blue': [
|
||||||
'#ebf2ff',
|
'#f0f9ff',
|
||||||
'#d6e4ff',
|
'#e0f2fe',
|
||||||
'#adc8ff',
|
'#bae6fd',
|
||||||
'#85acff',
|
'#7dd3fc',
|
||||||
'#5c90ff',
|
'#38bdf8',
|
||||||
'#2563eb', // Primary Blue
|
'#0ea5e9', // Primary Blue (Sky)
|
||||||
'#1e4fb8',
|
'#0284c7',
|
||||||
'#173b85',
|
'#0369a1',
|
||||||
'#102752',
|
'#075985',
|
||||||
'#09131f',
|
'#0c4a6e',
|
||||||
],
|
],
|
||||||
'brand-purple': [
|
'brand-purple': [
|
||||||
'#f3ebff',
|
'#faf5ff',
|
||||||
'#e7d6ff',
|
'#f3e8ff',
|
||||||
'#cfadff',
|
'#e9d5ff',
|
||||||
'#b785ff',
|
'#d8b4fe',
|
||||||
'#9f5cff',
|
'#c084fc',
|
||||||
'#7c3aed', // Primary Purple
|
'#a855f7', // Primary Purple
|
||||||
'#632eb8',
|
'#9333ea',
|
||||||
'#4a2285',
|
'#7e22ce',
|
||||||
'#311652',
|
'#6b21a8',
|
||||||
'#180b1f',
|
'#581c87',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif',
|
fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif',
|
||||||
headings: {
|
headings: {
|
||||||
fontFamily: 'Inter, system-ui, sans-serif',
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
fontWeight: '600',
|
fontWeight: '500', // Softer headings
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -59,8 +59,8 @@ declare module '@tanstack/react-router' {
|
|||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ColorSchemeScript defaultColorScheme="dark" />
|
<ColorSchemeScript defaultColorScheme="auto" />
|
||||||
<MantineProvider theme={theme} defaultColorScheme="dark" forceColorScheme="dark">
|
<MantineProvider theme={theme} defaultColorScheme="auto">
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Group, Text, ThemeIcon, Badge, Avatar, Stack, Button, Progress, Box } from '@mantine/core'
|
import { Card, Group, Text, ThemeIcon, Badge, Avatar, Stack, Button, Progress, Box, useComputedColorScheme } from '@mantine/core'
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb'
|
import { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb'
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ interface AppCardProps {
|
|||||||
|
|
||||||
export function AppCard({ id, name, status, users, errors, version }: AppCardProps) {
|
export function AppCard({ id, name, status, users, errors, version }: AppCardProps) {
|
||||||
const statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red'
|
const statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red'
|
||||||
|
const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -22,12 +23,12 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
|||||||
className="premium-card glass"
|
className="premium-card glass"
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: 'rgba(30, 41, 59, 0.4)',
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
borderColor: 'rgba(255,255,255,0.08)',
|
borderColor: scheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)',
|
||||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
transition: 'transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease, border-color 0.2s ease',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transform: 'translateY(-4px)',
|
transform: 'translateY(-4px)',
|
||||||
boxShadow: '0 12px 24px -8px rgba(0, 0, 0, 0.4)',
|
boxShadow: theme.shadows.md,
|
||||||
borderColor: 'rgba(37, 99, 235, 0.3)',
|
borderColor: 'rgba(37, 99, 235, 0.3)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -45,7 +46,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Text fw={700} size="lg" style={{ letterSpacing: '-0.3px' }}>{name}</Text>
|
<Text fw={700} size="lg" style={{ letterSpacing: '-0.3px' }}>{name}</Text>
|
||||||
<Text size="xs" c="dimmed" fw={600}>BUILD v{version}</Text>
|
<Text size="xs" c="dimmed" fw={600}>VERSION {version}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
<Badge color={statusColor} variant="dot" size="sm">
|
<Badge color={statusColor} variant="dot" size="sm">
|
||||||
@@ -53,7 +54,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
|||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap="md" mt="sm">
|
{/* <Stack gap="md" mt="sm">
|
||||||
<Box>
|
<Box>
|
||||||
<Group justify="space-between" mb={6}>
|
<Group justify="space-between" mb={6}>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
@@ -69,13 +70,13 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
|||||||
<Group justify="space-between" mb={6}>
|
<Group justify="space-between" mb={6}>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<TbAlertTriangle size={16} color={errors > 0 ? '#ef4444' : '#64748b'} />
|
<TbAlertTriangle size={16} color={errors > 0 ? '#ef4444' : '#64748b'} />
|
||||||
<Text size="xs" fw={700} c="dimmed">HEALTH INCIDENTS</Text>
|
<Text size="xs" fw={700} c="dimmed">ERROR</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" fw={700} color={errors > 0 ? 'red' : 'dimmed'}>{errors}</Text>
|
<Text size="sm" fw={700} color={errors > 0 ? 'red' : 'dimmed'}>{errors}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Progress value={errors > 0 ? 30 : 0} size="sm" color="red" radius="xl" />
|
<Progress value={errors > 0 ? 30 : 0} size="sm" color="red" radius="xl" />
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack> */}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
@@ -95,7 +96,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Center Intelligence
|
View
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { APP_CONFIGS } from '@/frontend/config/appMenus'
|
import { APP_CONFIGS } from '@/frontend/config/appMenus'
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
AppShell,
|
AppShell,
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
ThemeIcon
|
ThemeIcon
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { useDisclosure } from '@mantine/hooks'
|
import { useDisclosure } from '@mantine/hooks'
|
||||||
|
import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core'
|
||||||
import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router'
|
import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
TbApps,
|
TbApps,
|
||||||
@@ -23,7 +25,11 @@ import {
|
|||||||
TbDeviceMobile,
|
TbDeviceMobile,
|
||||||
TbLogout,
|
TbLogout,
|
||||||
TbSettings,
|
TbSettings,
|
||||||
TbUserCircle
|
TbUserCircle,
|
||||||
|
TbSun,
|
||||||
|
TbMoon,
|
||||||
|
TbUser,
|
||||||
|
TbHistory
|
||||||
} from 'react-icons/tb'
|
} from 'react-icons/tb'
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
@@ -31,7 +37,10 @@ interface DashboardLayoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
const [opened, { toggle }] = useDisclosure()
|
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
|
||||||
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
|
||||||
|
const { toggleColorScheme } = useMantineColorScheme()
|
||||||
|
const computedColorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { appId } = useParams({ strict: false }) as { appId?: string }
|
const { appId } = useParams({ strict: false }) as { appId?: string }
|
||||||
@@ -42,7 +51,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
const globalNav = [
|
const globalNav = [
|
||||||
{ label: 'Dashboard', icon: TbDashboard, to: '/dashboard' },
|
{ label: 'Dashboard', icon: TbDashboard, to: '/dashboard' },
|
||||||
{ label: 'Applications', icon: TbApps, to: '/apps' },
|
{ label: 'Applications', icon: TbApps, to: '/apps' },
|
||||||
{ label: 'Settings', icon: TbSettings, to: '/settings' },
|
{ label: 'Log Activity', icon: TbHistory, to: '/logs' },
|
||||||
|
{ label: 'Users', icon: TbUser, to: '/users' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const activeApp = appId ? APP_CONFIGS[appId] : null
|
const activeApp = appId ? APP_CONFIGS[appId] : null
|
||||||
@@ -54,19 +64,21 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 260,
|
width: 260,
|
||||||
breakpoint: 'sm',
|
breakpoint: 'sm',
|
||||||
collapsed: { mobile: !opened },
|
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||||
}}
|
}}
|
||||||
padding="xl"
|
padding="xl"
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
main: {
|
main: {
|
||||||
backgroundColor: theme.colors.dark[7], // Dark mode background
|
backgroundColor: computedColorScheme === 'dark' ? theme.colors.dark[9] : theme.colors.gray[0],
|
||||||
|
transition: 'background-color 0.2s ease',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AppShell.Header px="xl">
|
<AppShell.Header px="xl">
|
||||||
<Group h="100%" justify="space-between">
|
<Group h="100%" justify="space-between">
|
||||||
<Group>
|
<Group>
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||||
|
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<ThemeIcon
|
<ThemeIcon
|
||||||
size={34}
|
size={34}
|
||||||
@@ -88,6 +100,14 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => toggleColorScheme()}
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Toggle color scheme"
|
||||||
|
>
|
||||||
|
{computedColorScheme === 'dark' ? <TbSun size={18} /> : <TbMoon size={18} />}
|
||||||
|
</ActionIcon>
|
||||||
<Menu shadow="md" width={200} position="bottom-end">
|
<Menu shadow="md" width={200} position="bottom-end">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -149,7 +169,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
mb={"md"}
|
mb={"md"}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
input: { border: '1px solid rgba(255,255,255,0.1)' }
|
input: { border: computedColorScheme === 'dark' ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -198,7 +218,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
<Box
|
<Box
|
||||||
p="md"
|
p="md"
|
||||||
className="glass"
|
className="glass"
|
||||||
style={{ borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}
|
style={{ borderRadius: '12px', border: computedColorScheme === 'dark' ? '1px solid rgba(255,255,255,0.05)' : '1px solid rgba(0,0,0,0.05)' }}
|
||||||
>
|
>
|
||||||
<Text size="xs" c="dimmed" fw={600} mb="xs">SYSTEM STATUS</Text>
|
<Text size="xs" c="dimmed" fw={600} mb="xs">SYSTEM STATUS</Text>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { useDisclosure } from '@mantine/hooks'
|
import { useDisclosure } from '@mantine/hooks'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
import { TbMessageReport, TbHistory, TbExternalLink, TbBug } from 'react-icons/tb'
|
import { TbMessageReport, TbHistory, TbExternalLink, TbBug } from 'react-icons/tb'
|
||||||
|
|
||||||
const mockErrors = [
|
const mockErrors = [
|
||||||
@@ -85,7 +86,7 @@ export function ErrorDataTable() {
|
|||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Text fw={700}>LATEST ERROR REPORTS</Text>
|
<Text fw={700}>LATEST ERROR REPORTS</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Button variant="subtle" size="compact-xs" color="blue" rightSection={<TbExternalLink size={14} />}>
|
<Button component={Link} to='/apps/desa-plus/errors' variant="subtle" size="compact-xs" color="blue" rightSection={<TbExternalLink size={14} />}>
|
||||||
View All Reports
|
View All Reports
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export function StatsCard({ title, value, description, icon: Icon, color, trend
|
|||||||
className="premium-card"
|
className="premium-card"
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: theme.colors.dark[6],
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
borderColor: 'rgba(255,255,255,0.05)',
|
borderColor: 'rgba(128,128,128,0.1)',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Group, Text, ThemeIcon, Stack, Progress, Badge } from '@mantine/core'
|
import { Card, Group, Text, ThemeIcon, Stack, Progress, Badge, useComputedColorScheme } from '@mantine/core'
|
||||||
import { IconType } from 'react-icons'
|
import { IconType } from 'react-icons'
|
||||||
import { TbTrendingUp, TbTrendingDown } from 'react-icons/tb'
|
import { TbTrendingUp, TbTrendingDown } from 'react-icons/tb'
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ export function SummaryCard({
|
|||||||
progress,
|
progress,
|
||||||
isError
|
isError
|
||||||
}: SummaryCardProps) {
|
}: SummaryCardProps) {
|
||||||
|
const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
withBorder
|
withBorder
|
||||||
@@ -35,9 +37,13 @@ export function SummaryCard({
|
|||||||
className="glass"
|
className="glass"
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: isError && Number(value) > 0 ? 'rgba(239, 68, 68, 0.05)' : 'rgba(30, 41, 59, 0.4)',
|
backgroundColor: isError && Number(value) > 0
|
||||||
borderColor: isError && Number(value) > 10 ? 'rgba(239, 68, 68, 0.3)' : 'rgba(255, 255, 255, 0.08)',
|
? (scheme === 'dark' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(255, 241, 242, 1)') // light pink for error in light mode
|
||||||
transition: 'transform 0.2s ease',
|
: 'var(--mantine-color-body)',
|
||||||
|
borderColor: isError && Number(value) > 10
|
||||||
|
? 'rgba(239, 68, 68, 0.3)'
|
||||||
|
: scheme === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)',
|
||||||
|
transition: 'transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transform: 'translateY(-4px)',
|
transform: 'translateY(-4px)',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IconType } from 'react-icons'
|
import { IconType } from 'react-icons'
|
||||||
import { TbChartBar, TbHistory, TbAlertTriangle, TbSettings, TbShoppingCart, TbPackage, TbCreditCard } from 'react-icons/tb'
|
import { TbChartBar, TbHistory, TbAlertTriangle, TbSettings, TbShoppingCart, TbPackage, TbCreditCard, TbBuilding } from 'react-icons/tb'
|
||||||
|
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
value: string
|
value: string
|
||||||
@@ -22,7 +22,7 @@ export const APP_CONFIGS: Record<string, AppConfig> = {
|
|||||||
{ value: 'overview', label: 'Overview', icon: TbChartBar, to: '/apps/desa-plus' },
|
{ value: 'overview', label: 'Overview', icon: TbChartBar, to: '/apps/desa-plus' },
|
||||||
{ value: 'logs', label: 'Log Activity', icon: TbHistory, to: '/apps/desa-plus/logs' },
|
{ value: 'logs', label: 'Log Activity', icon: TbHistory, to: '/apps/desa-plus/logs' },
|
||||||
{ value: 'errors', label: 'Error Reports', icon: TbAlertTriangle, to: '/apps/desa-plus/errors' },
|
{ value: 'errors', label: 'Error Reports', icon: TbAlertTriangle, to: '/apps/desa-plus/errors' },
|
||||||
{ value: 'manage', label: 'Manage', icon: TbSettings, to: '/apps/desa-plus/manage' },
|
{ value: 'villages', label: 'Villages', icon: TbBuilding, to: '/apps/desa-plus/villages' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'e-commerce': {
|
'e-commerce': {
|
||||||
|
|||||||
@@ -1,36 +1,22 @@
|
|||||||
|
import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
|
||||||
|
import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
|
||||||
|
import { SummaryCard } from '@/frontend/components/SummaryCard'
|
||||||
import {
|
import {
|
||||||
Badge,
|
ActionIcon,
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Group,
|
Group,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Paper,
|
|
||||||
Box,
|
|
||||||
ThemeIcon,
|
|
||||||
Select,
|
|
||||||
ActionIcon,
|
|
||||||
Container,
|
|
||||||
Divider,
|
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { createFileRoute, Link, useParams } from '@tanstack/react-router'
|
import { createFileRoute, useParams } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
TbUsers,
|
|
||||||
TbActivity,
|
TbActivity,
|
||||||
TbRefresh,
|
|
||||||
TbAlertTriangle,
|
TbAlertTriangle,
|
||||||
TbCalendar,
|
|
||||||
TbFilter,
|
|
||||||
TbChevronRight,
|
|
||||||
TbArrowUpRight,
|
|
||||||
TbBuildingCommunity,
|
TbBuildingCommunity,
|
||||||
|
TbRefresh,
|
||||||
TbVersions
|
TbVersions
|
||||||
} from 'react-icons/tb'
|
} from 'react-icons/tb'
|
||||||
import { SummaryCard } from '@/frontend/components/SummaryCard'
|
|
||||||
import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
|
|
||||||
import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/apps/$appId/')({
|
export const Route = createFileRoute('/apps/$appId/')({
|
||||||
component: AppOverviewPage,
|
component: AppOverviewPage,
|
||||||
@@ -43,41 +29,36 @@ function AppOverviewPage() {
|
|||||||
return (
|
return (
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
{/* 🔝 HEADER SECTION */}
|
{/* 🔝 HEADER SECTION */}
|
||||||
<Paper withBorder p="lg" radius="2xl" className="glass">
|
{/* <Paper withBorder p="lg" radius="2xl" className="glass"> */}
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Title order={2} className="gradient-text" style={{ fontSize: '1.8rem' }}>Overview</Title>
|
<Title order={3}>Overview</Title>
|
||||||
<Group gap="xs" mt={4}>
|
<Text size="sm" c="dimmed">Last updated: Just now</Text>
|
||||||
<Badge variant="light" size="lg" radius="sm" color="brand-blue" leftSection={<TbBuildingCommunity size={14} />}>
|
|
||||||
APP: {isDesaPlus ? 'DESA+' : appId.toUpperCase()}
|
|
||||||
</Badge>
|
|
||||||
<Text size="xs" c="dimmed" fw={600}>LAST UPDATED: JUST NOW</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Group gap="md">
|
<Group gap="md">
|
||||||
<Select
|
{/* <Select
|
||||||
placeholder="Date Range"
|
placeholder="Date Range"
|
||||||
data={['Today', '7 Days', '30 Days']}
|
data={['Today', '7 Days', '30 Days']}
|
||||||
defaultValue="Today"
|
defaultValue="Today"
|
||||||
leftSection={<TbCalendar size={16} />}
|
leftSection={<TbCalendar size={16} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
w={140}
|
w={140}
|
||||||
/>
|
/> */}
|
||||||
<ActionIcon variant="light" color="brand-blue" size="lg" radius="md">
|
<ActionIcon variant="light" color="brand-blue" size="lg" radius="md">
|
||||||
<TbRefresh size={20} />
|
<TbRefresh size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Button
|
{/* <Button
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={{ from: '#2563EB', to: '#7C3AED' }}
|
gradient={{ from: '#2563EB', to: '#7C3AED' }}
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<TbFilter size={18} />}
|
leftSection={<TbFilter size={18} />}
|
||||||
>
|
>
|
||||||
Add Filter
|
Add Filter
|
||||||
</Button>
|
</Button> */}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Paper>
|
{/* </Paper> */}
|
||||||
|
|
||||||
{/* 📊 1. SUMMARY CARDS */}
|
{/* 📊 1. SUMMARY CARDS */}
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
|
||||||
@@ -86,7 +67,6 @@ function AppOverviewPage() {
|
|||||||
value="v1.2.0"
|
value="v1.2.0"
|
||||||
icon={TbVersions}
|
icon={TbVersions}
|
||||||
color="brand-blue"
|
color="brand-blue"
|
||||||
progress={{ value: 92, label: 'User Adoption' }}
|
|
||||||
/>
|
/>
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
title="Total Activity Today"
|
title="Total Activity Today"
|
||||||
@@ -100,7 +80,6 @@ function AppOverviewPage() {
|
|||||||
value="138"
|
value="138"
|
||||||
icon={TbBuildingCommunity}
|
icon={TbBuildingCommunity}
|
||||||
color="indigo"
|
color="indigo"
|
||||||
progress={{ value: 98, label: 'Integration Health' }}
|
|
||||||
/>
|
/>
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
title="Errors Today"
|
title="Errors Today"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@@ -27,7 +28,7 @@ function AppDetailLayout() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Container size="xl" py="lg">
|
<Container size="xl" py="lg">
|
||||||
<Stack gap="xl">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" align="flex-end">
|
<Group justify="space-between" align="flex-end">
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
<Title order={1} className="gradient-text" style={{ fontSize: '2.5rem' }}>{appName}</Title>
|
<Title order={1} className="gradient-text" style={{ fontSize: '2.5rem' }}>{appName}</Title>
|
||||||
@@ -35,7 +36,9 @@ function AppDetailLayout() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box mt="md">
|
<Divider />
|
||||||
|
|
||||||
|
<Box>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import {
|
|||||||
} from 'react-icons/tb'
|
} from 'react-icons/tb'
|
||||||
import { StatsCard } from '@/frontend/components/StatsCard'
|
import { StatsCard } from '@/frontend/components/StatsCard'
|
||||||
|
|
||||||
export const Route = createFileRoute('/apps/$appId/manage')({
|
export const Route = createFileRoute('/apps/$appId/villages')({
|
||||||
component: AppManagePage,
|
component: AppVillagesPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
const mockDevelopers = [
|
const mockDevelopers = [
|
||||||
@@ -45,7 +45,7 @@ const mockDevelopers = [
|
|||||||
{ value: 'rahmat', label: 'Rahmat Hidayat', avatar: null },
|
{ value: 'rahmat', label: 'Rahmat Hidayat', avatar: null },
|
||||||
]
|
]
|
||||||
|
|
||||||
function AppManagePage() {
|
function AppVillagesPage() {
|
||||||
const { appId } = useParams({ from: '/apps/$appId' })
|
const { appId } = useParams({ from: '/apps/$appId' })
|
||||||
const [initModalOpened, { open: openInit, close: closeInit }] = useDisclosure(false)
|
const [initModalOpened, { open: openInit, close: closeInit }] = useDisclosure(false)
|
||||||
const [assignModalOpened, { open: openAssign, close: closeAssign }] = useDisclosure(false)
|
const [assignModalOpened, { open: openAssign, close: closeAssign }] = useDisclosure(false)
|
||||||
214
src/frontend/routes/logs.tsx
Normal file
214
src/frontend/routes/logs.tsx
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Paper,
|
||||||
|
TextInput,
|
||||||
|
Select,
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { useState, useMemo } from 'react'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { TbSearch, TbClock, TbCheck, TbX } from 'react-icons/tb'
|
||||||
|
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/logs')({
|
||||||
|
component: GlobalLogsPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
const timelineData = [
|
||||||
|
{
|
||||||
|
date: 'TODAY',
|
||||||
|
logs: [
|
||||||
|
{ id: 1, time: '12:12 PM', operator: 'Budi Santoso', app: 'Desa+', color: 'blue', content: <>generated document <Badge variant="light" color="gray" radius="sm">Surat Domisili</Badge> for <Badge variant="light" color="blue" radius="sm">Sukatani</Badge></> },
|
||||||
|
{ id: 2, time: '11:42 AM', operator: 'Siti Aminah', app: 'Desa+', color: 'teal', content: <>uploaded financial report <Badge variant="light" color="gray" radius="sm">Realisasi Q1</Badge> for <Badge variant="light" color="teal" radius="sm">Sukamaju</Badge></> },
|
||||||
|
{ id: 3, time: '10:12 AM', operator: 'System', app: 'Desa+', color: 'red', icon: TbX, content: <>experienced failure in <Badge variant="light" color="violet" radius="sm">SIAK Sync</Badge> at <Badge variant="light" color="red" radius="sm" leftSection={<TbX size={12}/>}>Cikini</Badge></>, message: { title: 'Sync Operation Failed (NullPointerException)', text: 'NullPointerException at village_sync.dart:45. The server returned a timeout error while waiting for the master database replica connection. Auto-retry scheduled in 15 minutes.' } },
|
||||||
|
{ id: 4, time: '09:42 AM', operator: 'Jane Smith', app: 'E-Commerce', color: 'orange', icon: TbCheck, content: <>resolved payment gateway issue for <Badge variant="light" color="orange" radius="sm">E-Commerce</Badge> checkout</> },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: 'YESTERDAY',
|
||||||
|
logs: [
|
||||||
|
{ id: 5, time: '05:10 AM', operator: 'System', app: 'System', color: 'cyan', content: <>completed automated <Badge variant="light" color="cyan" radius="sm">Nightly Backup</Badge> for all 138 villages</> },
|
||||||
|
{ id: 6, time: '04:50 AM', operator: 'Rahmat Hidayat', app: 'Desa+', color: 'green', content: <>granted Admin access to <Text component="span" fw={600}>Desa Bojong Gede</Text> operator</> },
|
||||||
|
{ id: 7, time: '03:42 AM', operator: 'System', app: 'Fitness App', color: 'red', icon: TbX, content: <>detected SocketException across <Badge variant="light" color="violet" radius="sm">Fitness App</Badge> wearable sync operations.</> },
|
||||||
|
{ id: 8, time: '02:33 AM', operator: 'Agus Setiawan', app: 'Desa+', color: 'blue', content: <>verified 145 <Badge variant="light" color="gray" radius="sm">Surat Kematian</Badge> entries in batch.</> },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '12 APRIL, 2026',
|
||||||
|
logs: [
|
||||||
|
{ id: 9, time: '03:42 AM', operator: 'Amel', app: 'Desa+', color: 'indigo', content: <>changed version configurations rolling out <Badge variant="light" color="gray" radius="sm">Desa+ v2.4.1</Badge></> },
|
||||||
|
{ id: 10, time: '02:10 AM', operator: 'John Doe', app: 'E-Commerce', color: 'pink', content: <>updated App setting <Badge variant="light" color="gray" radius="sm">Require OTP on Login</Badge> <Text component="span" c="violet" fw={600} size="sm" style={{ cursor: 'pointer' }}>View Details</Text></> },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function GlobalLogsPage() {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [appFilter, setAppFilter] = useState<string | null>(null)
|
||||||
|
const [operatorFilter, setOperatorFilter] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const filteredTimeline = useMemo(() => {
|
||||||
|
return timelineData
|
||||||
|
.map(group => {
|
||||||
|
const filteredLogs = group.logs.filter(log => {
|
||||||
|
if (appFilter && log.app !== appFilter) return false;
|
||||||
|
if (operatorFilter && log.operator !== operatorFilter) return false;
|
||||||
|
if (search) {
|
||||||
|
const lSearch = search.toLowerCase();
|
||||||
|
if (!log.operator.toLowerCase().includes(lSearch) && !log.app.toLowerCase().includes(lSearch)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return { ...group, logs: filteredLogs };
|
||||||
|
})
|
||||||
|
.filter(group => group.logs.length > 0);
|
||||||
|
}, [search, appFilter, operatorFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<Container size="xl" py="lg">
|
||||||
|
|
||||||
|
{/* Header Controls */}
|
||||||
|
<Group mb="xl" gap="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search operator or app..."
|
||||||
|
leftSection={<TbSearch size={16} />}
|
||||||
|
radius="md"
|
||||||
|
w={220}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="All Applications"
|
||||||
|
data={['Desa+', 'E-Commerce', 'Fitness App', 'System']}
|
||||||
|
radius="md"
|
||||||
|
w={160}
|
||||||
|
clearable
|
||||||
|
value={appFilter}
|
||||||
|
onChange={setAppFilter}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="All Operators"
|
||||||
|
data={['Agus Setiawan', 'Amel', 'Budi Santoso', 'Jane Smith', 'John Doe', 'Rahmat Hidayat', 'Siti Aminah', 'System']}
|
||||||
|
radius="md"
|
||||||
|
w={160}
|
||||||
|
clearable
|
||||||
|
value={operatorFilter}
|
||||||
|
onChange={setOperatorFilter}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Timeline Content */}
|
||||||
|
<Paper withBorder p="xl" radius="2xl" className="glass" style={{ background: 'var(--mantine-color-body)' }}>
|
||||||
|
{filteredTimeline.length === 0 ? (
|
||||||
|
<Text c="dimmed" ta="center" py="xl">No logs found matching your filters.</Text>
|
||||||
|
) : filteredTimeline.map((group, groupIndex) => (
|
||||||
|
<Box key={group.date}>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={700}
|
||||||
|
c="dimmed"
|
||||||
|
mt={groupIndex > 0 ? "xl" : 0}
|
||||||
|
mb="lg"
|
||||||
|
style={{ textTransform: 'uppercase' }}
|
||||||
|
>
|
||||||
|
{group.date}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Stack gap={0} pl={4}>
|
||||||
|
{group.logs.map((log, logIndex) => {
|
||||||
|
const isLastLog = logIndex === group.logs.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
key={log.id}
|
||||||
|
wrap="nowrap"
|
||||||
|
align="flex-start"
|
||||||
|
gap="lg"
|
||||||
|
style={{ position: 'relative', paddingBottom: isLastLog ? 0 : 32 }}
|
||||||
|
>
|
||||||
|
{/* Left: Time */}
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="dimmed"
|
||||||
|
w={70}
|
||||||
|
style={{ flexShrink: 0, marginTop: 4, textAlign: 'left' }}
|
||||||
|
>
|
||||||
|
{log.time}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Middle: Line & Avatar */}
|
||||||
|
<Box style={{ position: 'relative', width: 20, flexShrink: 0, alignSelf: 'stretch' }}>
|
||||||
|
{/* Vertical Line */}
|
||||||
|
{!isLastLog && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 24,
|
||||||
|
bottom: -8,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: 1,
|
||||||
|
backgroundColor: 'rgba(128,128,128,0.2)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Avatar */}
|
||||||
|
<Box style={{ position: 'relative', zIndex: 2 }}>
|
||||||
|
{log.icon ? (
|
||||||
|
<Avatar size={24} radius="xl" color={log.color} variant="light">
|
||||||
|
<log.icon size={14} />
|
||||||
|
</Avatar>
|
||||||
|
) : (
|
||||||
|
<Avatar size={24} radius="xl" color={log.color}>
|
||||||
|
{log.operator.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Right: Content */}
|
||||||
|
<Box style={{ flexGrow: 1, marginTop: 2 }}>
|
||||||
|
<Text size="sm">
|
||||||
|
<Text component="span" fw={600} mr={4}>{log.operator}</Text>
|
||||||
|
{log.content}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{log.message && (
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
mt="sm"
|
||||||
|
style={{ maxWidth: 800, backgroundColor: 'transparent' }}
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={600} mb={4}>{log.message.title}</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{log.message.text}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{groupIndex < timelineData.length - 1 && (
|
||||||
|
<Divider my="xl" color="rgba(128,128,128,0.1)" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</DashboardLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -35,8 +35,8 @@ import {
|
|||||||
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
||||||
import { StatsCard } from '@/frontend/components/StatsCard'
|
import { StatsCard } from '@/frontend/components/StatsCard'
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings')({
|
export const Route = createFileRoute('/users')({
|
||||||
component: SettingsPage,
|
component: UsersPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
const mockUsers = [
|
const mockUsers = [
|
||||||
@@ -67,14 +67,14 @@ const roles = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function SettingsPage() {
|
function UsersPage() {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Container size="xl" py="lg">
|
<Container size="xl" py="lg">
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Title order={2} className="gradient-text">Settings</Title>
|
<Title order={2} className="gradient-text">Users</Title>
|
||||||
<Text size="sm" c="dimmed">Manage system users, security roles, and application access control.</Text>
|
<Text size="sm" c="dimmed">Manage system users, security roles, and application access control.</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -27,8 +27,8 @@ html, body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: var(--font-inter);
|
font-family: var(--font-inter);
|
||||||
background-color: var(--bg-dark); /* Default to Dark Mode as per App.tsx */
|
/* background-color handled by Mantine */
|
||||||
color: #F8FAFC;
|
color: var(--mantine-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -53,9 +53,9 @@ body {
|
|||||||
|
|
||||||
/* Premium Dashboard Utilities */
|
/* Premium Dashboard Utilities */
|
||||||
.glass {
|
.glass {
|
||||||
background: rgba(30, 41, 59, 0.7);
|
background: var(--mantine-color-default);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(128, 128, 128, 0.1);
|
||||||
border-radius: 24px; /* XL rounding for cards */
|
border-radius: 24px; /* XL rounding for cards */
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user