import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import { setUserDataCache, getUserDataCache, clearUserDataCache } from '../utils/userCache'; import api from '../api'; import { usePermissions, type UserPermissions } from './usePermissions'; import { fetchCurrentUser as fetchCurrentUserApi, logoutUser as logoutUserApi, fetchUsers as fetchUsersApi, fetchUserById as fetchUserByIdApi, createUser as createUserApi, updateUser as updateUserApi, deleteUser as deleteUserApi, sendPasswordLink as sendPasswordLinkApi, type User, type UserUpdateData, type AttributeDefinition, type PaginationParams } from '../api/userApi'; // Re-export types for backward compatibility export type { User, UserUpdateData, AttributeDefinition, PaginationParams }; // Current user hook export function useCurrentUser() { const [user, setUser] = useState(null); const { request, isLoading, error } = useApiRequest(); const fetchCurrentUser = async (retryCount = 0) => { try { // Check if we already have user data in sessionStorage cache const cachedUser = getUserDataCache(); if (cachedUser && cachedUser.username) { // Check if cached user has roleLabels - if empty, refetch from API const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; if (!hasRoleLabels) { console.warn('โš ๏ธ Cached user data has no roleLabels, refetching from API:', { username: cachedUser.username, roleLabels: cachedUser.roleLabels }); // Clear cache and continue to fetch from API clearUserDataCache(); } else { // Use cached user data - permissions are checked via RBAC API, not client-side setUser(cachedUser); console.log('โœ… Using cached user data from sessionStorage (persists during session):', { username: cachedUser.username, roleLabels: cachedUser.roleLabels }); return; } } // JWT tokens are now stored in httpOnly cookies, so we fetch user data from API console.log('๐Ÿช JWT tokens are in httpOnly cookies, fetching user data from API'); // Determine the correct endpoint based on authentication authority const authAuthority = sessionStorage.getItem('auth_authority'); let endpoint = '/api/local/me'; if (authAuthority === 'msft') { endpoint = '/api/msft/me'; } else if (authAuthority === 'google') { endpoint = '/api/google/me'; } console.log('๐Ÿ” Fetching user data from API:', { endpoint, authAuthority, hasAuthCookies: document.cookie.includes('access_token') || document.cookie.includes('refresh_token') }); // Add a small delay to ensure cookies are properly set after authentication if (authAuthority === 'msft' || authAuthority === 'google') { console.log('โณ Adding delay for OAuth authentication cookie propagation...'); await new Promise(resolve => setTimeout(resolve, 500)); } const data = await fetchCurrentUserApi(request, authAuthority || undefined); // Log full response for debugging console.log('๐Ÿ“ฆ User data received from API:', { username: data?.username, roleLabels: data?.roleLabels, hasRoleLabels: !!data?.roleLabels, roleLabelsLength: Array.isArray(data?.roleLabels) ? data.roleLabels.length : 0, roleLabelsContent: Array.isArray(data?.roleLabels) ? data.roleLabels : 'not an array', allKeys: data ? Object.keys(data) : [], fullData: JSON.stringify(data, null, 2) }); // Always cache user data - permissions are checked via RBAC API, not client-side // roleLabels are optional metadata for display/logging purposes if (!data || !data.username) { console.error('โŒ User data from API is invalid:', { username: data?.username, dataKeys: data ? Object.keys(data) : [], fullResponse: data }); throw new Error('Invalid user data received from API'); } // Check if API returned roleLabels - if not, log warning but still cache const hasRoleLabels = Array.isArray(data.roleLabels) && data.roleLabels.length > 0; if (!hasRoleLabels) { console.warn('โš ๏ธ User data from API has no roleLabels - this may cause RBAC issues:', { username: data.username, roleLabels: data.roleLabels, allKeys: Object.keys(data), fullResponse: JSON.stringify(data, null, 2) }); // Still cache it, but log the issue - backend RBAC should handle permissions // However, if backend expects roleLabels, this will cause problems } // Cache user data (permissions are checked via RBAC API) setUserDataCache(data); console.log('โœ… User data fetched from API and cached in sessionStorage (secure):', { username: data.username, roleLabels: data.roleLabels, roleLabelsLength: Array.isArray(data.roleLabels) ? data.roleLabels.length : 0, hasRoleLabels }); setUser(data); } catch (error: any) { console.error('โŒ Failed to fetch user data:', error); // Display debug information if this is a Microsoft/Google auth failure const authAuthority = sessionStorage.getItem('auth_authority'); if (authAuthority === 'msft' || authAuthority === 'google') { console.log('๐Ÿ”ง Current state:', { authAuthority, currentCookies: document.cookie || 'No cookies', hasAccessToken: document.cookie.includes('access_token'), hasRefreshToken: document.cookie.includes('refresh_token'), retryCount, error: error.message }); } // If authentication failed and we haven't retried yet, try again after a delay const isOAuth = authAuthority === 'msft' || authAuthority === 'google'; const maxRetries = isOAuth ? 2 : 0; // Only retry for OAuth if (retryCount < maxRetries && (error.message?.includes('Not authenticated') || error.message?.includes('Request aborted'))) { console.log(`๐Ÿ”„ Retrying user data fetch (attempt ${retryCount + 1}/${maxRetries + 1}) in 2 seconds...`); setTimeout(() => { fetchCurrentUser(retryCount + 1); }, 2000); return; } // If all retries failed or this is not a retryable error setUser(null); clearUserDataCache(); // If authentication failed after all retries, clear auth data if (error.message?.includes('Not authenticated') || error.message?.includes('401')) { console.log('๐Ÿ” Authentication failed after retries - clearing auth data'); // Clear auth authority sessionStorage.removeItem('auth_authority'); clearUserDataCache(); // Trigger a redirect to login by forcing a page reload setTimeout(() => { window.location.href = '/login'; }, 1000); } } }; const logout = async (msalInstance?: any) => { if (!user) { throw new Error('No user to logout'); } try { // Determine the correct logout endpoint based on authentication authority // Note: logoutEndpoint is determined by logoutUserApi based on authenticationAuthority await logoutUserApi(request, user.authenticationAuthority); console.log('โœ… Logout API call completed, waiting for browser to process cookies...'); // CRITICAL: Wait for browser to process Set-Cookie headers from logout response // This gives the browser time to clear httpOnly cookies before redirect await new Promise(resolve => setTimeout(resolve, 1000)); // Clear user state after successful logout setUser(null); // CRITICAL: Clear all authentication data BEFORE any redirects // This ensures cleanup happens even if MSAL redirect interrupts the process console.log('๐Ÿงน Starting comprehensive cleanup...'); // Clear user data cache from sessionStorage clearUserDataCache(); // Clear auth authority from sessionStorage sessionStorage.removeItem('auth_authority'); // Clear MSAL cache tokens from localStorage // MSAL stores tokens with keys starting with 'msal.' const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && ( key.startsWith('msal.') || key === 'auth_token' || key === 'refresh_token' || key.includes('token') || key.includes('auth') || key.includes('msal') )) { keysToRemove.push(key); } } keysToRemove.forEach(key => { console.log('๐Ÿ—‘๏ธ Removing token:', key); localStorage.removeItem(key); }); // Clear ALL MSAL cache data (including account keys, token keys, version) const msalKeysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('msal.')) { msalKeysToRemove.push(key); } } msalKeysToRemove.forEach(key => { console.log('๐Ÿ—‘๏ธ Removing MSAL cache:', key); localStorage.removeItem(key); }); // Clear sessionStorage as well (CSRF tokens, etc.) sessionStorage.clear(); // Clear cookies as backup (in case backend doesn't clear them properly) // Note: This only works for cookies that are accessible to JavaScript console.log('๐Ÿช Checking cookies for cleanup...'); console.log('๐Ÿช All cookies:', document.cookie); const cookies = document.cookie.split(";"); console.log('๐Ÿช Cookie count:', cookies.length); cookies.forEach(function(c) { const cookieName = c.split("=")[0].trim(); console.log('๐Ÿช Checking cookie:', cookieName); if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) { console.log('๐Ÿ—‘๏ธ Clearing cookie:', cookieName); document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; } }); console.log('๐Ÿช Cookies after cleanup attempt:', document.cookie); console.log('โœ… Cleanup completed'); // Handle MSAL logout for Microsoft authentication if (user.authenticationAuthority === 'msft' && msalInstance) { try { console.log('๐Ÿ”„ Starting MSAL logout redirect...'); await msalInstance.logoutRedirect({ onRedirectNavigate: () => { console.log('๐Ÿ”„ MSAL redirect initiated - cleanup already completed'); return true; } }); return; // MSAL will handle the redirect } catch (msalError) { console.error('MSAL logout failed:', msalError); // Continue with regular redirect if MSAL logout fails } } // Redirect to login or home page console.log('๐Ÿ”„ Redirecting to login page...'); window.location.href = '/login'; } catch (error) { console.error('Logout failed:', error); throw error; } }; useEffect(() => { // Try to load user from sessionStorage cache first for faster initial load const cachedUser = getUserDataCache(); if (cachedUser && cachedUser.username) { // Check if cached user has roleLabels - if empty, refetch from API const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; if (!hasRoleLabels) { console.warn('โš ๏ธ Cached user data has no roleLabels, refetching from API:', { username: cachedUser.username, roleLabels: cachedUser.roleLabels }); // Clear cache and refetch clearUserDataCache(); fetchCurrentUser(); return; } // Use cached user data - permissions are checked via RBAC API setUser(cachedUser); console.log('โœ… Using cached user data from sessionStorage on mount (persists during session):', { username: cachedUser.username, roleLabels: cachedUser.roleLabels }); } // For OAuth authentication, wait a bit longer before fetching user data const authAuthority = sessionStorage.getItem('auth_authority'); const isOAuth = authAuthority === 'msft' || authAuthority === 'google'; if (isOAuth) { console.log('โณ OAuth authentication detected, delaying user data fetch...'); // Wait for authentication cookies to be properly set const timer = setTimeout(() => { fetchCurrentUser(); }, 1000); return () => clearTimeout(timer); } else { // For local authentication, fetch immediately fetchCurrentUser(); } }, []); return { user, error, isLoading, refetch: fetchCurrentUser, logout }; } // Re-export AttributeOption for backward compatibility export interface AttributeOption { value: string | number; label: string | { [key: string]: string }; } // Organization users hook (list, update, delete) - following prompts/workflows pattern export function useOrgUsers() { const [users, setUsers] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { try { const response = await api.get('/api/attributes/User'); // Extract attributes from response let attrs: AttributeDefinition[] = []; if (response.data?.attributes && Array.isArray(response.data.attributes)) { attrs = response.data.attributes; } else if (Array.isArray(response.data)) { attrs = response.data; } else if (response.data && typeof response.data === 'object') { const keys = Object.keys(response.data); for (const key of keys) { if (Array.isArray(response.data[key])) { attrs = response.data[key]; break; } } } setAttributes(attrs); return attrs; } catch (error: any) { // Don't log 429 errors as errors (they're rate limit warnings) if (error.response?.status === 429) { console.warn('Rate limit exceeded while fetching user attributes. Please wait.'); } else if (error.response?.status !== 401) { // Only log non-auth errors (401 is expected when not logged in) console.error('Error fetching attributes:', error); } setAttributes([]); return []; } }, []); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'User'); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchUsers = useCallback(async (params?: PaginationParams) => { try { const requestParams: any = {}; // Build pagination object if provided if (params) { const paginationObj: any = {}; if (params.page !== undefined) paginationObj.page = params.page; if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); } } const data = await fetchUsersApi(request, params); // Handle paginated response if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; setUsers(items); if (data.pagination) { setPagination(data.pagination); } } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; setUsers(items); setPagination(null); } } catch (error: any) { // Error is already handled by useApiRequest setUsers([]); setPagination(null); } }, [request]); // Optimistically remove a user from the local state const removeOptimistically = (userId: string) => { setUsers(prevUsers => prevUsers.filter(user => user.id !== userId)); }; // Optimistically update a user in the local state const updateOptimistically = (userId: string, updateData: Partial) => { setUsers(prevUsers => prevUsers.map(user => user.id === userId ? { ...user, ...updateData } : user ) ); }; // Fetch a single user by ID const fetchUserById = useCallback(async (userId: string): Promise => { return await fetchUserByIdApi(request, userId); }, [request]); // Generate edit fields from attributes dynamically const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; editable?: boolean; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; // For options that need to be fetched (e.g., "user.role") }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { // Filter out non-editable fields based on readonly/editable flags if (attr.readonly === true || attr.editable === false) { return false; // Don't show readonly fields in edit form } // Also filter out common non-editable fields const nonEditableFields = ['id', 'mandateId', '_createdBy', '_hideDelete']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map backend attribute type to form field type let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; // Handle readonly fields if (attr.readonly === true || attr.editable === false) { fieldType = 'readonly'; } // Map backend types to form field types else if (attr.type === 'checkbox') { fieldType = 'boolean'; } else if (attr.type === 'email') { fieldType = 'email'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'select') { fieldType = 'enum'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attr.options; } } else if (attr.type === 'textarea') { fieldType = 'textarea'; } else if (attr.type === 'text') { // Check if it should be textarea based on name if (attr.name.toLowerCase().includes('description') || attr.name.toLowerCase().includes('content') || attr.name.toLowerCase().includes('note')) { fieldType = 'textarea'; } else { fieldType = 'string'; } } // Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union // If needed, they should be handled via type casting: (attr as any).type === 'boolean' // Define validators and required fields let required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; let minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; // Email validation if (fieldType === 'email') { validator = (value: any) => { if (required && (!value || (typeof value === 'string' && value.trim() === ''))) { return 'Email cannot be empty'; } if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { return 'Invalid email format'; } return null; }; } // Textarea settings else if (fieldType === 'textarea') { minRows = 4; maxRows = 8; if (attr.name.toLowerCase().includes('content')) { minRows = 6; maxRows = 12; } } // Multiselect validation else if (fieldType === 'multiselect' && required) { validator = (value: any[]) => { if (!value || !Array.isArray(value) || value.length === 0) { return `${attr.label} is required`; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: (attr as any).editable !== false && (attr as any).readonly !== true, required, validator, minRows, maxRows, options, optionsReference }; }); return editableFields; }, [attributes]); // Generate create fields from attributes dynamically // For users, we add a password field that's not in the backend attributes (since passwords are hashed) const generateCreateFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; placeholder?: string; }> => { if (!attributes || attributes.length === 0) { return []; } const createFields = attributes .filter(attr => { // Filter out non-editable fields and auto-generated fields for create forms if (attr.readonly === true || attr.editable === false) { return false; } // Filter out ID fields and other auto-generated fields const nonEditableFields = ['id', 'mandateId', '_createdBy', '_hideDelete', 'authenticationAuthority']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map backend attribute type to form field type let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; // Map backend types to form field types // Cast to string to handle all possible backend type values const attrType = attr.type as string; if (attrType === 'checkbox' || attrType === 'boolean') { fieldType = 'boolean'; } else if (attrType === 'email') { fieldType = 'email'; } else if (attrType === 'timestamp' || attrType === 'date' || attrType === 'time') { fieldType = 'date'; } else if (attrType === 'select' || attrType === 'enum') { fieldType = 'enum'; if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attrType === 'multiselect') { fieldType = 'multiselect'; if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attrType === 'textarea') { fieldType = 'textarea'; } // Determine if required and build validator const required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; // Email validation if (attr.type === 'email') { validator = (value: any) => { if (required && (!value || (typeof value === 'string' && value.trim() === ''))) { return 'Email cannot be empty'; } if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { return 'Invalid email format'; } return null; }; } // Required string validation else if (required && fieldType === 'string') { validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return `${attr.label} is required`; } return null; }; } // Multiselect validation else if (fieldType === 'multiselect' && required) { validator = (value: any[]) => { if (!value || !Array.isArray(value) || value.length === 0) { return `${attr.label} is required`; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, required, validator, options, optionsReference }; }); // Note: Password field removed - users are created without password // Admin can send password setup link after user creation using handleSendPasswordLink return createFields; }, [attributes]); // Ensure attributes are loaded - can be called by EditActionButton const ensureAttributesLoaded = useCallback(async () => { // Don't fetch attributes if user is not authenticated (prevents 401 errors) const currentUser = getUserDataCache(); if (!currentUser) { return []; } if (attributes && attributes.length > 0) { return attributes; } const fetchedAttributes = await fetchAttributes(); return fetchedAttributes; }, [attributes, fetchAttributes]); // Fetch attributes and permissions on mount (only if user is authenticated) useEffect(() => { const currentUser = getUserDataCache(); if (currentUser) { fetchAttributes(); fetchPermissions(); } }, [fetchAttributes, fetchPermissions]); // Initial fetch useEffect(() => { fetchUsers(); }, [fetchUsers]); return { data: users, loading, error, refetch: fetchUsers, removeOptimistically, updateOptimistically, attributes, permissions, pagination, fetchUserById, generateEditFieldsFromAttributes, generateCreateFieldsFromAttributes, ensureAttributesLoaded }; } // User operations hook export function useUserOperations() { const [deletingUsers, setDeletingUsers] = useState>(new Set()); const [editingUsers, setEditingUsers] = useState>(new Set()); const [sendingPasswordLink, setSendingPasswordLink] = useState>(new Set()); const [creatingUser, setCreatingUser] = useState(false); const { request, isLoading } = useApiRequest(); const [deleteError, setDeleteError] = useState(null); const [createError, setCreateError] = useState(null); const [updateError, setUpdateError] = useState(null); const [passwordLinkError, setPasswordLinkError] = useState(null); const handleUserDelete = async (userId: string) => { setDeleteError(null); setDeletingUsers(prev => new Set(prev).add(userId)); try { await deleteUserApi(request, userId); await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { setDeleteError(error.message); return false; } finally { setDeletingUsers(prev => { const newSet = new Set(prev); newSet.delete(userId); return newSet; }); } }; const handleUserCreate = async (userData: Omit) => { setCreateError(null); setCreatingUser(true); try { const currentUserData = getUserDataCache(); const mandateId = currentUserData?.mandateId || ''; const requestBody = { mandateId: mandateId, ...userData }; const newUser = await createUserApi(request, requestBody); return { success: true, userData: newUser }; } catch (error: any) { setCreateError(error.message); return { success: false, error: error.message }; } finally { setCreatingUser(false); } }; // Send password setup link to a user (admin function) const handleSendPasswordLink = async (userId: string) => { setPasswordLinkError(null); setSendingPasswordLink(prev => new Set(prev).add(userId)); try { // Get frontend URL from current window location const frontendUrl = `${window.location.protocol}//${window.location.host}`; const result = await sendPasswordLinkApi(request, userId, frontendUrl); return { success: true, message: result.message, email: result.email }; } catch (error: any) { const errorMessage = error.response?.data?.detail || error.message || 'Failed to send password link'; setPasswordLinkError(errorMessage); return { success: false, error: errorMessage }; } finally { setSendingPasswordLink(prev => { const newSet = new Set(prev); newSet.delete(userId); return newSet; }); } }; const handleUserUpdate = async (userId: string, updateData: UserUpdateData, _originalData?: any) => { setUpdateError(null); setEditingUsers(prev => new Set(prev).add(userId)); try { const currentUserData = getUserDataCache(); const mandateId = currentUserData?.mandateId || ''; const requestBody = { mandateId: mandateId, ...updateData }; const updatedUser = await updateUserApi(request, userId, requestBody); return { success: true, userData: updatedUser }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update user'; const statusCode = error.response?.status; setUpdateError(errorMessage); return { success: false, error: errorMessage, statusCode, isPermissionError: statusCode === 403, isValidationError: statusCode === 400 }; } finally { setEditingUsers(prev => { const newSet = new Set(prev); newSet.delete(userId); return newSet; }); } }; // Generic inline update handler for FormGeneratorTable // Must merge changes with existing row data because backend requires full object // The existingRow parameter is passed from FormGeneratorTable which has access to row data const handleInlineUpdate = async (userId: string, changes: Partial, existingRow?: any) => { if (!existingRow) { throw new Error(`Existing row data required for inline update`); } // Merge changes with existing row data (backend requires full object with required fields) const mergedData: UserUpdateData = { username: existingRow.username, email: existingRow.email, enabled: existingRow.enabled, roleLabels: existingRow.roleLabels, ...changes }; const result = await handleUserUpdate(userId, mergedData); if (!result.success) { throw new Error(result.error || 'Failed to update'); } return result; }; return { deletingUsers, editingUsers, sendingPasswordLink, creatingUser, deleteError, createError, updateError, passwordLinkError, handleUserDelete, handleUserCreate, handleUserUpdate, handleInlineUpdate, handleSendPasswordLink, isLoading }; } // Individual user operations hook (for use when you don't need the full list) export function useUser() { const { request, isLoading, error } = useApiRequest(); const getUser = async (userId: string): Promise => { const user = await fetchUserByIdApi(request, userId); if (!user) { throw new Error('User not found'); } return user; }; const updateUser = async (userId: string, userData: UserUpdateData): Promise => { return await updateUserApi(request, userId, userData); }; const deleteUser = async (userId: string): Promise => { await deleteUserApi(request, userId); }; return { getUser, updateUser, deleteUser, isLoading, error }; }