upd: connected api

Deskripsi:
- tambah desa

No Issues
This commit is contained in:
2026-04-09 17:30:55 +08:00
parent c63b8cd385
commit cc49a1fcd3
2 changed files with 193 additions and 22 deletions

View File

@@ -17,4 +17,5 @@ export const API_URLS = {
getDailyActivity: () => `${API_BASE_URL}/api/monitoring/daily-activity`, getDailyActivity: () => `${API_BASE_URL}/api/monitoring/daily-activity`,
getComparisonActivity: () => `${API_BASE_URL}/api/monitoring/comparison-activity`, getComparisonActivity: () => `${API_BASE_URL}/api/monitoring/comparison-activity`,
postVersionUpdate: () => `${API_BASE_URL}/api/monitoring/version-update`, postVersionUpdate: () => `${API_BASE_URL}/api/monitoring/version-update`,
createVillages: () => `${API_BASE_URL}/api/monitoring/create-villages`,
} }

View File

@@ -1,41 +1,45 @@
import { useState } from 'react'
import useSWR from 'swr'
import { import {
ActionIcon,
Badge, Badge,
Box,
Button,
Card,
Container, Container,
Divider,
Group, Group,
Modal,
Pagination,
Paper,
SegmentedControl,
Select,
SimpleGrid,
Stack, Stack,
Text, Text,
Title, Textarea,
Paper,
Button,
ActionIcon,
TextInput, TextInput,
Tooltip,
SimpleGrid,
Avatar,
Box,
SegmentedControl,
Card,
Divider,
ThemeIcon, ThemeIcon,
Pagination, Title,
Tooltip
} from '@mantine/core' } from '@mantine/core'
import { notifications } from '@mantine/notifications'
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { useState } from 'react'
import { useDisclosure } from '@mantine/hooks'
import { import {
TbPlus, TbArrowRight,
TbSearch,
TbBuildingCommunity, TbBuildingCommunity,
TbCalendar,
TbChevronRight,
TbHome2,
TbLayoutGrid, TbLayoutGrid,
TbList, TbList,
TbMapPin, TbMapPin,
TbCalendar, TbPlus,
TbSearch,
TbUser, TbUser,
TbHome2,
TbArrowRight,
TbChevronRight,
TbX, TbX,
} from 'react-icons/tb' } from 'react-icons/tb'
import useSWR from 'swr'
import { API_URLS } from '../config/api' import { API_URLS } from '../config/api'
export const Route = createFileRoute('/apps/$appId/villages/')({ export const Route = createFileRoute('/apps/$appId/villages/')({
@@ -214,14 +218,27 @@ function AppVillagesIndexPage() {
const { appId } = useParams({ from: '/apps/$appId' }) const { appId } = useParams({ from: '/apps/$appId' })
const navigate = useNavigate() const navigate = useNavigate()
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [createModalOpened, { open: openCreateModal, close: closeCreateModal }] = useDisclosure(false)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
// Form State
const [isSubmitting, setIsSubmitting] = useState(false)
const [form, setForm] = useState({
name: '',
desc: '',
username: '',
phone: '',
nik: '',
email: '',
gender: ''
})
const isDesaPlus = appId === 'desa-plus' const isDesaPlus = appId === 'desa-plus'
const apiUrl = isDesaPlus ? API_URLS.getVillages(page, searchQuery) : null const apiUrl = isDesaPlus ? API_URLS.getVillages(page, searchQuery) : null
const { data: response, error, isLoading } = useSWR(apiUrl, fetcher) const { data: response, error, isLoading, mutate } = useSWR(apiUrl, fetcher)
const villages: APIVillage[] = response?.data || [] const villages: APIVillage[] = response?.data || []
const handleVillageClick = (villageId: string) => { const handleVillageClick = (villageId: string) => {
@@ -242,6 +259,64 @@ function AppVillagesIndexPage() {
setPage(1) setPage(1)
} }
const handleCreateVillage = async () => {
const requiredFields = ['name', 'desc', 'username', 'phone', 'nik', 'email', 'gender'] as const
const isFormValid = requiredFields.every(field => !!form[field])
if (!isFormValid) {
notifications.show({
title: 'Validation Error',
message: 'All fields are required to register a new village.',
color: 'red'
})
return
}
setIsSubmitting(true)
try {
const res = await fetch(API_URLS.createVillages(), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(form)
})
if (res.ok) {
notifications.show({
title: 'Success',
message: 'Village has been successfully registered.',
color: 'teal'
})
mutate() // Refresh list
closeCreateModal()
setForm({
name: '',
desc: '',
username: '',
phone: '',
nik: '',
email: '',
gender: ''
})
} else {
notifications.show({
title: 'Error',
message: 'Failed to create village. Please try again.',
color: 'red'
})
}
} catch (e) {
notifications.show({
title: 'Network Error',
message: 'Unable to reach API server.',
color: 'red'
})
} finally {
setIsSubmitting(false)
}
}
if (!isDesaPlus) { if (!isDesaPlus) {
return ( return (
<Container size="xl" py="xl"> <Container size="xl" py="xl">
@@ -256,6 +331,100 @@ function AppVillagesIndexPage() {
return ( return (
<Stack gap="xl"> <Stack gap="xl">
<Modal
opened={createModalOpened}
onClose={closeCreateModal}
title={<Text fw={700} size="lg">Register New Village</Text>}
radius="xl"
size="lg"
>
<Stack gap="md">
<Box>
<Text size="xs" fw={700} c="dimmed" mb={8} style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Village Data
</Text>
<Stack gap="sm">
<TextInput
label="Village Name"
placeholder="e.g. Darmasaba"
required
value={form.name}
onChange={(e) => setForm(prev => ({ ...prev, name: e.currentTarget.value }))}
/>
<Textarea
label="Description"
placeholder="Short description about the village..."
minRows={3}
required
value={form.desc}
onChange={(e) => setForm(prev => ({ ...prev, desc: e.currentTarget.value }))}
/>
</Stack>
</Box>
<Divider label="Village Head Information" labelPosition="center" my="sm" />
<Box>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Head Name (Username)"
placeholder="Full name of village head"
required
value={form.username}
onChange={(e) => setForm(prev => ({ ...prev, username: e.currentTarget.value }))}
/>
<TextInput
label="NIK"
placeholder="16-digit identity number"
required
value={form.nik}
onChange={(e) => setForm(prev => ({ ...prev, nik: e.currentTarget.value }))}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md" mt="sm">
<TextInput
label="Email"
placeholder="Email address"
required
value={form.email}
onChange={(e) => setForm(prev => ({ ...prev, email: e.currentTarget.value }))}
/>
<TextInput
label="Phone"
placeholder="Active WhatsApp number"
required
value={form.phone}
onChange={(e) => setForm(prev => ({ ...prev, phone: e.currentTarget.value }))}
/>
</SimpleGrid>
<Select
label="Gender"
placeholder="Select gender"
data={['Male', 'Female']}
mt="sm"
required
value={form.gender}
onChange={(val) => setForm(prev => ({ ...prev, gender: val || '' }))}
/>
</Box>
<Button
fullWidth
mt="lg"
radius="md"
size="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
loading={isSubmitting}
onClick={handleCreateVillage}
>
Create Village
</Button>
</Stack>
</Modal>
<Group justify="space-between" align="flex-end"> <Group justify="space-between" align="flex-end">
<Stack gap={4}> <Stack gap={4}>
<Title order={3}>Village List</Title> <Title order={3}>Village List</Title>
@@ -268,8 +437,9 @@ function AppVillagesIndexPage() {
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }} gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbPlus size={18} />} leftSection={<TbPlus size={18} />}
radius="md" radius="md"
onClick={openCreateModal}
> >
Add Village Create New Village
</Button> </Button>
</Group> </Group>