Add comprehensive error message parsing to prevent raw JSON display
- Created ErrorMessageParser utility for both iOS (Swift) and Android (Kotlin) - Parser detects JSON-formatted error messages and extracts user-friendly text - Identifies when data objects (not errors) are returned and provides generic messages - Updated all API error handling to pass raw error bodies instead of concatenating - Applied ErrorMessageParser across all ViewModels and screens on both platforms - Fixed ContractorApi and DocumentApi to not concatenate error bodies with messages - Updated ApiResultHandler to automatically parse all error messages - Error messages now show "Request failed. Please check your input and try again." instead of raw JSON 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -62,13 +62,12 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(response.body())
|
ApiResult.Success(response.body())
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = try {
|
val errorBody = try {
|
||||||
val errorBody: String = response.body()
|
response.body<String>()
|
||||||
"Failed to create contractor: $errorBody"
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
"Failed to create contractor"
|
"Failed to create contractor"
|
||||||
}
|
}
|
||||||
ApiResult.Error(errorMessage, response.status.value)
|
ApiResult.Error(errorBody, response.status.value)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
@@ -86,13 +85,12 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(response.body())
|
ApiResult.Success(response.body())
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = try {
|
val errorBody = try {
|
||||||
val errorBody: String = response.body()
|
response.body<String>()
|
||||||
"Failed to update contractor: $errorBody"
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
"Failed to update contractor"
|
"Failed to update contractor"
|
||||||
}
|
}
|
||||||
ApiResult.Error(errorMessage, response.status.value)
|
ApiResult.Error(errorBody, response.status.value)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
|
|||||||
@@ -175,13 +175,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(response.body())
|
ApiResult.Success(response.body())
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = try {
|
val errorBody = try {
|
||||||
val errorBody: String = response.body()
|
response.body<String>()
|
||||||
"Failed to create document: $errorBody"
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
"Failed to create document"
|
"Failed to create document"
|
||||||
}
|
}
|
||||||
ApiResult.Error(errorMessage, response.status.value)
|
ApiResult.Error(errorBody, response.status.value)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
@@ -284,13 +283,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(response.body())
|
ApiResult.Success(response.body())
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = try {
|
val errorBody = try {
|
||||||
val errorBody: String = response.body()
|
response.body<String>()
|
||||||
"Failed to update document: $errorBody"
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
"Failed to update document"
|
"Failed to update document"
|
||||||
}
|
}
|
||||||
ApiResult.Error(errorMessage, response.status.value)
|
ApiResult.Error(errorBody, response.status.value)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
@@ -403,13 +401,12 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(response.body())
|
ApiResult.Success(response.body())
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = try {
|
val errorBody = try {
|
||||||
val errorBody: String = response.body()
|
response.body<String>()
|
||||||
"Failed to upload image: $errorBody"
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
"Failed to upload image"
|
"Failed to upload image"
|
||||||
}
|
}
|
||||||
ApiResult.Error(errorMessage, response.status.value)
|
ApiResult.Error(errorBody, response.status.value)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ fun <T> ApiResult<T>.HandleErrors(
|
|||||||
|
|
||||||
LaunchedEffect(this) {
|
LaunchedEffect(this) {
|
||||||
if (this@HandleErrors is ApiResult.Error) {
|
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
|
showErrorDialog = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.mycrib.android.ui.components.AddNewTaskWithResidenceDialog
|
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.CompleteTaskDialog
|
||||||
|
import com.mycrib.android.ui.components.HandleErrors
|
||||||
import com.mycrib.android.ui.components.task.TaskCard
|
import com.mycrib.android.ui.components.task.TaskCard
|
||||||
import com.mycrib.android.ui.components.task.DynamicTaskKanbanView
|
import com.mycrib.android.ui.components.task.DynamicTaskKanbanView
|
||||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||||
@@ -59,6 +61,12 @@ fun AllTasksScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle task creation success
|
// Handle task creation success
|
||||||
|
// Handle errors for task creation
|
||||||
|
createTaskState.HandleErrors(
|
||||||
|
onRetry = { /* Retry handled in dialog */ },
|
||||||
|
errorTitle = "Failed to Create Task"
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(createTaskState) {
|
LaunchedEffect(createTaskState) {
|
||||||
println("AllTasksScreen: createTaskState changed to $createTaskState")
|
println("AllTasksScreen: createTaskState changed to $createTaskState")
|
||||||
when (createTaskState) {
|
when (createTaskState) {
|
||||||
@@ -68,15 +76,7 @@ fun AllTasksScreen(
|
|||||||
viewModel.resetAddTaskState()
|
viewModel.resetAddTaskState()
|
||||||
viewModel.loadTasks()
|
viewModel.loadTasks()
|
||||||
}
|
}
|
||||||
is ApiResult.Error -> {
|
else -> {}
|
||||||
println("AllTasksScreen: Task creation error: ${(createTaskState as ApiResult.Error).message}")
|
|
||||||
}
|
|
||||||
is ApiResult.Loading -> {
|
|
||||||
println("AllTasksScreen: Task creation loading")
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
println("AllTasksScreen: Task creation idle")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,48 +115,12 @@ fun AllTasksScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
when (tasksState) {
|
ApiResultHandler(
|
||||||
is ApiResult.Idle, is ApiResult.Loading -> {
|
state = tasksState,
|
||||||
Box(
|
onRetry = { viewModel.loadTasks(forceRefresh = true) },
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(paddingValues),
|
||||||
.fillMaxSize()
|
errorTitle = "Failed to Load Tasks"
|
||||||
.padding(paddingValues),
|
) { taskData ->
|
||||||
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
|
|
||||||
val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() }
|
val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() }
|
||||||
|
|
||||||
if (hasNoTasks) {
|
if (hasNoTasks) {
|
||||||
@@ -263,10 +227,7 @@ fun AllTasksScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (showCompleteDialog && selectedTask != null) {
|
if (showCompleteDialog && selectedTask != null) {
|
||||||
CompleteTaskDialog(
|
CompleteTaskDialog(
|
||||||
@@ -303,7 +264,7 @@ fun AllTasksScreen(
|
|||||||
},
|
},
|
||||||
isLoading = createTaskState is ApiResult.Loading,
|
isLoading = createTaskState is ApiResult.Loading,
|
||||||
errorMessage = if (createTaskState is ApiResult.Error) {
|
errorMessage = if (createTaskState is ApiResult.Error) {
|
||||||
(createTaskState as ApiResult.Error).message
|
com.mycrib.android.util.ErrorMessageParser.parse((createTaskState as ApiResult.Error).message)
|
||||||
} else null
|
} else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.mycrib.android.ui.components.AddContractorDialog
|
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.android.viewmodel.ContractorViewModel
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
|
|
||||||
@@ -39,6 +41,18 @@ fun ContractorDetailScreen(
|
|||||||
viewModel.loadContractorDetail(contractorId)
|
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) {
|
LaunchedEffect(deleteState) {
|
||||||
if (deleteState is ApiResult.Success) {
|
if (deleteState is ApiResult.Success) {
|
||||||
viewModel.resetDeleteState()
|
viewModel.resetDeleteState()
|
||||||
@@ -94,18 +108,14 @@ fun ContractorDetailScreen(
|
|||||||
.padding(padding)
|
.padding(padding)
|
||||||
.background(Color(0xFFF9FAFB))
|
.background(Color(0xFFF9FAFB))
|
||||||
) {
|
) {
|
||||||
when (val state = contractorState) {
|
ApiResultHandler(
|
||||||
is ApiResult.Loading -> {
|
state = contractorState,
|
||||||
Box(
|
onRetry = { viewModel.loadContractorDetail(contractorId) },
|
||||||
modifier = Modifier.fillMaxSize(),
|
errorTitle = "Failed to Load Contractor",
|
||||||
contentAlignment = Alignment.Center
|
loadingContent = {
|
||||||
) {
|
CircularProgressIndicator(color = Color(0xFF2563EB))
|
||||||
CircularProgressIndicator(color = Color(0xFF2563EB))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
) { contractor ->
|
||||||
val contractor = state.data
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
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) {
|
if (showEditDialog) {
|
||||||
AddContractorDialog(
|
AddContractorDialog(
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.mycrib.android.ui.components.AddContractorDialog
|
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.android.viewmodel.ContractorViewModel
|
||||||
import com.mycrib.shared.models.ContractorSummary
|
import com.mycrib.shared.models.ContractorSummary
|
||||||
import com.mycrib.shared.network.ApiResult
|
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) {
|
LaunchedEffect(deleteState) {
|
||||||
if (deleteState is ApiResult.Success) {
|
if (deleteState is ApiResult.Success) {
|
||||||
viewModel.loadContractors()
|
viewModel.loadContractors()
|
||||||
@@ -216,81 +230,25 @@ fun ContractorsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val state = contractorsState) {
|
ApiResultHandler(
|
||||||
is ApiResult.Loading -> {
|
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) {
|
if (!isRefreshing) {
|
||||||
Box(
|
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
) { state ->
|
||||||
val contractors = state.data.results
|
val contractors = state.results
|
||||||
|
|
||||||
if (contractors.isEmpty()) {
|
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 -> {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -300,24 +258,55 @@ fun ContractorsScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.ErrorOutline,
|
Icons.Default.PersonAdd,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(64.dp),
|
||||||
tint = MaterialTheme.colorScheme.error
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Text(state.message, color = MaterialTheme.colorScheme.error)
|
Text(
|
||||||
Button(
|
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
|
||||||
onClick = { viewModel.loadContractors() },
|
"No contractors found"
|
||||||
colors = ButtonDefaults.buttonColors(
|
else
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
"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 -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.android.viewmodel.DocumentViewModel
|
||||||
import com.mycrib.shared.models.*
|
import com.mycrib.shared.models.*
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
@@ -51,6 +53,12 @@ fun DocumentDetailScreen(
|
|||||||
documentViewModel.loadDocumentDetail(documentId)
|
documentViewModel.loadDocumentDetail(documentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle errors for document deletion
|
||||||
|
deleteState.HandleErrors(
|
||||||
|
onRetry = { documentViewModel.deleteDocument(documentId) },
|
||||||
|
errorTitle = "Failed to Delete Document"
|
||||||
|
)
|
||||||
|
|
||||||
// Handle successful deletion
|
// Handle successful deletion
|
||||||
LaunchedEffect(deleteState) {
|
LaunchedEffect(deleteState) {
|
||||||
if (deleteState is ApiResult.Success) {
|
if (deleteState is ApiResult.Success) {
|
||||||
@@ -89,17 +97,11 @@ fun DocumentDetailScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
when (val state = documentState) {
|
ApiResultHandler(
|
||||||
is ApiResult.Loading -> {
|
state = documentState,
|
||||||
Box(
|
onRetry = { documentViewModel.loadDocumentDetail(documentId) },
|
||||||
modifier = Modifier.fillMaxSize(),
|
errorTitle = "Failed to Load Document"
|
||||||
contentAlignment = Alignment.Center
|
) { document ->
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is ApiResult.Success -> {
|
|
||||||
val document = state.data
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -409,16 +411,8 @@ fun DocumentDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ApiResult.Error -> {
|
|
||||||
ErrorState(
|
|
||||||
message = state.message,
|
|
||||||
onRetry = { documentViewModel.loadDocumentDetail(documentId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is ApiResult.Idle -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Delete confirmation dialog
|
// Delete confirmation dialog
|
||||||
if (showDeleteDialog) {
|
if (showDeleteDialog) {
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ fun DocumentFormScreen(
|
|||||||
}
|
}
|
||||||
is ApiResult.Error -> {
|
is ApiResult.Error -> {
|
||||||
Text(
|
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
|
color = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -596,7 +596,7 @@ fun DocumentFormScreen(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
(operationState as ApiResult.Error).message,
|
com.mycrib.android.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message),
|
||||||
modifier = Modifier.padding(12.dp),
|
modifier = Modifier.padding(12.dp),
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer
|
color = MaterialTheme.colorScheme.onErrorContainer
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.mycrib.android.ui.components.HandleErrors
|
||||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||||
import com.mycrib.repository.LookupsRepository
|
import com.mycrib.repository.LookupsRepository
|
||||||
import com.mycrib.shared.models.*
|
import com.mycrib.shared.models.*
|
||||||
@@ -49,6 +50,12 @@ fun EditTaskScreen(
|
|||||||
var titleError by remember { mutableStateOf("") }
|
var titleError by remember { mutableStateOf("") }
|
||||||
var dueDateError 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
|
// Handle update state changes
|
||||||
LaunchedEffect(updateTaskState) {
|
LaunchedEffect(updateTaskState) {
|
||||||
when (updateTaskState) {
|
when (updateTaskState) {
|
||||||
@@ -279,7 +286,7 @@ fun EditTaskScreen(
|
|||||||
// Error message
|
// Error message
|
||||||
if (updateTaskState is ApiResult.Error) {
|
if (updateTaskState is ApiResult.Error) {
|
||||||
Text(
|
Text(
|
||||||
text = (updateTaskState as ApiResult.Error).message,
|
text = com.mycrib.android.util.ErrorMessageParser.parse((updateTaskState as ApiResult.Error).message),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.auth.AuthHeader
|
||||||
import com.mycrib.android.ui.components.common.ErrorCard
|
import com.mycrib.android.ui.components.common.ErrorCard
|
||||||
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
||||||
@@ -30,6 +31,15 @@ fun ForgotPasswordScreen(
|
|||||||
val forgotPasswordState by viewModel.forgotPasswordState.collectAsState()
|
val forgotPasswordState by viewModel.forgotPasswordState.collectAsState()
|
||||||
val currentStep by viewModel.currentStep.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
|
// Handle automatic navigation to next step
|
||||||
LaunchedEffect(currentStep) {
|
LaunchedEffect(currentStep) {
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
@@ -40,7 +50,7 @@ fun ForgotPasswordScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val errorMessage = when (forgotPasswordState) {
|
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 -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.mycrib.android.ui.components.HandleErrors
|
||||||
import com.mycrib.android.ui.theme.AppRadius
|
import com.mycrib.android.ui.theme.AppRadius
|
||||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
@@ -33,6 +34,12 @@ fun HomeScreen(
|
|||||||
viewModel.loadResidenceSummary()
|
viewModel.loadResidenceSummary()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle errors for loading summary
|
||||||
|
summaryState.HandleErrors(
|
||||||
|
onRetry = { viewModel.loadResidenceSummary() },
|
||||||
|
errorTitle = "Failed to Load Summary"
|
||||||
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.auth.AuthHeader
|
||||||
import com.mycrib.android.ui.components.common.ErrorCard
|
import com.mycrib.android.ui.components.common.ErrorCard
|
||||||
import com.mycrib.android.viewmodel.AuthViewModel
|
import com.mycrib.android.viewmodel.AuthViewModel
|
||||||
@@ -38,6 +39,12 @@ fun LoginScreen(
|
|||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
val loginState by viewModel.loginState.collectAsState()
|
val loginState by viewModel.loginState.collectAsState()
|
||||||
|
|
||||||
|
// Handle errors for login
|
||||||
|
loginState.HandleErrors(
|
||||||
|
onRetry = { viewModel.login(username, password) },
|
||||||
|
errorTitle = "Login Failed"
|
||||||
|
)
|
||||||
|
|
||||||
// Handle login state changes
|
// Handle login state changes
|
||||||
LaunchedEffect(loginState) {
|
LaunchedEffect(loginState) {
|
||||||
when (loginState) {
|
when (loginState) {
|
||||||
@@ -50,7 +57,7 @@ fun LoginScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val errorMessage = when (loginState) {
|
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 -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.ui.components.common.ErrorCard
|
||||||
import com.mycrib.android.viewmodel.AuthViewModel
|
import com.mycrib.android.viewmodel.AuthViewModel
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
@@ -36,6 +37,18 @@ fun ProfileScreen(
|
|||||||
|
|
||||||
val updateState by viewModel.updateProfileState.collectAsState()
|
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
|
// Load current user data
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
val token = TokenStorage.getToken()
|
val token = TokenStorage.getToken()
|
||||||
@@ -68,7 +81,7 @@ fun ProfileScreen(
|
|||||||
viewModel.resetUpdateProfileState()
|
viewModel.resetUpdateProfileState()
|
||||||
}
|
}
|
||||||
is ApiResult.Error -> {
|
is ApiResult.Error -> {
|
||||||
errorMessage = (updateState as ApiResult.Error).message
|
errorMessage = com.mycrib.android.util.ErrorMessageParser.parse((updateState as ApiResult.Error).message)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
successMessage = ""
|
successMessage = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.auth.AuthHeader
|
||||||
import com.mycrib.android.ui.components.common.ErrorCard
|
import com.mycrib.android.ui.components.common.ErrorCard
|
||||||
import com.mycrib.android.viewmodel.AuthViewModel
|
import com.mycrib.android.viewmodel.AuthViewModel
|
||||||
@@ -34,6 +35,13 @@ fun RegisterScreen(
|
|||||||
var isLoading by remember { mutableStateOf(false) }
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val createState by viewModel.registerState.collectAsState()
|
val createState by viewModel.registerState.collectAsState()
|
||||||
|
|
||||||
|
// Handle errors for registration
|
||||||
|
createState.HandleErrors(
|
||||||
|
onRetry = { viewModel.register(username, email, password) },
|
||||||
|
errorTitle = "Registration Failed"
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(createState) {
|
LaunchedEffect(createState) {
|
||||||
when (createState) {
|
when (createState) {
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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.AuthHeader
|
||||||
import com.mycrib.android.ui.components.auth.RequirementItem
|
import com.mycrib.android.ui.components.auth.RequirementItem
|
||||||
import com.mycrib.android.ui.components.common.ErrorCard
|
import com.mycrib.android.ui.components.common.ErrorCard
|
||||||
@@ -35,8 +36,14 @@ fun ResetPasswordScreen(
|
|||||||
val resetPasswordState by viewModel.resetPasswordState.collectAsState()
|
val resetPasswordState by viewModel.resetPasswordState.collectAsState()
|
||||||
val currentStep by viewModel.currentStep.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) {
|
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 -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ fun ResidenceDetailScreen(
|
|||||||
// Handle generate report state
|
// Handle generate report state
|
||||||
// Handle errors for generate report
|
// Handle errors for generate report
|
||||||
generateReportState.HandleErrors(
|
generateReportState.HandleErrors(
|
||||||
onRetry = { residenceViewModel.generateMaintenanceReport(residenceId) },
|
onRetry = { residenceViewModel.generateTasksReport(residenceId) },
|
||||||
errorTitle = "Failed to Generate Report"
|
errorTitle = "Failed to Generate Report"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -597,7 +597,7 @@ fun ResidenceDetailScreen(
|
|||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
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,
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
@@ -691,5 +691,4 @@ fun ResidenceDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ fun ResidenceFormScreen(
|
|||||||
// Error message
|
// Error message
|
||||||
if (operationState is ApiResult.Error) {
|
if (operationState is ApiResult.Error) {
|
||||||
Text(
|
Text(
|
||||||
text = (operationState as ApiResult.Error).message,
|
text = com.mycrib.android.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -399,5 +399,4 @@ fun ResidencesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ fun TasksScreen(
|
|||||||
// Show error dialog when tasks fail to load
|
// Show error dialog when tasks fail to load
|
||||||
LaunchedEffect(tasksState) {
|
LaunchedEffect(tasksState) {
|
||||||
if (tasksState is ApiResult.Error) {
|
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
|
showErrorDialog = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.auth.AuthHeader
|
||||||
import com.mycrib.android.ui.components.common.ErrorCard
|
import com.mycrib.android.ui.components.common.ErrorCard
|
||||||
import com.mycrib.android.viewmodel.AuthViewModel
|
import com.mycrib.android.viewmodel.AuthViewModel
|
||||||
@@ -34,6 +35,12 @@ fun VerifyEmailScreen(
|
|||||||
|
|
||||||
val verifyState by viewModel.verifyEmailState.collectAsState()
|
val verifyState by viewModel.verifyEmailState.collectAsState()
|
||||||
|
|
||||||
|
// Handle errors for email verification
|
||||||
|
verifyState.HandleErrors(
|
||||||
|
onRetry = { viewModel.verifyEmail(code) },
|
||||||
|
errorTitle = "Verification Failed"
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(verifyState) {
|
LaunchedEffect(verifyState) {
|
||||||
when (verifyState) {
|
when (verifyState) {
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
@@ -41,7 +48,7 @@ fun VerifyEmailScreen(
|
|||||||
onVerifySuccess()
|
onVerifySuccess()
|
||||||
}
|
}
|
||||||
is ApiResult.Error -> {
|
is ApiResult.Error -> {
|
||||||
errorMessage = (verifyState as ApiResult.Error).message
|
errorMessage = com.mycrib.android.util.ErrorMessageParser.parse((verifyState as ApiResult.Error).message)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
is ApiResult.Loading -> {
|
is ApiResult.Loading -> {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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.AuthHeader
|
||||||
import com.mycrib.android.ui.components.common.ErrorCard
|
import com.mycrib.android.ui.components.common.ErrorCard
|
||||||
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
||||||
@@ -31,6 +32,12 @@ fun VerifyResetCodeScreen(
|
|||||||
val verifyCodeState by viewModel.verifyCodeState.collectAsState()
|
val verifyCodeState by viewModel.verifyCodeState.collectAsState()
|
||||||
val currentStep by viewModel.currentStep.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
|
// Handle automatic navigation to next step
|
||||||
LaunchedEffect(currentStep) {
|
LaunchedEffect(currentStep) {
|
||||||
if (currentStep == com.mycrib.android.viewmodel.PasswordResetStep.RESET_PASSWORD) {
|
if (currentStep == com.mycrib.android.viewmodel.PasswordResetStep.RESET_PASSWORD) {
|
||||||
@@ -39,7 +46,7 @@ fun VerifyResetCodeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val errorMessage = when (verifyCodeState) {
|
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 -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -213,6 +213,10 @@ struct ContractorDetailView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.loadContractorDetail(id: contractorId)
|
viewModel.loadContractorDetail(id: contractorId)
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.loadContractorDetail(id: contractorId) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteContractor() {
|
private func deleteContractor() {
|
||||||
|
|||||||
@@ -281,6 +281,10 @@ struct ContractorFormSheet: View {
|
|||||||
loadContractorData()
|
loadContractorData()
|
||||||
loadContractorSpecialties()
|
loadContractorSpecialties()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { saveContractor() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class ContractorViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -87,7 +87,7 @@ class ContractorViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -119,7 +119,7 @@ class ContractorViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isCreating = false
|
self.isCreating = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetCreateState()
|
sharedViewModel.resetCreateState()
|
||||||
@@ -153,7 +153,7 @@ class ContractorViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isUpdating = false
|
self.isUpdating = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetUpdateState()
|
sharedViewModel.resetUpdateState()
|
||||||
@@ -187,7 +187,7 @@ class ContractorViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isDeleting = false
|
self.isDeleting = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetDeleteState()
|
sharedViewModel.resetDeleteState()
|
||||||
@@ -210,7 +210,7 @@ class ContractorViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
}
|
}
|
||||||
sharedViewModel.resetToggleFavoriteState()
|
sharedViewModel.resetToggleFavoriteState()
|
||||||
completion(false)
|
completion(false)
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ struct ContractorsListView: View {
|
|||||||
.onChange(of: searchText) { newValue in
|
.onChange(of: searchText) { newValue in
|
||||||
loadContractors()
|
loadContractors()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { loadContractors() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadContractors(forceRefresh: Bool = false) {
|
private func loadContractors(forceRefresh: Bool = false) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class DocumentViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -142,7 +142,7 @@ class DocumentViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetCreateState()
|
sharedViewModel.resetCreateState()
|
||||||
@@ -219,7 +219,7 @@ class DocumentViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetUpdateState()
|
sharedViewModel.resetUpdateState()
|
||||||
@@ -251,7 +251,7 @@ class DocumentViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetDeleteState()
|
sharedViewModel.resetDeleteState()
|
||||||
|
|||||||
50
iosApp/iosApp/Helpers/ErrorMessageParser.swift
Normal file
50
iosApp/iosApp/Helpers/ErrorMessageParser.swift
Normal file
@@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -265,6 +265,10 @@ struct LoginView: View {
|
|||||||
showPasswordReset = true
|
showPasswordReset = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.login() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ class LoginViewModel: ObservableObject {
|
|||||||
case 500...599:
|
case 500...599:
|
||||||
self.errorMessage = "Server error. Please try again later."
|
self.errorMessage = "Server error. Please try again later."
|
||||||
default:
|
default:
|
||||||
self.errorMessage = self.cleanErrorMessage(error.message)
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = self.cleanErrorMessage(error.message)
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
print("API Error: \(error.message)")
|
print("API Error: \(error.message)")
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ struct ForgotPasswordView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
isEmailFocused = true
|
isEmailFocused = true
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.requestPasswordReset() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,10 +290,10 @@ class PasswordResetViewModel: ObservableObject {
|
|||||||
} else if message.contains("Invalid") && message.contains("token") {
|
} else if message.contains("Invalid") && message.contains("token") {
|
||||||
self.errorMessage = "Invalid or expired reset token. Please start over."
|
self.errorMessage = "Invalid or expired reset token. Please start over."
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = message
|
self.errorMessage = ErrorMessageParser.parse(message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = errorResult.message
|
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
print("API Error: \(errorResult.message)")
|
print("API Error: \(errorResult.message)")
|
||||||
|
|||||||
@@ -232,6 +232,10 @@ struct ResetPasswordView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
focusedField = .newPassword
|
focusedField = .newPassword
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.resetPassword() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,10 @@ struct VerifyResetCodeView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
isCodeFocused = true
|
isCodeFocused = true
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.verifyResetCode() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,10 @@ struct ProfileView: View {
|
|||||||
.onChange(of: viewModel.email) { _, _ in
|
.onChange(of: viewModel.email) { _, _ in
|
||||||
viewModel.clearMessages()
|
viewModel.clearMessages()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.updateProfile() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class ProfileViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoadingUser = false
|
self.isLoadingUser = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetCurrentUserState()
|
sharedViewModel.resetCurrentUserState()
|
||||||
@@ -114,7 +114,7 @@ class ProfileViewModel: ObservableObject {
|
|||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.successMessage = nil
|
self.successMessage = nil
|
||||||
}
|
}
|
||||||
sharedViewModel.resetUpdateProfileState()
|
sharedViewModel.resetUpdateProfileState()
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ struct RegisterView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.register() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class RegisterViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetRegisterState()
|
sharedViewModel.resetRegisterState()
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ struct JoinResidenceView: View {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
viewModel.errorMessage = error.message
|
viewModel.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
viewModel.sharedViewModel.resetJoinResidenceState()
|
viewModel.sharedViewModel.resetJoinResidenceState()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ struct ManageUsersView: View {
|
|||||||
self.ownerId = responseData.ownerId as? Int32
|
self.ownerId = responseData.ownerId as? Int32
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
} else if let errorResult = result as? ApiResultError {
|
} else if let errorResult = result as? ApiResultError {
|
||||||
self.errorMessage = errorResult.message
|
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = "Failed to load users"
|
self.errorMessage = "Failed to load users"
|
||||||
@@ -149,7 +149,7 @@ struct ManageUsersView: View {
|
|||||||
self.shareCode = successResult.data
|
self.shareCode = successResult.data
|
||||||
self.isGeneratingCode = false
|
self.isGeneratingCode = false
|
||||||
} else if let errorResult = result as? ApiResultError {
|
} else if let errorResult = result as? ApiResultError {
|
||||||
self.errorMessage = errorResult.message
|
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||||
self.isGeneratingCode = false
|
self.isGeneratingCode = false
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = "Failed to generate share code"
|
self.errorMessage = "Failed to generate share code"
|
||||||
@@ -177,7 +177,7 @@ struct ManageUsersView: View {
|
|||||||
// Remove user from local list
|
// Remove user from local list
|
||||||
self.users.removeAll { $0.id == userId }
|
self.users.removeAll { $0.id == userId }
|
||||||
} else if let errorResult = result as? ApiResultError {
|
} else if let errorResult = result as? ApiResultError {
|
||||||
self.errorMessage = errorResult.message
|
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||||
} else {
|
} else {
|
||||||
self.errorMessage = "Failed to remove user"
|
self.errorMessage = "Failed to remove user"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ struct ResidenceDetailView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
loadResidenceData()
|
loadResidenceData()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { loadResidenceData() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,10 +162,6 @@ private extension ResidenceDetailView {
|
|||||||
var mainContent: some View {
|
var mainContent: some View {
|
||||||
if !hasAppeared || viewModel.isLoading {
|
if !hasAppeared || viewModel.isLoading {
|
||||||
loadingView
|
loadingView
|
||||||
} else if let error = viewModel.errorMessage {
|
|
||||||
ErrorView(message: error) {
|
|
||||||
loadResidenceData()
|
|
||||||
}
|
|
||||||
} else if let residence = viewModel.selectedResidence {
|
} else if let residence = viewModel.selectedResidence {
|
||||||
contentView(for: residence)
|
contentView(for: residence)
|
||||||
}
|
}
|
||||||
@@ -328,7 +328,7 @@ private extension ResidenceDetailView {
|
|||||||
if result is ApiResultSuccess<KotlinUnit> {
|
if result is ApiResultSuccess<KotlinUnit> {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else if let errorResult = result as? ApiResultError {
|
} else if let errorResult = result as? ApiResultError {
|
||||||
self.viewModel.errorMessage = errorResult.message
|
self.viewModel.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||||
} else {
|
} else {
|
||||||
self.viewModel.errorMessage = "Failed to delete residence"
|
self.viewModel.errorMessage = "Failed to delete residence"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ResidenceViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -74,7 +74,7 @@ class ResidenceViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -93,7 +93,7 @@ class ResidenceViewModel: ObservableObject {
|
|||||||
self.selectedResidence = success.data
|
self.selectedResidence = success.data
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
} else if let error = result as? ApiResultError {
|
} else if let error = result as? ApiResultError {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ class ResidenceViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetCreateState()
|
sharedViewModel.resetCreateState()
|
||||||
@@ -156,7 +156,7 @@ class ResidenceViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetUpdateState()
|
sharedViewModel.resetUpdateState()
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ struct ResidencesListView: View {
|
|||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundColor(Color(.secondaryLabel))
|
.foregroundColor(Color(.secondaryLabel))
|
||||||
}
|
}
|
||||||
} else if let error = viewModel.errorMessage {
|
|
||||||
ErrorView(message: error) {
|
|
||||||
viewModel.loadMyResidences()
|
|
||||||
}
|
|
||||||
} else if let response = viewModel.myResidences {
|
} else if let response = viewModel.myResidences {
|
||||||
if response.residences.isEmpty {
|
if response.residences.isEmpty {
|
||||||
EmptyResidencesView()
|
EmptyResidencesView()
|
||||||
@@ -100,6 +96,10 @@ struct ResidencesListView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.loadMyResidences()
|
viewModel.loadMyResidences()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.loadMyResidences() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ struct ResidenceFormView: View {
|
|||||||
loadResidenceTypes()
|
loadResidenceTypes()
|
||||||
initializeForm()
|
initializeForm()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { submitForm() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -278,6 +278,10 @@ struct CompleteTaskView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
contractorViewModel.loadContractors()
|
contractorViewModel.loadContractors()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: errorMessage,
|
||||||
|
onRetry: { handleComplete() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ struct EditTaskView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
loadLookups()
|
loadLookups()
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { submitForm() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,3 +185,4 @@ struct EditTaskView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class TaskViewModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
} else if let error = state as? ApiResultError {
|
} else if let error = state as? ApiResultError {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.errorMessage = error.message
|
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
sharedViewModel.resetAddTaskState()
|
sharedViewModel.resetAddTaskState()
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ struct VerifyEmailView: View {
|
|||||||
onVerifySuccess()
|
onVerifySuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.handleErrors(
|
||||||
|
error: viewModel.errorMessage,
|
||||||
|
onRetry: { viewModel.verifyEmail() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user