diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 09b029f..838a693 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -4,38 +4,38 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRouter, RouterProvider } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' -const theme = createTheme({ + const theme = createTheme({ primaryColor: 'brand-blue', colors: { 'brand-blue': [ - '#ebf2ff', - '#d6e4ff', - '#adc8ff', - '#85acff', - '#5c90ff', - '#2563eb', // Primary Blue - '#1e4fb8', - '#173b85', - '#102752', - '#09131f', + '#f0f9ff', + '#e0f2fe', + '#bae6fd', + '#7dd3fc', + '#38bdf8', + '#0ea5e9', // Primary Blue (Sky) + '#0284c7', + '#0369a1', + '#075985', + '#0c4a6e', ], 'brand-purple': [ - '#f3ebff', - '#e7d6ff', - '#cfadff', - '#b785ff', - '#9f5cff', - '#7c3aed', // Primary Purple - '#632eb8', - '#4a2285', - '#311652', - '#180b1f', + '#faf5ff', + '#f3e8ff', + '#e9d5ff', + '#d8b4fe', + '#c084fc', + '#a855f7', // Primary Purple + '#9333ea', + '#7e22ce', + '#6b21a8', + '#581c87', ], }, fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif', headings: { fontFamily: 'Inter, system-ui, sans-serif', - fontWeight: '600', + fontWeight: '500', // Softer headings }, }) @@ -59,8 +59,8 @@ declare module '@tanstack/react-router' { export function App() { return ( <> - - + + diff --git a/src/frontend/components/AppCard.tsx b/src/frontend/components/AppCard.tsx index 4880e26..3a8d485 100644 --- a/src/frontend/components/AppCard.tsx +++ b/src/frontend/components/AppCard.tsx @@ -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 { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb' @@ -13,6 +13,7 @@ interface AppCardProps { export function AppCard({ id, name, status, users, errors, version }: AppCardProps) { const statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red' + const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true }) return ( ({ root: { - backgroundColor: 'rgba(30, 41, 59, 0.4)', - borderColor: 'rgba(255,255,255,0.08)', - transition: 'transform 0.2s ease, box-shadow 0.2s ease', + backgroundColor: 'var(--mantine-color-body)', + 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, background-color 0.2s ease, border-color 0.2s ease', '&:hover': { 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)', }, }, @@ -45,7 +46,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro {name} - BUILD v{version} + VERSION {version} @@ -53,7 +54,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro - + {/* @@ -69,13 +70,13 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro 0 ? '#ef4444' : '#64748b'} /> - HEALTH INCIDENTS + ERROR 0 ? 'red' : 'dimmed'}>{errors} 0 ? 30 : 0} size="sm" color="red" radius="xl" /> - + */} ) diff --git a/src/frontend/components/DashboardLayout.tsx b/src/frontend/components/DashboardLayout.tsx index 1a119e7..1f59101 100644 --- a/src/frontend/components/DashboardLayout.tsx +++ b/src/frontend/components/DashboardLayout.tsx @@ -1,5 +1,6 @@ import { APP_CONFIGS } from '@/frontend/config/appMenus' import { + ActionIcon, AppShell, Avatar, Box, @@ -14,6 +15,7 @@ import { ThemeIcon } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' +import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core' import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router' import { TbApps, @@ -23,7 +25,11 @@ import { TbDeviceMobile, TbLogout, TbSettings, - TbUserCircle + TbUserCircle, + TbSun, + TbMoon, + TbUser, + TbHistory } from 'react-icons/tb' interface DashboardLayoutProps { @@ -31,7 +37,10 @@ interface 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 navigate = useNavigate() const { appId } = useParams({ strict: false }) as { appId?: string } @@ -42,7 +51,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { const globalNav = [ { label: 'Dashboard', icon: TbDashboard, to: '/dashboard' }, { 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 @@ -54,19 +64,21 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { navbar={{ width: 260, breakpoint: 'sm', - collapsed: { mobile: !opened }, + collapsed: { mobile: !mobileOpened, desktop: !desktopOpened }, }} padding="xl" styles={(theme) => ({ 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', }, })} > - + + + toggleColorScheme()} + variant="default" + size="lg" + aria-label="Toggle color scheme" + > + {computedColorScheme === 'dark' ? : } + ({ - 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) { SYSTEM STATUS diff --git a/src/frontend/components/ErrorDataTable.tsx b/src/frontend/components/ErrorDataTable.tsx index 34d7924..d543e93 100644 --- a/src/frontend/components/ErrorDataTable.tsx +++ b/src/frontend/components/ErrorDataTable.tsx @@ -15,6 +15,7 @@ import { } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import { useState } from 'react' +import { Link } from '@tanstack/react-router' import { TbMessageReport, TbHistory, TbExternalLink, TbBug } from 'react-icons/tb' const mockErrors = [ @@ -85,7 +86,7 @@ export function ErrorDataTable() { LATEST ERROR REPORTS - diff --git a/src/frontend/components/StatsCard.tsx b/src/frontend/components/StatsCard.tsx index d87b2ee..8525456 100644 --- a/src/frontend/components/StatsCard.tsx +++ b/src/frontend/components/StatsCard.tsx @@ -22,8 +22,8 @@ export function StatsCard({ title, value, description, icon: Icon, color, trend className="premium-card" styles={(theme) => ({ root: { - backgroundColor: theme.colors.dark[6], - borderColor: 'rgba(255,255,255,0.05)', + backgroundColor: 'var(--mantine-color-body)', + borderColor: 'rgba(128,128,128,0.1)', }, })} > diff --git a/src/frontend/components/SummaryCard.tsx b/src/frontend/components/SummaryCard.tsx index d3c3681..e1238dc 100644 --- a/src/frontend/components/SummaryCard.tsx +++ b/src/frontend/components/SummaryCard.tsx @@ -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 { TbTrendingUp, TbTrendingDown } from 'react-icons/tb' @@ -27,6 +27,8 @@ export function SummaryCard({ progress, isError }: SummaryCardProps) { + const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true }) + return ( ({ root: { - backgroundColor: isError && Number(value) > 0 ? 'rgba(239, 68, 68, 0.05)' : 'rgba(30, 41, 59, 0.4)', - borderColor: isError && Number(value) > 10 ? 'rgba(239, 68, 68, 0.3)' : 'rgba(255, 255, 255, 0.08)', - transition: 'transform 0.2s ease', + backgroundColor: isError && Number(value) > 0 + ? (scheme === 'dark' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(255, 241, 242, 1)') // light pink for error in light mode + : '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': { transform: 'translateY(-4px)', } diff --git a/src/frontend/config/appMenus.ts b/src/frontend/config/appMenus.ts index ae77749..2af209b 100644 --- a/src/frontend/config/appMenus.ts +++ b/src/frontend/config/appMenus.ts @@ -1,5 +1,5 @@ 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 { value: string @@ -22,7 +22,7 @@ export const APP_CONFIGS: Record = { { value: 'overview', label: 'Overview', icon: TbChartBar, to: '/apps/desa-plus' }, { 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: 'manage', label: 'Manage', icon: TbSettings, to: '/apps/desa-plus/manage' }, + { value: 'villages', label: 'Villages', icon: TbBuilding, to: '/apps/desa-plus/villages' }, ], }, 'e-commerce': { diff --git a/src/frontend/routes/apps.$appId.index.tsx b/src/frontend/routes/apps.$appId.index.tsx index a206fd1..47f7525 100644 --- a/src/frontend/routes/apps.$appId.index.tsx +++ b/src/frontend/routes/apps.$appId.index.tsx @@ -1,36 +1,22 @@ +import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts' +import { ErrorDataTable } from '@/frontend/components/ErrorDataTable' +import { SummaryCard } from '@/frontend/components/SummaryCard' import { - Badge, - Button, - Card, + ActionIcon, Group, SimpleGrid, Stack, Text, - Title, - Paper, - Box, - ThemeIcon, - Select, - ActionIcon, - Container, - Divider, + Title } from '@mantine/core' -import { createFileRoute, Link, useParams } from '@tanstack/react-router' -import { - TbUsers, - TbActivity, - TbRefresh, - TbAlertTriangle, - TbCalendar, - TbFilter, - TbChevronRight, - TbArrowUpRight, +import { createFileRoute, useParams } from '@tanstack/react-router' +import { + TbActivity, + TbAlertTriangle, TbBuildingCommunity, + TbRefresh, TbVersions } 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/')({ component: AppOverviewPage, @@ -43,66 +29,59 @@ function AppOverviewPage() { return ( {/* 🔝 HEADER SECTION */} - - - - Overview - - }> - APP: {isDesaPlus ? 'DESA+' : appId.toUpperCase()} - - LAST UPDATED: JUST NOW - - + {/* */} + + + Overview + Last updated: Just now + - - } radius="md" w={140} - /> - - - - - + */} - + + {/* */} {/* 📊 1. SUMMARY CARDS */} - - - - - + {appName} @@ -35,7 +36,9 @@ function AppDetailLayout() { - + + + diff --git a/src/frontend/routes/apps.$appId.manage.tsx b/src/frontend/routes/apps.$appId.villages.tsx similarity index 98% rename from src/frontend/routes/apps.$appId.manage.tsx rename to src/frontend/routes/apps.$appId.villages.tsx index 1dfb05c..ef065c6 100644 --- a/src/frontend/routes/apps.$appId.manage.tsx +++ b/src/frontend/routes/apps.$appId.villages.tsx @@ -34,8 +34,8 @@ import { } from 'react-icons/tb' import { StatsCard } from '@/frontend/components/StatsCard' -export const Route = createFileRoute('/apps/$appId/manage')({ - component: AppManagePage, +export const Route = createFileRoute('/apps/$appId/villages')({ + component: AppVillagesPage, }) const mockDevelopers = [ @@ -45,7 +45,7 @@ const mockDevelopers = [ { value: 'rahmat', label: 'Rahmat Hidayat', avatar: null }, ] -function AppManagePage() { +function AppVillagesPage() { const { appId } = useParams({ from: '/apps/$appId' }) const [initModalOpened, { open: openInit, close: closeInit }] = useDisclosure(false) const [assignModalOpened, { open: openAssign, close: closeAssign }] = useDisclosure(false) diff --git a/src/frontend/routes/logs.tsx b/src/frontend/routes/logs.tsx new file mode 100644 index 0000000..4fbc5e6 --- /dev/null +++ b/src/frontend/routes/logs.tsx @@ -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 Surat Domisili for Sukatani }, + { id: 2, time: '11:42 AM', operator: 'Siti Aminah', app: 'Desa+', color: 'teal', content: <>uploaded financial report Realisasi Q1 for Sukamaju }, + { id: 3, time: '10:12 AM', operator: 'System', app: 'Desa+', color: 'red', icon: TbX, content: <>experienced failure in SIAK Sync at }>Cikini, 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 E-Commerce checkout }, + ] + }, + { + date: 'YESTERDAY', + logs: [ + { id: 5, time: '05:10 AM', operator: 'System', app: 'System', color: 'cyan', content: <>completed automated Nightly Backup for all 138 villages }, + { id: 6, time: '04:50 AM', operator: 'Rahmat Hidayat', app: 'Desa+', color: 'green', content: <>granted Admin access to Desa Bojong Gede operator }, + { id: 7, time: '03:42 AM', operator: 'System', app: 'Fitness App', color: 'red', icon: TbX, content: <>detected SocketException across Fitness App wearable sync operations. }, + { id: 8, time: '02:33 AM', operator: 'Agus Setiawan', app: 'Desa+', color: 'blue', content: <>verified 145 Surat Kematian 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 Desa+ v2.4.1 }, + { id: 10, time: '02:10 AM', operator: 'John Doe', app: 'E-Commerce', color: 'pink', content: <>updated App setting Require OTP on Login View Details }, + ] + } +] + +function GlobalLogsPage() { + const [search, setSearch] = useState('') + const [appFilter, setAppFilter] = useState(null) + const [operatorFilter, setOperatorFilter] = useState(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 ( + + + + {/* Header Controls */} + + } + radius="md" + w={220} + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + + {/* Timeline Content */} + + {filteredTimeline.length === 0 ? ( + No logs found matching your filters. + ) : filteredTimeline.map((group, groupIndex) => ( + + 0 ? "xl" : 0} + mb="lg" + style={{ textTransform: 'uppercase' }} + > + {group.date} + + + + {group.logs.map((log, logIndex) => { + const isLastLog = logIndex === group.logs.length - 1; + + return ( + + {/* Left: Time */} + + {log.time} + + + {/* Middle: Line & Avatar */} + + {/* Vertical Line */} + {!isLastLog && ( + + )} + {/* Avatar */} + + {log.icon ? ( + + + + ) : ( + + {log.operator.charAt(0)} + + )} + + + + {/* Right: Content */} + + + {log.operator} + {log.content} + + + {log.message && ( + + {log.message.title} + + {log.message.text} + + + )} + + + ) + })} + + + {groupIndex < timelineData.length - 1 && ( + + )} + + ))} + + + + ) +} diff --git a/src/frontend/routes/settings.tsx b/src/frontend/routes/users.tsx similarity index 97% rename from src/frontend/routes/settings.tsx rename to src/frontend/routes/users.tsx index c4a3ada..fcbf60a 100644 --- a/src/frontend/routes/settings.tsx +++ b/src/frontend/routes/users.tsx @@ -35,8 +35,8 @@ import { import { DashboardLayout } from '@/frontend/components/DashboardLayout' import { StatsCard } from '@/frontend/components/StatsCard' -export const Route = createFileRoute('/settings')({ - component: SettingsPage, +export const Route = createFileRoute('/users')({ + component: UsersPage, }) const mockUsers = [ @@ -67,14 +67,14 @@ const roles = [ }, ] -function SettingsPage() { +function UsersPage() { return ( - Settings + Users Manage system users, security roles, and application access control. diff --git a/src/index.css b/src/index.css index 774be0a..6b5e2f4 100644 --- a/src/index.css +++ b/src/index.css @@ -27,8 +27,8 @@ html, body { height: 100%; width: 100%; font-family: var(--font-inter); - background-color: var(--bg-dark); /* Default to Dark Mode as per App.tsx */ - color: #F8FAFC; + /* background-color handled by Mantine */ + color: var(--mantine-color-text); } body { @@ -53,9 +53,9 @@ body { /* Premium Dashboard Utilities */ .glass { - background: rgba(30, 41, 59, 0.7); + background: var(--mantine-color-default); 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 */ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }