diff --git a/bun.lock b/bun.lock index 1a60939..6a37e41 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "bun-react-template", "dependencies": { "@elysiajs/cors": "^1.4.1", + "@elysiajs/eden": "^1.4.9", "@elysiajs/html": "^1.4.0", "@mantine/charts": "^9.0.0", "@mantine/core": "^8.3.18", @@ -21,6 +22,7 @@ "react-dom": "^19", "react-icons": "^5.6.0", "recharts": "^3.8.1", + "swr": "^2.4.1", }, "devDependencies": { "@biomejs/biome": "^2.4.10", @@ -99,6 +101,8 @@ "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], + "@elysiajs/eden": ["@elysiajs/eden@1.4.9", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-3CKVD4ycVjB8nCNssfmhnUuq3SzSHkUES3v5PNCFr9LxIrx39/HVRAZ8z2sLxrFqzUs48dCBZaxoZzJ5UUVHDA=="], + "@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="], "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], @@ -427,6 +431,8 @@ "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -705,6 +711,8 @@ "sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="], + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="], diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..a4112fe Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index eddf26e..d0c4921 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ }, "dependencies": { "@elysiajs/cors": "^1.4.1", + "@elysiajs/eden": "^1.4.9", "@elysiajs/html": "^1.4.0", "@mantine/charts": "^9.0.0", "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", + "@mantine/notifications": "^8.3.18", "@prisma/client": "6", "@tanstack/react-query": "^5.95.2", "@tanstack/react-router": "^1.168.10", @@ -36,7 +38,8 @@ "react": "^19", "react-dom": "^19", "react-icons": "^5.6.0", - "recharts": "^3.8.1" + "recharts": "^3.8.1", + "swr": "^2.4.1" }, "devDependencies": { "@biomejs/biome": "^2.4.10", diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 838a693..453a205 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -1,5 +1,7 @@ import { ColorSchemeScript, MantineProvider, createTheme } from '@mantine/core' import '@mantine/core/styles.css' +import '@mantine/notifications/styles.css' +import { Notifications } from '@mantine/notifications' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRouter, RouterProvider } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' @@ -61,6 +63,7 @@ export function App() { <> + diff --git a/src/frontend/components/DashboardCharts.tsx b/src/frontend/components/DashboardCharts.tsx index c2ddf93..bd67156 100644 --- a/src/frontend/components/DashboardCharts.tsx +++ b/src/frontend/components/DashboardCharts.tsx @@ -11,25 +11,12 @@ import { import { LineChart, BarChart } from '@mantine/charts' import { TbTimeline, TbChartBar, TbArrowUpRight } from 'react-icons/tb' -const activityData = [ - { date: 'Mar 26', logs: 1200 }, - { date: 'Mar 27', logs: 1900 }, - { date: 'Mar 28', logs: 1540 }, - { date: 'Mar 29', logs: 2400 }, - { date: 'Mar 30', logs: 2100 }, - { date: 'Mar 31', logs: 3200 }, - { date: 'Apr 01', logs: 3800 }, -] +interface ChartProps { + data?: any[] + isLoading?: boolean +} -const villageComparisonData = [ - { village: 'Sukatani', activity: 4500 }, - { village: 'Sukamaju', activity: 3800 }, - { village: 'Bojong Gede', activity: 3200 }, - { village: 'Beji', activity: 2800 }, - { village: 'Tapos', activity: 2400 }, -] - -export function VillageActivityLineChart() { +export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) { const theme = useMantineTheme() return ( @@ -46,14 +33,14 @@ export function VillageActivityLineChart() { }> - Growing + {isLoading ? '...' : 'Live'} USAGE COMPARISON BETWEEN VILLAGES - Top 5 most active village deployments + Most active village deployments @@ -97,7 +84,7 @@ export function VillageComparisonBarChart() { - {/* Custom SVG Gradient definitions for Premium SaaS look */} diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts new file mode 100644 index 0000000..c5ee969 --- /dev/null +++ b/src/frontend/config/api.ts @@ -0,0 +1,21 @@ +export const API_BASE_URL = import.meta.env.VITE_URL_API_DESA_PLUS + +export const API_URLS = { + getVillages: (page: number, search: string) => + `${API_BASE_URL}/api/monitoring/get-villages?page=${page}&search=${encodeURIComponent(search)}`, + infoVillages: (id: string) => + `${API_BASE_URL}/api/monitoring/info-villages?id=${id}`, + gridVillages: (id: string) => + `${API_BASE_URL}/api/monitoring/grid-villages?id=${id}`, + graphLogVillages: (id: string, time: string) => + `${API_BASE_URL}/api/monitoring/graph-log-villages?id=${id}&time=${time}`, + getUsers: (page: number, search: string) => + `${API_BASE_URL}/api/monitoring/user?page=${page}&search=${encodeURIComponent(search)}`, + getLogsAllVillages: (page: number, search: string) => + `${API_BASE_URL}/api/monitoring/log-all-villages?page=${page}&search=${encodeURIComponent(search)}`, + getGridOverview: () => `${API_BASE_URL}/api/monitoring/grid-overview`, + getDailyActivity: () => `${API_BASE_URL}/api/monitoring/daily-activity`, + getComparisonActivity: () => `${API_BASE_URL}/api/monitoring/comparison-activity`, + postVersionUpdate: () => `${API_BASE_URL}/api/monitoring/version-update`, + createVillages: () => `${API_BASE_URL}/api/monitoring/create-villages`, +} diff --git a/src/frontend/config/appMenus.ts b/src/frontend/config/appMenus.ts index 2af209b..fb39f1b 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, TbBuilding } from 'react-icons/tb' +import { TbAlertTriangle, TbBuilding, TbChartBar, TbCreditCard, TbHistory, TbPackage, TbShoppingCart, TbUsers } from 'react-icons/tb' export interface MenuItem { value: string @@ -23,6 +23,7 @@ export const APP_CONFIGS: Record = { { 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: 'villages', label: 'Villages', icon: TbBuilding, to: '/apps/desa-plus/villages' }, + { value: 'users', label: 'Users', icon: TbUsers, to: '/apps/desa-plus/users' }, ], }, 'e-commerce': { diff --git a/src/frontend/routes/apps.$appId.index.tsx b/src/frontend/routes/apps.$appId.index.tsx index 4112379..acb394f 100644 --- a/src/frontend/routes/apps.$appId.index.tsx +++ b/src/frontend/routes/apps.$appId.index.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from 'react' +import { notifications } from '@mantine/notifications' import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts' import { ErrorDataTable } from '@/frontend/components/ErrorDataTable' import { SummaryCard } from '@/frontend/components/SummaryCard' @@ -13,10 +15,12 @@ import { TextInput, Switch, Badge, - Textarea + Textarea, + Skeleton } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router' +import useSWR from 'swr' import { TbActivity, TbAlertTriangle, @@ -24,125 +28,200 @@ import { TbRefresh, TbVersions } from 'react-icons/tb' +import { API_URLS } from '../config/api' export const Route = createFileRoute('/apps/$appId/')({ component: AppOverviewPage, }) +const fetcher = (url: string) => fetch(url).then((res) => res.json()) + function AppOverviewPage() { const { appId } = useParams({ from: '/apps/$appId/' }) const navigate = useNavigate() const isDesaPlus = appId === 'desa-plus' const [versionModalOpened, { open: openVersionModal, close: closeVersionModal }] = useDisclosure(false) + // Form State + const [latestVersion, setLatestVersion] = useState('') + const [minVersion, setMinVersion] = useState('') + const [messageUpdate, setMessageUpdate] = useState('') + const [maintenance, setMaintenance] = useState(false) + const [isSaving, setIsSaving] = useState(false) + + // Data Fetching + const { data: gridRes, isLoading: gridLoading, mutate: mutateGrid } = useSWR(isDesaPlus ? API_URLS.getGridOverview() : null, fetcher) + const { data: dailyRes, isLoading: dailyLoading, mutate: mutateDaily } = useSWR(isDesaPlus ? API_URLS.getDailyActivity() : null, fetcher) + const { data: comparisonRes, isLoading: comparisonLoading, mutate: mutateComparison } = useSWR(isDesaPlus ? API_URLS.getComparisonActivity() : null, fetcher) + + const grid = gridRes?.data + const dailyData = dailyRes?.data || [] + const comparisonData = comparisonRes?.data || [] + + // Initialize form when data loads or modal opens + useEffect(() => { + if (grid?.version && versionModalOpened) { + setLatestVersion(grid.version.mobile_latest_version || '') + setMinVersion(grid.version.mobile_minimum_version || '') + setMessageUpdate(grid.version.mobile_message_update || '') + setMaintenance(grid.version.mobile_maintenance === 'true') + } + }, [grid, versionModalOpened]) + + const handleRefresh = () => { + mutateGrid() + mutateDaily() + mutateComparison() + } + + const handleSaveVersion = async () => { + setIsSaving(true) + try { + const response = await fetch(API_URLS.postVersionUpdate(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mobile_latest_version: latestVersion, + mobile_minimum_version: minVersion, + mobile_maintenance: maintenance, + mobile_message_update: messageUpdate, + }), + }) + + if (response.ok) { + notifications.show({ + title: 'Update Successful', + message: 'Application version information has been updated.', + color: 'teal', + }) + mutateGrid() + closeVersionModal() + } else { + notifications.show({ + title: 'Update Failed', + message: 'Failed to update version information. Please check your data.', + color: 'red', + }) + } + } catch (error) { + notifications.show({ + title: 'Network Error', + message: 'Could not connect to the server. Please try again later.', + color: 'red', + }) + } finally { + setIsSaving(false) + } + } + return ( <> - - + setLatestVersion(e.currentTarget.value)} + /> + setMinVersion(e.currentTarget.value)} + />