import { ActionIcon, Avatar, Badge, Box, Button, Container, Divider, Group, Modal, Pagination, Paper, ScrollArea, Select, SimpleGrid, Stack, Table, Text, TextInput, ThemeIcon, Title, Switch, } from '@mantine/core' import { useDisclosure, useMediaQuery } from '@mantine/hooks' import { notifications } from '@mantine/notifications' import { createFileRoute, useParams } from '@tanstack/react-router' import { useState } from 'react' import { TbBriefcase, TbCircleCheck, TbCircleX, TbEdit, TbHome2, TbId, TbMail, TbPhone, TbPlus, TbSearch, TbUsers, TbX, } from 'react-icons/tb' import useSWR from 'swr' import { API_URLS } from '../config/api' export const Route = createFileRoute('/apps/$appId/users/')({ component: UsersIndexPage, }) interface APIUser { id: string name: string nik: string phone: string email: string gender: string isWithoutOTP: boolean isActive: boolean role: string village: string group: string position?: string idUserRole: string idVillage: string idGroup: string idPosition: string } const fetcher = (url: string) => fetch(url).then((res) => res.json()) function UsersIndexPage() { const { appId } = useParams({ from: '/apps/$appId/users/' }) const [page, setPage] = useState(1) const [search, setSearch] = useState('') const [searchQuery, setSearchQuery] = useState('') const isDesaPlus = appId === 'desa-plus' const apiUrl = isDesaPlus ? API_URLS.getUsers(page, searchQuery) : null const { data: response, error, isLoading, mutate } = useSWR(apiUrl, fetcher) const users: APIUser[] = response?.data?.user || [] const handleSearchChange = (val: string) => { setSearch(val) if (val.length >= 3 || val.length === 0) { setSearchQuery(val) setPage(1) } } // --- 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) { await fetch(API_URLS.createLog(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'CREATE', message: `Didaftarkan user (${appId}) baru: ${form.name}-${form.nik}` }) }).catch(console.error) 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) { await fetch(API_URLS.createLog(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'UPDATE', message: `Data user (${appId}) diperbarui: ${editForm.name}-${editForm.id}` }) }).catch(console.error) 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('') setPage(1) } const getRoleColor = (role: string) => { const r = role.toLowerCase() if (r.includes('super')) return 'red' if (r.includes('admin')) return 'brand-blue' if (r.includes('developer')) return 'violet' return 'gray' } const isMobile = useMediaQuery('(max-width: 768px)') if (!isDesaPlus) { return ( User Management This feature is currently customized for Desa+. Other apps coming soon. ) } return ( User Management {isLoading ? 'Loading users...' : `${response?.data?.total || 0} users registered in the Desa+ system`} 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 }))} /> } size="md" rightSection={ search ? ( ) : null } value={search} onChange={(e) => handleSearchChange(e.currentTarget.value)} radius="md" style={{ maxWidth: 500 }} ml={40} /> {isLoading ? ( Loading user data... ) : error ? ( Failed to load data from API. ) : users.length === 0 ? ( No users match your criteria. ) : ( User & ID Contact Detail Organization Role Status {users.map((user) => ( {handleEditOpen(user)}}> {user.name.charAt(0)} {user.name} {user.nik} {user.email} {user.phone} {user.village} {user.group} ยท {user.position || 'Staff'} {user.role} {user.isActive ? ( ) : ( )} {user.isActive ? 'ACTIVE' : 'INACTIVE'} {user.isWithoutOTP && ( NO OTP )} ))}
)} {!isLoading && !error && response?.data?.totalPage > 0 && ( )}
) }