From ac3c673500869f210a43543e7d5e5aae75a779b8 Mon Sep 17 00:00:00 2001 From: amal Date: Fri, 10 Apr 2026 13:41:38 +0800 Subject: [PATCH] upd: api monitoring user --- src/frontend/config/api.ts | 5 + src/frontend/routes/apps.$appId.logs.tsx | 4 +- .../routes/apps.$appId.users.index.tsx | 561 ++++++++++++++++-- 3 files changed, 529 insertions(+), 41 deletions(-) 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`} - + */} 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 }))} + /> + + + setForm(f => ({ ...f, idUserRole: v || '' }))} + /> + + { + setForm(f => ({ ...f, idGroup: v || '', idPosition: '' })) + }} + /> + setEditForm(f => ({ ...f, gender: v || '' }))} + /> + + + + + + { + setEditForm(f => ({ ...f, idVillage: v || '', idGroup: '', 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 & IDContact Detail - Organization - Role - Status + Organization + Role + Status + Actions @@ -192,9 +664,9 @@ function UsersIndexPage() { - - + + handleEditOpen(user)} + size="md" + radius="md" + > + + + ))} -- 2.49.1