diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt index eafc083..0b744bb 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt @@ -62,13 +62,12 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) { if (response.status.isSuccess()) { ApiResult.Success(response.body()) } else { - val errorMessage = try { - val errorBody: String = response.body() - "Failed to create contractor: $errorBody" + val errorBody = try { + response.body() } catch (e: Exception) { "Failed to create contractor" } - ApiResult.Error(errorMessage, response.status.value) + ApiResult.Error(errorBody, response.status.value) } } catch (e: Exception) { ApiResult.Error(e.message ?: "Unknown error occurred") @@ -86,13 +85,12 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) { if (response.status.isSuccess()) { ApiResult.Success(response.body()) } else { - val errorMessage = try { - val errorBody: String = response.body() - "Failed to update contractor: $errorBody" + val errorBody = try { + response.body() } catch (e: Exception) { "Failed to update contractor" } - ApiResult.Error(errorMessage, response.status.value) + ApiResult.Error(errorBody, response.status.value) } } catch (e: Exception) { ApiResult.Error(e.message ?: "Unknown error occurred") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt index 36d97fd..62fdca4 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt @@ -175,13 +175,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) { if (response.status.isSuccess()) { ApiResult.Success(response.body()) } else { - val errorMessage = try { - val errorBody: String = response.body() - "Failed to create document: $errorBody" + val errorBody = try { + response.body() } catch (e: Exception) { "Failed to create document" } - ApiResult.Error(errorMessage, response.status.value) + ApiResult.Error(errorBody, response.status.value) } } catch (e: Exception) { ApiResult.Error(e.message ?: "Unknown error occurred") @@ -284,13 +283,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) { if (response.status.isSuccess()) { ApiResult.Success(response.body()) } else { - val errorMessage = try { - val errorBody: String = response.body() - "Failed to update document: $errorBody" + val errorBody = try { + response.body() } catch (e: Exception) { "Failed to update document" } - ApiResult.Error(errorMessage, response.status.value) + ApiResult.Error(errorBody, response.status.value) } } catch (e: Exception) { ApiResult.Error(e.message ?: "Unknown error occurred") @@ -403,13 +401,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) { if (response.status.isSuccess()) { ApiResult.Success(response.body()) } else { - val errorMessage = try { - val errorBody: String = response.body() - "Failed to upload image: $errorBody" + val errorBody = try { + response.body() } catch (e: Exception) { "Failed to upload image" } - ApiResult.Error(errorMessage, response.status.value) + ApiResult.Error(errorBody, response.status.value) } } catch (e: Exception) { ApiResult.Error(e.message ?: "Unknown error occurred") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ApiResultHandler.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ApiResultHandler.kt index a02b074..283f01b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ApiResultHandler.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ApiResultHandler.kt @@ -125,7 +125,7 @@ fun ApiResult.HandleErrors( LaunchedEffect(this) { if (this@HandleErrors is ApiResult.Error) { - errorMessage = (this@HandleErrors as ApiResult.Error).message + errorMessage = com.mycrib.android.util.ErrorMessageParser.parse((this@HandleErrors as ApiResult.Error).message) showErrorDialog = true } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt index 765f27d..1dbeb46 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt @@ -13,7 +13,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.mycrib.android.ui.components.AddNewTaskWithResidenceDialog +import com.mycrib.android.ui.components.ApiResultHandler import com.mycrib.android.ui.components.CompleteTaskDialog +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.task.TaskCard import com.mycrib.android.ui.components.task.DynamicTaskKanbanView import com.mycrib.android.viewmodel.ResidenceViewModel @@ -59,6 +61,12 @@ fun AllTasksScreen( } // Handle task creation success + // Handle errors for task creation + createTaskState.HandleErrors( + onRetry = { /* Retry handled in dialog */ }, + errorTitle = "Failed to Create Task" + ) + LaunchedEffect(createTaskState) { println("AllTasksScreen: createTaskState changed to $createTaskState") when (createTaskState) { @@ -68,15 +76,7 @@ fun AllTasksScreen( viewModel.resetAddTaskState() viewModel.loadTasks() } - is ApiResult.Error -> { - println("AllTasksScreen: Task creation error: ${(createTaskState as ApiResult.Error).message}") - } - is ApiResult.Loading -> { - println("AllTasksScreen: Task creation loading") - } - else -> { - println("AllTasksScreen: Task creation idle") - } + else -> {} } } @@ -115,48 +115,12 @@ fun AllTasksScreen( ) } ) { paddingValues -> - when (tasksState) { - is ApiResult.Idle, is ApiResult.Loading -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = androidx.compose.ui.Alignment.Center - ) { - CircularProgressIndicator() - } - } - is ApiResult.Error -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = androidx.compose.ui.Alignment.Center - ) { - Column( - horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Icon( - Icons.Default.Error, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.error - ) - Text( - text = "Error: ${(tasksState as ApiResult.Error).message}", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { viewModel.loadTasks(forceRefresh = true) }) { - Text("Retry") - } - } - } - } - is ApiResult.Success -> { - val taskData = (tasksState as ApiResult.Success).data + ApiResultHandler( + state = tasksState, + onRetry = { viewModel.loadTasks(forceRefresh = true) }, + modifier = Modifier.padding(paddingValues), + errorTitle = "Failed to Load Tasks" + ) { taskData -> val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() } if (hasNoTasks) { @@ -263,10 +227,7 @@ fun AllTasksScreen( ) } } - - else -> {} } - } if (showCompleteDialog && selectedTask != null) { CompleteTaskDialog( @@ -303,7 +264,7 @@ fun AllTasksScreen( }, isLoading = createTaskState is ApiResult.Loading, errorMessage = if (createTaskState is ApiResult.Error) { - (createTaskState as ApiResult.Error).message + com.mycrib.android.util.ErrorMessageParser.parse((createTaskState as ApiResult.Error).message) } else null ) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorDetailScreen.kt index 3969cae..e35a854 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorDetailScreen.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.mycrib.android.ui.components.AddContractorDialog +import com.mycrib.android.ui.components.ApiResultHandler +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.viewmodel.ContractorViewModel import com.mycrib.shared.network.ApiResult @@ -39,6 +41,18 @@ fun ContractorDetailScreen( viewModel.loadContractorDetail(contractorId) } + // Handle errors for delete contractor + deleteState.HandleErrors( + onRetry = { viewModel.deleteContractor(contractorId) }, + errorTitle = "Failed to Delete Contractor" + ) + + // Handle errors for toggle favorite + toggleFavoriteState.HandleErrors( + onRetry = { viewModel.toggleFavorite(contractorId) }, + errorTitle = "Failed to Update Favorite" + ) + LaunchedEffect(deleteState) { if (deleteState is ApiResult.Success) { viewModel.resetDeleteState() @@ -94,18 +108,14 @@ fun ContractorDetailScreen( .padding(padding) .background(Color(0xFFF9FAFB)) ) { - when (val state = contractorState) { - is ApiResult.Loading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = Color(0xFF2563EB)) - } + ApiResultHandler( + state = contractorState, + onRetry = { viewModel.loadContractorDetail(contractorId) }, + errorTitle = "Failed to Load Contractor", + loadingContent = { + CircularProgressIndicator(color = Color(0xFF2563EB)) } - is ApiResult.Success -> { - val contractor = state.data - + ) { contractor -> LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), @@ -346,35 +356,8 @@ fun ContractorDetailScreen( } } } - is ApiResult.Error -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Default.ErrorOutline, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = Color(0xFFEF4444) - ) - Text(state.message, color = Color(0xFFEF4444)) - Button( - onClick = { viewModel.loadContractorDetail(contractorId) }, - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2563EB)) - ) { - Text("Retry") - } - } - } - } - else -> {} } } - } if (showEditDialog) { AddContractorDialog( diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt index 4b3bd0f..5f87998 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.mycrib.android.ui.components.AddContractorDialog +import com.mycrib.android.ui.components.ApiResultHandler +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.viewmodel.ContractorViewModel import com.mycrib.shared.models.ContractorSummary import com.mycrib.shared.network.ApiResult @@ -64,6 +66,18 @@ fun ContractorsScreen( ) } + // Handle errors for delete contractor + deleteState.HandleErrors( + onRetry = { /* Handled in UI */ }, + errorTitle = "Failed to Delete Contractor" + ) + + // Handle errors for toggle favorite + toggleFavoriteState.HandleErrors( + onRetry = { /* Handled in UI */ }, + errorTitle = "Failed to Update Favorite" + ) + LaunchedEffect(deleteState) { if (deleteState is ApiResult.Success) { viewModel.loadContractors() @@ -216,81 +230,25 @@ fun ContractorsScreen( } } - when (val state = contractorsState) { - is ApiResult.Loading -> { + ApiResultHandler( + state = contractorsState, + onRetry = { + viewModel.loadContractors( + specialty = selectedFilter, + isFavorite = if (showFavoritesOnly) true else null, + search = searchQuery.takeIf { it.isNotBlank() } + ) + }, + errorTitle = "Failed to Load Contractors", + loadingContent = { if (!isRefreshing) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) - } + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) } } - is ApiResult.Success -> { - val contractors = state.data.results + ) { state -> + val contractors = state.results - if (contractors.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Default.PersonAdd, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly) - "No contractors found" - else - "No contractors yet", - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) { - Text( - "Add your first contractor to get started", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall - ) - } - } - } - } else { - PullToRefreshBox( - isRefreshing = isRefreshing, - onRefresh = { - isRefreshing = true - viewModel.loadContractors( - specialty = selectedFilter, - isFavorite = if (showFavoritesOnly) true else null, - search = searchQuery.takeIf { it.isNotBlank() } - ) - }, - modifier = Modifier.fillMaxSize() - ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(contractors, key = { it.id }) { contractor -> - ContractorCard( - contractor = contractor, - onToggleFavorite = { viewModel.toggleFavorite(it) }, - onClick = { onNavigateToContractorDetail(it) } - ) - } - } - } - } - } - is ApiResult.Error -> { + if (contractors.isEmpty()) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -300,24 +258,55 @@ fun ContractorsScreen( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( - Icons.Default.ErrorOutline, + Icons.Default.PersonAdd, contentDescription = null, modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.error + tint = MaterialTheme.colorScheme.onSurfaceVariant ) - Text(state.message, color = MaterialTheme.colorScheme.error) - Button( - onClick = { viewModel.loadContractors() }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary + Text( + if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly) + "No contractors found" + else + "No contractors yet", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) { + Text( + "Add your first contractor to get started", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + } else { + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + viewModel.loadContractors( + specialty = selectedFilter, + isFavorite = if (showFavoritesOnly) true else null, + search = searchQuery.takeIf { it.isNotBlank() } + ) + }, + modifier = Modifier.fillMaxSize() + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(contractors, key = { it.id }) { contractor -> + ContractorCard( + contractor = contractor, + onToggleFavorite = { viewModel.toggleFavorite(it) }, + onClick = { onNavigateToContractorDetail(it) } ) - ) { - Text("Retry") } } } } - else -> {} } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt index 83b7f74..a71f546 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.ApiResultHandler +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.viewmodel.DocumentViewModel import com.mycrib.shared.models.* import com.mycrib.shared.network.ApiResult @@ -51,6 +53,12 @@ fun DocumentDetailScreen( documentViewModel.loadDocumentDetail(documentId) } + // Handle errors for document deletion + deleteState.HandleErrors( + onRetry = { documentViewModel.deleteDocument(documentId) }, + errorTitle = "Failed to Delete Document" + ) + // Handle successful deletion LaunchedEffect(deleteState) { if (deleteState is ApiResult.Success) { @@ -89,17 +97,11 @@ fun DocumentDetailScreen( .fillMaxSize() .padding(padding) ) { - when (val state = documentState) { - is ApiResult.Loading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - is ApiResult.Success -> { - val document = state.data + ApiResultHandler( + state = documentState, + onRetry = { documentViewModel.loadDocumentDetail(documentId) }, + errorTitle = "Failed to Load Document" + ) { document -> Column( modifier = Modifier .fillMaxSize() @@ -409,16 +411,8 @@ fun DocumentDetailScreen( } } } - is ApiResult.Error -> { - ErrorState( - message = state.message, - onRetry = { documentViewModel.loadDocumentDetail(documentId) } - ) - } - is ApiResult.Idle -> {} } } - } // Delete confirmation dialog if (showDeleteDialog) { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentFormScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentFormScreen.kt index 3c9aaf5..0b9f048 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentFormScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentFormScreen.kt @@ -239,7 +239,7 @@ fun DocumentFormScreen( } is ApiResult.Error -> { Text( - "Failed to load residences: ${(residencesState as ApiResult.Error).message}", + "Failed to load residences: ${com.mycrib.android.util.ErrorMessageParser.parse((residencesState as ApiResult.Error).message)}", color = MaterialTheme.colorScheme.error ) } @@ -596,7 +596,7 @@ fun DocumentFormScreen( ) ) { Text( - (operationState as ApiResult.Error).message, + com.mycrib.android.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message), modifier = Modifier.padding(12.dp), color = MaterialTheme.colorScheme.onErrorContainer ) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt index 54c2804..0746b7b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.viewmodel.ResidenceViewModel import com.mycrib.repository.LookupsRepository import com.mycrib.shared.models.* @@ -49,6 +50,12 @@ fun EditTaskScreen( var titleError by remember { mutableStateOf("") } var dueDateError by remember { mutableStateOf("") } + // Handle errors for task update + updateTaskState.HandleErrors( + onRetry = { /* Retry handled in UI */ }, + errorTitle = "Failed to Update Task" + ) + // Handle update state changes LaunchedEffect(updateTaskState) { when (updateTaskState) { @@ -279,7 +286,7 @@ fun EditTaskScreen( // Error message if (updateTaskState is ApiResult.Error) { Text( - text = (updateTaskState as ApiResult.Error).message, + text = com.mycrib.android.util.ErrorMessageParser.parse((updateTaskState as ApiResult.Error).message), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall ) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ForgotPasswordScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ForgotPasswordScreen.kt index dd0fe8f..b46310f 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ForgotPasswordScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ForgotPasswordScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.auth.AuthHeader import com.mycrib.android.ui.components.common.ErrorCard import com.mycrib.android.viewmodel.PasswordResetViewModel @@ -30,6 +31,15 @@ fun ForgotPasswordScreen( val forgotPasswordState by viewModel.forgotPasswordState.collectAsState() val currentStep by viewModel.currentStep.collectAsState() + // Handle errors for forgot password + forgotPasswordState.HandleErrors( + onRetry = { + viewModel.setEmail(email) + viewModel.requestPasswordReset(email) + }, + errorTitle = "Failed to Send Reset Code" + ) + // Handle automatic navigation to next step LaunchedEffect(currentStep) { when (currentStep) { @@ -40,7 +50,7 @@ fun ForgotPasswordScreen( } val errorMessage = when (forgotPasswordState) { - is ApiResult.Error -> (forgotPasswordState as ApiResult.Error).message + is ApiResult.Error -> com.mycrib.android.util.ErrorMessageParser.parse((forgotPasswordState as ApiResult.Error).message) else -> "" } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt index 9f9eacd..fcf221d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.theme.AppRadius import com.mycrib.android.viewmodel.ResidenceViewModel import com.mycrib.shared.network.ApiResult @@ -33,6 +34,12 @@ fun HomeScreen( viewModel.loadResidenceSummary() } + // Handle errors for loading summary + summaryState.HandleErrors( + onRetry = { viewModel.loadResidenceSummary() }, + errorTitle = "Failed to Load Summary" + ) + Scaffold( topBar = { TopAppBar( diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/LoginScreen.kt index 0af22fc..5374306 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/LoginScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.auth.AuthHeader import com.mycrib.android.ui.components.common.ErrorCard import com.mycrib.android.viewmodel.AuthViewModel @@ -38,6 +39,12 @@ fun LoginScreen( var passwordVisible by remember { mutableStateOf(false) } val loginState by viewModel.loginState.collectAsState() + // Handle errors for login + loginState.HandleErrors( + onRetry = { viewModel.login(username, password) }, + errorTitle = "Login Failed" + ) + // Handle login state changes LaunchedEffect(loginState) { when (loginState) { @@ -50,7 +57,7 @@ fun LoginScreen( } val errorMessage = when (loginState) { - is ApiResult.Error -> (loginState as ApiResult.Error).message + is ApiResult.Error -> com.mycrib.android.util.ErrorMessageParser.parse((loginState as ApiResult.Error).message) else -> "" } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt index 6e9a151..62f14e9 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.common.ErrorCard import com.mycrib.android.viewmodel.AuthViewModel import com.mycrib.shared.network.ApiResult @@ -36,6 +37,18 @@ fun ProfileScreen( val updateState by viewModel.updateProfileState.collectAsState() + // Handle errors for profile update + updateState.HandleErrors( + onRetry = { + viewModel.updateProfile( + firstName = firstName.ifBlank { null }, + lastName = lastName.ifBlank { null }, + email = email + ) + }, + errorTitle = "Failed to Update Profile" + ) + // Load current user data LaunchedEffect(Unit) { val token = TokenStorage.getToken() @@ -68,7 +81,7 @@ fun ProfileScreen( viewModel.resetUpdateProfileState() } is ApiResult.Error -> { - errorMessage = (updateState as ApiResult.Error).message + errorMessage = com.mycrib.android.util.ErrorMessageParser.parse((updateState as ApiResult.Error).message) isLoading = false successMessage = "" } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/RegisterScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/RegisterScreen.kt index dc27444..f65d745 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/RegisterScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/RegisterScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.auth.AuthHeader import com.mycrib.android.ui.components.common.ErrorCard import com.mycrib.android.viewmodel.AuthViewModel @@ -34,6 +35,13 @@ fun RegisterScreen( var isLoading by remember { mutableStateOf(false) } val createState by viewModel.registerState.collectAsState() + + // Handle errors for registration + createState.HandleErrors( + onRetry = { viewModel.register(username, email, password) }, + errorTitle = "Registration Failed" + ) + LaunchedEffect(createState) { when (createState) { is ApiResult.Success -> { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResetPasswordScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResetPasswordScreen.kt index 38cb3ef..9ee8a01 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResetPasswordScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResetPasswordScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.auth.AuthHeader import com.mycrib.android.ui.components.auth.RequirementItem import com.mycrib.android.ui.components.common.ErrorCard @@ -35,8 +36,14 @@ fun ResetPasswordScreen( val resetPasswordState by viewModel.resetPasswordState.collectAsState() val currentStep by viewModel.currentStep.collectAsState() + // Handle errors for password reset + resetPasswordState.HandleErrors( + onRetry = { viewModel.resetPassword(newPassword, confirmPassword) }, + errorTitle = "Password Reset Failed" + ) + val errorMessage = when (resetPasswordState) { - is ApiResult.Error -> (resetPasswordState as ApiResult.Error).message + is ApiResult.Error -> com.mycrib.android.util.ErrorMessageParser.parse((resetPasswordState as ApiResult.Error).message) else -> "" } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt index 2b91259..a45be0d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt @@ -120,7 +120,7 @@ fun ResidenceDetailScreen( // Handle generate report state // Handle errors for generate report generateReportState.HandleErrors( - onRetry = { residenceViewModel.generateMaintenanceReport(residenceId) }, + onRetry = { residenceViewModel.generateTasksReport(residenceId) }, errorTitle = "Failed to Generate Report" ) @@ -597,7 +597,7 @@ fun ResidenceDetailScreen( shape = RoundedCornerShape(12.dp) ) { Text( - text = "Error loading tasks: ${(tasksState as ApiResult.Error).message}", + text = "Error loading tasks: ${com.mycrib.android.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}", color = MaterialTheme.colorScheme.onErrorContainer, modifier = Modifier.padding(16.dp) ) @@ -691,5 +691,4 @@ fun ResidenceDetailScreen( } } } - } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt index 16e6e44..0b4d645 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt @@ -331,7 +331,7 @@ fun ResidenceFormScreen( // Error message if (operationState is ApiResult.Error) { Text( - text = (operationState as ApiResult.Error).message, + text = com.mycrib.android.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall ) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt index dbafe10..78360cc 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt @@ -399,5 +399,4 @@ fun ResidencesScreen( } } } - } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt index d3af6f8..4f26d12 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt @@ -39,7 +39,7 @@ fun TasksScreen( // Show error dialog when tasks fail to load LaunchedEffect(tasksState) { if (tasksState is ApiResult.Error) { - errorMessage = (tasksState as ApiResult.Error).message + errorMessage = com.mycrib.android.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message) showErrorDialog = true } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt index ea54801..731043e 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.auth.AuthHeader import com.mycrib.android.ui.components.common.ErrorCard import com.mycrib.android.viewmodel.AuthViewModel @@ -34,6 +35,12 @@ fun VerifyEmailScreen( val verifyState by viewModel.verifyEmailState.collectAsState() + // Handle errors for email verification + verifyState.HandleErrors( + onRetry = { viewModel.verifyEmail(code) }, + errorTitle = "Verification Failed" + ) + LaunchedEffect(verifyState) { when (verifyState) { is ApiResult.Success -> { @@ -41,7 +48,7 @@ fun VerifyEmailScreen( onVerifySuccess() } is ApiResult.Error -> { - errorMessage = (verifyState as ApiResult.Error).message + errorMessage = com.mycrib.android.util.ErrorMessageParser.parse((verifyState as ApiResult.Error).message) isLoading = false } is ApiResult.Loading -> { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyResetCodeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyResetCodeScreen.kt index 69eef24..a0afb08 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyResetCodeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyResetCodeScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.mycrib.android.ui.components.HandleErrors import com.mycrib.android.ui.components.auth.AuthHeader import com.mycrib.android.ui.components.common.ErrorCard import com.mycrib.android.viewmodel.PasswordResetViewModel @@ -31,6 +32,12 @@ fun VerifyResetCodeScreen( val verifyCodeState by viewModel.verifyCodeState.collectAsState() val currentStep by viewModel.currentStep.collectAsState() + // Handle errors for code verification + verifyCodeState.HandleErrors( + onRetry = { viewModel.verifyResetCode(email, code) }, + errorTitle = "Code Verification Failed" + ) + // Handle automatic navigation to next step LaunchedEffect(currentStep) { if (currentStep == com.mycrib.android.viewmodel.PasswordResetStep.RESET_PASSWORD) { @@ -39,7 +46,7 @@ fun VerifyResetCodeScreen( } val errorMessage = when (verifyCodeState) { - is ApiResult.Error -> (verifyCodeState as ApiResult.Error).message + is ApiResult.Error -> com.mycrib.android.util.ErrorMessageParser.parse((verifyCodeState as ApiResult.Error).message) else -> "" } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/util/ErrorMessageParser.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/util/ErrorMessageParser.kt new file mode 100644 index 0000000..daca24b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/util/ErrorMessageParser.kt @@ -0,0 +1,54 @@ +package com.mycrib.android.util + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +/** + * Utility object for parsing and cleaning error messages from API responses + */ +object ErrorMessageParser { + + /** + * Parses error messages to extract user-friendly text + * If the error message is JSON, extract relevant error details + * @param rawMessage The raw error message from the API + * @return A user-friendly error message + */ + fun parse(rawMessage: String): String { + val trimmed = rawMessage.trim() + + // Check if the message looks like JSON (starts with { or [) + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) { + // Not JSON, return as-is + return rawMessage + } + + // If it's JSON, it's not meant for user display + // Try to parse and extract meaningful error info + return try { + val jsonElement = Json.parseToJsonElement(trimmed) + + if (jsonElement is kotlinx.serialization.json.JsonObject) { + val json = jsonElement.jsonObject + + // Try to find common error fields + json["error"]?.jsonPrimitive?.content?.let { return it } + json["message"]?.jsonPrimitive?.content?.let { return it } + json["detail"]?.jsonPrimitive?.content?.let { return it } + + // Check if this looks like a data object (has id, title/name, etc) + // rather than an error response + if (json.containsKey("id") && (json.containsKey("title") || json.containsKey("name"))) { + return "Request failed. Please check your input and try again." + } + } + + // If we couldn't extract a message, return a generic error + "An error occurred. Please try again." + } catch (e: Exception) { + // JSON parsing failed, return generic error + "An error occurred. Please try again." + } + } +} diff --git a/iosApp/iosApp/Contractor/ContractorDetailView.swift b/iosApp/iosApp/Contractor/ContractorDetailView.swift index 2fd4a62..76959fe 100644 --- a/iosApp/iosApp/Contractor/ContractorDetailView.swift +++ b/iosApp/iosApp/Contractor/ContractorDetailView.swift @@ -213,6 +213,10 @@ struct ContractorDetailView: View { .onAppear { viewModel.loadContractorDetail(id: contractorId) } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.loadContractorDetail(id: contractorId) } + ) } private func deleteContractor() { diff --git a/iosApp/iosApp/Contractor/ContractorFormSheet.swift b/iosApp/iosApp/Contractor/ContractorFormSheet.swift index 9c63460..58ce918 100644 --- a/iosApp/iosApp/Contractor/ContractorFormSheet.swift +++ b/iosApp/iosApp/Contractor/ContractorFormSheet.swift @@ -281,6 +281,10 @@ struct ContractorFormSheet: View { loadContractorData() loadContractorSpecialties() } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { saveContractor() } + ) } } diff --git a/iosApp/iosApp/Contractor/ContractorViewModel.swift b/iosApp/iosApp/Contractor/ContractorViewModel.swift index d799fb1..beb6c41 100644 --- a/iosApp/iosApp/Contractor/ContractorViewModel.swift +++ b/iosApp/iosApp/Contractor/ContractorViewModel.swift @@ -57,7 +57,7 @@ class ContractorViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } break @@ -87,7 +87,7 @@ class ContractorViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } break @@ -119,7 +119,7 @@ class ContractorViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isCreating = false } sharedViewModel.resetCreateState() @@ -153,7 +153,7 @@ class ContractorViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isUpdating = false } sharedViewModel.resetUpdateState() @@ -187,7 +187,7 @@ class ContractorViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isDeleting = false } sharedViewModel.resetDeleteState() @@ -210,7 +210,7 @@ class ContractorViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) } sharedViewModel.resetToggleFavoriteState() completion(false) diff --git a/iosApp/iosApp/Contractor/ContractorsListView.swift b/iosApp/iosApp/Contractor/ContractorsListView.swift index d398f07..bde111b 100644 --- a/iosApp/iosApp/Contractor/ContractorsListView.swift +++ b/iosApp/iosApp/Contractor/ContractorsListView.swift @@ -166,6 +166,10 @@ struct ContractorsListView: View { .onChange(of: searchText) { newValue in loadContractors() } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { loadContractors() } + ) } private func loadContractors(forceRefresh: Bool = false) { diff --git a/iosApp/iosApp/Documents/DocumentViewModel.swift b/iosApp/iosApp/Documents/DocumentViewModel.swift index 76a8c97..9f1d8f4 100644 --- a/iosApp/iosApp/Documents/DocumentViewModel.swift +++ b/iosApp/iosApp/Documents/DocumentViewModel.swift @@ -57,7 +57,7 @@ class DocumentViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } break @@ -142,7 +142,7 @@ class DocumentViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } sharedViewModel.resetCreateState() @@ -219,7 +219,7 @@ class DocumentViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } sharedViewModel.resetUpdateState() @@ -251,7 +251,7 @@ class DocumentViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } sharedViewModel.resetDeleteState() diff --git a/iosApp/iosApp/Helpers/ErrorMessageParser.swift b/iosApp/iosApp/Helpers/ErrorMessageParser.swift new file mode 100644 index 0000000..1f894e6 --- /dev/null +++ b/iosApp/iosApp/Helpers/ErrorMessageParser.swift @@ -0,0 +1,50 @@ +import Foundation + +/// Utility for parsing and cleaning error messages from API responses +enum ErrorMessageParser { + + /// Parses error messages to extract user-friendly text + /// If the error message is JSON, extract relevant error details + /// - Parameter rawMessage: The raw error message from the API + /// - Returns: A user-friendly error message + static func parse(_ rawMessage: String) -> String { + // Check if the message looks like JSON (starts with { or [) + let trimmed = rawMessage.trimmingCharacters(in: .whitespacesAndNewlines) + guard trimmed.hasPrefix("{") || trimmed.hasPrefix("[") else { + // Not JSON, return as-is + return rawMessage + } + + // If it's JSON, it's not meant for user display + // Try to parse and extract meaningful error info + guard let data = trimmed.data(using: .utf8) else { + return "An error occurred. Please try again." + } + + do { + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + // Try to find common error fields + if let errorMsg = json["error"] as? String { + return errorMsg + } + if let message = json["message"] as? String { + return message + } + if let detail = json["detail"] as? String { + return detail + } + + // Check if this looks like a data object (has id, title, etc) + // rather than an error response + if json["id"] != nil && (json["title"] != nil || json["name"] != nil) { + return "Request failed. Please check your input and try again." + } + } + } catch { + // JSON parsing failed + } + + // If we couldn't parse or extract a message, return a generic error + return "An error occurred. Please try again." + } +} diff --git a/iosApp/iosApp/Login/LoginView.swift b/iosApp/iosApp/Login/LoginView.swift index 4c02d00..cf21fc9 100644 --- a/iosApp/iosApp/Login/LoginView.swift +++ b/iosApp/iosApp/Login/LoginView.swift @@ -265,6 +265,10 @@ struct LoginView: View { showPasswordReset = true } } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.login() } + ) } } diff --git a/iosApp/iosApp/Login/LoginViewModel.swift b/iosApp/iosApp/Login/LoginViewModel.swift index 88f10be..2024e6f 100644 --- a/iosApp/iosApp/Login/LoginViewModel.swift +++ b/iosApp/iosApp/Login/LoginViewModel.swift @@ -108,10 +108,10 @@ class LoginViewModel: ObservableObject { case 500...599: self.errorMessage = "Server error. Please try again later." default: - self.errorMessage = self.cleanErrorMessage(error.message) + self.errorMessage = ErrorMessageParser.parse(error.message) } } else { - self.errorMessage = self.cleanErrorMessage(error.message) + self.errorMessage = ErrorMessageParser.parse(error.message) } print("API Error: \(error.message)") diff --git a/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift b/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift index 0bde637..0d8ef95 100644 --- a/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift +++ b/iosApp/iosApp/PasswordReset/ForgotPasswordView.swift @@ -110,6 +110,10 @@ struct ForgotPasswordView: View { .onAppear { isEmailFocused = true } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.requestPasswordReset() } + ) } } } diff --git a/iosApp/iosApp/PasswordReset/PasswordResetViewModel.swift b/iosApp/iosApp/PasswordReset/PasswordResetViewModel.swift index cd338fb..18ebb71 100644 --- a/iosApp/iosApp/PasswordReset/PasswordResetViewModel.swift +++ b/iosApp/iosApp/PasswordReset/PasswordResetViewModel.swift @@ -290,10 +290,10 @@ class PasswordResetViewModel: ObservableObject { } else if message.contains("Invalid") && message.contains("token") { self.errorMessage = "Invalid or expired reset token. Please start over." } else { - self.errorMessage = message + self.errorMessage = ErrorMessageParser.parse(message) } } else { - self.errorMessage = errorResult.message + self.errorMessage = ErrorMessageParser.parse(errorResult.message) } print("API Error: \(errorResult.message)") diff --git a/iosApp/iosApp/PasswordReset/ResetPasswordView.swift b/iosApp/iosApp/PasswordReset/ResetPasswordView.swift index eff63ad..ef7c055 100644 --- a/iosApp/iosApp/PasswordReset/ResetPasswordView.swift +++ b/iosApp/iosApp/PasswordReset/ResetPasswordView.swift @@ -232,6 +232,10 @@ struct ResetPasswordView: View { .onAppear { focusedField = .newPassword } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.resetPassword() } + ) } } diff --git a/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift b/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift index d46818d..b6616f7 100644 --- a/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift +++ b/iosApp/iosApp/PasswordReset/VerifyResetCodeView.swift @@ -158,6 +158,10 @@ struct VerifyResetCodeView: View { .onAppear { isCodeFocused = true } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.verifyResetCode() } + ) } } } diff --git a/iosApp/iosApp/Profile/ProfileView.swift b/iosApp/iosApp/Profile/ProfileView.swift index 949f200..93d0f00 100644 --- a/iosApp/iosApp/Profile/ProfileView.swift +++ b/iosApp/iosApp/Profile/ProfileView.swift @@ -132,6 +132,10 @@ struct ProfileView: View { .onChange(of: viewModel.email) { _, _ in viewModel.clearMessages() } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.updateProfile() } + ) } } } diff --git a/iosApp/iosApp/Profile/ProfileViewModel.swift b/iosApp/iosApp/Profile/ProfileViewModel.swift index fcb8043..f1d69f3 100644 --- a/iosApp/iosApp/Profile/ProfileViewModel.swift +++ b/iosApp/iosApp/Profile/ProfileViewModel.swift @@ -60,7 +60,7 @@ class ProfileViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoadingUser = false } sharedViewModel.resetCurrentUserState() @@ -114,7 +114,7 @@ class ProfileViewModel: ObservableObject { } else if let error = state as? ApiResultError { await MainActor.run { self.isLoading = false - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.successMessage = nil } sharedViewModel.resetUpdateProfileState() diff --git a/iosApp/iosApp/Register/RegisterView.swift b/iosApp/iosApp/Register/RegisterView.swift index 2d4d768..3dc5c0f 100644 --- a/iosApp/iosApp/Register/RegisterView.swift +++ b/iosApp/iosApp/Register/RegisterView.swift @@ -127,6 +127,10 @@ struct RegisterView: View { } ) } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.register() } + ) } } } diff --git a/iosApp/iosApp/Register/RegisterViewModel.swift b/iosApp/iosApp/Register/RegisterViewModel.swift index 4936d2b..63c6886 100644 --- a/iosApp/iosApp/Register/RegisterViewModel.swift +++ b/iosApp/iosApp/Register/RegisterViewModel.swift @@ -82,7 +82,7 @@ class RegisterViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } sharedViewModel.resetRegisterState() diff --git a/iosApp/iosApp/Residence/JoinResidenceView.swift b/iosApp/iosApp/Residence/JoinResidenceView.swift index 5f04c78..f9ca98c 100644 --- a/iosApp/iosApp/Residence/JoinResidenceView.swift +++ b/iosApp/iosApp/Residence/JoinResidenceView.swift @@ -89,7 +89,7 @@ struct JoinResidenceView: View { break } else if let error = state as? ApiResultError { await MainActor.run { - viewModel.errorMessage = error.message + viewModel.errorMessage = ErrorMessageParser.parse(error.message) viewModel.sharedViewModel.resetJoinResidenceState() } break diff --git a/iosApp/iosApp/Residence/ManageUsersView.swift b/iosApp/iosApp/Residence/ManageUsersView.swift index eef0606..9eb813d 100644 --- a/iosApp/iosApp/Residence/ManageUsersView.swift +++ b/iosApp/iosApp/Residence/ManageUsersView.swift @@ -100,7 +100,7 @@ struct ManageUsersView: View { self.ownerId = responseData.ownerId as? Int32 self.isLoading = false } else if let errorResult = result as? ApiResultError { - self.errorMessage = errorResult.message + self.errorMessage = ErrorMessageParser.parse(errorResult.message) self.isLoading = false } else { self.errorMessage = "Failed to load users" @@ -149,7 +149,7 @@ struct ManageUsersView: View { self.shareCode = successResult.data self.isGeneratingCode = false } else if let errorResult = result as? ApiResultError { - self.errorMessage = errorResult.message + self.errorMessage = ErrorMessageParser.parse(errorResult.message) self.isGeneratingCode = false } else { self.errorMessage = "Failed to generate share code" @@ -177,7 +177,7 @@ struct ManageUsersView: View { // Remove user from local list self.users.removeAll { $0.id == userId } } else if let errorResult = result as? ApiResultError { - self.errorMessage = errorResult.message + self.errorMessage = ErrorMessageParser.parse(errorResult.message) } else { self.errorMessage = "Failed to remove user" } diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index eef8255..1360da0 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -148,6 +148,10 @@ struct ResidenceDetailView: View { .onAppear { loadResidenceData() } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { loadResidenceData() } + ) } } @@ -158,10 +162,6 @@ private extension ResidenceDetailView { var mainContent: some View { if !hasAppeared || viewModel.isLoading { loadingView - } else if let error = viewModel.errorMessage { - ErrorView(message: error) { - loadResidenceData() - } } else if let residence = viewModel.selectedResidence { contentView(for: residence) } @@ -328,7 +328,7 @@ private extension ResidenceDetailView { if result is ApiResultSuccess { dismiss() } else if let errorResult = result as? ApiResultError { - self.viewModel.errorMessage = errorResult.message + self.viewModel.errorMessage = ErrorMessageParser.parse(errorResult.message) } else { self.viewModel.errorMessage = "Failed to delete residence" } diff --git a/iosApp/iosApp/Residence/ResidenceViewModel.swift b/iosApp/iosApp/Residence/ResidenceViewModel.swift index c300b74..679f82c 100644 --- a/iosApp/iosApp/Residence/ResidenceViewModel.swift +++ b/iosApp/iosApp/Residence/ResidenceViewModel.swift @@ -44,7 +44,7 @@ class ResidenceViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } break @@ -74,7 +74,7 @@ class ResidenceViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } break @@ -93,7 +93,7 @@ class ResidenceViewModel: ObservableObject { self.selectedResidence = success.data self.isLoading = false } else if let error = result as? ApiResultError { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } } @@ -122,7 +122,7 @@ class ResidenceViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } sharedViewModel.resetCreateState() @@ -156,7 +156,7 @@ class ResidenceViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } sharedViewModel.resetUpdateState() diff --git a/iosApp/iosApp/Residence/ResidencesListView.swift b/iosApp/iosApp/Residence/ResidencesListView.swift index b92943f..dad6a7b 100644 --- a/iosApp/iosApp/Residence/ResidencesListView.swift +++ b/iosApp/iosApp/Residence/ResidencesListView.swift @@ -19,10 +19,6 @@ struct ResidencesListView: View { .font(.body) .foregroundColor(Color(.secondaryLabel)) } - } else if let error = viewModel.errorMessage { - ErrorView(message: error) { - viewModel.loadMyResidences() - } } else if let response = viewModel.myResidences { if response.residences.isEmpty { EmptyResidencesView() @@ -100,6 +96,10 @@ struct ResidencesListView: View { .onAppear { viewModel.loadMyResidences() } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.loadMyResidences() } + ) } } diff --git a/iosApp/iosApp/ResidenceFormView.swift b/iosApp/iosApp/ResidenceFormView.swift index 8778562..fa67465 100644 --- a/iosApp/iosApp/ResidenceFormView.swift +++ b/iosApp/iosApp/ResidenceFormView.swift @@ -177,6 +177,10 @@ struct ResidenceFormView: View { loadResidenceTypes() initializeForm() } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { submitForm() } + ) } } diff --git a/iosApp/iosApp/Task/CompleteTaskView.swift b/iosApp/iosApp/Task/CompleteTaskView.swift index dd56d30..9701843 100644 --- a/iosApp/iosApp/Task/CompleteTaskView.swift +++ b/iosApp/iosApp/Task/CompleteTaskView.swift @@ -278,6 +278,10 @@ struct CompleteTaskView: View { .onAppear { contractorViewModel.loadContractors() } + .handleErrors( + error: errorMessage, + onRetry: { handleComplete() } + ) } } diff --git a/iosApp/iosApp/Task/EditTaskView.swift b/iosApp/iosApp/Task/EditTaskView.swift index 6949876..f529e62 100644 --- a/iosApp/iosApp/Task/EditTaskView.swift +++ b/iosApp/iosApp/Task/EditTaskView.swift @@ -128,6 +128,10 @@ struct EditTaskView: View { .onAppear { loadLookups() } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { submitForm() } + ) } } @@ -181,3 +185,4 @@ struct EditTaskView: View { } } } + diff --git a/iosApp/iosApp/Task/TaskViewModel.swift b/iosApp/iosApp/Task/TaskViewModel.swift index 7000863..c1f32ba 100644 --- a/iosApp/iosApp/Task/TaskViewModel.swift +++ b/iosApp/iosApp/Task/TaskViewModel.swift @@ -49,7 +49,7 @@ class TaskViewModel: ObservableObject { break } else if let error = state as? ApiResultError { await MainActor.run { - self.errorMessage = error.message + self.errorMessage = ErrorMessageParser.parse(error.message) self.isLoading = false } sharedViewModel.resetAddTaskState() diff --git a/iosApp/iosApp/VerifyEmail/VerifyEmailView.swift b/iosApp/iosApp/VerifyEmail/VerifyEmailView.swift index 433451d..9652e94 100644 --- a/iosApp/iosApp/VerifyEmail/VerifyEmailView.swift +++ b/iosApp/iosApp/VerifyEmail/VerifyEmailView.swift @@ -146,6 +146,10 @@ struct VerifyEmailView: View { onVerifySuccess() } } + .handleErrors( + error: viewModel.errorMessage, + onRetry: { viewModel.verifyEmail() } + ) } } }