diff --git a/src/app/components/UserDetailModal.tsx b/src/app/components/UserDetailModal.tsx index 46439e3..9f411f7 100644 --- a/src/app/components/UserDetailModal.tsx +++ b/src/app/components/UserDetailModal.tsx @@ -53,6 +53,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated const [selectedStatus, setSelectedStatus] = useState('pending') const token = useAuthStore(state => state.accessToken) + const [allPermissions, setAllPermissions] = useState>([]) + const [permissionsLoading, setPermissionsLoading] = useState(false) + const [permissionsError, setPermissionsError] = useState(null) + const [selectedPermissions, setSelectedPermissions] = useState([]) + const [permissionsSaving, setPermissionsSaving] = useState(false) + // Contract preview state (lazy-loaded, per contract type) const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract') const [previewState, setPreviewState] = useState({ @@ -86,6 +92,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated useEffect(() => { if (isOpen && userId && token) { fetchUserDetails() + fetchAllPermissions() loadContractFiles() } }, [isOpen, userId, token]) @@ -104,6 +111,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated }) setContractFiles({ contract: [], gdpr: [] }) setSelectedFile({}) + setPermissionsError(null) }, [isOpen, userId]) useEffect(() => { @@ -112,6 +120,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated } }, [userDetails]) + useEffect(() => { + if (userDetails?.permissions) { + setSelectedPermissions(userDetails.permissions.map(p => p.name)) + } + }, [userDetails]) + const fetchUserDetails = async () => { if (!userId || !token) return @@ -134,6 +148,58 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated } } + const fetchAllPermissions = async () => { + if (!token) return + setPermissionsLoading(true) + setPermissionsError(null) + try { + const response = await AdminAPI.getPermissions(token) + if (response.success) { + setAllPermissions(response.permissions || []) + } else { + throw new Error(response.message || 'Failed to fetch permissions') + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch permissions' + setPermissionsError(errorMessage) + console.error('UserDetailModal.fetchAllPermissions error:', err) + } finally { + setPermissionsLoading(false) + } + } + + const togglePermission = (permName: string) => { + setSelectedPermissions(prev => { + if (prev.includes(permName)) { + return prev.filter(p => p !== permName) + } + return [...prev, permName] + }) + } + + const handleSavePermissions = async () => { + if (!userId || !token) return + setPermissionsSaving(true) + setPermissionsError(null) + try { + const response = await AdminAPI.updateUserPermissions(token, userId, selectedPermissions) + if (response.success) { + await fetchUserDetails() + if (onUserUpdated) { + onUserUpdated() + } + } else { + throw new Error(response.message || 'Failed to update permissions') + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to update permissions' + setPermissionsError(errorMessage) + console.error('UserDetailModal.handleSavePermissions error:', err) + } finally { + setPermissionsSaving(false) + } + } + const handleStatusChange = async (newStatus: UserStatus) => { if (!userId || !token || newStatus === selectedStatus) return @@ -794,42 +860,69 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated )} {/* Permissions */} - {userDetails.permissions.length > 0 && ( -
-
-

- - Permissions ({userDetails.permissions.length}) -

-
-
-
- {userDetails.permissions.map((perm) => ( -
- {perm.is_active ? ( - - ) : ( - - )} -
-
{perm.name}
- {perm.description && ( -
{perm.description}
- )} -
-
- ))} -
-
+
+
+

+ + Permissions ({selectedPermissions.length}) +

+
- )} +
+ {permissionsError && ( +
+ {permissionsError} +
+ )} + {permissionsLoading ? ( +
+
+ Loading permissions… +
+ ) : allPermissions.length === 0 ? ( +
No permissions available.
+ ) : ( +
+ {allPermissions.map((perm) => { + const checked = selectedPermissions.includes(perm.name) + const disabled = !perm.is_active + return ( + + ) + })} +
+ )} +
+
{/* Action Buttons */}
diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts index b4da925..c8a515a 100644 --- a/src/app/utils/api.ts +++ b/src/app/utils/api.ts @@ -32,6 +32,9 @@ export const API_ENDPOINTS = { // Documents USER_DOCUMENTS: '/api/users/:id/documents', + + // Permissions + PERMISSIONS_LIST: '/api/permissions', // Admin ADMIN_USERS: '/api/admin/users/:id/full', @@ -46,6 +49,7 @@ export const API_ENDPOINTS = { ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id', ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id', ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id', + ADMIN_UPDATE_USER_PERMISSIONS: '/api/admin/users/:id/permissions', // Pools (admin) ADMIN_POOL_MEMBERS: '/api/admin/pools/:id/members', // Coffee products (admin) @@ -395,6 +399,25 @@ export class AdminAPI { return response.json() } + static async getPermissions(token: string) { + const response = await ApiClient.get(API_ENDPOINTS.PERMISSIONS_LIST, token) + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to fetch permissions' })) + throw new Error(error.message || 'Failed to fetch permissions') + } + return response.json() + } + + static async updateUserPermissions(token: string, userId: string, permissions: string[]) { + const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_PERMISSIONS.replace(':id', userId) + const response = await ApiClient.put(endpoint, { permissions }, token) + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to update user permissions' })) + throw new Error(error.message || 'Failed to update user permissions') + } + return response.json() + } + // Coffee products (admin) static async listCoffee(token: string) { const response = await ApiClient.get(API_ENDPOINTS.ADMIN_COFFEE_LIST, token)