upd: connected api
Deskripsi: - tambah desa No Issues
This commit is contained in:
@@ -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`,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user