279 lines
9.7 KiB
TypeScript
279 lines
9.7 KiB
TypeScript
import { useState } from 'react'
|
|
import {
|
|
Badge,
|
|
Container,
|
|
Group,
|
|
Stack,
|
|
Text,
|
|
Title,
|
|
Paper,
|
|
Table,
|
|
Button,
|
|
ActionIcon,
|
|
TextInput,
|
|
Select,
|
|
Tooltip,
|
|
SimpleGrid,
|
|
Modal,
|
|
Avatar,
|
|
Box,
|
|
NumberInput,
|
|
} from '@mantine/core'
|
|
import { useDisclosure } from '@mantine/hooks'
|
|
import { createFileRoute, useParams } from '@tanstack/react-router'
|
|
import {
|
|
TbPlus,
|
|
TbSearch,
|
|
TbPencil,
|
|
TbTrash,
|
|
TbUserPlus,
|
|
TbCircleCheck,
|
|
TbRefresh,
|
|
TbUser,
|
|
TbBuildingCommunity,
|
|
} from 'react-icons/tb'
|
|
import { StatsCard } from '@/frontend/components/StatsCard'
|
|
|
|
export const Route = createFileRoute('/apps/$appId/manage')({
|
|
component: AppManagePage,
|
|
})
|
|
|
|
const mockDevelopers = [
|
|
{ value: 'john-doe', label: 'John Doe', avatar: null },
|
|
{ value: 'amel', label: 'Amel', avatar: null },
|
|
{ value: 'jane-smith', label: 'Jane Smith', avatar: null },
|
|
{ value: 'rahmat', label: 'Rahmat Hidayat', avatar: null },
|
|
]
|
|
|
|
function AppManagePage() {
|
|
const { appId } = useParams({ from: '/apps/$appId' })
|
|
const [initModalOpened, { open: openInit, close: closeInit }] = useDisclosure(false)
|
|
const [assignModalOpened, { open: openAssign, close: closeAssign }] = useDisclosure(false)
|
|
const [selectedVillage, setSelectedVillage] = useState<any>(null)
|
|
|
|
const isDesaPlus = appId === 'desa-plus'
|
|
|
|
const mockVillages = [
|
|
{ id: 1, name: 'Sukatani', kecamatan: 'Tapos', population: 4500, status: 'fully integrated', developer: 'John Doe', lastUpdate: '2 mins ago' },
|
|
{ id: 2, name: 'Sukamaju', kecamatan: 'Cilodong', population: 3800, status: 'sync active', developer: 'Amel', lastUpdate: '15 mins ago' },
|
|
{ id: 3, name: 'Cikini', kecamatan: 'Menteng', population: 2100, status: 'sync pending', developer: 'Jane Smith', lastUpdate: '-' },
|
|
{ id: 4, name: 'Bojong Gede', kecamatan: 'Bojong Gede', population: 6700, status: 'fully integrated', developer: 'Rahmat', lastUpdate: '1 hour ago' },
|
|
]
|
|
|
|
if (!isDesaPlus) {
|
|
return (
|
|
<Container size="xl" py="xl">
|
|
<Paper p="xl" radius="xl" className="glass" style={{ textAlign: 'center' }}>
|
|
<TbBuildingCommunity size={48} color="gray" opacity={0.5} />
|
|
<Title order={3} mt="md">General Management</Title>
|
|
<Text c="dimmed">This feature is currently customized for Desa+. Other apps coming soon.</Text>
|
|
</Paper>
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Stack gap="xl">
|
|
{/* Metrics Row */}
|
|
<SimpleGrid cols={{ base: 1, sm: 4 }} spacing="lg">
|
|
<StatsCard
|
|
title="Total Integrations"
|
|
value={140}
|
|
icon={TbBuildingCommunity}
|
|
color="brand-blue"
|
|
trend={{ value: '12%', positive: true }}
|
|
/>
|
|
<StatsCard
|
|
title="Daily Sync Rate"
|
|
value="94.2%"
|
|
icon={TbRefresh}
|
|
color="teal"
|
|
trend={{ value: '2.5%', positive: true }}
|
|
/>
|
|
<StatsCard
|
|
title="Avg. Sync Delay"
|
|
value="45s"
|
|
icon={TbRefresh}
|
|
color="orange"
|
|
/>
|
|
<StatsCard
|
|
title="Pending Documents"
|
|
value={124}
|
|
icon={TbUser}
|
|
color="red"
|
|
/>
|
|
</SimpleGrid>
|
|
|
|
<Group justify="space-between" align="flex-end">
|
|
<Stack gap={0}>
|
|
<Title order={3}>Village Deployment Center</Title>
|
|
<Text size="sm" c="dimmed">Monitor and configure **Desa+** village instances across all districts.</Text>
|
|
</Stack>
|
|
<Button
|
|
variant="gradient"
|
|
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
|
leftSection={<TbPlus size={18} />}
|
|
radius="md"
|
|
onClick={openInit}
|
|
>
|
|
Initialize New Village
|
|
</Button>
|
|
</Group>
|
|
|
|
<Paper withBorder radius="2xl" className="glass" p="md">
|
|
<Group mb="md">
|
|
<TextInput
|
|
placeholder="Search village or district..."
|
|
leftSection={<TbSearch size={16} />}
|
|
style={{ flex: 1 }}
|
|
radius="md"
|
|
/>
|
|
</Group>
|
|
|
|
<Table className="data-table" verticalSpacing="md" highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Village Profile</Table.Th>
|
|
<Table.Th>District</Table.Th>
|
|
<Table.Th>Integration Status</Table.Th>
|
|
<Table.Th>Lead Developer</Table.Th>
|
|
<Table.Th>Last Sync</Table.Th>
|
|
<Table.Th>Actions</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{mockVillages.map((village) => (
|
|
<Table.Tr key={village.id}>
|
|
<Table.Td>
|
|
<Stack gap={0}>
|
|
<Text fw={700} size="sm">{village.name}</Text>
|
|
<Text size="xs" c="dimmed">{village.population.toLocaleString()} Residents</Text>
|
|
</Stack>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" fw={500}>{village.kecamatan}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge
|
|
color={
|
|
village.status === 'fully integrated' ? 'teal' :
|
|
village.status === 'sync active' ? 'brand-blue' : 'orange'
|
|
}
|
|
variant={village.status === 'sync pending' ? 'outline' : 'light'}
|
|
leftSection={village.status !== 'sync pending' && <TbCircleCheck size={12} />}
|
|
radius="sm"
|
|
style={{ textTransform: 'uppercase', fontVariant: 'small-caps' }}
|
|
>
|
|
{village.status}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Group gap="xs">
|
|
<Avatar size="xs" radius="xl" color="brand-blue" src={null} />
|
|
<Text size="sm">{village.developer}</Text>
|
|
<ActionIcon
|
|
variant="subtle"
|
|
size="xs"
|
|
onClick={() => { setSelectedVillage(village); openAssign(); }}
|
|
>
|
|
<TbUserPlus size={12} />
|
|
</ActionIcon>
|
|
</Group>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="xs" fw={500} c={village.lastUpdate === '-' ? 'dimmed' : 'teal'}>
|
|
{village.lastUpdate}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Group gap="xs">
|
|
{village.status === 'sync pending' && (
|
|
<Button variant="light" size="compact-xs" color="blue" onClick={openInit}>
|
|
START SYNC
|
|
</Button>
|
|
)}
|
|
<Tooltip label="Village Settings">
|
|
<ActionIcon variant="light" size="sm" color="gray">
|
|
<TbPencil size={14} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
<Tooltip label="Unlink Village">
|
|
<ActionIcon variant="light" size="sm" color="red">
|
|
<TbTrash size={14} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Paper>
|
|
|
|
{/* MODALS */}
|
|
<Modal
|
|
opened={initModalOpened}
|
|
onClose={closeInit}
|
|
title={<Title order={4}>Desa+ Instance Initialization</Title>}
|
|
radius="xl"
|
|
centered
|
|
padding="xl"
|
|
>
|
|
<Stack gap="md">
|
|
<SimpleGrid cols={2}>
|
|
<TextInput label="Village Name" placeholder="e.g. Sukatani" radius="md" required />
|
|
<TextInput label="Kecamatan" placeholder="e.g. Tapos" radius="md" required />
|
|
</SimpleGrid>
|
|
<Group grow>
|
|
<Select
|
|
label="Population Data Source"
|
|
placeholder="Select source..."
|
|
data={['SIAK Terpusat', 'BPS Proyeksi', 'Manual Upload']}
|
|
radius="md"
|
|
/>
|
|
<NumberInput label="Target Residents" placeholder="1000" radius="md" />
|
|
</Group>
|
|
<Box>
|
|
<Text size="xs" fw={700} c="dimmed" mb="xs">INITIAL SYNC MODULES</Text>
|
|
<Group gap="xs">
|
|
<Badge variant="outline" color="blue">PENDUDUK</Badge>
|
|
<Badge variant="outline" color="teal">KEUANGAN</Badge>
|
|
<Badge variant="outline" color="brand-purple">PELAYANAN</Badge>
|
|
<Badge variant="outline" color="orange">APBDes</Badge>
|
|
</Group>
|
|
</Box>
|
|
<Group justify="flex-end" mt="md">
|
|
<Button variant="subtle" color="gray" onClick={closeInit}>Cancel</Button>
|
|
<Button variant="gradient" gradient={{ from: '#2563EB', to: '#7C3AED' }} radius="md">Deploy Instance</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
<Modal
|
|
opened={assignModalOpened}
|
|
onClose={closeAssign}
|
|
title={<Title order={4}>Assign Lead Developer</Title>}
|
|
radius="xl"
|
|
centered
|
|
padding="xl"
|
|
>
|
|
<Stack gap="md">
|
|
<Text size="sm">Assign a dedicated reviewer for <b>{selectedVillage?.name}</b> instance stability.</Text>
|
|
<Select
|
|
label="Technical Lead"
|
|
placeholder="Search developer..."
|
|
data={mockDevelopers}
|
|
leftSection={<TbUser size={16} />}
|
|
radius="md"
|
|
searchable
|
|
/>
|
|
<Group justify="flex-end" mt="md">
|
|
<Button variant="subtle" color="gray" onClick={closeAssign}>Cancel</Button>
|
|
<Button variant="gradient" gradient={{ from: '#2563EB', to: '#7C3AED' }} radius="md">Set Lead</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
</Stack>
|
|
)
|
|
}
|