diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts
index c5ee969..5489f44 100644
--- a/src/frontend/config/api.ts
+++ b/src/frontend/config/api.ts
@@ -18,4 +18,9 @@ export const API_URLS = {
getComparisonActivity: () => `${API_BASE_URL}/api/monitoring/comparison-activity`,
postVersionUpdate: () => `${API_BASE_URL}/api/monitoring/version-update`,
createVillages: () => `${API_BASE_URL}/api/monitoring/create-villages`,
+ createUser: () => `${API_BASE_URL}/api/monitoring/create-user`,
+ listRole: () => `${API_BASE_URL}/api/monitoring/list-userrole-villages`,
+ 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`,
}
diff --git a/src/frontend/routes/apps.$appId.logs.tsx b/src/frontend/routes/apps.$appId.logs.tsx
index 16c9cf7..ce4bdd8 100644
--- a/src/frontend/routes/apps.$appId.logs.tsx
+++ b/src/frontend/routes/apps.$appId.logs.tsx
@@ -112,7 +112,7 @@ function AppLogsPage() {
{isLoading ? 'Loading logs...' : `Auditing ${response?.data?.total || 0} events across all villages`}
- }
@@ -120,7 +120,7 @@ function AppLogsPage() {
size="md"
>
Export
-
+ */}
fetch(url).then((res) => res.json())
@@ -65,7 +76,7 @@ function UsersIndexPage() {
const isDesaPlus = appId === 'desa-plus'
const apiUrl = isDesaPlus ? API_URLS.getUsers(page, searchQuery) : null
- const { data: response, error, isLoading } = useSWR(apiUrl, fetcher)
+ const { data: response, error, isLoading, mutate } = useSWR(apiUrl, fetcher)
const users: APIUser[] = response?.data?.user || []
const handleSearchChange = (val: string) => {
@@ -76,6 +87,198 @@ function UsersIndexPage() {
}
}
+ // --- ADD USER LOGIC ---
+ const [opened, { open, close }] = useDisclosure(false)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [villageSearch, setVillageSearch] = useState('')
+ const [form, setForm] = useState({
+ name: '',
+ nik: '',
+ phone: '',
+ email: '',
+ gender: '',
+ idUserRole: '',
+ idVillage: '',
+ idGroup: '',
+ idPosition: ''
+ })
+
+ const [editOpened, { open: openEdit, close: closeEdit }] = useDisclosure(false)
+ const [editForm, setEditForm] = useState({
+ id: '',
+ name: '',
+ nik: '',
+ phone: '',
+ email: '',
+ gender: '',
+ idUserRole: '',
+ idVillage: '',
+ idGroup: '',
+ idPosition: '',
+ isActive: true,
+ isWithoutOTP: false
+ })
+
+ // Options Data (Shared for both Add and Edit modals)
+ const isAnyModalOpened = opened || editOpened
+ const targetVillageId = opened ? form.idVillage : editForm.idVillage
+ const targetGroupId = opened ? form.idGroup : editForm.idGroup
+
+ const { data: rolesResp } = useSWR(isAnyModalOpened ? API_URLS.listRole() : null, fetcher)
+ const { data: villagesResp } = useSWR(
+ isAnyModalOpened && villageSearch.length >= 1 ? API_URLS.getVillages(1, villageSearch) : null,
+ fetcher
+ )
+ const { data: groupsResp } = useSWR(
+ isAnyModalOpened && targetVillageId ? API_URLS.listGroup(targetVillageId) : null,
+ fetcher
+ )
+ const { data: positionsResp } = useSWR(
+ isAnyModalOpened && targetGroupId ? API_URLS.listPosition(targetGroupId) : null,
+ fetcher
+ )
+
+ const rolesOptions = (rolesResp?.data || []).map((r: any) => ({ value: r.id, label: r.name }))
+ const villagesOptions = (villagesResp?.data || []).map((v: any) => ({ value: v.id, label: v.name }))
+ const groupsOptions = (groupsResp?.data || []).map((g: any) => ({ value: g.id, label: g.name }))
+ const positionsOptions = (positionsResp?.data || []).map((p: any) => ({ value: p.id, label: p.name }))
+
+ const handleCreateUser = async () => {
+ const requiredFields = ['name', 'nik', 'phone', 'email', 'gender', 'idUserRole', 'idVillage', 'idGroup']
+ const missing = requiredFields.filter(f => !form[f as keyof typeof form])
+
+ if (missing.length > 0) {
+ notifications.show({
+ title: 'Validation Error',
+ message: `Please fill in all required fields: ${missing.join(', ')}`,
+ color: 'red'
+ })
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ const res = await fetch(API_URLS.createUser(), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(form)
+ })
+
+ const result = await res.json()
+
+ if (result.success) {
+ notifications.show({
+ title: 'Success',
+ message: 'User has been created successfully.',
+ color: 'teal',
+ icon:
+ })
+ mutate() // Refresh user list
+ close()
+ setForm({
+ name: '',
+ nik: '',
+ phone: '',
+ email: '',
+ gender: '',
+ idUserRole: '',
+ idVillage: '',
+ idGroup: '',
+ idPosition: ''
+ })
+ } else {
+ notifications.show({
+ title: 'Error',
+ message: result.message || 'Failed to create user.',
+ color: 'red',
+ icon:
+ })
+ }
+ } catch (e) {
+ notifications.show({
+ title: 'Network Error',
+ message: 'Unable to connect to the server.',
+ color: 'red'
+ })
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const handleEditOpen = (user: APIUser) => {
+ setEditForm({
+ id: user.id,
+ name: user.name,
+ nik: user.nik,
+ phone: user.phone,
+ email: user.email,
+ gender: user.gender,
+ idUserRole: user.idUserRole,
+ idVillage: user.idVillage,
+ idGroup: user.idGroup,
+ idPosition: user.idPosition,
+ isActive: user.isActive,
+ isWithoutOTP: user.isWithoutOTP
+ })
+ setVillageSearch(user.village)
+ openEdit()
+ }
+
+ const handleUpdateUser = async () => {
+ const requiredFields = ['name', 'nik', 'phone', 'email', 'gender', 'idUserRole', 'idVillage', 'idGroup']
+ const missing = requiredFields.filter(f => !editForm[f as keyof typeof editForm])
+
+ if (missing.length > 0) {
+ notifications.show({
+ title: 'Validation Error',
+ message: `Please fill in all required fields: ${missing.join(', ')}`,
+ color: 'red'
+ })
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ const res = await fetch(API_URLS.editUser(), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(editForm)
+ })
+
+ const result = await res.json()
+
+ if (result.success) {
+ notifications.show({
+ title: 'Success',
+ message: 'User has been updated successfully.',
+ color: 'teal',
+ icon:
+ })
+ mutate()
+ closeEdit()
+ } else {
+ notifications.show({
+ title: 'Error',
+ message: result.message || 'Failed to update user.',
+ color: 'red',
+ icon:
+ })
+ }
+ } catch (e) {
+ notifications.show({
+ title: 'Network Error',
+ message: 'Unable to connect to the server.',
+ color: 'red'
+ })
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
const handleClearSearch = () => {
setSearch('')
setSearchQuery('')
@@ -126,11 +329,279 @@ function UsersIndexPage() {
leftSection={}
radius="md"
size="md"
+ onClick={open}
>
Add User
+ Add New User}
+ radius="xl"
+ size="lg"
+ overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
+ >
+
+
+
+ Personal Information
+
+
+ setForm(f => ({ ...f, name: e.target.value }))}
+ />
+ setForm(f => ({ ...f, nik: e.target.value }))}
+ />
+
+
+
+ setForm(f => ({ ...f, email: e.target.value }))}
+ />
+ setForm(f => ({ ...f, phone: e.target.value }))}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit User}
+ radius="xl"
+ size="lg"
+ overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
+ >
+
+
+
+ Personal Information
+
+
+ setEditForm(f => ({ ...f, name: e.target.value }))}
+ />
+ setEditForm(f => ({ ...f, nik: e.target.value }))}
+ />
+
+
+
+ setEditForm(f => ({ ...f, email: e.target.value }))}
+ />
+ setEditForm(f => ({ ...f, phone: e.target.value }))}
+ />
+
+
+ setEditForm(f => ({ ...f, gender: v || '' }))}
+ />
+
+
+
+
+
+ setEditForm(f => ({ ...f, idUserRole: v || '' }))}
+ />
+
+ {
+ setEditForm(f => ({ ...f, idVillage: v || '', idGroup: '', idPosition: '' }))
+ }}
+ />
+
+
+ {
+ setEditForm(f => ({ ...f, idGroup: v || '', idPosition: '' }))
+ }}
+ />
+ setEditForm(f => ({ ...f, idPosition: v || '' }))}
+ />
+
+
+
+
+
+
+ setEditForm(f => ({ ...f, isActive: event.currentTarget.checked }))}
+ />
+ setEditForm(f => ({ ...f, isWithoutOTP: event.currentTarget.checked }))}
+ />
+
+
+
+
+
+
}
@@ -167,13 +638,13 @@ function UsersIndexPage() {
) : (
-
User & ID
Contact Detail
- Organization
- Role
- Status
+ Organization
+ Role
+ Status
+ Actions
@@ -192,9 +664,9 @@ function UsersIndexPage() {
-
-
+
+ handleEditOpen(user)}
+ size="md"
+ radius="md"
+ >
+
+
+
))}