From fd28bb6b6f580110fa9338f6b72da9fa53b41d3c Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 08:44:19 +0000 Subject: [PATCH] feat(users): use role dropdowns instead of admin checkboxes --- src/pages/RoleEditorPage.tsx | 2 +- src/pages/UsersPage.tsx | 131 ++++++++++++++++++++++------------- src/types/index.ts | 2 + 3 files changed, 85 insertions(+), 50 deletions(-) diff --git a/src/pages/RoleEditorPage.tsx b/src/pages/RoleEditorPage.tsx index a314414..3ce15fe 100644 --- a/src/pages/RoleEditorPage.tsx +++ b/src/pages/RoleEditorPage.tsx @@ -92,7 +92,7 @@ export default function RoleEditorPage() { } } - const canDeleteRole = selectedRole && selectedRole.name !== 'admin' && isAdmin + const canDeleteRole = selectedRole && !['admin', 'guest', 'account-manager'].includes(selectedRole.name) && isAdmin const handleCreateRole = async () => { if (!newRoleName.trim()) return diff --git a/src/pages/UsersPage.tsx b/src/pages/UsersPage.tsx index c34bd9d..182cd97 100644 --- a/src/pages/UsersPage.tsx +++ b/src/pages/UsersPage.tsx @@ -3,20 +3,10 @@ import api from '@/services/api' import { useAuth } from '@/hooks/useAuth' import type { User } from '@/types' -interface UserCreatePayload { - username: string - email: string - full_name: string - password: string - is_admin: boolean -} - -interface UserUpdatePayload { - email?: string - full_name?: string | null - password?: string - is_admin?: boolean - is_active?: boolean +interface RoleOption { + id: number + name: string + description?: string | null } export default function UsersPage() { @@ -24,24 +14,25 @@ export default function UsersPage() { const isAdmin = user?.is_admin === true const [users, setUsers] = useState([]) + const [roles, setRoles] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [message, setMessage] = useState('') const [selectedId, setSelectedId] = useState(null) - const [createForm, setCreateForm] = useState({ + const [createForm, setCreateForm] = useState({ username: '', email: '', full_name: '', password: '', - is_admin: false, + role_id: '', }) const [editForm, setEditForm] = useState({ email: '', full_name: '', password: '', - is_admin: false, + role_id: '', is_active: true, }) @@ -55,7 +46,7 @@ export default function UsersPage() { setLoading(false) return } - fetchUsers() + fetchData() }, [isAdmin]) useEffect(() => { @@ -64,22 +55,40 @@ export default function UsersPage() { email: selectedUser.email, full_name: selectedUser.full_name || '', password: '', - is_admin: selectedUser.is_admin, + role_id: selectedUser.role_id ? String(selectedUser.role_id) : '', is_active: selectedUser.is_active, }) }, [selectedUser]) - const fetchUsers = async () => { + useEffect(() => { + if (!createForm.role_id && roles.length > 0) { + const guestRole = roles.find((r) => r.name === 'guest') ?? roles[0] + if (guestRole) { + setCreateForm((prev) => ({ ...prev, role_id: String(guestRole.id) })) + } + } + }, [roles, createForm.role_id]) + + const fetchData = async () => { try { - const { data } = await api.get('/users') - setUsers(data) - if (!selectedId && data.length > 0) { - setSelectedId(data[0].id) - } else if (selectedId && !data.some((u) => u.id === selectedId)) { - setSelectedId(data[0]?.id ?? null) + const [usersRes, rolesRes] = await Promise.all([ + api.get('/users'), + api.get('/roles'), + ]) + const assignableRoles = rolesRes.data + .filter((role) => role.name !== 'admin') + .sort((a, b) => a.name.localeCompare(b.name)) + + setUsers(usersRes.data) + setRoles(assignableRoles) + + if (!selectedId && usersRes.data.length > 0) { + setSelectedId(usersRes.data[0].id) + } else if (selectedId && !usersRes.data.some((u) => u.id === selectedId)) { + setSelectedId(usersRes.data[0]?.id ?? null) } } catch (err: any) { - setMessage(err.response?.data?.detail || 'Failed to load users') + setMessage(err.response?.data?.detail || 'Failed to load user management data') } finally { setLoading(false) } @@ -95,12 +104,19 @@ export default function UsersPage() { email: createForm.email.trim(), full_name: createForm.full_name.trim() || null, password: createForm.password, - is_admin: createForm.is_admin, + role_id: createForm.role_id ? Number(createForm.role_id) : undefined, } const { data } = await api.post('/users', payload) - setCreateForm({ username: '', email: '', full_name: '', password: '', is_admin: false }) + const guestRole = roles.find((r) => r.name === 'guest') ?? roles[0] + setCreateForm({ + username: '', + email: '', + full_name: '', + password: '', + role_id: guestRole ? String(guestRole.id) : '', + }) setMessage('User created successfully') - await fetchUsers() + await fetchData() setSelectedId(data.id) } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to create user') @@ -114,18 +130,20 @@ export default function UsersPage() { setSaving(true) setMessage('') try { - const payload: UserUpdatePayload = { + const payload: Record = { email: editForm.email.trim(), full_name: editForm.full_name.trim() || null, - is_admin: editForm.is_admin, is_active: editForm.is_active, } + if (!selectedUser.is_admin) { + payload.role_id = editForm.role_id ? Number(editForm.role_id) : undefined + } if (editForm.password.trim()) { payload.password = editForm.password } await api.patch(`/users/${selectedUser.id}`, payload) setMessage('User updated successfully') - await fetchUsers() + await fetchData() setEditForm((prev) => ({ ...prev, password: '' })) } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to update user') @@ -142,7 +160,7 @@ export default function UsersPage() { try { await api.delete(`/users/${selectedUser.id}`) setMessage('User deleted successfully') - await fetchUsers() + await fetchData() } catch (err: any) { setMessage(err.response?.data?.detail || 'Failed to delete user') } finally { @@ -166,7 +184,7 @@ export default function UsersPage() {

👥 User Management

-
Create, edit, activate, and remove HarborForge users.
+
Create accounts, assign one non-admin role, and manage activation state.
@@ -198,17 +216,22 @@ export default function UsersPage() { - + +
+
Role
+ {selectedUser.is_admin ? ( +
{selectedUser.role_name || 'admin'} (admin accounts are managed outside this screen)
+ ) : ( + + )} +
+
-