upd: tampilan

This commit is contained in:
2026-04-02 10:30:21 +08:00
parent 39d659acd0
commit 47d26799ad
28 changed files with 2701 additions and 237 deletions

View File

@@ -0,0 +1,278 @@
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>
)
}