fix: repair user management CRUD operations (create, update, delete)
Problems Fixed: 1. **User Creation (Invite)** - ❌ Missing password field (required by API) - ❌ Hardcoded organizationId 'default-org-id' - ❌ Wrong role format (lowercase instead of ADMIN/USER/MANAGER) - ✅ Now uses currentUser.organizationId from auth context - ✅ Added password field with validation (min 8 chars) - ✅ Fixed role enum to match backend (ADMIN, USER, MANAGER, VIEWER) 2. **Role Change (PATCH)** - ❌ Used 'as any' masking type errors - ❌ Lowercase role values - ✅ Proper typing with uppercase roles - ✅ Added success/error feedback - ✅ Disabled state during mutation 3. **Toggle Active (PATCH)** - ✅ Was working but added better feedback - ✅ Added disabled state during mutation 4. **Delete User (DELETE)** - ✅ Was working but added better feedback - ✅ Added disabled state during mutation 5. **UI Improvements** - Added success messages with auto-dismiss (3s) - Added error messages with auto-dismiss (5s) - Added loading states on all action buttons - Fixed role badge colors to use uppercase keys - Better form validation before API call 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
84c31f38a0
commit
b2f5d9968d
@ -9,15 +9,18 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { listUsers, createUser, updateUser, deleteUser } from '@/lib/api';
|
||||
import { useAuth } from '@/lib/context/auth-context';
|
||||
|
||||
export default function UsersManagementPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { user: currentUser } = useAuth();
|
||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||
const [inviteForm, setInviteForm] = useState({
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
role: 'user' as 'admin' | 'manager' | 'user' | 'viewer',
|
||||
role: 'USER' as 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER',
|
||||
password: '',
|
||||
phoneNumber: '',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
@ -30,8 +33,14 @@ export default function UsersManagementPage() {
|
||||
|
||||
const inviteMutation = useMutation({
|
||||
mutationFn: (data: typeof inviteForm & { organizationId: string }) => {
|
||||
// TODO: API should generate password or send invitation email
|
||||
return createUser(data as any);
|
||||
return createUser({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
role: data.role,
|
||||
organizationId: data.organizationId,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
@ -41,33 +50,45 @@ export default function UsersManagementPage() {
|
||||
email: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
role: 'user',
|
||||
role: 'USER',
|
||||
password: '',
|
||||
phoneNumber: '',
|
||||
});
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to invite user');
|
||||
setTimeout(() => setError(''), 5000);
|
||||
},
|
||||
});
|
||||
|
||||
const changeRoleMutation = useMutation({
|
||||
mutationFn: ({ id, role }: { id: string; role: 'admin' | 'manager' | 'user' | 'viewer' }) => {
|
||||
// TODO: Implement changeRole API endpoint
|
||||
return updateUser(id, { role } as any);
|
||||
mutationFn: ({ id, role }: { id: string; role: 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER' }) => {
|
||||
return updateUser(id, { role });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setSuccess('Role updated successfully');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to update role');
|
||||
setTimeout(() => setError(''), 5000);
|
||||
},
|
||||
});
|
||||
|
||||
const toggleActiveMutation = useMutation({
|
||||
mutationFn: ({ id, isActive }: { id: string; isActive: boolean }) => {
|
||||
// TODO: Implement activate/deactivate API endpoints
|
||||
return updateUser(id, { isActive: !isActive } as any);
|
||||
return updateUser(id, { isActive: !isActive });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setSuccess('User status updated successfully');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to update user status');
|
||||
setTimeout(() => setError(''), 5000);
|
||||
},
|
||||
});
|
||||
|
||||
@ -75,18 +96,34 @@ export default function UsersManagementPage() {
|
||||
mutationFn: (id: string) => deleteUser(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||
setSuccess('User deleted successfully');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to delete user');
|
||||
setTimeout(() => setError(''), 5000);
|
||||
},
|
||||
});
|
||||
|
||||
const handleInvite = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
// TODO: Get actual organizationId from auth context
|
||||
inviteMutation.mutate({ ...inviteForm, organizationId: 'default-org-id' });
|
||||
|
||||
if (!currentUser?.organizationId) {
|
||||
setError('Organization ID not found. Please log in again.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inviteForm.password || inviteForm.password.length < 8) {
|
||||
setError('Password must be at least 8 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
inviteMutation.mutate({ ...inviteForm, organizationId: currentUser.organizationId });
|
||||
};
|
||||
|
||||
const handleRoleChange = (userId: string, newRole: string) => {
|
||||
changeRoleMutation.mutate({ id: userId, role: newRole as any });
|
||||
changeRoleMutation.mutate({ id: userId, role: newRole as 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER' });
|
||||
};
|
||||
|
||||
const handleToggleActive = (userId: string, isActive: boolean) => {
|
||||
@ -107,10 +144,10 @@ export default function UsersManagementPage() {
|
||||
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
admin: 'bg-red-100 text-red-800',
|
||||
manager: 'bg-blue-100 text-blue-800',
|
||||
user: 'bg-green-100 text-green-800',
|
||||
viewer: 'bg-gray-100 text-gray-800',
|
||||
ADMIN: 'bg-red-100 text-red-800',
|
||||
MANAGER: 'bg-blue-100 text-blue-800',
|
||||
USER: 'bg-green-100 text-green-800',
|
||||
VIEWER: 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
return colors[role] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
@ -205,21 +242,23 @@ export default function UsersManagementPage() {
|
||||
className={`text-xs font-semibold rounded-full px-3 py-1 ${getRoleBadgeColor(
|
||||
user.role
|
||||
)}`}
|
||||
disabled={changeRoleMutation.isPending}
|
||||
>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="manager">Manager</option>
|
||||
<option value="user">User</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="MANAGER">Manager</option>
|
||||
<option value="USER">User</option>
|
||||
<option value="VIEWER">Viewer</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => handleToggleActive(user.id, user.isActive)}
|
||||
disabled={toggleActiveMutation.isPending}
|
||||
className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||
user.isActive
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
: 'bg-red-100 text-red-800 hover:bg-red-200'
|
||||
}`}
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{user.isActive ? 'Active' : 'Inactive'}
|
||||
</button>
|
||||
@ -230,7 +269,8 @@ export default function UsersManagementPage() {
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleDelete(user.id)}
|
||||
className="text-red-600 hover:text-red-900 ml-4"
|
||||
disabled={deleteMutation.isPending}
|
||||
className="text-red-600 hover:text-red-900 ml-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@ -335,6 +375,22 @@ export default function UsersManagementPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={8}
|
||||
value={inviteForm.password}
|
||||
onChange={e => setInviteForm({ ...inviteForm, password: e.target.value })}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
|
||||
placeholder="Minimum 8 characters"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Provide a temporary password (minimum 8 characters). User can change it after first login.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||
<input
|
||||
@ -352,14 +408,11 @@ export default function UsersManagementPage() {
|
||||
onChange={e => setInviteForm({ ...inviteForm, role: e.target.value as any })}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="manager">Manager</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
<option value="USER">User</option>
|
||||
<option value="MANAGER">Manager</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="VIEWER">Viewer</option>
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
A temporary password will be sent to the user's email
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user