From b2f5d9968d2b0a320f6fe7d3fefa96ff04c24016 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Nov 2025 23:35:10 +0100 Subject: [PATCH] fix: repair user management CRUD operations (create, update, delete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../app/dashboard/settings/users/page.tsx | 111 +++++++++++++----- 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/apps/frontend/app/dashboard/settings/users/page.tsx b/apps/frontend/app/dashboard/settings/users/page.tsx index bb16dc9..b2e8679 100644 --- a/apps/frontend/app/dashboard/settings/users/page.tsx +++ b/apps/frontend/app/dashboard/settings/users/page.tsx @@ -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 = { - 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} > - - - - + + + + @@ -230,7 +269,8 @@ export default function UsersManagementPage() { @@ -335,6 +375,22 @@ export default function UsersManagementPage() { /> +
+ + 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" + /> +

+ Provide a temporary password (minimum 8 characters). User can change it after first login. +

+
+
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" > - - - - + + + + -

- A temporary password will be sent to the user's email -