amalia/09-apr-26 #5
@@ -17,4 +17,5 @@ export const API_URLS = {
|
||||
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`,
|
||||
}
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
Pagination,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Paper,
|
||||
Button,
|
||||
ActionIcon,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
SimpleGrid,
|
||||
Avatar,
|
||||
Box,
|
||||
SegmentedControl,
|
||||
Card,
|
||||
Divider,
|
||||
ThemeIcon,
|
||||
Pagination,
|
||||
Title,
|
||||
Tooltip
|
||||
} from '@mantine/core'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import {
|
||||
TbPlus,
|
||||
TbSearch,
|
||||
TbArrowRight,
|
||||
TbBuildingCommunity,
|
||||
TbCalendar,
|
||||
TbChevronRight,
|
||||
TbHome2,
|
||||
TbLayoutGrid,
|
||||
TbList,
|
||||
TbMapPin,
|
||||
TbCalendar,
|
||||
TbPlus,
|
||||
TbSearch,
|
||||
TbUser,
|
||||
TbHome2,
|
||||
TbArrowRight,
|
||||
TbChevronRight,
|
||||
TbX,
|
||||
} from 'react-icons/tb'
|
||||
import useSWR from 'swr'
|
||||
import { API_URLS } from '../config/api'
|
||||
|
||||
export const Route = createFileRoute('/apps/$appId/villages/')({
|
||||
@@ -214,14 +218,27 @@ function AppVillagesIndexPage() {
|
||||
const { appId } = useParams({ from: '/apps/$appId' })
|
||||
const navigate = useNavigate()
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||
const [createModalOpened, { open: openCreateModal, close: closeCreateModal }] = useDisclosure(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [search, setSearch] = 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 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 handleVillageClick = (villageId: string) => {
|
||||
@@ -242,6 +259,64 @@ function AppVillagesIndexPage() {
|
||||
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) {
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
@@ -256,6 +331,100 @@ function AppVillagesIndexPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Stack gap={4}>
|
||||
<Title order={3}>Village List</Title>
|
||||
@@ -268,8 +437,9 @@ function AppVillagesIndexPage() {
|
||||
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
||||
leftSection={<TbPlus size={18} />}
|
||||
radius="md"
|
||||
onClick={openCreateModal}
|
||||
>
|
||||
Add Village
|
||||
Create New Village
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user