feat(users): use role dropdowns instead of admin checkboxes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<User[]>([])
|
||||
const [roles, setRoles] = useState<RoleOption[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||
|
||||
const [createForm, setCreateForm] = useState<UserCreatePayload>({
|
||||
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<User[]>('/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<User[]>('/users'),
|
||||
api.get<RoleOption[]>('/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<User>('/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<string, any> = {
|
||||
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<User>(`/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() {
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h2>👥 User Management</h2>
|
||||
<div className="text-dim">Create, edit, activate, and remove HarborForge users.</div>
|
||||
<div className="text-dim">Create accounts, assign one non-admin role, and manage activation state.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -198,17 +216,22 @@ export default function UsersPage() {
|
||||
</label>
|
||||
<label>
|
||||
Full Name
|
||||
<input value={createForm.full_name ?? ''} onChange={(e) => setCreateForm({ ...createForm, full_name: e.target.value })} />
|
||||
<input value={createForm.full_name} onChange={(e) => setCreateForm({ ...createForm, full_name: e.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" value={createForm.password} onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })} />
|
||||
</label>
|
||||
<label className="filter-check">
|
||||
<input type="checkbox" checked={createForm.is_admin} onChange={(e) => setCreateForm({ ...createForm, is_admin: e.target.checked })} />
|
||||
Admin user
|
||||
<label>
|
||||
Role
|
||||
<select value={createForm.role_id} onChange={(e) => setCreateForm({ ...createForm, role_id: e.target.value })}>
|
||||
<option value="" disabled>Select role</option>
|
||||
{roles.map((role) => (
|
||||
<option key={role.id} value={String(role.id)}>{role.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button className="btn-primary" disabled={saving || !createForm.username.trim() || !createForm.email.trim() || !createForm.password.trim()} onClick={handleCreateUser}>
|
||||
<button className="btn-primary" disabled={saving || !createForm.username.trim() || !createForm.email.trim() || !createForm.password.trim() || !createForm.role_id} onClick={handleCreateUser}>
|
||||
{saving ? 'Saving...' : 'Create User'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -230,6 +253,7 @@ export default function UsersPage() {
|
||||
<div style={{ fontWeight: 600 }}>{u.username}</div>
|
||||
<div className="text-dim">{u.email}</div>
|
||||
<div style={{ display: 'flex', gap: '6px', marginTop: '6px', flexWrap: 'wrap' }}>
|
||||
<span className="badge">{u.role_name || 'unassigned'}</span>
|
||||
{u.is_admin && <span className="badge">admin</span>}
|
||||
<span className={'badge ' + (u.is_active ? 'status-online' : 'status-offline')}>{u.is_active ? 'active' : 'inactive'}</span>
|
||||
</div>
|
||||
@@ -273,16 +297,25 @@ export default function UsersPage() {
|
||||
Reset Password
|
||||
<input type="password" placeholder="Leave blank to keep current password" value={editForm.password} onChange={(e) => setEditForm({ ...editForm, password: e.target.value })} />
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
|
||||
<label className="filter-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editForm.is_admin}
|
||||
disabled={user?.id === selectedUser.id}
|
||||
onChange={(e) => setEditForm({ ...editForm, is_admin: e.target.checked })}
|
||||
/>
|
||||
Admin user
|
||||
|
||||
<div style={{ marginTop: '8px', padding: '12px', border: '1px solid var(--border)', borderRadius: '8px' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '10px' }}>Role</div>
|
||||
{selectedUser.is_admin ? (
|
||||
<div className="text-dim">{selectedUser.role_name || 'admin'} (admin accounts are managed outside this screen)</div>
|
||||
) : (
|
||||
<label>
|
||||
Account Role
|
||||
<select value={editForm.role_id} onChange={(e) => setEditForm({ ...editForm, role_id: e.target.value })}>
|
||||
<option value="" disabled>Select role</option>
|
||||
{roles.map((role) => (
|
||||
<option key={role.id} value={String(role.id)}>{role.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
|
||||
<label className="filter-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface User {
|
||||
full_name: string | null
|
||||
is_admin: boolean
|
||||
is_active: boolean
|
||||
role_id: number | null
|
||||
role_name: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user