amalia/13-apr-26 #7
@@ -23,4 +23,6 @@ export const API_URLS = {
|
||||
listGroup: (id: string) => `${API_BASE_URL}/api/monitoring/list-group-villages?id=${id}`,
|
||||
listPosition: (id: string) => `${API_BASE_URL}/api/monitoring/list-position-villages?id=${id}`,
|
||||
editUser: () => `${API_BASE_URL}/api/monitoring/edit-user`,
|
||||
updateStatusVillages: () => `${API_BASE_URL}/api/monitoring/update-status-villages`,
|
||||
editVillages: () => `${API_BASE_URL}/api/monitoring/edit-villages`,
|
||||
}
|
||||
|
||||
@@ -639,8 +639,8 @@ function UsersIndexPage() {
|
||||
<Paper withBorder radius="2xl" className="glass" style={{ overflow: 'hidden' }}>
|
||||
<ScrollArea h={isMobile ? undefined : 'auto'} offsetScrollbars>
|
||||
<Table
|
||||
verticalSpacing="lg"
|
||||
horizontalSpacing="xl"
|
||||
verticalSpacing="md"
|
||||
horizontalSpacing="md"
|
||||
highlightOnHover
|
||||
withColumnBorders={false}
|
||||
style={{
|
||||
@@ -654,14 +654,13 @@ function UsersIndexPage() {
|
||||
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '28%' }}>User & ID</Table.Th>
|
||||
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '25%' }}>Contact Detail</Table.Th>
|
||||
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '22%' }}>Organization</Table.Th>
|
||||
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '12%' }}>Role</Table.Th>
|
||||
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '20%' }}>Role</Table.Th>
|
||||
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '10%' }}>Status</Table.Th>
|
||||
<Table.Th style={{ border: 'none', width: isMobile ? undefined : '8%' }}>Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{users.map((user) => (
|
||||
<Table.Tr key={user.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<Table.Tr key={user.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }} onClick={()=>{handleEditOpen(user)}}>
|
||||
<Table.Td>
|
||||
<Group gap="md" wrap="nowrap">
|
||||
<Avatar
|
||||
@@ -745,17 +744,6 @@ function UsersIndexPage() {
|
||||
)}
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="brand-blue"
|
||||
onClick={() => handleEditOpen(user)}
|
||||
size="md"
|
||||
radius="md"
|
||||
>
|
||||
<TbEdit size={18} />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
||||
@@ -4,14 +4,19 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
@@ -144,12 +149,120 @@ function VillageDetailPage() {
|
||||
const { appId, villageId } = useParams({ from: '/apps/$appId/villages/$villageId' })
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { data: infoRes, isLoading: infoLoading } = useSWR(API_URLS.infoVillages(villageId), fetcher)
|
||||
const { data: infoRes, isLoading: infoLoading, mutate } = useSWR(API_URLS.infoVillages(villageId), fetcher)
|
||||
const { data: gridRes, isLoading: gridLoading } = useSWR(API_URLS.gridVillages(villageId), fetcher)
|
||||
|
||||
const [confirmModalOpened, { open: openConfirmModal, close: closeConfirmModal }] = useDisclosure(false)
|
||||
const [editModalOpened, { open: openEditModal, close: closeEditModal }] = useDisclosure(false)
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editForm, setEditForm] = useState({ name: '', desc: '' })
|
||||
|
||||
const village = infoRes?.data
|
||||
const stats = gridRes?.data
|
||||
|
||||
const openEdit = () => {
|
||||
setEditForm({
|
||||
name: village?.name || '',
|
||||
desc: village?.desc || ''
|
||||
})
|
||||
openEditModal()
|
||||
}
|
||||
|
||||
const handleEditVillage = async () => {
|
||||
if (!village) return
|
||||
|
||||
if (!editForm.name.trim() || !editForm.desc.trim()) {
|
||||
notifications.show({
|
||||
title: 'Validation Error',
|
||||
message: 'All fields are required.',
|
||||
color: 'red'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsEditing(true)
|
||||
try {
|
||||
const res = await fetch(API_URLS.editVillages(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: village.id,
|
||||
name: editForm.name,
|
||||
desc: editForm.desc
|
||||
})
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Village data has been updated successfully.',
|
||||
color: 'teal'
|
||||
})
|
||||
mutate()
|
||||
closeEditModal()
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to update village data.',
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'A network error occurred.',
|
||||
color: 'red'
|
||||
})
|
||||
} finally {
|
||||
setIsEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmToggle = async () => {
|
||||
if (!village) return
|
||||
|
||||
setIsUpdating(true)
|
||||
try {
|
||||
const res = await fetch(API_URLS.updateStatusVillages(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: village.id,
|
||||
active: !village.isActive
|
||||
})
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: `Village status has been ${!village.isActive ? 'activated' : 'deactivated'}.`,
|
||||
color: 'teal'
|
||||
})
|
||||
mutate()
|
||||
closeConfirmModal()
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to update village status.',
|
||||
color: 'red'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'A network error occurred.',
|
||||
color: 'red'
|
||||
})
|
||||
} finally {
|
||||
setIsUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => navigate({ to: '/apps/$appId/villages', params: { appId } })
|
||||
|
||||
if (infoLoading || gridLoading) {
|
||||
@@ -195,8 +308,9 @@ function VillageDetailPage() {
|
||||
variant="filled"
|
||||
color={village.isActive ? 'red' : 'green'}
|
||||
leftSection={village.isActive ? <TbPower size={16} /> : <TbPower size={16} />}
|
||||
onClick={() => alert(`Toggle status for ${village.name}`)}
|
||||
onClick={openConfirmModal}
|
||||
radius="md"
|
||||
loading={isUpdating}
|
||||
>
|
||||
{village.isActive ? 'Deactivate' : 'Active'}
|
||||
</Button>
|
||||
@@ -204,7 +318,7 @@ function VillageDetailPage() {
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<TbEdit size={16} />}
|
||||
onClick={() => alert(`Edit settings for ${village.name}`)}
|
||||
onClick={openEdit}
|
||||
radius="md"
|
||||
>
|
||||
Edit
|
||||
@@ -347,6 +461,75 @@ function VillageDetailPage() {
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* ── Confirmation Modal ── */}
|
||||
<Modal
|
||||
opened={confirmModalOpened}
|
||||
onClose={closeConfirmModal}
|
||||
title={<Text fw={700}>Confirm Status Change</Text>}
|
||||
radius="xl"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm">
|
||||
Are you sure you want to <strong>{village.isActive ? 'deactivate' : 'activate'}</strong> village <strong>{village.name}</strong>?
|
||||
</Text>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="light" color="gray" onClick={closeConfirmModal} radius="md">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color={village.isActive ? 'red' : 'green'}
|
||||
onClick={handleConfirmToggle}
|
||||
loading={isUpdating}
|
||||
radius="md"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* ── Edit Village Modal ── */}
|
||||
<Modal
|
||||
opened={editModalOpened}
|
||||
onClose={closeEditModal}
|
||||
title={<Text fw={700}>Edit Village Details</Text>}
|
||||
radius="xl"
|
||||
size="md"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Village Name"
|
||||
placeholder="Enter village name"
|
||||
required
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, name: e.currentTarget.value }))}
|
||||
/>
|
||||
<Textarea
|
||||
label="Description"
|
||||
placeholder="Enter village description..."
|
||||
minRows={3}
|
||||
required
|
||||
value={editForm.desc}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, desc: e.currentTarget.value }))}
|
||||
/>
|
||||
<Group justify="flex-end" gap="sm" mt="md">
|
||||
<Button variant="light" color="gray" onClick={closeEditModal} radius="md">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="blue"
|
||||
onClick={handleEditVillage}
|
||||
loading={isEditing}
|
||||
radius="md"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user