android ui
This commit is contained in:
@@ -23,6 +23,7 @@ import com.example.casera.viewmodel.TaskCompletionViewModel
|
||||
import com.example.casera.viewmodel.TaskViewModel
|
||||
import com.example.casera.models.TaskDetail
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -105,164 +106,156 @@ fun AllTasksScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"All Tasks",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { viewModel.loadTasks(forceRefresh = true) }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Refresh,
|
||||
contentDescription = "Refresh"
|
||||
WarmGradientBackground {
|
||||
Scaffold(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"All Tasks",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showNewTaskDialog = true },
|
||||
enabled = myResidencesState is ApiResult.Success &&
|
||||
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "Add Task"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
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) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(24.dp)
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { viewModel.loadTasks(forceRefresh = true) }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Assignment,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
Icons.Default.Refresh,
|
||||
contentDescription = "Refresh"
|
||||
)
|
||||
Text(
|
||||
"No tasks yet",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showNewTaskDialog = true },
|
||||
enabled = myResidencesState is ApiResult.Success &&
|
||||
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "Add Task"
|
||||
)
|
||||
Text(
|
||||
"Create your first task to get started",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = { showNewTaskDialog = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.7f)
|
||||
.height(56.dp),
|
||||
enabled = myResidencesState is ApiResult.Success &&
|
||||
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
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) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
|
||||
modifier = Modifier.padding(OrganicSpacing.comfortable)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Assignment,
|
||||
size = 80.dp,
|
||||
iconScale = 0.6f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
|
||||
iconColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"No tasks yet",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
Text(
|
||||
"Create your first task to get started",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
OrganicPrimaryButton(
|
||||
text = "Add Task",
|
||||
onClick = { showNewTaskDialog = true },
|
||||
modifier = Modifier.fillMaxWidth(0.7f),
|
||||
enabled = myResidencesState is ApiResult.Success &&
|
||||
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
|
||||
)
|
||||
if (myResidencesState is ApiResult.Success &&
|
||||
(myResidencesState as ApiResult.Success).data.residences.isEmpty()) {
|
||||
Text(
|
||||
"Add Task",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
"Add a property first from the Residences tab",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
if (myResidencesState is ApiResult.Success &&
|
||||
(myResidencesState as ApiResult.Success).data.residences.isEmpty()) {
|
||||
Text(
|
||||
"Add a property first from the Residences tab",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DynamicTaskKanbanView(
|
||||
columns = taskData.columns,
|
||||
onCompleteTask = { task ->
|
||||
if (onNavigateToCompleteTask != null) {
|
||||
// Use full-screen navigation
|
||||
val residenceName = (myResidencesState as? ApiResult.Success)
|
||||
?.data?.residences?.find { it.id == task.residenceId }?.name ?: ""
|
||||
onNavigateToCompleteTask(task, residenceName)
|
||||
} else {
|
||||
// Fall back to dialog
|
||||
selectedTask = task
|
||||
showCompleteDialog = true
|
||||
}
|
||||
},
|
||||
onEditTask = { task ->
|
||||
onNavigateToEditTask(task)
|
||||
},
|
||||
onCancelTask = { task ->
|
||||
} else {
|
||||
DynamicTaskKanbanView(
|
||||
columns = taskData.columns,
|
||||
onCompleteTask = { task ->
|
||||
if (onNavigateToCompleteTask != null) {
|
||||
// Use full-screen navigation
|
||||
val residenceName = (myResidencesState as? ApiResult.Success)
|
||||
?.data?.residences?.find { it.id == task.residenceId }?.name ?: ""
|
||||
onNavigateToCompleteTask(task, residenceName)
|
||||
} else {
|
||||
// Fall back to dialog
|
||||
selectedTask = task
|
||||
showCompleteDialog = true
|
||||
}
|
||||
},
|
||||
onEditTask = { task ->
|
||||
onNavigateToEditTask(task)
|
||||
},
|
||||
onCancelTask = { task ->
|
||||
// viewModel.cancelTask(task.id) { _ ->
|
||||
// viewModel.loadTasks()
|
||||
// }
|
||||
},
|
||||
onUncancelTask = { task ->
|
||||
},
|
||||
onUncancelTask = { task ->
|
||||
// viewModel.uncancelTask(task.id) { _ ->
|
||||
// viewModel.loadTasks()
|
||||
// }
|
||||
},
|
||||
onMarkInProgress = { task ->
|
||||
viewModel.markInProgress(task.id) { success ->
|
||||
if (success) {
|
||||
viewModel.loadTasks()
|
||||
},
|
||||
onMarkInProgress = { task ->
|
||||
viewModel.markInProgress(task.id) { success ->
|
||||
if (success) {
|
||||
viewModel.loadTasks()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onArchiveTask = { task ->
|
||||
viewModel.archiveTask(task.id) { success ->
|
||||
if (success) {
|
||||
viewModel.loadTasks()
|
||||
},
|
||||
onArchiveTask = { task ->
|
||||
viewModel.archiveTask(task.id) { success ->
|
||||
if (success) {
|
||||
viewModel.loadTasks()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onUnarchiveTask = { task ->
|
||||
viewModel.unarchiveTask(task.id) { success ->
|
||||
if (success) {
|
||||
viewModel.loadTasks()
|
||||
},
|
||||
onUnarchiveTask = { task ->
|
||||
viewModel.unarchiveTask(task.id) { success ->
|
||||
if (success) {
|
||||
viewModel.loadTasks()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
bottomPadding = bottomNavBarPadding,
|
||||
scrollToColumnIndex = scrollToColumnIndex,
|
||||
onScrollComplete = {
|
||||
scrollToColumnIndex = null
|
||||
onClearNavigateToTask()
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
bottomPadding = bottomNavBarPadding,
|
||||
scrollToColumnIndex = scrollToColumnIndex,
|
||||
onScrollComplete = {
|
||||
scrollToColumnIndex = null
|
||||
onClearNavigateToTask()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -30,8 +29,7 @@ import com.example.casera.models.TaskCompletionCreateRequest
|
||||
import com.example.casera.models.ContractorSummary
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.platform.*
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.ContractorViewModel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -74,378 +72,354 @@ fun CompleteTaskScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(Res.string.completions_complete_task_title, taskTitle),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.common_cancel))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Task Info Section
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(AppSpacing.lg)
|
||||
) {
|
||||
Text(
|
||||
text = taskTitle,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (residenceName.isNotEmpty()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = residenceName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contractor Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_select_contractor),
|
||||
subtitle = stringResource(Res.string.completions_contractor_helper)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.lg)
|
||||
.clickable { showContractorPicker = true },
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Build,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
WarmGradientBackground {
|
||||
Scaffold(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(Res.string.completions_complete_task_title, taskTitle),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = selectedContractor?.name
|
||||
?: stringResource(Res.string.completions_none_manual),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
selectedContractor?.company?.let { company ->
|
||||
Text(
|
||||
text = company,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
|
||||
// Completion Details Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_details_section),
|
||||
subtitle = stringResource(Res.string.completions_optional_info)
|
||||
)
|
||||
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
// Completed By Name
|
||||
OutlinedTextField(
|
||||
value = completedByName,
|
||||
onValueChange = { completedByName = it },
|
||||
label = { Text(stringResource(Res.string.completions_completed_by_name)) },
|
||||
placeholder = { Text(stringResource(Res.string.completions_completed_by_placeholder)) },
|
||||
leadingIcon = { Icon(Icons.Default.Person, null) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
enabled = selectedContractor == null,
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
)
|
||||
|
||||
// Actual Cost
|
||||
OutlinedTextField(
|
||||
value = actualCost,
|
||||
onValueChange = { actualCost = it },
|
||||
label = { Text(stringResource(Res.string.completions_actual_cost_optional)) },
|
||||
leadingIcon = { Icon(Icons.Default.AttachMoney, null) },
|
||||
prefix = { Text("$") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
|
||||
// Notes Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_notes_optional),
|
||||
subtitle = stringResource(Res.string.completions_notes_helper)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = notes,
|
||||
onValueChange = { notes = it },
|
||||
placeholder = { Text(stringResource(Res.string.completions_notes_placeholder)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.lg)
|
||||
.height(120.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
|
||||
// Rating Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_quality_rating),
|
||||
subtitle = stringResource(Res.string.completions_rate_quality)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.lg),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(
|
||||
// Task Info Section
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
text = "$rating / 5",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Column(
|
||||
modifier = Modifier.padding(OrganicSpacing.lg)
|
||||
) {
|
||||
(1..5).forEach { star ->
|
||||
val isSelected = star <= rating
|
||||
val starColor by animateColorAsState(
|
||||
targetValue = if (isSelected) Color(0xFFFFD700)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "starColor"
|
||||
)
|
||||
Text(
|
||||
text = taskTitle,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Selection)
|
||||
rating = star
|
||||
},
|
||||
modifier = Modifier.size(56.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isSelected) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = "$star stars",
|
||||
tint = starColor,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (residenceName.isNotEmpty()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = residenceName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
// Contractor Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_select_contractor),
|
||||
subtitle = stringResource(Res.string.completions_contractor_helper)
|
||||
)
|
||||
|
||||
// Photos Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_photos_count, selectedImages.size, MAX_IMAGES),
|
||||
subtitle = stringResource(Res.string.completions_add_photos_helper)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = OrganicSpacing.lg)
|
||||
.clickable { showContractorPicker = true }
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Light)
|
||||
cameraPicker()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = selectedImages.size < MAX_IMAGES
|
||||
) {
|
||||
Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(stringResource(Res.string.completions_camera))
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Light)
|
||||
imagePicker()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = selectedImages.size < MAX_IMAGES
|
||||
) {
|
||||
Icon(Icons.Default.PhotoLibrary, null, modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(stringResource(Res.string.completions_library))
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedImages.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
selectedImages.forEachIndexed { index, imageData ->
|
||||
ImageThumbnailCard(
|
||||
imageData = imageData,
|
||||
onRemove = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Light)
|
||||
selectedImages = selectedImages.toMutableList().also {
|
||||
it.removeAt(index)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Build,
|
||||
size = 24.dp
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = selectedContractor?.name
|
||||
?: stringResource(Res.string.completions_none_manual),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
selectedContractor?.company?.let { company ->
|
||||
Text(
|
||||
text = company,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Completion Details Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_details_section),
|
||||
subtitle = stringResource(Res.string.completions_optional_info)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
// Completed By Name
|
||||
OutlinedTextField(
|
||||
value = completedByName,
|
||||
onValueChange = { completedByName = it },
|
||||
label = { Text(stringResource(Res.string.completions_completed_by_name)) },
|
||||
placeholder = { Text(stringResource(Res.string.completions_completed_by_placeholder)) },
|
||||
leadingIcon = { Icon(Icons.Default.Person, null) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
enabled = selectedContractor == null,
|
||||
shape = OrganicShapes.medium
|
||||
)
|
||||
|
||||
// Actual Cost
|
||||
OutlinedTextField(
|
||||
value = actualCost,
|
||||
onValueChange = { actualCost = it },
|
||||
label = { Text(stringResource(Res.string.completions_actual_cost_optional)) },
|
||||
leadingIcon = { Icon(Icons.Default.AttachMoney, null) },
|
||||
prefix = { Text("$") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
shape = OrganicShapes.medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Notes Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_notes_optional),
|
||||
subtitle = stringResource(Res.string.completions_notes_helper)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = notes,
|
||||
onValueChange = { notes = it },
|
||||
placeholder = { Text(stringResource(Res.string.completions_notes_placeholder)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = OrganicSpacing.lg)
|
||||
.height(120.dp),
|
||||
shape = OrganicShapes.medium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Rating Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_quality_rating),
|
||||
subtitle = stringResource(Res.string.completions_rate_quality)
|
||||
)
|
||||
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = OrganicSpacing.lg)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "$rating / 5",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
(1..5).forEach { star ->
|
||||
val isSelected = star <= rating
|
||||
val starColor by animateColorAsState(
|
||||
targetValue = if (isSelected) Color(0xFFFFD700)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "starColor"
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Selection)
|
||||
rating = star
|
||||
},
|
||||
modifier = Modifier.size(56.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isSelected) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = "$star stars",
|
||||
tint = starColor,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Complete Button
|
||||
Button(
|
||||
onClick = {
|
||||
isSubmitting = true
|
||||
val notesWithContractor = buildString {
|
||||
selectedContractor?.let {
|
||||
append("Contractor: ${it.name}")
|
||||
it.company?.let { company -> append(" ($company)") }
|
||||
append("\n")
|
||||
}
|
||||
if (completedByName.isNotBlank()) {
|
||||
append("Completed by: $completedByName\n")
|
||||
}
|
||||
if (notes.isNotBlank()) {
|
||||
append(notes)
|
||||
}
|
||||
}.ifBlank { null }
|
||||
// Photos Section
|
||||
SectionHeader(
|
||||
title = stringResource(Res.string.completions_photos_count, selectedImages.size, MAX_IMAGES),
|
||||
subtitle = stringResource(Res.string.completions_add_photos_helper)
|
||||
)
|
||||
|
||||
onComplete(
|
||||
TaskCompletionCreateRequest(
|
||||
taskId = taskId,
|
||||
completedAt = null,
|
||||
actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(),
|
||||
notes = notesWithContractor,
|
||||
rating = rating,
|
||||
imageUrls = null
|
||||
),
|
||||
selectedImages
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.lg)
|
||||
.height(56.dp),
|
||||
enabled = !isSubmitting,
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
) {
|
||||
if (isSubmitting) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.CheckCircle, null)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(
|
||||
stringResource(Res.string.completions_complete_button),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Light)
|
||||
cameraPicker()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = selectedImages.size < MAX_IMAGES
|
||||
) {
|
||||
Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text(stringResource(Res.string.completions_camera))
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Light)
|
||||
imagePicker()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = selectedImages.size < MAX_IMAGES
|
||||
) {
|
||||
Icon(Icons.Default.PhotoLibrary, null, modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text(stringResource(Res.string.completions_library))
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedImages.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
selectedImages.forEachIndexed { index, imageData ->
|
||||
ImageThumbnailCard(
|
||||
imageData = imageData,
|
||||
onRemove = {
|
||||
hapticFeedback.perform(HapticFeedbackType.Light)
|
||||
selectedImages = selectedImages.toMutableList().also {
|
||||
it.removeAt(index)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
// Complete Button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.completions_complete_button),
|
||||
onClick = {
|
||||
isSubmitting = true
|
||||
val notesWithContractor = buildString {
|
||||
selectedContractor?.let {
|
||||
append("Contractor: ${it.name}")
|
||||
it.company?.let { company -> append(" ($company)") }
|
||||
append("\n")
|
||||
}
|
||||
if (completedByName.isNotBlank()) {
|
||||
append("Completed by: $completedByName\n")
|
||||
}
|
||||
if (notes.isNotBlank()) {
|
||||
append(notes)
|
||||
}
|
||||
}.ifBlank { null }
|
||||
|
||||
onComplete(
|
||||
TaskCompletionCreateRequest(
|
||||
taskId = taskId,
|
||||
completedAt = null,
|
||||
actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(),
|
||||
notes = notesWithContractor,
|
||||
rating = rating,
|
||||
imageUrls = null
|
||||
),
|
||||
selectedImages
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = OrganicSpacing.lg),
|
||||
enabled = !isSubmitting,
|
||||
isLoading = isSubmitting,
|
||||
icon = Icons.Default.CheckCircle
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +449,7 @@ private fun SectionHeader(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm)
|
||||
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
@@ -503,7 +477,7 @@ private fun ImageThumbnailCard(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.clip(RoundedCornerShape(AppRadius.md))
|
||||
.clip(OrganicShapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
if (imageBitmap != null) {
|
||||
@@ -530,7 +504,7 @@ private fun ImageThumbnailCard(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(AppSpacing.xs)
|
||||
.padding(OrganicSpacing.xs)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.error)
|
||||
@@ -565,16 +539,16 @@ private fun ContractorPickerSheet(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = AppSpacing.xl)
|
||||
.padding(bottom = OrganicSpacing.xl)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.completions_select_contractor),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md)
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
OrganicDivider()
|
||||
|
||||
// None option
|
||||
ListItem(
|
||||
@@ -592,13 +566,13 @@ private fun ContractorPickerSheet(
|
||||
modifier = Modifier.clickable { onSelect(null) }
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
OrganicDivider()
|
||||
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.xl),
|
||||
.padding(OrganicSpacing.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
@@ -31,6 +27,7 @@ import com.example.casera.network.ApiResult
|
||||
import com.example.casera.platform.rememberShareContractor
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -123,64 +120,52 @@ fun ContractorDetailScreen(
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFFF9FAFB)
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.background(Color(0xFFF9FAFB))
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val residences = DataManager.residences.value
|
||||
WarmGradientBackground {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val residences = DataManager.residences.value
|
||||
|
||||
ApiResultHandler(
|
||||
state = contractorState,
|
||||
onRetry = { viewModel.loadContractorDetail(contractorId) },
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_load),
|
||||
loadingContent = {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
) { contractor ->
|
||||
ApiResultHandler(
|
||||
state = contractorState,
|
||||
onRetry = { viewModel.loadContractorDetail(contractorId) },
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_load),
|
||||
loadingContent = {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
) { contractor ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
contentPadding = PaddingValues(OrganicSpacing.medium),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
|
||||
) {
|
||||
// Header Card
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
.padding(OrganicSpacing.large),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Avatar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Person,
|
||||
size = 80.dp,
|
||||
iconSize = 48.dp,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
iconTint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.medium))
|
||||
|
||||
Text(
|
||||
text = contractor.name,
|
||||
@@ -198,18 +183,18 @@ fun ContractorDetailScreen(
|
||||
}
|
||||
|
||||
if (contractor.specialties.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.medium))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small, Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
|
||||
) {
|
||||
contractor.specialties.forEach { specialty ->
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
shape = OrganicShapes.large,
|
||||
color = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
@@ -218,7 +203,7 @@ fun ContractorDetailScreen(
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
|
||||
Text(
|
||||
text = specialty.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -232,7 +217,7 @@ fun ContractorDetailScreen(
|
||||
}
|
||||
|
||||
if (contractor.rating != null && contractor.rating > 0) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.medium))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
repeat(5) { index ->
|
||||
Icon(
|
||||
@@ -242,7 +227,7 @@ fun ContractorDetailScreen(
|
||||
tint = Color(0xFFF59E0B)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.small))
|
||||
Text(
|
||||
text = ((contractor.rating * 10).toInt() / 10.0).toString(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@@ -253,7 +238,7 @@ fun ContractorDetailScreen(
|
||||
}
|
||||
|
||||
if (contractor.taskCount > 0) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.extraSmall))
|
||||
Text(
|
||||
text = stringResource(Res.string.contractors_completed_tasks, contractor.taskCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -269,7 +254,7 @@ fun ContractorDetailScreen(
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
|
||||
) {
|
||||
contractor.phone?.let { phone ->
|
||||
QuickActionButton(
|
||||
@@ -388,7 +373,7 @@ fun ContractorDetailScreen(
|
||||
text = stringResource(Res.string.contractors_no_contact_info),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.medium)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -459,8 +444,8 @@ fun ContractorDetailScreen(
|
||||
item {
|
||||
DetailSection(title = stringResource(Res.string.contractors_notes)) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Notes,
|
||||
@@ -484,7 +469,7 @@ fun ContractorDetailScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(OrganicSpacing.medium),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
StatCard(
|
||||
@@ -516,7 +501,7 @@ fun ContractorDetailScreen(
|
||||
value = createdBy.username,
|
||||
iconTint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
OrganicDivider(modifier = Modifier.padding(horizontal = OrganicSpacing.medium))
|
||||
}
|
||||
|
||||
DetailRow(
|
||||
@@ -531,6 +516,7 @@ fun ContractorDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showEditDialog) {
|
||||
AddContractorDialog(
|
||||
@@ -565,8 +551,8 @@ fun ContractorDetailScreen(
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
},
|
||||
containerColor = Color.White,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
shape = OrganicShapes.large
|
||||
)
|
||||
}
|
||||
|
||||
@@ -591,19 +577,14 @@ fun DetailSection(
|
||||
title: String,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(16.dp).padding(bottom = 0.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.medium).padding(bottom = 0.dp)
|
||||
)
|
||||
content()
|
||||
}
|
||||
@@ -620,7 +601,7 @@ fun DetailRow(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
@@ -629,7 +610,7 @@ fun DetailRow(
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.medium))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = label,
|
||||
@@ -658,7 +639,7 @@ fun ClickableDetailRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
@@ -667,7 +648,7 @@ fun ClickableDetailRow(
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.medium))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = label,
|
||||
@@ -698,33 +679,23 @@ fun QuickActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
OrganicCard(
|
||||
modifier = modifier.clickable(onClick = onClick)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
.padding(vertical = OrganicSpacing.medium),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OrganicIconContainer(
|
||||
icon = icon,
|
||||
size = 44.dp,
|
||||
iconSize = 22.dp,
|
||||
containerColor = color.copy(alpha = 0.1f),
|
||||
iconTint = color
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.small))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
@@ -745,21 +716,14 @@ fun StatCard(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OrganicIconContainer(
|
||||
icon = icon,
|
||||
size = 44.dp,
|
||||
iconSize = 22.dp,
|
||||
containerColor = color.copy(alpha = 0.1f),
|
||||
iconTint = color
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.small))
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@@ -14,8 +11,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -31,6 +26,7 @@ import com.example.casera.ui.subscription.UpgradeFeatureScreen
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -215,130 +211,135 @@ fun ContractorsScreen(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
// Search bar
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
WarmGradientBackground {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
placeholder = { Text(stringResource(Res.string.contractors_search)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) },
|
||||
trailingIcon = {
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { searchQuery = "" }) {
|
||||
Icon(Icons.Default.Close, stringResource(Res.string.contractors_clear_search))
|
||||
}
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
|
||||
// Active filters display
|
||||
if (selectedFilter != null || showFavoritesOnly) {
|
||||
Row(
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
// Search bar
|
||||
OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (showFavoritesOnly) {
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = { showFavoritesOnly = false },
|
||||
label = { Text(stringResource(Res.string.contractors_favorites)) },
|
||||
leadingIcon = { Icon(Icons.Default.Star, null, modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
if (selectedFilter != null) {
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = { selectedFilter = null },
|
||||
label = { Text(selectedFilter!!) },
|
||||
trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApiResultHandler(
|
||||
state = contractorsState,
|
||||
onRetry = {
|
||||
viewModel.loadContractors()
|
||||
},
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_load),
|
||||
loadingContent = {
|
||||
if (!isRefreshing) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
) { _ ->
|
||||
// Use filteredContractors for client-side filtering
|
||||
if (filteredContractors.isEmpty()) {
|
||||
// Empty state
|
||||
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)
|
||||
stringResource(Res.string.contractors_no_results)
|
||||
else
|
||||
stringResource(Res.string.contractors_empty_title),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
|
||||
Text(
|
||||
stringResource(Res.string.contractors_empty_subtitle_first),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.small),
|
||||
placeholder = { Text(stringResource(Res.string.contractors_search)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) },
|
||||
trailingIcon = {
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { searchQuery = "" }) {
|
||||
Icon(Icons.Default.Close, stringResource(Res.string.contractors_clear_search))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
isRefreshing = true
|
||||
viewModel.loadContractors()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
},
|
||||
singleLine = true,
|
||||
shape = OrganicShapes.medium,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
|
||||
// Active filters display
|
||||
if (selectedFilter != null || showFavoritesOnly) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
|
||||
) {
|
||||
LazyColumn(
|
||||
if (showFavoritesOnly) {
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = { showFavoritesOnly = false },
|
||||
label = { Text(stringResource(Res.string.contractors_favorites)) },
|
||||
leadingIcon = { Icon(Icons.Default.Star, null, modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
if (selectedFilter != null) {
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = { selectedFilter = null },
|
||||
label = { Text(selectedFilter!!) },
|
||||
trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApiResultHandler(
|
||||
state = contractorsState,
|
||||
onRetry = {
|
||||
viewModel.loadContractors()
|
||||
},
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_load),
|
||||
loadingContent = {
|
||||
if (!isRefreshing) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
) { _ ->
|
||||
// Use filteredContractors for client-side filtering
|
||||
if (filteredContractors.isEmpty()) {
|
||||
// Empty state with organic styling
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
items(filteredContractors, key = { it.id }) { contractor ->
|
||||
ContractorCard(
|
||||
contractor = contractor,
|
||||
onToggleFavorite = { viewModel.toggleFavorite(it) },
|
||||
onClick = { onNavigateToContractorDetail(it) }
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.PersonAdd,
|
||||
size = 80.dp,
|
||||
iconSize = 40.dp,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
iconTint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.small))
|
||||
Text(
|
||||
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
|
||||
stringResource(Res.string.contractors_no_results)
|
||||
else
|
||||
stringResource(Res.string.contractors_empty_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
|
||||
Text(
|
||||
stringResource(Res.string.contractors_empty_subtitle_first),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
isRefreshing = true
|
||||
viewModel.loadContractors()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(OrganicSpacing.medium),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
|
||||
) {
|
||||
items(filteredContractors, key = { it.id }) { contractor ->
|
||||
ContractorCard(
|
||||
contractor = contractor,
|
||||
onToggleFavorite = { viewModel.toggleFavorite(it) },
|
||||
onClick = { onNavigateToContractorDetail(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,41 +382,27 @@ fun ContractorCard(
|
||||
onToggleFavorite: (Int) -> Unit,
|
||||
onClick: (Int) -> Unit
|
||||
) {
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick(contractor.id) },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = 1.dp
|
||||
)
|
||||
.clickable { onClick(contractor.id) }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(OrganicSpacing.medium),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Avatar/Icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Person,
|
||||
size = 56.dp,
|
||||
iconSize = 32.dp,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
iconTint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.medium))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -428,7 +415,7 @@ fun ContractorCard(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (contractor.isFavorite) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = stringResource(Res.string.contractors_favorite),
|
||||
@@ -448,10 +435,10 @@ fun ContractorCard(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.small))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (contractor.specialties.isNotEmpty()) {
|
||||
@@ -462,7 +449,7 @@ fun ContractorCard(
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
|
||||
Text(
|
||||
text = contractor.specialties.first().name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -479,7 +466,7 @@ fun ContractorCard(
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
|
||||
Text(
|
||||
text = "${(contractor.rating * 10).toInt() / 10.0}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -497,7 +484,7 @@ fun ContractorCard(
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
|
||||
Text(
|
||||
text = stringResource(Res.string.contractors_tasks_count, contractor.taskCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -528,4 +515,3 @@ fun ContractorCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import coil3.compose.SubcomposeAsyncImageContent
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import com.example.casera.ui.components.AuthenticatedImage
|
||||
import com.example.casera.util.DateUtils
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -96,22 +97,23 @@ fun DocumentDetailScreen(
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
ApiResultHandler(
|
||||
state = documentState,
|
||||
onRetry = { documentViewModel.loadDocumentDetail(documentId) },
|
||||
errorTitle = stringResource(Res.string.documents_failed_to_load)
|
||||
) { document ->
|
||||
WarmGradientBackground {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
ApiResultHandler(
|
||||
state = documentState,
|
||||
onRetry = { documentViewModel.loadDocumentDetail(documentId) },
|
||||
errorTitle = stringResource(Res.string.documents_failed_to_load)
|
||||
) { document ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
.padding(start = OrganicSpacing.lg, end = OrganicSpacing.lg, top = OrganicSpacing.lg, bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
|
||||
) {
|
||||
// Status badge (for warranties)
|
||||
if (document.documentType == "warranty") {
|
||||
@@ -124,16 +126,14 @@ fun DocumentDetailScreen(
|
||||
else -> Color(0xFF10B981)
|
||||
}
|
||||
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = statusColor.copy(alpha = 0.1f)
|
||||
)
|
||||
accentColor = statusColor.copy(alpha = 0.1f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -175,17 +175,17 @@ fun DocumentDetailScreen(
|
||||
}
|
||||
|
||||
// Basic Information
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_basic_info),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
DetailRow(stringResource(Res.string.documents_title_label), document.title)
|
||||
DetailRow(stringResource(Res.string.documents_type_label), DocumentType.fromValue(document.documentType).displayName)
|
||||
@@ -202,17 +202,17 @@ fun DocumentDetailScreen(
|
||||
if (document.documentType == "warranty" &&
|
||||
(document.itemName != null || document.modelNumber != null ||
|
||||
document.serialNumber != null || document.provider != null)) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_item_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
document.itemName?.let { DetailRow(stringResource(Res.string.documents_item_name), it) }
|
||||
document.modelNumber?.let { DetailRow(stringResource(Res.string.documents_model_number), it) }
|
||||
@@ -227,17 +227,17 @@ fun DocumentDetailScreen(
|
||||
if (document.documentType == "warranty" &&
|
||||
(document.claimPhone != null || document.claimEmail != null ||
|
||||
document.claimWebsite != null)) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_claim_info),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
document.claimPhone?.let { DetailRow(stringResource(Res.string.documents_claim_phone), it) }
|
||||
document.claimEmail?.let { DetailRow(stringResource(Res.string.documents_claim_email), it) }
|
||||
@@ -249,17 +249,17 @@ fun DocumentDetailScreen(
|
||||
// Dates
|
||||
if (document.purchaseDate != null || document.startDate != null ||
|
||||
document.endDate != null) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_important_dates),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
document.purchaseDate?.let { DetailRow(stringResource(Res.string.documents_purchase_date), DateUtils.formatDateMedium(it)) }
|
||||
document.startDate?.let { DetailRow(stringResource(Res.string.documents_start_date), DateUtils.formatDateMedium(it)) }
|
||||
@@ -269,17 +269,17 @@ fun DocumentDetailScreen(
|
||||
}
|
||||
|
||||
// Residence & Contractor
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_associations),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
document.residenceAddress?.let { DetailRow(stringResource(Res.string.documents_residence), it) }
|
||||
document.contractorName?.let { DetailRow(stringResource(Res.string.documents_contractor), it) }
|
||||
@@ -289,17 +289,17 @@ fun DocumentDetailScreen(
|
||||
|
||||
// Additional Information
|
||||
if (document.tags != null || document.notes != null) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_additional_info),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
document.tags?.let { DetailRow(stringResource(Res.string.documents_tags), it) }
|
||||
document.notes?.let { DetailRow(stringResource(Res.string.documents_notes), it) }
|
||||
@@ -309,22 +309,22 @@ fun DocumentDetailScreen(
|
||||
|
||||
// Images
|
||||
if (document.images.isNotEmpty()) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_images, document.images.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
// Image grid
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
document.images.take(4).forEachIndexed { index, image ->
|
||||
Box(
|
||||
@@ -366,17 +366,17 @@ fun DocumentDetailScreen(
|
||||
|
||||
// File Information
|
||||
if (document.fileUrl != null) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_attached_file),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
document.fileType?.let { DetailRow(stringResource(Res.string.documents_file_type), it) }
|
||||
document.fileSize?.let {
|
||||
@@ -388,7 +388,7 @@ fun DocumentDetailScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(Icons.Default.Download, null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text(stringResource(Res.string.documents_download_file))
|
||||
}
|
||||
}
|
||||
@@ -396,17 +396,17 @@ fun DocumentDetailScreen(
|
||||
}
|
||||
|
||||
// Metadata
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.documents_metadata),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
document.uploadedByUsername?.let { DetailRow(stringResource(Res.string.documents_uploaded_by), it) }
|
||||
document.createdAt?.let { DetailRow(stringResource(Res.string.documents_created), DateUtils.formatDateMedium(it)) }
|
||||
@@ -417,6 +417,7 @@ fun DocumentDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete confirmation dialog
|
||||
if (showDeleteDialog) {
|
||||
@@ -463,7 +464,7 @@ fun DetailRow(label: String, value: String) {
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xs))
|
||||
Text(
|
||||
value,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
@@ -498,7 +499,7 @@ fun DocumentImageViewer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.95f)
|
||||
.fillMaxHeight(0.9f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
shape = RoundedCornerShape(OrganicSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Column(
|
||||
@@ -508,7 +509,7 @@ fun DocumentImageViewer(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -531,7 +532,7 @@ fun DocumentImageViewer(
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
OrganicDivider()
|
||||
|
||||
// Content
|
||||
if (showFullImage) {
|
||||
@@ -539,7 +540,7 @@ fun DocumentImageViewer(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
@@ -553,16 +554,13 @@ fun DocumentImageViewer(
|
||||
)
|
||||
|
||||
images[selectedIndex].caption?.let { caption ->
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = caption,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
@@ -570,7 +568,7 @@ fun DocumentImageViewer(
|
||||
|
||||
// Navigation buttons
|
||||
if (images.size > 1) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
@@ -580,7 +578,7 @@ fun DocumentImageViewer(
|
||||
enabled = selectedIndex > 0
|
||||
) {
|
||||
Icon(Icons.Default.ArrowBack, "Previous")
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text("Previous")
|
||||
}
|
||||
Button(
|
||||
@@ -588,7 +586,7 @@ fun DocumentImageViewer(
|
||||
enabled = selectedIndex < images.size - 1
|
||||
) {
|
||||
Text("Next")
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Icon(Icons.Default.ArrowForward, "Next")
|
||||
}
|
||||
}
|
||||
@@ -600,19 +598,19 @@ fun DocumentImageViewer(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
items(images.size) { index ->
|
||||
val image = images[index]
|
||||
Card(
|
||||
onClick = {
|
||||
selectedIndex = index
|
||||
showFullImage = true
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
selectedIndex = index
|
||||
showFullImage = true
|
||||
}
|
||||
) {
|
||||
Column {
|
||||
AuthenticatedImage(
|
||||
@@ -627,7 +625,7 @@ fun DocumentImageViewer(
|
||||
image.caption?.let { caption ->
|
||||
Text(
|
||||
text = caption,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
modifier = Modifier.padding(OrganicSpacing.sm),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import com.example.casera.viewmodel.DocumentViewModel
|
||||
import com.example.casera.models.*
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -120,7 +121,7 @@ fun DocumentsScreen(
|
||||
showFiltersMenu = false
|
||||
}
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
DocumentCategory.values().forEach { category ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(category.displayName) },
|
||||
@@ -138,7 +139,7 @@ fun DocumentsScreen(
|
||||
showFiltersMenu = false
|
||||
}
|
||||
)
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
DocumentType.values().forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(type.displayName) },
|
||||
@@ -199,31 +200,33 @@ fun DocumentsScreen(
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
if (isBlocked.allowed) {
|
||||
// Screen is blocked (limit=0) - show upgrade prompt
|
||||
UpgradeFeatureScreen(
|
||||
triggerKey = isBlocked.triggerKey ?: "view_documents",
|
||||
icon = Icons.Default.Description,
|
||||
onNavigateBack = onNavigateBack
|
||||
)
|
||||
} else {
|
||||
// Pro users see normal content - use client-side filtered documents
|
||||
DocumentsTabContent(
|
||||
state = documentsState,
|
||||
filteredDocuments = filteredDocuments,
|
||||
isWarrantyTab = selectedTab == DocumentTab.WARRANTIES,
|
||||
onDocumentClick = onNavigateToDocumentDetail,
|
||||
onRetry = {
|
||||
// Reload all documents on pull-to-refresh
|
||||
documentViewModel.loadAllDocuments(residenceId = residenceId)
|
||||
},
|
||||
onNavigateBack = onNavigateBack
|
||||
)
|
||||
WarmGradientBackground {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
if (isBlocked.allowed) {
|
||||
// Screen is blocked (limit=0) - show upgrade prompt
|
||||
UpgradeFeatureScreen(
|
||||
triggerKey = isBlocked.triggerKey ?: "view_documents",
|
||||
icon = Icons.Default.Description,
|
||||
onNavigateBack = onNavigateBack
|
||||
)
|
||||
} else {
|
||||
// Pro users see normal content - use client-side filtered documents
|
||||
DocumentsTabContent(
|
||||
state = documentsState,
|
||||
filteredDocuments = filteredDocuments,
|
||||
isWarrantyTab = selectedTab == DocumentTab.WARRANTIES,
|
||||
onDocumentClick = onNavigateToDocumentDetail,
|
||||
onRetry = {
|
||||
// Reload all documents on pull-to-refresh
|
||||
documentViewModel.loadAllDocuments(residenceId = residenceId)
|
||||
},
|
||||
onNavigateBack = onNavigateBack
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.example.casera.viewmodel.ResidenceViewModel
|
||||
import com.example.casera.repository.LookupsRepository
|
||||
import com.example.casera.models.*
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -103,239 +104,233 @@ fun EditTaskScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Required fields section
|
||||
Text(
|
||||
text = stringResource(Res.string.tasks_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text(stringResource(Res.string.tasks_title_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = titleError.isNotEmpty(),
|
||||
supportingText = if (titleError.isNotEmpty()) {
|
||||
{ Text(titleError) }
|
||||
} else null
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text(stringResource(Res.string.tasks_description_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
)
|
||||
|
||||
// Category dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = categoryExpanded,
|
||||
onExpandedChange = { categoryExpanded = it }
|
||||
WarmGradientBackground {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(OrganicSpacing.cozy)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedCategory?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.tasks_category_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = categories.isNotEmpty()
|
||||
// Required fields section
|
||||
Text(
|
||||
text = stringResource(Res.string.tasks_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = categoryExpanded,
|
||||
onDismissRequest = { categoryExpanded = false }
|
||||
) {
|
||||
categories.forEach { category ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(category.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedCategory = category
|
||||
categoryExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Frequency dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = frequencyExpanded,
|
||||
onExpandedChange = { frequencyExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedFrequency?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.tasks_frequency_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = frequencyExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = frequencies.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = frequencyExpanded,
|
||||
onDismissRequest = { frequencyExpanded = false }
|
||||
) {
|
||||
frequencies.forEach { frequency ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(frequency.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedFrequency = frequency
|
||||
frequencyExpanded = false
|
||||
// Clear custom interval if not Custom frequency
|
||||
if (!frequency.name.equals("Custom", ignoreCase = true)) {
|
||||
customIntervalDays = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Interval Days (only for "Custom" frequency)
|
||||
if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true) {
|
||||
OutlinedTextField(
|
||||
value = customIntervalDays,
|
||||
onValueChange = { customIntervalDays = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.tasks_interval_days)) },
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text(stringResource(Res.string.tasks_title_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) },
|
||||
singleLine = true
|
||||
isError = titleError.isNotEmpty(),
|
||||
supportingText = if (titleError.isNotEmpty()) {
|
||||
{ Text(titleError) }
|
||||
} else null
|
||||
)
|
||||
}
|
||||
|
||||
// Priority dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = priorityExpanded,
|
||||
onExpandedChange = { priorityExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedPriority?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.tasks_priority_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = priorityExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = priorities.isNotEmpty()
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text(stringResource(Res.string.tasks_description_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = priorityExpanded,
|
||||
onDismissRequest = { priorityExpanded = false }
|
||||
|
||||
// Category dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = categoryExpanded,
|
||||
onExpandedChange = { categoryExpanded = it }
|
||||
) {
|
||||
priorities.forEach { priority ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(priority.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedPriority = priority
|
||||
priorityExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In Progress toggle
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.tasks_in_progress_label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Switch(
|
||||
checked = inProgress,
|
||||
onCheckedChange = { inProgress = it }
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = dueDate,
|
||||
onValueChange = { dueDate = it },
|
||||
label = { Text(stringResource(Res.string.tasks_due_date_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = dueDateError.isNotEmpty(),
|
||||
supportingText = if (dueDateError.isNotEmpty()) {
|
||||
{ Text(dueDateError) }
|
||||
} else null,
|
||||
placeholder = { Text(stringResource(Res.string.tasks_due_date_placeholder)) }
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = estimatedCost,
|
||||
onValueChange = { estimatedCost = it },
|
||||
label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = { Text("$") }
|
||||
)
|
||||
|
||||
// Error message
|
||||
if (updateTaskState is ApiResult.Error) {
|
||||
Text(
|
||||
text = com.example.casera.util.ErrorMessageParser.parse((updateTaskState as ApiResult.Error).message),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
// Submit button
|
||||
Button(
|
||||
onClick = {
|
||||
if (validateForm() && selectedCategory != null &&
|
||||
selectedFrequency != null && selectedPriority != null) {
|
||||
viewModel.updateTask(
|
||||
taskId = task.id,
|
||||
request = TaskCreateRequest(
|
||||
residenceId = task.residenceId,
|
||||
title = title,
|
||||
description = description.ifBlank { null },
|
||||
categoryId = selectedCategory!!.id,
|
||||
frequencyId = selectedFrequency!!.id,
|
||||
customIntervalDays = if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true && customIntervalDays.isNotBlank()) {
|
||||
customIntervalDays.toIntOrNull()
|
||||
} else null,
|
||||
priorityId = selectedPriority!!.id,
|
||||
inProgress = inProgress,
|
||||
dueDate = dueDate,
|
||||
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = validateForm() && selectedCategory != null &&
|
||||
selectedFrequency != null && selectedPriority != null
|
||||
) {
|
||||
if (updateTaskState is ApiResult.Loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
OutlinedTextField(
|
||||
value = selectedCategory?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.tasks_category_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = categories.isNotEmpty()
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(Res.string.tasks_update))
|
||||
ExposedDropdownMenu(
|
||||
expanded = categoryExpanded,
|
||||
onDismissRequest = { categoryExpanded = false }
|
||||
) {
|
||||
categories.forEach { category ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(category.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedCategory = category
|
||||
categoryExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
// Frequency dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = frequencyExpanded,
|
||||
onExpandedChange = { frequencyExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedFrequency?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.tasks_frequency_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = frequencyExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = frequencies.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = frequencyExpanded,
|
||||
onDismissRequest = { frequencyExpanded = false }
|
||||
) {
|
||||
frequencies.forEach { frequency ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(frequency.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedFrequency = frequency
|
||||
frequencyExpanded = false
|
||||
// Clear custom interval if not Custom frequency
|
||||
if (!frequency.name.equals("Custom", ignoreCase = true)) {
|
||||
customIntervalDays = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Interval Days (only for "Custom" frequency)
|
||||
if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true) {
|
||||
OutlinedTextField(
|
||||
value = customIntervalDays,
|
||||
onValueChange = { customIntervalDays = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.tasks_interval_days)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) },
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
// Priority dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = priorityExpanded,
|
||||
onExpandedChange = { priorityExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedPriority?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.tasks_priority_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = priorityExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = priorities.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = priorityExpanded,
|
||||
onDismissRequest = { priorityExpanded = false }
|
||||
) {
|
||||
priorities.forEach { priority ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(priority.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedPriority = priority
|
||||
priorityExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In Progress toggle
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.tasks_in_progress_label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Switch(
|
||||
checked = inProgress,
|
||||
onCheckedChange = { inProgress = it }
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = dueDate,
|
||||
onValueChange = { dueDate = it },
|
||||
label = { Text(stringResource(Res.string.tasks_due_date_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = dueDateError.isNotEmpty(),
|
||||
supportingText = if (dueDateError.isNotEmpty()) {
|
||||
{ Text(dueDateError) }
|
||||
} else null,
|
||||
placeholder = { Text(stringResource(Res.string.tasks_due_date_placeholder)) }
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = estimatedCost,
|
||||
onValueChange = { estimatedCost = it },
|
||||
label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = { Text("$") }
|
||||
)
|
||||
|
||||
// Error message
|
||||
if (updateTaskState is ApiResult.Error) {
|
||||
Text(
|
||||
text = com.example.casera.util.ErrorMessageParser.parse((updateTaskState as ApiResult.Error).message),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
// Submit button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.tasks_update),
|
||||
onClick = {
|
||||
if (validateForm() && selectedCategory != null &&
|
||||
selectedFrequency != null && selectedPriority != null) {
|
||||
viewModel.updateTask(
|
||||
taskId = task.id,
|
||||
request = TaskCreateRequest(
|
||||
residenceId = task.residenceId,
|
||||
title = title,
|
||||
description = description.ifBlank { null },
|
||||
categoryId = selectedCategory!!.id,
|
||||
frequencyId = selectedFrequency!!.id,
|
||||
customIntervalDays = if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true && customIntervalDays.isNotBlank()) {
|
||||
customIntervalDays.toIntOrNull()
|
||||
} else null,
|
||||
priorityId = selectedPriority!!.id,
|
||||
inProgress = inProgress,
|
||||
dueDate = dueDate,
|
||||
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
enabled = validateForm() && selectedCategory != null &&
|
||||
selectedFrequency != null && selectedPriority != null,
|
||||
isLoading = updateTaskState is ApiResult.Loading
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@@ -18,6 +16,7 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.PasswordResetViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -74,128 +73,127 @@ fun ForgotPasswordScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
WarmGradientBackground {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
showBlob = true,
|
||||
blobVariation = 0
|
||||
) {
|
||||
AuthHeader(
|
||||
icon = Icons.Default.Key,
|
||||
title = stringResource(Res.string.auth_forgot_title),
|
||||
subtitle = stringResource(Res.string.auth_forgot_subtitle)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = {
|
||||
email = it
|
||||
viewModel.resetForgotPasswordState()
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_forgot_email_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
Text(
|
||||
"We'll send a 6-digit verification code to this address",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
if (isSuccess) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
"Check your email for a 6-digit verification code",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.setEmail(email)
|
||||
viewModel.requestPasswordReset(email)
|
||||
},
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = email.isNotEmpty() && !isLoading,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
.padding(OrganicSpacing.spacious),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.Send, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Key,
|
||||
size = 80.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_forgot_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_forgot_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = {
|
||||
email = it
|
||||
viewModel.resetForgotPasswordState()
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_forgot_email_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
Text(
|
||||
"We'll send a 6-digit verification code to this address",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
if (isSuccess) {
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.primary,
|
||||
showBlob = false
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"Check your email for a 6-digit verification code",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_forgot_button),
|
||||
onClick = {
|
||||
viewModel.setEmail(email)
|
||||
viewModel.requestPasswordReset(email)
|
||||
},
|
||||
enabled = email.isNotEmpty() && !isLoading,
|
||||
isLoading = isLoading
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.auth_forgot_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
"Remember your password? Back to Login",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Remember your password? Back to Login",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.example.casera.ui.components.HandleErrors
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.ResidenceViewModel
|
||||
import com.example.casera.viewmodel.TaskViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
@@ -67,194 +63,145 @@ fun HomeScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
// Personalized Greeting
|
||||
WarmGradientBackground {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.home_welcome),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.home_manage_properties),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
// Summary Card
|
||||
when (summaryState) {
|
||||
is ApiResult.Success -> {
|
||||
val summary = (summaryState as ApiResult.Success).data
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
// Personalized Greeting
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = OrganicSpacing.cozy)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.home_welcome),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.home_manage_properties),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
// Summary Card
|
||||
when (summaryState) {
|
||||
is ApiResult.Success -> {
|
||||
val summary = (summaryState as ApiResult.Success).data
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showBlob = true,
|
||||
blobVariation = 0
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Gradient circular icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
Color(0xFF2563EB),
|
||||
Color(0xFF8B5CF6)
|
||||
)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
// Gradient circular icon
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Home,
|
||||
size = 44.dp
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(Res.string.home_overview),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.home_property_stats),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(Res.string.home_overview),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.generous))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
OrganicStatPill(
|
||||
icon = Icons.Default.Home,
|
||||
value = "${summary.residences.size}",
|
||||
label = stringResource(Res.string.home_properties),
|
||||
color = Color(0xFF3B82F6)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.home_property_stats),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
OrganicDivider(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.width(1.dp),
|
||||
vertical = true
|
||||
)
|
||||
OrganicStatPill(
|
||||
icon = Icons.Default.Task,
|
||||
value = "${totalSummary?.totalTasks ?: 0}",
|
||||
label = stringResource(Res.string.home_total_tasks),
|
||||
color = Color(0xFF8B5CF6)
|
||||
)
|
||||
OrganicDivider(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.width(1.dp),
|
||||
vertical = true
|
||||
)
|
||||
OrganicStatPill(
|
||||
icon = Icons.Default.Schedule,
|
||||
value = "${totalSummary?.totalPending ?: 0}",
|
||||
label = stringResource(Res.string.home_pending),
|
||||
color = Color(0xFFF59E0B)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
}
|
||||
}
|
||||
is ApiResult.Idle, is ApiResult.Loading -> {
|
||||
OrganicCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
StatItem(
|
||||
value = "${summary.residences.size}",
|
||||
label = stringResource(Res.string.home_properties),
|
||||
color = Color(0xFF3B82F6)
|
||||
)
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.width(1.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
StatItem(
|
||||
value = "${totalSummary?.totalTasks ?: 0}",
|
||||
label = stringResource(Res.string.home_total_tasks),
|
||||
color = Color(0xFF8B5CF6)
|
||||
)
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.width(1.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
StatItem(
|
||||
value = "${totalSummary?.totalPending ?: 0}",
|
||||
label = stringResource(Res.string.home_pending),
|
||||
color = Color(0xFFF59E0B)
|
||||
)
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ApiResult.Idle, is ApiResult.Loading -> {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
// Don't show error card, just let navigation cards show
|
||||
}
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
// Don't show error card, just let navigation cards show
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
// Residences Card
|
||||
NavigationCard(
|
||||
title = stringResource(Res.string.home_properties),
|
||||
subtitle = stringResource(Res.string.home_manage_residences),
|
||||
icon = Icons.Default.Home,
|
||||
iconColor = Color(0xFF3B82F6),
|
||||
onClick = onNavigateToResidences
|
||||
)
|
||||
|
||||
// Tasks Card
|
||||
NavigationCard(
|
||||
title = stringResource(Res.string.home_tasks),
|
||||
subtitle = stringResource(Res.string.home_view_manage_tasks),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
iconColor = Color(0xFF10B981),
|
||||
onClick = onNavigateToTasks
|
||||
)
|
||||
}
|
||||
|
||||
// Residences Card
|
||||
NavigationCard(
|
||||
title = stringResource(Res.string.home_properties),
|
||||
subtitle = stringResource(Res.string.home_manage_residences),
|
||||
icon = Icons.Default.Home,
|
||||
iconColor = Color(0xFF3B82F6),
|
||||
onClick = onNavigateToResidences
|
||||
)
|
||||
|
||||
// Tasks Card
|
||||
NavigationCard(
|
||||
title = stringResource(Res.string.home_tasks),
|
||||
subtitle = stringResource(Res.string.home_view_manage_tasks),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
iconColor = Color(0xFF10B981),
|
||||
onClick = onNavigateToTasks
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatItem(
|
||||
value: String,
|
||||
label: String,
|
||||
color: Color
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavigationCard(
|
||||
title: String,
|
||||
@@ -263,45 +210,24 @@ private fun NavigationCard(
|
||||
iconColor: Color,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
shape = MaterialTheme.shapes.large,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||
showBlob = true,
|
||||
blobVariation = 1
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.comfortable)
|
||||
) {
|
||||
// Gradient circular icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
iconColor,
|
||||
iconColor.copy(alpha = 0.7f)
|
||||
)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
OrganicIconContainer(
|
||||
icon = icon,
|
||||
size = 56.dp,
|
||||
iconColor = iconColor
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
@@ -311,7 +237,7 @@ private fun NavigationCard(
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.minimal))
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -93,186 +94,142 @@ fun LoginScreen(
|
||||
|
||||
val isLoading = loginState is ApiResult.Loading || googleSignInState is ApiResult.Loading
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
WarmGradientBackground {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
showBlob = true,
|
||||
blobVariation = 0
|
||||
) {
|
||||
AuthHeader(
|
||||
icon = Icons.Default.Home,
|
||||
title = stringResource(Res.string.app_name),
|
||||
subtitle = stringResource(Res.string.app_tagline)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(stringResource(Res.string.auth_login_username_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(Res.string.auth_login_password_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (passwordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
// Clear Google error when user starts typing
|
||||
LaunchedEffect(username, password) {
|
||||
googleSignInError = null
|
||||
}
|
||||
|
||||
// Gradient button
|
||||
Box(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.then(
|
||||
if (username.isNotEmpty() && password.isNotEmpty() && !isLoading) {
|
||||
Modifier.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
Color(0xFF2563EB),
|
||||
Color(0xFF8B5CF6)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Modifier.background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f))
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
.padding(OrganicSpacing.xxl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
|
||||
) {
|
||||
Button(
|
||||
AuthHeader(
|
||||
icon = Icons.Default.Home,
|
||||
title = stringResource(Res.string.app_name),
|
||||
subtitle = stringResource(Res.string.app_tagline)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(stringResource(Res.string.auth_login_username_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(Res.string.auth_login_password_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (passwordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(OrganicRadius.md)
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
// Clear Google error when user starts typing
|
||||
LaunchedEffect(username, password) {
|
||||
googleSignInError = null
|
||||
}
|
||||
|
||||
// Organic Primary Button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_login_button),
|
||||
onClick = {
|
||||
viewModel.login(username, password)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = username.isNotEmpty() && password.isNotEmpty(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent
|
||||
)
|
||||
isLoading = isLoading
|
||||
)
|
||||
|
||||
// Divider with "or"
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(Res.string.auth_login_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
OrganicDivider(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "or",
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OrganicDivider(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Divider with "or"
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
// Google Sign In button (only shows on Android)
|
||||
GoogleSignInButton(
|
||||
onSignInStarted = {
|
||||
googleSignInError = null
|
||||
},
|
||||
onSignInSuccess = { idToken ->
|
||||
viewModel.googleSignIn(idToken)
|
||||
},
|
||||
onSignInError = { error ->
|
||||
googleSignInError = error
|
||||
},
|
||||
enabled = !isLoading
|
||||
)
|
||||
Text(
|
||||
text = "or",
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
|
||||
// Google Sign In button (only shows on Android)
|
||||
GoogleSignInButton(
|
||||
onSignInStarted = {
|
||||
googleSignInError = null
|
||||
},
|
||||
onSignInSuccess = { idToken ->
|
||||
viewModel.googleSignIn(idToken)
|
||||
},
|
||||
onSignInError = { error ->
|
||||
googleSignInError = error
|
||||
},
|
||||
enabled = !isLoading
|
||||
)
|
||||
TextButton(
|
||||
onClick = onNavigateToForgotPassword,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.auth_forgot_password),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onNavigateToForgotPassword,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.auth_forgot_password),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onNavigateToRegister,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.auth_no_account),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
TextButton(
|
||||
onClick = onNavigateToRegister,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.auth_no_account),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.example.casera.models.Residence
|
||||
import com.example.casera.models.TaskDetail
|
||||
import com.example.casera.storage.TokenStorage
|
||||
import com.example.casera.ui.subscription.UpgradeScreen
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -45,304 +46,289 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
tonalElevation = 3.dp
|
||||
WarmGradientBackground {
|
||||
Scaffold(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
tonalElevation = 3.dp
|
||||
) {
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Home, contentDescription = stringResource(Res.string.properties_title)) },
|
||||
label = { Text(stringResource(Res.string.properties_title)) },
|
||||
selected = selectedTab == 0,
|
||||
onClick = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = true }
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.CheckCircle, contentDescription = stringResource(Res.string.tasks_title)) },
|
||||
label = { Text(stringResource(Res.string.tasks_title)) },
|
||||
selected = selectedTab == 1,
|
||||
onClick = {
|
||||
selectedTab = 1
|
||||
navController.navigate(MainTabTasksRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = false }
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Build, contentDescription = stringResource(Res.string.contractors_title)) },
|
||||
label = { Text(stringResource(Res.string.contractors_title)) },
|
||||
selected = selectedTab == 2,
|
||||
onClick = {
|
||||
selectedTab = 2
|
||||
navController.navigate(MainTabContractorsRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = false }
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Description, contentDescription = stringResource(Res.string.documents_title)) },
|
||||
label = { Text(stringResource(Res.string.documents_title)) },
|
||||
selected = selectedTab == 3,
|
||||
onClick = {
|
||||
selectedTab = 3
|
||||
navController.navigate(MainTabDocumentsRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = false }
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = MainTabResidencesRoute,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Home, contentDescription = stringResource(Res.string.properties_title)) },
|
||||
label = { Text(stringResource(Res.string.properties_title)) },
|
||||
selected = selectedTab == 0,
|
||||
onClick = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = true }
|
||||
composable<MainTabResidencesRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ResidencesScreen(
|
||||
onResidenceClick = onResidenceClick,
|
||||
onAddResidence = onAddResidence,
|
||||
onLogout = onLogout,
|
||||
onNavigateToProfile = {
|
||||
// Don't change selectedTab since Profile isn't in the bottom nav
|
||||
navController.navigate(MainTabProfileRoute)
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.CheckCircle, contentDescription = stringResource(Res.string.tasks_title)) },
|
||||
label = { Text(stringResource(Res.string.tasks_title)) },
|
||||
selected = selectedTab == 1,
|
||||
onClick = {
|
||||
selectedTab = 1
|
||||
navController.navigate(MainTabTasksRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = false }
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Build, contentDescription = stringResource(Res.string.contractors_title)) },
|
||||
label = { Text(stringResource(Res.string.contractors_title)) },
|
||||
selected = selectedTab == 2,
|
||||
onClick = {
|
||||
selectedTab = 2
|
||||
navController.navigate(MainTabContractorsRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = false }
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Description, contentDescription = stringResource(Res.string.documents_title)) },
|
||||
label = { Text(stringResource(Res.string.documents_title)) },
|
||||
selected = selectedTab == 3,
|
||||
onClick = {
|
||||
selectedTab = 3
|
||||
navController.navigate(MainTabDocumentsRoute) {
|
||||
popUpTo(MainTabResidencesRoute) { inclusive = false }
|
||||
}
|
||||
},
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
// NavigationBarItem(
|
||||
// icon = { Icon(Icons.Default.Person, contentDescription = "Profile") },
|
||||
// label = { Text("Profile") },
|
||||
// selected = selectedTab == 4,
|
||||
// onClick = {
|
||||
// selectedTab = 4
|
||||
// navController.navigate(MainTabProfileRoute) {
|
||||
// popUpTo(MainTabResidencesRoute) { inclusive = false }
|
||||
// }
|
||||
// },
|
||||
// colors = NavigationBarItemDefaults.colors(
|
||||
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
// selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
// indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
// unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
// )
|
||||
// )
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = MainTabResidencesRoute,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
composable<MainTabResidencesRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ResidencesScreen(
|
||||
onResidenceClick = onResidenceClick,
|
||||
onAddResidence = onAddResidence,
|
||||
onLogout = onLogout,
|
||||
onNavigateToProfile = {
|
||||
// Don't change selectedTab since Profile isn't in the bottom nav
|
||||
navController.navigate(MainTabProfileRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<MainTabTasksRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AllTasksScreen(
|
||||
onNavigateToEditTask = onNavigateToEditTask,
|
||||
onAddTask = onAddTask,
|
||||
bottomNavBarPadding = paddingValues.calculateBottomPadding(),
|
||||
navigateToTaskId = navigateToTaskId,
|
||||
onClearNavigateToTask = onClearNavigateToTask,
|
||||
onNavigateToCompleteTask = { task, residenceName ->
|
||||
navController.navigate(
|
||||
CompleteTaskRoute(
|
||||
taskId = task.id,
|
||||
taskTitle = task.title,
|
||||
residenceName = residenceName
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<MainTabContractorsRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ContractorsScreen(
|
||||
onNavigateBack = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute)
|
||||
},
|
||||
onNavigateToContractorDetail = { contractorId ->
|
||||
navController.navigate(ContractorDetailRoute(contractorId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<ContractorDetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<ContractorDetailRoute>()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ContractorDetailScreen(
|
||||
contractorId = route.contractorId,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<MainTabDocumentsRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
DocumentsScreen(
|
||||
onNavigateBack = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute)
|
||||
},
|
||||
residenceId = null,
|
||||
onNavigateToAddDocument = { residenceId, documentType ->
|
||||
navController.navigate(
|
||||
AddDocumentRoute(
|
||||
residenceId = residenceId,
|
||||
initialDocumentType = documentType
|
||||
)
|
||||
)
|
||||
},
|
||||
onNavigateToDocumentDetail = { documentId ->
|
||||
navController.navigate(DocumentDetailRoute(documentId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<AddDocumentRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<AddDocumentRoute>()
|
||||
AddDocumentScreen(
|
||||
residenceId = route.residenceId,
|
||||
initialDocumentType = route.initialDocumentType,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onDocumentCreated = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<DocumentDetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<DocumentDetailRoute>()
|
||||
DocumentDetailScreen(
|
||||
documentId = route.documentId,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToEdit = { documentId ->
|
||||
navController.navigate(EditDocumentRoute(documentId))
|
||||
composable<MainTabTasksRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AllTasksScreen(
|
||||
onNavigateToEditTask = onNavigateToEditTask,
|
||||
onAddTask = onAddTask,
|
||||
bottomNavBarPadding = paddingValues.calculateBottomPadding(),
|
||||
navigateToTaskId = navigateToTaskId,
|
||||
onClearNavigateToTask = onClearNavigateToTask,
|
||||
onNavigateToCompleteTask = { task, residenceName ->
|
||||
navController.navigate(
|
||||
CompleteTaskRoute(
|
||||
taskId = task.id,
|
||||
taskTitle = task.title,
|
||||
residenceName = residenceName
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<EditDocumentRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<EditDocumentRoute>()
|
||||
EditDocumentScreen(
|
||||
documentId = route.documentId,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable<MainTabProfileRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ProfileScreen(
|
||||
onNavigateBack = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute)
|
||||
},
|
||||
onLogout = onLogout,
|
||||
onNavigateToNotificationPreferences = {
|
||||
navController.navigate(NotificationPreferencesRoute)
|
||||
},
|
||||
onNavigateToUpgrade = {
|
||||
navController.navigate(UpgradeRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<NotificationPreferencesRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
NotificationPreferencesScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
composable<MainTabContractorsRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ContractorsScreen(
|
||||
onNavigateBack = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute)
|
||||
},
|
||||
onNavigateToContractorDetail = { contractorId ->
|
||||
navController.navigate(ContractorDetailRoute(contractorId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composable<CompleteTaskRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<CompleteTaskRoute>()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CompleteTaskScreen(
|
||||
taskId = route.taskId,
|
||||
taskTitle = route.taskTitle,
|
||||
residenceName = route.residenceName,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onComplete = { request, images ->
|
||||
// Navigation back happens in the screen after successful completion
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
composable<ContractorDetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<ContractorDetailRoute>()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ContractorDetailScreen(
|
||||
contractorId = route.contractorId,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composable<ManageUsersRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<ManageUsersRoute>()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ManageUsersScreen(
|
||||
composable<MainTabDocumentsRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
DocumentsScreen(
|
||||
onNavigateBack = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute)
|
||||
},
|
||||
residenceId = null,
|
||||
onNavigateToAddDocument = { residenceId, documentType ->
|
||||
navController.navigate(
|
||||
AddDocumentRoute(
|
||||
residenceId = residenceId,
|
||||
initialDocumentType = documentType
|
||||
)
|
||||
)
|
||||
},
|
||||
onNavigateToDocumentDetail = { documentId ->
|
||||
navController.navigate(DocumentDetailRoute(documentId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<AddDocumentRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<AddDocumentRoute>()
|
||||
AddDocumentScreen(
|
||||
residenceId = route.residenceId,
|
||||
residenceName = route.residenceName,
|
||||
isPrimaryOwner = route.isPrimaryOwner,
|
||||
residenceOwnerId = route.residenceOwnerId,
|
||||
onNavigateBack = {
|
||||
initialDocumentType = route.initialDocumentType,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onDocumentCreated = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onUserRemoved = {
|
||||
// Could trigger a refresh if needed
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<UpgradeRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
UpgradeScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onPurchase = { planId ->
|
||||
// Handle purchase - integrate with billing system
|
||||
navController.popBackStack()
|
||||
},
|
||||
onRestorePurchases = {
|
||||
// Handle restore - integrate with billing system
|
||||
composable<DocumentDetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<DocumentDetailRoute>()
|
||||
DocumentDetailScreen(
|
||||
documentId = route.documentId,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToEdit = { documentId ->
|
||||
navController.navigate(EditDocumentRoute(documentId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<EditDocumentRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<EditDocumentRoute>()
|
||||
EditDocumentScreen(
|
||||
documentId = route.documentId,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable<MainTabProfileRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ProfileScreen(
|
||||
onNavigateBack = {
|
||||
selectedTab = 0
|
||||
navController.navigate(MainTabResidencesRoute)
|
||||
},
|
||||
onLogout = onLogout,
|
||||
onNavigateToNotificationPreferences = {
|
||||
navController.navigate(NotificationPreferencesRoute)
|
||||
},
|
||||
onNavigateToUpgrade = {
|
||||
navController.navigate(UpgradeRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<NotificationPreferencesRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
NotificationPreferencesScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<CompleteTaskRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<CompleteTaskRoute>()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CompleteTaskScreen(
|
||||
taskId = route.taskId,
|
||||
taskTitle = route.taskTitle,
|
||||
residenceName = route.residenceName,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onComplete = { request, images ->
|
||||
// Navigation back happens in the screen after successful completion
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<ManageUsersRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<ManageUsersRoute>()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ManageUsersScreen(
|
||||
residenceId = route.residenceId,
|
||||
residenceName = route.residenceName,
|
||||
isPrimaryOwner = route.isPrimaryOwner,
|
||||
residenceOwnerId = route.residenceOwnerId,
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onUserRemoved = {
|
||||
// Could trigger a refresh if needed
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable<UpgradeRoute> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
UpgradeScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onPurchase = { planId ->
|
||||
// Handle purchase - integrate with billing system
|
||||
navController.popBackStack()
|
||||
},
|
||||
onRestorePurchases = {
|
||||
// Handle restore - integrate with billing system
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.example.casera.ui.screens
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@@ -22,8 +21,7 @@ import com.example.casera.models.ResidenceShareCode
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.network.ResidenceApi
|
||||
import com.example.casera.storage.TokenStorage
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -68,308 +66,281 @@ fun ManageUsersScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
stringResource(Res.string.manage_users_invite_title),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
residenceName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (error != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = error ?: "Unknown error",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(AppSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
|
||||
) {
|
||||
// Share sections (primary owner only)
|
||||
if (isPrimaryOwner) {
|
||||
// Easy Share Section
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(AppSpacing.lg)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Share,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_easy_share),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
|
||||
Button(
|
||||
onClick = onSharePackage,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Send, null, modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(
|
||||
stringResource(Res.string.manage_users_send_invite),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_easy_share_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divider with "or"
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
WarmGradientBackground {
|
||||
Scaffold(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_or),
|
||||
stringResource(Res.string.manage_users_invite_title),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
residenceName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
||||
)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (error != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = error ?: "Unknown error",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
// Share Code Section
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(AppSpacing.lg)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
|
||||
) {
|
||||
// Share sections (primary owner only)
|
||||
if (isPrimaryOwner) {
|
||||
// Easy Share Section
|
||||
item {
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Key,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_share_code),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
|
||||
// Share code display
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(OrganicSpacing.lg)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
if (shareCode != null) {
|
||||
Text(
|
||||
text = shareCode!!.code,
|
||||
style = MaterialTheme.typography.headlineMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
letterSpacing = 4.sp
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(shareCode!!.code))
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("Code copied to clipboard")
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy code",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_no_code),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
isGeneratingCode = true
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
when (val result = residenceApi.generateShareCode(token, residenceId)) {
|
||||
is ApiResult.Success -> {
|
||||
shareCode = result.data.shareCode
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
error = result.message
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
isGeneratingCode = false
|
||||
}
|
||||
},
|
||||
enabled = !isGeneratingCode,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (isGeneratingCode) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
Icon(
|
||||
Icons.Default.Share,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_easy_share),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.Refresh, null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(
|
||||
if (shareCode != null) stringResource(Res.string.manage_users_generate_new)
|
||||
else stringResource(Res.string.manage_users_generate)
|
||||
)
|
||||
}
|
||||
|
||||
if (shareCode != null) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.manage_users_send_invite),
|
||||
onClick = onSharePackage,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
icon = Icons.Default.Send
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_code_desc),
|
||||
text = stringResource(Res.string.manage_users_easy_share_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divider with "or"
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OrganicDivider(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_or),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
|
||||
)
|
||||
OrganicDivider(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
// Share Code Section
|
||||
item {
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(OrganicSpacing.lg)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Key,
|
||||
size = 24.dp
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_share_code),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Share code display
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (shareCode != null) {
|
||||
Text(
|
||||
text = shareCode!!.code,
|
||||
style = MaterialTheme.typography.headlineMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
letterSpacing = 4.sp
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(shareCode!!.code))
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("Code copied to clipboard")
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy code",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_no_code),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = if (shareCode != null) stringResource(Res.string.manage_users_generate_new)
|
||||
else stringResource(Res.string.manage_users_generate),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
isGeneratingCode = true
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
when (val result = residenceApi.generateShareCode(token, residenceId)) {
|
||||
is ApiResult.Success -> {
|
||||
shareCode = result.data.shareCode
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
error = result.message
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
isGeneratingCode = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isGeneratingCode,
|
||||
isLoading = isGeneratingCode,
|
||||
icon = Icons.Default.Refresh
|
||||
)
|
||||
|
||||
if (shareCode != null) {
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_code_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
OrganicDivider()
|
||||
}
|
||||
}
|
||||
|
||||
// Users Header
|
||||
item {
|
||||
HorizontalDivider()
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_users_count, users.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Users Header
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_users_count, users.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
// Users List
|
||||
items(users) { user ->
|
||||
UserCard(
|
||||
user = user,
|
||||
isOwner = user.id == residenceOwnerId,
|
||||
canRemove = isPrimaryOwner && user.id != residenceOwnerId,
|
||||
onRemove = { showRemoveConfirmation = user }
|
||||
)
|
||||
}
|
||||
|
||||
// Users List
|
||||
items(users) { user ->
|
||||
UserCard(
|
||||
user = user,
|
||||
isOwner = user.id == residenceOwnerId,
|
||||
canRemove = isPrimaryOwner && user.id != residenceOwnerId,
|
||||
onRemove = { showRemoveConfirmation = user }
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom spacing
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
// Bottom spacing
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,27 +395,23 @@ private fun UserCard(
|
||||
canRemove: Boolean,
|
||||
onRemove: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
// Avatar
|
||||
Surface(
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
shape = OrganicShapes.medium,
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
@@ -461,7 +428,7 @@ private fun UserCard(
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = user.username,
|
||||
@@ -471,13 +438,13 @@ private fun UserCard(
|
||||
if (isOwner) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = RoundedCornerShape(AppRadius.xs)
|
||||
shape = OrganicShapes.extraSmall
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.manage_users_owner_badge),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp)
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.example.casera.ui.screens
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
@@ -15,8 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.util.DateUtils
|
||||
import com.example.casera.viewmodel.NotificationPreferencesViewModel
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
@@ -93,377 +91,353 @@ fun NotificationPreferencesScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
WarmGradientBackground {
|
||||
Scaffold(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
// Header
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(60.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(Res.string.notifications_preferences),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(Res.string.notifications_choose),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (preferencesState) {
|
||||
is ApiResult.Loading -> {
|
||||
Box(
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
// Header
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
.padding(OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Notifications,
|
||||
size = 60.dp
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(Res.string.notifications_preferences),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(Res.string.notifications_choose),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ApiResult.Error -> {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
) {
|
||||
Column(
|
||||
when (preferencesState) {
|
||||
is ApiResult.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
.padding(OrganicSpacing.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
is ApiResult.Error -> {
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.errorContainer
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
(preferencesState as ApiResult.Error).message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.loadPreferences() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(Res.string.common_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ApiResult.Success, is ApiResult.Idle -> {
|
||||
// Task Notifications Section
|
||||
Text(
|
||||
stringResource(Res.string.notifications_task_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = AppSpacing.md)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_due_soon),
|
||||
description = stringResource(Res.string.notifications_task_due_soon_desc),
|
||||
icon = Icons.Default.Schedule,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = taskDueSoon,
|
||||
onCheckedChange = {
|
||||
taskDueSoon = it
|
||||
viewModel.updatePreference(taskDueSoon = it)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
(preferencesState as ApiResult.Error).message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// Time picker for Task Due Soon
|
||||
if (taskDueSoon) {
|
||||
NotificationTimePickerRow(
|
||||
currentHour = taskDueSoonHour,
|
||||
onSetCustomTime = {
|
||||
val localHour = defaultTaskDueSoonLocalHour
|
||||
taskDueSoonHour = localHour
|
||||
val utcHour = DateUtils.localHourToUtc(localHour)
|
||||
viewModel.updatePreference(taskDueSoonHour = utcHour)
|
||||
},
|
||||
onChangeTime = { showTaskDueSoonTimePicker = true }
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_overdue),
|
||||
description = stringResource(Res.string.notifications_task_overdue_desc),
|
||||
icon = Icons.Default.Warning,
|
||||
iconTint = MaterialTheme.colorScheme.error,
|
||||
checked = taskOverdue,
|
||||
onCheckedChange = {
|
||||
taskOverdue = it
|
||||
viewModel.updatePreference(taskOverdue = it)
|
||||
}
|
||||
)
|
||||
|
||||
// Time picker for Task Overdue
|
||||
if (taskOverdue) {
|
||||
NotificationTimePickerRow(
|
||||
currentHour = taskOverdueHour,
|
||||
onSetCustomTime = {
|
||||
val localHour = defaultTaskOverdueLocalHour
|
||||
taskOverdueHour = localHour
|
||||
val utcHour = DateUtils.localHourToUtc(localHour)
|
||||
viewModel.updatePreference(taskOverdueHour = utcHour)
|
||||
},
|
||||
onChangeTime = { showTaskOverdueTimePicker = true }
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_completed),
|
||||
description = stringResource(Res.string.notifications_task_completed_desc),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = taskCompleted,
|
||||
onCheckedChange = {
|
||||
taskCompleted = it
|
||||
viewModel.updatePreference(taskCompleted = it)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_assigned),
|
||||
description = stringResource(Res.string.notifications_task_assigned_desc),
|
||||
icon = Icons.Default.PersonAdd,
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
checked = taskAssigned,
|
||||
onCheckedChange = {
|
||||
taskAssigned = it
|
||||
viewModel.updatePreference(taskAssigned = it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Time picker dialogs
|
||||
if (showTaskDueSoonTimePicker) {
|
||||
HourPickerDialog(
|
||||
currentHour = taskDueSoonHour ?: defaultTaskDueSoonLocalHour,
|
||||
onHourSelected = { hour ->
|
||||
taskDueSoonHour = hour
|
||||
val utcHour = DateUtils.localHourToUtc(hour)
|
||||
viewModel.updatePreference(taskDueSoonHour = utcHour)
|
||||
showTaskDueSoonTimePicker = false
|
||||
},
|
||||
onDismiss = { showTaskDueSoonTimePicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showTaskOverdueTimePicker) {
|
||||
HourPickerDialog(
|
||||
currentHour = taskOverdueHour ?: defaultTaskOverdueLocalHour,
|
||||
onHourSelected = { hour ->
|
||||
taskOverdueHour = hour
|
||||
val utcHour = DateUtils.localHourToUtc(hour)
|
||||
viewModel.updatePreference(taskOverdueHour = utcHour)
|
||||
showTaskOverdueTimePicker = false
|
||||
},
|
||||
onDismiss = { showTaskOverdueTimePicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
// Other Notifications Section
|
||||
Text(
|
||||
stringResource(Res.string.notifications_other_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = AppSpacing.md)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_property_shared),
|
||||
description = stringResource(Res.string.notifications_property_shared_desc),
|
||||
icon = Icons.Default.Home,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = residenceShared,
|
||||
onCheckedChange = {
|
||||
residenceShared = it
|
||||
viewModel.updatePreference(residenceShared = it)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_warranty_expiring),
|
||||
description = stringResource(Res.string.notifications_warranty_expiring_desc),
|
||||
icon = Icons.Default.Description,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = warrantyExpiring,
|
||||
onCheckedChange = {
|
||||
warrantyExpiring = it
|
||||
viewModel.updatePreference(warrantyExpiring = it)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_daily_digest),
|
||||
description = stringResource(Res.string.notifications_daily_digest_desc),
|
||||
icon = Icons.Default.Summarize,
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
checked = dailyDigest,
|
||||
onCheckedChange = {
|
||||
dailyDigest = it
|
||||
viewModel.updatePreference(dailyDigest = it)
|
||||
}
|
||||
)
|
||||
|
||||
// Time picker for Daily Digest
|
||||
if (dailyDigest) {
|
||||
NotificationTimePickerRow(
|
||||
currentHour = dailyDigestHour,
|
||||
onSetCustomTime = {
|
||||
val localHour = defaultDailyDigestLocalHour
|
||||
dailyDigestHour = localHour
|
||||
val utcHour = DateUtils.localHourToUtc(localHour)
|
||||
viewModel.updatePreference(dailyDigestHour = utcHour)
|
||||
},
|
||||
onChangeTime = { showDailyDigestTimePicker = true }
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.common_retry),
|
||||
onClick = { viewModel.loadPreferences() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Daily Digest time picker dialog
|
||||
if (showDailyDigestTimePicker) {
|
||||
HourPickerDialog(
|
||||
currentHour = dailyDigestHour ?: defaultDailyDigestLocalHour,
|
||||
onHourSelected = { hour ->
|
||||
dailyDigestHour = hour
|
||||
val utcHour = DateUtils.localHourToUtc(hour)
|
||||
viewModel.updatePreference(dailyDigestHour = utcHour)
|
||||
showDailyDigestTimePicker = false
|
||||
},
|
||||
onDismiss = { showDailyDigestTimePicker = false }
|
||||
is ApiResult.Success, is ApiResult.Idle -> {
|
||||
// Task Notifications Section
|
||||
Text(
|
||||
stringResource(Res.string.notifications_task_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = OrganicSpacing.md)
|
||||
)
|
||||
}
|
||||
|
||||
// Email Notifications Section
|
||||
Text(
|
||||
stringResource(Res.string.notifications_email_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = AppSpacing.md)
|
||||
)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_due_soon),
|
||||
description = stringResource(Res.string.notifications_task_due_soon_desc),
|
||||
icon = Icons.Default.Schedule,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = taskDueSoon,
|
||||
onCheckedChange = {
|
||||
taskDueSoon = it
|
||||
viewModel.updatePreference(taskDueSoon = it)
|
||||
}
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_email_task_completed),
|
||||
description = stringResource(Res.string.notifications_email_task_completed_desc),
|
||||
icon = Icons.Default.Email,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = emailTaskCompleted,
|
||||
onCheckedChange = {
|
||||
emailTaskCompleted = it
|
||||
viewModel.updatePreference(emailTaskCompleted = it)
|
||||
// Time picker for Task Due Soon
|
||||
if (taskDueSoon) {
|
||||
NotificationTimePickerRow(
|
||||
currentHour = taskDueSoonHour,
|
||||
onSetCustomTime = {
|
||||
val localHour = defaultTaskDueSoonLocalHour
|
||||
taskDueSoonHour = localHour
|
||||
val utcHour = DateUtils.localHourToUtc(localHour)
|
||||
viewModel.updatePreference(taskDueSoonHour = utcHour)
|
||||
},
|
||||
onChangeTime = { showTaskDueSoonTimePicker = true }
|
||||
)
|
||||
}
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_overdue),
|
||||
description = stringResource(Res.string.notifications_task_overdue_desc),
|
||||
icon = Icons.Default.Warning,
|
||||
iconTint = MaterialTheme.colorScheme.error,
|
||||
checked = taskOverdue,
|
||||
onCheckedChange = {
|
||||
taskOverdue = it
|
||||
viewModel.updatePreference(taskOverdue = it)
|
||||
}
|
||||
)
|
||||
|
||||
// Time picker for Task Overdue
|
||||
if (taskOverdue) {
|
||||
NotificationTimePickerRow(
|
||||
currentHour = taskOverdueHour,
|
||||
onSetCustomTime = {
|
||||
val localHour = defaultTaskOverdueLocalHour
|
||||
taskOverdueHour = localHour
|
||||
val utcHour = DateUtils.localHourToUtc(localHour)
|
||||
viewModel.updatePreference(taskOverdueHour = utcHour)
|
||||
},
|
||||
onChangeTime = { showTaskOverdueTimePicker = true }
|
||||
)
|
||||
}
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_completed),
|
||||
description = stringResource(Res.string.notifications_task_completed_desc),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = taskCompleted,
|
||||
onCheckedChange = {
|
||||
taskCompleted = it
|
||||
viewModel.updatePreference(taskCompleted = it)
|
||||
}
|
||||
)
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_task_assigned),
|
||||
description = stringResource(Res.string.notifications_task_assigned_desc),
|
||||
icon = Icons.Default.PersonAdd,
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
checked = taskAssigned,
|
||||
onCheckedChange = {
|
||||
taskAssigned = it
|
||||
viewModel.updatePreference(taskAssigned = it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Time picker dialogs
|
||||
if (showTaskDueSoonTimePicker) {
|
||||
HourPickerDialog(
|
||||
currentHour = taskDueSoonHour ?: defaultTaskDueSoonLocalHour,
|
||||
onHourSelected = { hour ->
|
||||
taskDueSoonHour = hour
|
||||
val utcHour = DateUtils.localHourToUtc(hour)
|
||||
viewModel.updatePreference(taskDueSoonHour = utcHour)
|
||||
showTaskDueSoonTimePicker = false
|
||||
},
|
||||
onDismiss = { showTaskDueSoonTimePicker = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
if (showTaskOverdueTimePicker) {
|
||||
HourPickerDialog(
|
||||
currentHour = taskOverdueHour ?: defaultTaskOverdueLocalHour,
|
||||
onHourSelected = { hour ->
|
||||
taskOverdueHour = hour
|
||||
val utcHour = DateUtils.localHourToUtc(hour)
|
||||
viewModel.updatePreference(taskOverdueHour = utcHour)
|
||||
showTaskOverdueTimePicker = false
|
||||
},
|
||||
onDismiss = { showTaskOverdueTimePicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
// Other Notifications Section
|
||||
Text(
|
||||
stringResource(Res.string.notifications_other_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = OrganicSpacing.md)
|
||||
)
|
||||
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_property_shared),
|
||||
description = stringResource(Res.string.notifications_property_shared_desc),
|
||||
icon = Icons.Default.Home,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = residenceShared,
|
||||
onCheckedChange = {
|
||||
residenceShared = it
|
||||
viewModel.updatePreference(residenceShared = it)
|
||||
}
|
||||
)
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_warranty_expiring),
|
||||
description = stringResource(Res.string.notifications_warranty_expiring_desc),
|
||||
icon = Icons.Default.Description,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = warrantyExpiring,
|
||||
onCheckedChange = {
|
||||
warrantyExpiring = it
|
||||
viewModel.updatePreference(warrantyExpiring = it)
|
||||
}
|
||||
)
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_daily_digest),
|
||||
description = stringResource(Res.string.notifications_daily_digest_desc),
|
||||
icon = Icons.Default.Summarize,
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
checked = dailyDigest,
|
||||
onCheckedChange = {
|
||||
dailyDigest = it
|
||||
viewModel.updatePreference(dailyDigest = it)
|
||||
}
|
||||
)
|
||||
|
||||
// Time picker for Daily Digest
|
||||
if (dailyDigest) {
|
||||
NotificationTimePickerRow(
|
||||
currentHour = dailyDigestHour,
|
||||
onSetCustomTime = {
|
||||
val localHour = defaultDailyDigestLocalHour
|
||||
dailyDigestHour = localHour
|
||||
val utcHour = DateUtils.localHourToUtc(localHour)
|
||||
viewModel.updatePreference(dailyDigestHour = utcHour)
|
||||
},
|
||||
onChangeTime = { showDailyDigestTimePicker = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Daily Digest time picker dialog
|
||||
if (showDailyDigestTimePicker) {
|
||||
HourPickerDialog(
|
||||
currentHour = dailyDigestHour ?: defaultDailyDigestLocalHour,
|
||||
onHourSelected = { hour ->
|
||||
dailyDigestHour = hour
|
||||
val utcHour = DateUtils.localHourToUtc(hour)
|
||||
viewModel.updatePreference(dailyDigestHour = utcHour)
|
||||
showDailyDigestTimePicker = false
|
||||
},
|
||||
onDismiss = { showDailyDigestTimePicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
// Email Notifications Section
|
||||
Text(
|
||||
stringResource(Res.string.notifications_email_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = OrganicSpacing.md)
|
||||
)
|
||||
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = stringResource(Res.string.notifications_email_task_completed),
|
||||
description = stringResource(Res.string.notifications_email_task_completed_desc),
|
||||
icon = Icons.Default.Email,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = emailTaskCompleted,
|
||||
onCheckedChange = {
|
||||
emailTaskCompleted = it
|
||||
viewModel.updatePreference(emailTaskCompleted = it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,8 +456,8 @@ private fun NotificationToggle(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(AppSpacing.lg),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md),
|
||||
.padding(OrganicSpacing.lg),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
@@ -528,8 +502,8 @@ private fun NotificationTimePickerRow(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = AppSpacing.lg + 24.dp + AppSpacing.md, end = AppSpacing.lg, bottom = AppSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
.padding(start = OrganicSpacing.lg + 24.dp + OrganicSpacing.md, end = OrganicSpacing.lg, bottom = OrganicSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
@@ -584,7 +558,7 @@ private fun HourPickerDialog(
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Text(
|
||||
text = DateUtils.formatHour(selectedHour),
|
||||
@@ -601,7 +575,7 @@ private fun HourPickerDialog(
|
||||
// AM hours (6 AM - 11 AM)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
"AM",
|
||||
@@ -620,7 +594,7 @@ private fun HourPickerDialog(
|
||||
// PM hours (12 PM - 5 PM)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
"PM",
|
||||
@@ -639,7 +613,7 @@ private fun HourPickerDialog(
|
||||
// Evening hours (6 PM - 11 PM)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
"EVE",
|
||||
@@ -687,7 +661,7 @@ private fun HourChip(
|
||||
modifier = Modifier
|
||||
.width(56.dp)
|
||||
.clickable { onClick() },
|
||||
shape = RoundedCornerShape(AppRadius.sm),
|
||||
shape = OrganicShapes.small,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Text(
|
||||
@@ -695,7 +669,7 @@ private fun HourChip(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = AppSpacing.xs),
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = OrganicSpacing.xs),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.casera.ui.components.HandleErrors
|
||||
import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
@@ -65,131 +66,129 @@ fun RegisterScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(Res.string.auth_register_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
WarmGradientBackground {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(Res.string.auth_register_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
AuthHeader(
|
||||
icon = Icons.Default.PersonAdd,
|
||||
title = stringResource(Res.string.auth_register_title),
|
||||
subtitle = stringResource(Res.string.auth_register_subtitle)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_username)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_email)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match)
|
||||
Button(
|
||||
onClick = {
|
||||
when {
|
||||
password != confirmPassword -> {
|
||||
errorMessage = passwordsDontMatchMessage
|
||||
}
|
||||
else -> {
|
||||
isLoading = true
|
||||
errorMessage = ""
|
||||
viewModel.register(username, email, password)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = username.isNotEmpty() && email.isNotEmpty() &&
|
||||
password.isNotEmpty() && !isLoading,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(Res.string.auth_register_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AuthHeader(
|
||||
icon = Icons.Default.PersonAdd,
|
||||
title = stringResource(Res.string.auth_register_title),
|
||||
subtitle = stringResource(Res.string.auth_register_subtitle)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_username)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_email)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
OrganicDivider()
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text(stringResource(Res.string.auth_register_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match)
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_register_button),
|
||||
onClick = {
|
||||
when {
|
||||
password != confirmPassword -> {
|
||||
errorMessage = passwordsDontMatchMessage
|
||||
}
|
||||
else -> {
|
||||
isLoading = true
|
||||
errorMessage = ""
|
||||
viewModel.register(username, email, password)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = username.isNotEmpty() && email.isNotEmpty() &&
|
||||
password.isNotEmpty() && !isLoading,
|
||||
isLoading = isLoading
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@@ -20,6 +18,7 @@ import com.example.casera.ui.components.auth.RequirementItem
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.PasswordResetViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -80,194 +79,199 @@ fun ResetPasswordScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
WarmGradientBackground {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
showBlob = true,
|
||||
blobVariation = 2
|
||||
) {
|
||||
if (isSuccess) {
|
||||
// Success State
|
||||
AuthHeader(
|
||||
icon = Icons.Default.CheckCircle,
|
||||
title = "Success!",
|
||||
subtitle = "Your password has been reset successfully"
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.spacious),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
if (isSuccess) {
|
||||
// Success State
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.CheckCircle,
|
||||
size = 80.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
) {
|
||||
|
||||
Text(
|
||||
"You can now log in with your new password",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
text = "Success!",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onPasswordResetSuccess,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Return to Login",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
text = "Your password has been reset successfully",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Reset Password Form
|
||||
AuthHeader(
|
||||
icon = Icons.Default.LockReset,
|
||||
title = "Set New Password",
|
||||
subtitle = "Create a strong password to secure your account"
|
||||
)
|
||||
|
||||
// Password Requirements
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.primary,
|
||||
showBlob = false
|
||||
) {
|
||||
Text(
|
||||
"Password Requirements",
|
||||
"You can now log in with your new password",
|
||||
modifier = Modifier.padding(OrganicSpacing.cozy),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
RequirementItem(
|
||||
"At least 8 characters",
|
||||
newPassword.length >= 8
|
||||
)
|
||||
RequirementItem(
|
||||
"Contains letters",
|
||||
hasLetter
|
||||
)
|
||||
RequirementItem(
|
||||
"Contains numbers",
|
||||
hasNumber
|
||||
)
|
||||
RequirementItem(
|
||||
"Passwords match",
|
||||
passwordsMatch
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = newPassword,
|
||||
onValueChange = {
|
||||
newPassword = it
|
||||
viewModel.resetResetPasswordState()
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_reset_new_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { newPasswordVisible = !newPasswordVisible }) {
|
||||
Icon(
|
||||
if (newPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (newPasswordVisible) "Hide password" else "Show password"
|
||||
OrganicDivider(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = "Return to Login",
|
||||
onClick = onPasswordResetSuccess
|
||||
)
|
||||
} else {
|
||||
// Reset Password Form
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.LockReset,
|
||||
size = 80.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Set New Password",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Create a strong password to secure your account",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// Password Requirements
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.secondary,
|
||||
showBlob = false
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Text(
|
||||
"Password Requirements",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
|
||||
RequirementItem(
|
||||
"At least 8 characters",
|
||||
newPassword.length >= 8
|
||||
)
|
||||
RequirementItem(
|
||||
"Contains letters",
|
||||
hasLetter
|
||||
)
|
||||
RequirementItem(
|
||||
"Contains numbers",
|
||||
hasNumber
|
||||
)
|
||||
RequirementItem(
|
||||
"Passwords match",
|
||||
passwordsMatch
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
viewModel.resetResetPasswordState()
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_reset_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
|
||||
Icon(
|
||||
if (confirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (confirmPasswordVisible) "Hide password" else "Show password"
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.resetPassword(newPassword, confirmPassword)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = isFormValid && !isLoading && !isLoggingIn,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
if (isLoading || isLoggingIn) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
if (isLoggingIn) "Logging in..." else "Resetting...",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.LockReset, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
stringResource(Res.string.auth_reset_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = newPassword,
|
||||
onValueChange = {
|
||||
newPassword = it
|
||||
viewModel.resetResetPasswordState()
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_reset_new_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { newPasswordVisible = !newPasswordVisible }) {
|
||||
Icon(
|
||||
if (newPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (newPasswordVisible) "Hide password" else "Show password"
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
viewModel.resetResetPasswordState()
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_reset_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
|
||||
Icon(
|
||||
if (confirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (confirmPasswordVisible) "Hide password" else "Show password"
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = if (isLoggingIn) "Logging in..." else stringResource(Res.string.auth_reset_button),
|
||||
onClick = {
|
||||
viewModel.resetPassword(newPassword, confirmPassword)
|
||||
},
|
||||
enabled = isFormValid && !isLoading && !isLoggingIn,
|
||||
isLoading = isLoading || isLoggingIn
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.example.casera.util.DateUtils
|
||||
import com.example.casera.platform.rememberShareResidence
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -539,50 +540,49 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
ApiResultHandler(
|
||||
state = residenceState,
|
||||
onRetry = {
|
||||
residenceViewModel.getResidence(residenceId) { result ->
|
||||
residenceState = result
|
||||
WarmGradientBackground {
|
||||
ApiResultHandler(
|
||||
state = residenceState,
|
||||
onRetry = {
|
||||
residenceViewModel.getResidence(residenceId) { result ->
|
||||
residenceState = result
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
errorTitle = stringResource(Res.string.properties_failed_to_load),
|
||||
loadingContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_loading),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
errorTitle = stringResource(Res.string.properties_failed_to_load),
|
||||
loadingContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_loading),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
) { residence ->
|
||||
LazyColumn(
|
||||
) { residence ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
contentPadding = PaddingValues(OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
// Property Header Card
|
||||
item {
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
accentColor = MaterialTheme.colorScheme.primary,
|
||||
showBlob = true,
|
||||
blobVariation = 0
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp)
|
||||
.padding(OrganicSpacing.comfortable)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -594,7 +594,7 @@ fun ResidenceDetailScreen(
|
||||
text = residence.name,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -607,21 +607,60 @@ fun ResidenceDetailScreen(
|
||||
residence.stateProvince != null || residence.postalCode != null ||
|
||||
residence.country != null) {
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.LocationOn,
|
||||
title = stringResource(Res.string.properties_address_section)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showBlob = true,
|
||||
blobVariation = 1
|
||||
) {
|
||||
if (residence.streetAddress != null) {
|
||||
Text(text = residence.streetAddress)
|
||||
}
|
||||
if (residence.apartmentUnit != null) {
|
||||
Text(text = "Unit: ${residence.apartmentUnit}")
|
||||
}
|
||||
if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) {
|
||||
Text(text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}")
|
||||
}
|
||||
if (residence.country != null) {
|
||||
Text(text = residence.country)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.LocationOn,
|
||||
size = 40.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_address_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
|
||||
if (residence.streetAddress != null) {
|
||||
Text(
|
||||
text = residence.streetAddress,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
if (residence.apartmentUnit != null) {
|
||||
Text(
|
||||
text = "Unit: ${residence.apartmentUnit}",
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) {
|
||||
Text(
|
||||
text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}",
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
if (residence.country != null) {
|
||||
Text(
|
||||
text = residence.country,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -631,30 +670,57 @@ fun ResidenceDetailScreen(
|
||||
if (residence.bedrooms != null || residence.bathrooms != null ||
|
||||
residence.squareFootage != null || residence.yearBuilt != null) {
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.Info,
|
||||
title = stringResource(Res.string.properties_property_details_section)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showBlob = true,
|
||||
blobVariation = 2
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
residence.bedrooms?.let {
|
||||
PropertyDetailItem(Icons.Default.Bed, "$it", "Bedrooms")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Info,
|
||||
size = 40.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_property_details_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
residence.bathrooms?.let {
|
||||
PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms")
|
||||
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
residence.bedrooms?.let {
|
||||
PropertyDetailItem(Icons.Default.Bed, "$it", "Bedrooms")
|
||||
}
|
||||
residence.bathrooms?.let {
|
||||
PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
residence.squareFootage?.let {
|
||||
DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft")
|
||||
}
|
||||
residence.lotSize?.let {
|
||||
DetailRow(Icons.Default.Landscape, "Lot Size", "$it acres")
|
||||
}
|
||||
residence.yearBuilt?.let {
|
||||
DetailRow(Icons.Default.CalendarToday, "Year Built", "$it")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
residence.squareFootage?.let {
|
||||
DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft")
|
||||
}
|
||||
residence.lotSize?.let {
|
||||
DetailRow(Icons.Default.Landscape, "Lot Size", "$it acres")
|
||||
}
|
||||
residence.yearBuilt?.let {
|
||||
DetailRow(Icons.Default.CalendarToday, "Year Built", "$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,15 +729,42 @@ fun ResidenceDetailScreen(
|
||||
// Description Card
|
||||
if (residence.description != null && !residence.description.isEmpty()) {
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.Description,
|
||||
title = stringResource(Res.string.properties_description_section)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showBlob = true,
|
||||
blobVariation = 0
|
||||
) {
|
||||
Text(
|
||||
text = residence.description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Description,
|
||||
size = 40.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_description_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
|
||||
Text(
|
||||
text = residence.description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -679,15 +772,42 @@ fun ResidenceDetailScreen(
|
||||
// Purchase Information
|
||||
if (residence.purchaseDate != null || residence.purchasePrice != null) {
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.AttachMoney,
|
||||
title = stringResource(Res.string.properties_purchase_info)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showBlob = true,
|
||||
blobVariation = 1
|
||||
) {
|
||||
residence.purchaseDate?.let {
|
||||
DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it))
|
||||
}
|
||||
residence.purchasePrice?.let {
|
||||
DetailRow(Icons.Default.Payment, "Purchase Price", "$$it")
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.AttachMoney,
|
||||
size = 40.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_purchase_info),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
|
||||
residence.purchaseDate?.let {
|
||||
DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it))
|
||||
}
|
||||
residence.purchasePrice?.let {
|
||||
DetailRow(Icons.Default.Payment, "Purchase Price", "$$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -698,21 +818,22 @@ fun ResidenceDetailScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(vertical = OrganicSpacing.compact),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Assignment,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(28.dp)
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Assignment,
|
||||
size = 36.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.tasks_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -732,16 +853,15 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
item {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.error,
|
||||
showBlob = false
|
||||
) {
|
||||
Text(
|
||||
text = "Error loading tasks: ${com.example.casera.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}",
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(OrganicSpacing.cozy)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -751,32 +871,35 @@ fun ResidenceDetailScreen(
|
||||
val allTasksEmpty = taskData.columns.all { it.tasks.isEmpty() }
|
||||
if (allTasksEmpty) {
|
||||
item {
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
showBlob = true,
|
||||
blobVariation = 2
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(OrganicSpacing.spacious),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Assignment,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Assignment,
|
||||
size = 64.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
||||
iconColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
stringResource(Res.string.properties_no_tasks),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.properties_add_task_start),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -833,25 +956,26 @@ fun ResidenceDetailScreen(
|
||||
|
||||
// Contractors Section Header
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(vertical = OrganicSpacing.compact),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.People,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(28.dp)
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.People,
|
||||
size = 36.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.contractors_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -871,16 +995,15 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
item {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.error,
|
||||
showBlob = false
|
||||
) {
|
||||
Text(
|
||||
text = "Error loading contractors: ${com.example.casera.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}",
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(OrganicSpacing.cozy)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -889,32 +1012,35 @@ fun ResidenceDetailScreen(
|
||||
val contractors = (contractorsState as ApiResult.Success<List<ContractorSummary>>).data
|
||||
if (contractors.isEmpty()) {
|
||||
item {
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
showBlob = true,
|
||||
blobVariation = 1
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(OrganicSpacing.comfortable),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.PersonAdd,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.PersonAdd,
|
||||
size = 56.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
||||
iconColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
stringResource(Res.string.properties_no_contractors),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.properties_add_contractors_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -939,4 +1065,5 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.example.casera.network.ResidenceApi
|
||||
import com.example.casera.storage.TokenStorage
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -170,282 +171,276 @@ fun ResidenceFormScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Basic Information section
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_name_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = nameError.isNotEmpty(),
|
||||
supportingText = if (nameError.isNotEmpty()) {
|
||||
{ Text(nameError, color = MaterialTheme.colorScheme.error) }
|
||||
} else {
|
||||
{ Text(stringResource(Res.string.properties_form_required), color = MaterialTheme.colorScheme.error) }
|
||||
}
|
||||
)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it }
|
||||
WarmGradientBackground {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(OrganicSpacing.cozy)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.properties_type_label)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = propertyTypes.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
propertyTypes.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(type.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
propertyType = type
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Address section
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_address_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = streetAddress,
|
||||
onValueChange = { streetAddress = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_street)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = apartmentUnit,
|
||||
onValueChange = { apartmentUnit = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_apartment)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = city,
|
||||
onValueChange = { city = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_city)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = stateProvince,
|
||||
onValueChange = { stateProvince = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_state)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = postalCode,
|
||||
onValueChange = { postalCode = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_postal)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = country,
|
||||
onValueChange = { country = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_country)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// Optional fields section
|
||||
Divider()
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_form_optional),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = bedrooms,
|
||||
onValueChange = { bedrooms = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.properties_bedrooms)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = bathrooms,
|
||||
onValueChange = { bathrooms = it },
|
||||
label = { Text(stringResource(Res.string.properties_bathrooms)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = squareFootage,
|
||||
onValueChange = { squareFootage = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.properties_form_sqft)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = lotSize,
|
||||
onValueChange = { lotSize = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_lot_size)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = yearBuilt,
|
||||
onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.properties_year_built)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_description)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(stringResource(Res.string.properties_form_primary))
|
||||
Switch(
|
||||
checked = isPrimary,
|
||||
onCheckedChange = { isPrimary = it }
|
||||
)
|
||||
}
|
||||
|
||||
// Users section (edit mode only, owner only)
|
||||
if (isEditMode && isCurrentUserOwner) {
|
||||
Divider()
|
||||
// Basic Information section
|
||||
Text(
|
||||
text = "Shared Users (${users.size})",
|
||||
text = stringResource(Res.string.properties_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
if (isLoadingUsers) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_name_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = nameError.isNotEmpty(),
|
||||
supportingText = if (nameError.isNotEmpty()) {
|
||||
{ Text(nameError, color = MaterialTheme.colorScheme.error) }
|
||||
} else {
|
||||
{ Text(stringResource(Res.string.properties_form_required), color = MaterialTheme.colorScheme.error) }
|
||||
}
|
||||
} else if (users.isEmpty()) {
|
||||
Text(
|
||||
text = "No shared users",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(Res.string.properties_type_label)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = propertyTypes.isNotEmpty()
|
||||
)
|
||||
} else {
|
||||
users.forEach { user ->
|
||||
UserListItem(
|
||||
user = user,
|
||||
onRemove = {
|
||||
userToRemove = user
|
||||
showRemoveUserConfirmation = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Users with access to this residence. Use the share button to invite others.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Error message
|
||||
if (operationState is ApiResult.Error) {
|
||||
Text(
|
||||
text = com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
// Submit button
|
||||
Button(
|
||||
onClick = {
|
||||
if (validateForm()) {
|
||||
val request = ResidenceCreateRequest(
|
||||
name = name,
|
||||
propertyTypeId = propertyType?.id,
|
||||
streetAddress = streetAddress.ifBlank { null },
|
||||
apartmentUnit = apartmentUnit.ifBlank { null },
|
||||
city = city.ifBlank { null },
|
||||
stateProvince = stateProvince.ifBlank { null },
|
||||
postalCode = postalCode.ifBlank { null },
|
||||
country = country.ifBlank { null },
|
||||
bedrooms = bedrooms.toIntOrNull(),
|
||||
bathrooms = bathrooms.toDoubleOrNull(),
|
||||
squareFootage = squareFootage.toIntOrNull(),
|
||||
lotSize = lotSize.toDoubleOrNull(),
|
||||
yearBuilt = yearBuilt.toIntOrNull(),
|
||||
description = description.ifBlank { null },
|
||||
isPrimary = isPrimary
|
||||
)
|
||||
|
||||
if (isEditMode && existingResidence != null) {
|
||||
viewModel.updateResidence(existingResidence.id, request)
|
||||
} else {
|
||||
viewModel.createResidence(request)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
propertyTypes.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(type.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
propertyType = type
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = validateForm()
|
||||
) {
|
||||
if (operationState is ApiResult.Loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text(if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
// Address section
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_address_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = streetAddress,
|
||||
onValueChange = { streetAddress = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_street)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = apartmentUnit,
|
||||
onValueChange = { apartmentUnit = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_apartment)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = city,
|
||||
onValueChange = { city = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_city)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = stateProvince,
|
||||
onValueChange = { stateProvince = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_state)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = postalCode,
|
||||
onValueChange = { postalCode = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_postal)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = country,
|
||||
onValueChange = { country = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_country)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// Optional fields section
|
||||
OrganicDivider()
|
||||
Text(
|
||||
text = stringResource(Res.string.properties_form_optional),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = bedrooms,
|
||||
onValueChange = { bedrooms = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.properties_bedrooms)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = bathrooms,
|
||||
onValueChange = { bathrooms = it },
|
||||
label = { Text(stringResource(Res.string.properties_bathrooms)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = squareFootage,
|
||||
onValueChange = { squareFootage = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.properties_form_sqft)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = lotSize,
|
||||
onValueChange = { lotSize = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_lot_size)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = yearBuilt,
|
||||
onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(Res.string.properties_year_built)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text(stringResource(Res.string.properties_form_description)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(stringResource(Res.string.properties_form_primary))
|
||||
Switch(
|
||||
checked = isPrimary,
|
||||
onCheckedChange = { isPrimary = it }
|
||||
)
|
||||
}
|
||||
|
||||
// Users section (edit mode only, owner only)
|
||||
if (isEditMode && isCurrentUserOwner) {
|
||||
OrganicDivider()
|
||||
Text(
|
||||
text = "Shared Users (${users.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
if (isLoadingUsers) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(OrganicSpacing.cozy),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
} else if (users.isEmpty()) {
|
||||
Text(
|
||||
text = "No shared users",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = OrganicSpacing.compact)
|
||||
)
|
||||
} else {
|
||||
users.forEach { user ->
|
||||
UserListItem(
|
||||
user = user,
|
||||
onRemove = {
|
||||
userToRemove = user
|
||||
showRemoveUserConfirmation = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Users with access to this residence. Use the share button to invite others.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Error message
|
||||
if (operationState is ApiResult.Error) {
|
||||
Text(
|
||||
text = com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
// Submit button
|
||||
OrganicPrimaryButton(
|
||||
text = if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create),
|
||||
onClick = {
|
||||
if (validateForm()) {
|
||||
val request = ResidenceCreateRequest(
|
||||
name = name,
|
||||
propertyTypeId = propertyType?.id,
|
||||
streetAddress = streetAddress.ifBlank { null },
|
||||
apartmentUnit = apartmentUnit.ifBlank { null },
|
||||
city = city.ifBlank { null },
|
||||
stateProvince = stateProvince.ifBlank { null },
|
||||
postalCode = postalCode.ifBlank { null },
|
||||
country = country.ifBlank { null },
|
||||
bedrooms = bedrooms.toIntOrNull(),
|
||||
bathrooms = bathrooms.toDoubleOrNull(),
|
||||
squareFootage = squareFootage.toIntOrNull(),
|
||||
lotSize = lotSize.toDoubleOrNull(),
|
||||
yearBuilt = yearBuilt.toIntOrNull(),
|
||||
description = description.ifBlank { null },
|
||||
isPrimary = isPrimary
|
||||
)
|
||||
|
||||
if (isEditMode && existingResidence != null) {
|
||||
viewModel.updateResidence(existingResidence.id, request)
|
||||
} else {
|
||||
viewModel.createResidence(request)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = validateForm(),
|
||||
isLoading = operationState is ApiResult.Loading
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,8 +503,9 @@ private fun UserListItem(
|
||||
user: ResidenceUser,
|
||||
onRemove: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
showBlob = false
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.example.casera.cache.SubscriptionCache
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import com.example.casera.data.DataManager
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -225,14 +226,14 @@ fun ResidencesScreen(
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(24.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
|
||||
modifier = Modifier.padding(OrganicSpacing.comfortable)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Home,
|
||||
size = 80.dp,
|
||||
iconScale = 0.6f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.properties_empty_title),
|
||||
@@ -244,7 +245,7 @@ fun ResidencesScreen(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
// Only show Add Property button if not blocked (limit>0)
|
||||
if (!isBlocked.allowed) {
|
||||
Button(
|
||||
@@ -263,7 +264,7 @@ fun ResidencesScreen(
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
@@ -274,7 +275,7 @@ fun ResidencesScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
val (allowed, triggerKey) = canAddProperty()
|
||||
@@ -291,7 +292,7 @@ fun ResidencesScreen(
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.GroupAdd, contentDescription = null)
|
||||
@@ -315,7 +316,7 @@ fun ResidencesScreen(
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Star, contentDescription = null)
|
||||
@@ -344,28 +345,27 @@ fun ResidencesScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 16.dp,
|
||||
start = OrganicSpacing.cozy,
|
||||
end = OrganicSpacing.cozy,
|
||||
top = OrganicSpacing.cozy,
|
||||
bottom = 96.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
// Summary Card
|
||||
item {
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
accentColor = MaterialTheme.colorScheme.primary,
|
||||
showBlob = true,
|
||||
blobVariation = 0,
|
||||
shadowIntensity = ShadowIntensity.Medium
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -373,15 +373,15 @@ fun ResidencesScreen(
|
||||
Icon(
|
||||
Icons.Default.Dashboard,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.compact))
|
||||
Text(
|
||||
text = stringResource(Res.string.home_overview),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -401,8 +401,8 @@ fun ResidencesScreen(
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f)
|
||||
OrganicDivider(
|
||||
color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.2f)
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -436,7 +436,7 @@ fun ResidencesScreen(
|
||||
text = stringResource(Res.string.home_your_properties),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
modifier = Modifier.padding(top = OrganicSpacing.compact)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -456,46 +456,41 @@ fun ResidencesScreen(
|
||||
label = "pulseScale"
|
||||
)
|
||||
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onResidenceClick(residence.id) },
|
||||
shape = MaterialTheme.shapes.large,
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
accentColor = if (hasOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
showBlob = true,
|
||||
blobVariation = residence.id % 3,
|
||||
shadowIntensity = ShadowIntensity.Subtle
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
.padding(OrganicSpacing.cozy)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Pulsing circular house icon when overdue
|
||||
// Pulsing organic icon container when overdue
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.then(
|
||||
if (hasOverdue) Modifier.scale(pulseScale) else Modifier
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (hasOverdue) MaterialTheme.colorScheme.errorContainer
|
||||
else MaterialTheme.colorScheme.primaryContainer
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
modifier = if (hasOverdue) Modifier.scale(pulseScale) else Modifier
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
tint = if (hasOverdue) MaterialTheme.colorScheme.onErrorContainer
|
||||
else MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(28.dp)
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Home,
|
||||
size = 56.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = if (hasOverdue)
|
||||
MaterialTheme.colorScheme.errorContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.primaryContainer,
|
||||
iconColor = if (hasOverdue)
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
@@ -582,9 +577,9 @@ fun ResidencesScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
|
||||
OrganicDivider(color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.15f))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
|
||||
|
||||
// Fully dynamic task summary from API - show first 3 categories
|
||||
val displayCategories = residence.taskSummary.categories.take(3)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -21,6 +22,7 @@ import com.example.casera.viewmodel.TaskViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.analytics.PostHogAnalytics
|
||||
import com.example.casera.analytics.AnalyticsEvents
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -66,163 +68,116 @@ fun TasksScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(Res.string.tasks_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
// No FAB on Tasks screen - tasks are added from within residences
|
||||
) { paddingValues ->
|
||||
when (tasksState) {
|
||||
is ApiResult.Idle, is ApiResult.Loading, is ApiResult.Error -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
is ApiResult.Success -> {
|
||||
val taskData = (tasksState as ApiResult.Success).data
|
||||
val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() }
|
||||
|
||||
if (hasNoTasks) {
|
||||
WarmGradientBackground {
|
||||
Scaffold(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(Res.string.tasks_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
||||
)
|
||||
)
|
||||
},
|
||||
// No FAB on Tasks screen - tasks are added from within residences
|
||||
) { paddingValues ->
|
||||
when (tasksState) {
|
||||
is ApiResult.Idle, is ApiResult.Loading, 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(16.dp),
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Assignment,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.tasks_empty_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.tasks_empty_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding() + 16.dp,
|
||||
bottom = paddingValues.calculateBottomPadding() + 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Task summary pills - dynamically generated from all columns
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
}
|
||||
is ApiResult.Success -> {
|
||||
val taskData = (tasksState as ApiResult.Success).data
|
||||
val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() }
|
||||
|
||||
if (hasNoTasks) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
|
||||
modifier = Modifier.padding(OrganicSpacing.comfortable)
|
||||
) {
|
||||
taskData.columns.forEach { column ->
|
||||
TaskPill(
|
||||
count = column.count,
|
||||
label = column.displayName,
|
||||
color = hexToColor(column.color)
|
||||
)
|
||||
}
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Assignment,
|
||||
size = 80.dp,
|
||||
iconScale = 0.6f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
|
||||
iconColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.tasks_empty_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.tasks_empty_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamically render all columns
|
||||
taskData.columns.forEachIndexed { index, column ->
|
||||
if (column.tasks.isNotEmpty()) {
|
||||
// First column (index 0) expanded by default, others collapsible
|
||||
if (index == 0) {
|
||||
// First column - always expanded, show tasks directly
|
||||
item {
|
||||
Text(
|
||||
text = "${column.displayName} (${column.tasks.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding() + OrganicSpacing.cozy,
|
||||
bottom = paddingValues.calculateBottomPadding() + OrganicSpacing.cozy,
|
||||
start = OrganicSpacing.cozy,
|
||||
end = OrganicSpacing.cozy
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
// Task summary pills - dynamically generated from all columns
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
taskData.columns.forEach { column ->
|
||||
TaskPill(
|
||||
count = column.count,
|
||||
label = column.displayName,
|
||||
color = hexToColor(column.color)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(column.tasks) { task ->
|
||||
TaskCard(
|
||||
task = task,
|
||||
onCompleteClick = {
|
||||
selectedTask = task
|
||||
showCompleteDialog = true
|
||||
},
|
||||
onEditClick = { },
|
||||
onCancelClick = { },
|
||||
onUncancelClick = { }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Other columns - collapsible
|
||||
val isExpanded = expandedColumns.contains(column.name)
|
||||
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
expandedColumns = if (isExpanded) {
|
||||
expandedColumns - column.name
|
||||
} else {
|
||||
expandedColumns + column.name
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
getIconFromName(column.icons["android"] ?: "List"),
|
||||
contentDescription = null,
|
||||
tint = hexToColor(column.color)
|
||||
)
|
||||
Text(
|
||||
text = "${column.displayName} (${column.tasks.size})",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = if (isExpanded) "Collapse" else "Expand"
|
||||
)
|
||||
}
|
||||
// Dynamically render all columns
|
||||
taskData.columns.forEachIndexed { index, column ->
|
||||
if (column.tasks.isNotEmpty()) {
|
||||
// First column (index 0) expanded by default, others collapsible
|
||||
if (index == 0) {
|
||||
// First column - always expanded, show tasks directly
|
||||
item {
|
||||
Text(
|
||||
text = "${column.displayName} (${column.tasks.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
modifier = Modifier.padding(top = OrganicSpacing.compact)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
items(column.tasks) { task ->
|
||||
TaskCard(
|
||||
task = task,
|
||||
@@ -235,15 +190,80 @@ fun TasksScreen(
|
||||
onUncancelClick = { }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Other columns - collapsible
|
||||
val isExpanded = expandedColumns.contains(column.name)
|
||||
|
||||
item {
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
expandedColumns = if (isExpanded) {
|
||||
expandedColumns - column.name
|
||||
} else {
|
||||
expandedColumns + column.name
|
||||
}
|
||||
},
|
||||
showBlob = false,
|
||||
shadowIntensity = ShadowIntensity.Subtle
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
OrganicIconContainer(
|
||||
icon = getIconFromName(column.icons["android"] ?: "List"),
|
||||
size = 40.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = hexToColor(column.color).copy(alpha = 0.2f),
|
||||
iconColor = hexToColor(column.color)
|
||||
)
|
||||
Text(
|
||||
text = "${column.displayName} (${column.tasks.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
||||
tint = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
items(column.tasks) { task ->
|
||||
TaskCard(
|
||||
task = task,
|
||||
onCompleteClick = {
|
||||
selectedTask = task
|
||||
showCompleteDialog = true
|
||||
},
|
||||
onEditClick = { },
|
||||
onCancelClick = { },
|
||||
onUncancelClick = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -21,6 +20,7 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -89,119 +89,118 @@ fun VerifyEmailScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
AuthHeader(
|
||||
icon = Icons.Default.MarkEmailRead,
|
||||
title = stringResource(Res.string.auth_verify_title),
|
||||
subtitle = stringResource(Res.string.auth_verify_subtitle)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Text(
|
||||
text = "Email verification is required. Check your inbox for a 6-digit code.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = {
|
||||
if (it.length <= 6 && it.all { char -> char.isDigit() }) {
|
||||
code = it
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_verify_code_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Pin, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
placeholder = { Text("000000") }
|
||||
)
|
||||
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
ErrorCard(
|
||||
message = errorMessage
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (code.length == 6) {
|
||||
isLoading = true
|
||||
viewModel.verifyEmail(code)
|
||||
} else {
|
||||
errorMessage = "Please enter a valid 6-digit code"
|
||||
}
|
||||
},
|
||||
WarmGradientBackground {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
enabled = !isLoading && code.length == 6
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(OrganicSpacing.comfortable),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.MarkEmailRead,
|
||||
size = 80.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_verify_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_verify_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
|
||||
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.error,
|
||||
showBlob = false
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(OrganicSpacing.cozy),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = null)
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
stringResource(Res.string.auth_verify_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
text = "Email verification is required. Check your inbox for a 6-digit code.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = {
|
||||
if (it.length <= 6 && it.all { char -> char.isDigit() }) {
|
||||
code = it
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_verify_code_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Pin, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
placeholder = { Text("000000") }
|
||||
)
|
||||
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
ErrorCard(
|
||||
message = errorMessage
|
||||
)
|
||||
}
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_verify_button),
|
||||
onClick = {
|
||||
if (code.length == 6) {
|
||||
isLoading = true
|
||||
viewModel.verifyEmail(code)
|
||||
} else {
|
||||
errorMessage = "Please enter a valid 6-digit code"
|
||||
}
|
||||
},
|
||||
enabled = !isLoading && code.length == 6,
|
||||
isLoading = isLoading
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
|
||||
Text(
|
||||
text = "Didn't receive the code? Check your spam folder or contact support.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Didn't receive the code? Check your spam folder or contact support.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.example.casera.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
@@ -19,6 +17,7 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.PasswordResetViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -70,184 +69,185 @@ fun VerifyResetCodeScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
WarmGradientBackground {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
showBlob = true,
|
||||
blobVariation = 1
|
||||
) {
|
||||
AuthHeader(
|
||||
icon = Icons.Default.MarkEmailRead,
|
||||
title = "Check Your Email",
|
||||
subtitle = "We sent a 6-digit code to"
|
||||
)
|
||||
|
||||
Text(
|
||||
email,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.spacious),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Timer,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
"Code expires in 15 minutes",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.MarkEmailRead,
|
||||
size = 80.dp,
|
||||
iconScale = 0.5f,
|
||||
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||
iconColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = {
|
||||
if (it.length <= 6 && it.all { char -> char.isDigit() }) {
|
||||
code = it
|
||||
viewModel.resetVerifyCodeState()
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_verify_code_label)) },
|
||||
placeholder = { Text("000000") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
enabled = !isLoading,
|
||||
textStyle = MaterialTheme.typography.headlineMedium.copy(
|
||||
Text(
|
||||
text = "Check Your Email",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
"Enter the 6-digit code from your email",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = "We sent a 6-digit code to",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
Text(
|
||||
email,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (isSuccess) {
|
||||
Card(
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
accentColor = MaterialTheme.colorScheme.secondary,
|
||||
showBlob = false
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
Icons.Default.Timer,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
"Code verified! Now set your new password",
|
||||
"Code expires in 15 minutes",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.verifyResetCode(email, code)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = code.length == 6 && !isLoading,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
stringResource(Res.string.auth_verify_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"Didn't receive the code?",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = {
|
||||
if (it.length <= 6 && it.all { char -> char.isDigit() }) {
|
||||
code = it
|
||||
viewModel.resetVerifyCodeState()
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_verify_code_label)) },
|
||||
placeholder = { Text("000000") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
enabled = !isLoading,
|
||||
textStyle = MaterialTheme.typography.headlineMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
|
||||
TextButton(onClick = {
|
||||
code = ""
|
||||
viewModel.resetVerifyCodeState()
|
||||
viewModel.moveToPreviousStep()
|
||||
onNavigateBack()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(Res.string.auth_verify_resend),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Check your spam folder if you don't see it",
|
||||
"Enter the 6-digit code from your email",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
ErrorCard(message = errorMessage)
|
||||
|
||||
if (isSuccess) {
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.primary,
|
||||
showBlob = false
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(OrganicSpacing.cozy),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"Code verified! Now set your new password",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OrganicDivider(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_verify_button),
|
||||
onClick = {
|
||||
viewModel.verifyResetCode(email, code)
|
||||
},
|
||||
enabled = code.length == 6 && !isLoading,
|
||||
isLoading = isLoading
|
||||
)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
|
||||
) {
|
||||
Text(
|
||||
"Didn't receive the code?",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
|
||||
TextButton(onClick = {
|
||||
code = ""
|
||||
viewModel.resetVerifyCodeState()
|
||||
viewModel.moveToPreviousStep()
|
||||
onNavigateBack()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(Res.string.auth_verify_resend),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Check your spam folder if you don't see it",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -17,15 +15,12 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.OnboardingViewModel
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -65,245 +60,224 @@ fun OnboardingCreateAccountContent(
|
||||
password.isNotBlank() &&
|
||||
password == confirmPassword
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = AppSpacing.xl)
|
||||
WarmGradientBackground(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
|
||||
// Header
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = OrganicSpacing.xl)
|
||||
) {
|
||||
// Icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PersonAdd,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_create_account_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_create_account_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
|
||||
// Create with Email section
|
||||
if (!isFormExpanded) {
|
||||
// Collapsed state - show button
|
||||
Button(
|
||||
onClick = { isFormExpanded = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_create_with_email),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded form
|
||||
AnimatedVisibility(
|
||||
visible = isFormExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
// Header
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
// Username
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = {
|
||||
username = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_username)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = !isLoading
|
||||
// Icon
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.PersonAdd,
|
||||
size = 80.dp,
|
||||
iconSize = 40.dp,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
// Email
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = {
|
||||
email = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_email)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = !isLoading
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_create_account_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// Password
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = !isLoading
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_create_account_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
// Confirm Password
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = !isLoading,
|
||||
isError = confirmPassword.isNotEmpty() && password != confirmPassword,
|
||||
supportingText = if (confirmPassword.isNotEmpty() && password != confirmPassword) {
|
||||
{ Text(stringResource(Res.string.auth_passwords_dont_match)) }
|
||||
} else null
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
|
||||
// Error message
|
||||
if (localErrorMessage != null) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(AppSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = localErrorMessage ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
|
||||
// Create Account button
|
||||
// Create with Email section
|
||||
if (!isFormExpanded) {
|
||||
// Collapsed state - show button
|
||||
Button(
|
||||
onClick = {
|
||||
if (password == confirmPassword) {
|
||||
viewModel.register(username, email, password)
|
||||
} else {
|
||||
localErrorMessage = "Passwords don't match"
|
||||
}
|
||||
},
|
||||
onClick = { isFormExpanded = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = isFormValid && !isLoading
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_register_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_create_with_email),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
// Expanded form
|
||||
AnimatedVisibility(
|
||||
visible = isFormExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
// Username
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = {
|
||||
username = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_username)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
// Already have an account
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_have_account).substringBefore("?") + "?",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
TextButton(onClick = { showLoginDialog = true }) {
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_login_button),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
// Email
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = {
|
||||
email = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_email)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
// Password
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
// Confirm Password
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = {
|
||||
confirmPassword = it
|
||||
localErrorMessage = null
|
||||
},
|
||||
label = { Text(stringResource(Res.string.auth_register_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
enabled = !isLoading,
|
||||
isError = confirmPassword.isNotEmpty() && password != confirmPassword,
|
||||
supportingText = if (confirmPassword.isNotEmpty() && password != confirmPassword) {
|
||||
{ Text(stringResource(Res.string.auth_passwords_dont_match)) }
|
||||
} else null
|
||||
)
|
||||
|
||||
// Error message
|
||||
if (localErrorMessage != null) {
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.error,
|
||||
showBlob = false
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(OrganicSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = localErrorMessage ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
// Create Account button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_register_button),
|
||||
onClick = {
|
||||
if (password == confirmPassword) {
|
||||
viewModel.register(username, email, password)
|
||||
} else {
|
||||
localErrorMessage = "Passwords don't match"
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = isFormValid && !isLoading,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
// Already have an account
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_have_account).substringBefore("?") + "?",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
TextButton(onClick = { showLoginDialog = true }) {
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_login_button),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
}
|
||||
}
|
||||
|
||||
// Login dialog
|
||||
@@ -356,7 +330,7 @@ private fun OnboardingLoginDialog(
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
@@ -364,7 +338,7 @@ private fun OnboardingLoginDialog(
|
||||
label = { Text(stringResource(Res.string.auth_login_username_label)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
@@ -374,7 +348,7 @@ private fun OnboardingLoginDialog(
|
||||
label = { Text(stringResource(Res.string.auth_login_password_label)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
@@ -24,8 +24,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.example.casera.data.DataManager
|
||||
import com.example.casera.models.TaskCreateRequest
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.OnboardingViewModel
|
||||
import casera.composeapp.generated.resources.*
|
||||
import com.example.casera.util.DateUtils
|
||||
@@ -163,7 +162,7 @@ fun OnboardingFirstTaskContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentPadding = PaddingValues(horizontal = AppSpacing.lg, vertical = AppSpacing.md)
|
||||
contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
|
||||
) {
|
||||
// Header
|
||||
item {
|
||||
@@ -171,30 +170,19 @@ fun OnboardingFirstTaskContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Celebration icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Celebration,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
// Celebration icon using OrganicIconContainer
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Celebration,
|
||||
size = 80.dp,
|
||||
iconSize = 40.dp,
|
||||
gradientColors = listOf(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
MaterialTheme.colorScheme.secondary
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_tasks_title),
|
||||
@@ -203,7 +191,7 @@ fun OnboardingFirstTaskContent(
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_tasks_subtitle),
|
||||
@@ -212,11 +200,11 @@ fun OnboardingFirstTaskContent(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Selection counter
|
||||
Surface(
|
||||
shape = RoundedCornerShape(AppRadius.xl),
|
||||
shape = RoundedCornerShape(OrganicRadius.xl),
|
||||
color = if (isAtMaxSelection) {
|
||||
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f)
|
||||
} else {
|
||||
@@ -224,8 +212,8 @@ fun OnboardingFirstTaskContent(
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
@@ -243,7 +231,7 @@ fun OnboardingFirstTaskContent(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +255,7 @@ fun OnboardingFirstTaskContent(
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
}
|
||||
|
||||
// Add popular tasks button
|
||||
@@ -291,7 +279,7 @@ fun OnboardingFirstTaskContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
shape = RoundedCornerShape(OrganicRadius.lg),
|
||||
border = ButtonDefaults.outlinedButtonBorder.copy(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
@@ -302,7 +290,7 @@ fun OnboardingFirstTaskContent(
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Default.AutoAwesome, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_tasks_add_popular),
|
||||
fontWeight = FontWeight.Medium
|
||||
@@ -318,9 +306,14 @@ fun OnboardingFirstTaskContent(
|
||||
shadowElevation = 8.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(AppSpacing.lg)
|
||||
modifier = Modifier.padding(OrganicSpacing.lg)
|
||||
) {
|
||||
Button(
|
||||
OrganicPrimaryButton(
|
||||
text = if (selectedCount > 0) {
|
||||
"Add $selectedCount Task${if (selectedCount == 1) "" else "s"} & Continue"
|
||||
} else {
|
||||
stringResource(Res.string.onboarding_tasks_skip)
|
||||
},
|
||||
onClick = {
|
||||
if (selectedTaskIds.isEmpty()) {
|
||||
onTasksAdded()
|
||||
@@ -360,32 +353,11 @@ fun OnboardingFirstTaskContent(
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
enabled = !isCreatingTasks
|
||||
) {
|
||||
if (isCreatingTasks) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = if (selectedCount > 0) {
|
||||
"Add $selectedCount Task${if (selectedCount == 1) "" else "s"} & Continue"
|
||||
} else {
|
||||
stringResource(Res.string.onboarding_tasks_skip)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Icon(Icons.Default.ArrowForward, contentDescription = null)
|
||||
}
|
||||
}
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isCreatingTasks,
|
||||
isLoading = isCreatingTasks,
|
||||
icon = Icons.Default.ArrowForward
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,39 +374,32 @@ private fun TaskCategorySection(
|
||||
) {
|
||||
val selectedInCategory = category.tasks.count { it.id in selectedTaskIds }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(AppRadius.lg))
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = category.color,
|
||||
showBlob = false
|
||||
) {
|
||||
// Header
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggleExpand() },
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier.padding(AppSpacing.md),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggleExpand() }
|
||||
.padding(OrganicSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Category icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(category.color),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = category.icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
OrganicIconContainer(
|
||||
icon = category.icon,
|
||||
size = 44.dp,
|
||||
iconSize = 24.dp,
|
||||
gradientColors = listOf(category.color),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(AppSpacing.md))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.md))
|
||||
|
||||
Text(
|
||||
text = category.name,
|
||||
@@ -459,7 +424,7 @@ private fun TaskCategorySection(
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
}
|
||||
|
||||
Icon(
|
||||
@@ -468,36 +433,34 @@ private fun TaskCategorySection(
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded content
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
// Expanded content
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
category.tasks.forEachIndexed { index, task ->
|
||||
val isSelected = task.id in selectedTaskIds
|
||||
val isDisabled = isAtMaxSelection && !isSelected
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
category.tasks.forEachIndexed { index, task ->
|
||||
val isSelected = task.id in selectedTaskIds
|
||||
val isDisabled = isAtMaxSelection && !isSelected
|
||||
|
||||
TaskTemplateRow(
|
||||
task = task,
|
||||
isSelected = isSelected,
|
||||
isDisabled = isDisabled,
|
||||
categoryColor = category.color,
|
||||
onClick = { onToggleTask(task.id) }
|
||||
)
|
||||
|
||||
if (index < category.tasks.lastIndex) {
|
||||
Divider(
|
||||
modifier = Modifier.padding(start = 60.dp),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
|
||||
TaskTemplateRow(
|
||||
task = task,
|
||||
isSelected = isSelected,
|
||||
isDisabled = isDisabled,
|
||||
categoryColor = category.color,
|
||||
onClick = { onToggleTask(task.id) }
|
||||
)
|
||||
|
||||
if (index < category.tasks.lastIndex) {
|
||||
OrganicDivider(
|
||||
modifier = Modifier.padding(start = 60.dp),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,7 +480,7 @@ private fun TaskTemplateRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = !isDisabled) { onClick() }
|
||||
.padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm),
|
||||
.padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Checkbox
|
||||
@@ -541,7 +504,7 @@ private fun TaskTemplateRow(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(AppSpacing.md))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.md))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.example.casera.ui.screens.onboarding
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -11,14 +9,12 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.OnboardingViewModel
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -48,163 +44,142 @@ fun OnboardingJoinResidenceContent(
|
||||
val isLoading = joinState is ApiResult.Loading
|
||||
val isCodeValid = shareCode.length == 6
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
WarmGradientBackground(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Header
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.GroupAdd,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(50.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Title and subtitle
|
||||
// Header
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_join_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
// Icon
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.GroupAdd,
|
||||
size = 100.dp,
|
||||
iconSize = 50.dp,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_join_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
// Title and subtitle
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_join_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
|
||||
// Share code input
|
||||
OutlinedTextField(
|
||||
value = shareCode,
|
||||
onValueChange = {
|
||||
if (it.length <= 6) {
|
||||
shareCode = it.uppercase()
|
||||
localErrorMessage = null
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_join_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
stringResource(Res.string.onboarding_join_placeholder),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Key, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Characters
|
||||
),
|
||||
enabled = !isLoading
|
||||
)
|
||||
}
|
||||
|
||||
// Error message
|
||||
if (localErrorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
Card(
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
|
||||
// Share code input
|
||||
OutlinedTextField(
|
||||
value = shareCode,
|
||||
onValueChange = {
|
||||
if (it.length <= 6) {
|
||||
shareCode = it.uppercase()
|
||||
localErrorMessage = null
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
stringResource(Res.string.onboarding_join_placeholder),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Key, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
) {
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Characters
|
||||
),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
// Error message
|
||||
if (localErrorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.error,
|
||||
showBlob = false
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(OrganicSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = localErrorMessage ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
Row(
|
||||
modifier = Modifier.padding(AppSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = localErrorMessage ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
text = "Joining residence...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Join button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.onboarding_join_button),
|
||||
onClick = { viewModel.joinResidence(shareCode) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = isCodeValid && !isLoading,
|
||||
isLoading = isLoading
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = "Joining residence...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Join button
|
||||
Button(
|
||||
onClick = { viewModel.joinResidence(shareCode) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = isCodeValid && !isLoading
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_join_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.example.casera.ui.screens.onboarding
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
@@ -11,14 +9,10 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.OnboardingViewModel
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -31,120 +25,95 @@ fun OnboardingNameResidenceContent(
|
||||
val residenceName by viewModel.residenceName.collectAsState()
|
||||
var localName by remember { mutableStateOf(residenceName) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
WarmGradientBackground(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Header with icon
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Icon with gradient background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.shadow(
|
||||
elevation = 16.dp,
|
||||
shape = CircleShape,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
||||
)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(50.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Title and subtitle
|
||||
// Header with icon
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_name_residence_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
// Icon with OrganicIconContainer
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Home,
|
||||
size = 100.dp,
|
||||
iconSize = 50.dp,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_name_residence_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
// Title and subtitle
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_name_residence_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_name_residence_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
|
||||
// Name input
|
||||
OutlinedTextField(
|
||||
value = localName,
|
||||
onValueChange = { localName = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
stringResource(Res.string.onboarding_name_residence_placeholder),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
// Name input
|
||||
OutlinedTextField(
|
||||
value = localName,
|
||||
onValueChange = { localName = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
stringResource(Res.string.onboarding_name_residence_placeholder),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_name_residence_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Continue button
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.setResidenceName(localName)
|
||||
onContinue()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = localName.isNotBlank()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_continue),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
text = stringResource(Res.string.onboarding_name_residence_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Icon(Icons.Default.ArrowForward, contentDescription = null)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Continue button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.onboarding_continue),
|
||||
onClick = {
|
||||
viewModel.setResidenceName(localName)
|
||||
onContinue()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = localName.isNotBlank(),
|
||||
icon = Icons.Default.ArrowForward
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.OnboardingStep
|
||||
import com.example.casera.viewmodel.OnboardingViewModel
|
||||
import com.example.casera.viewmodel.OnboardingIntent
|
||||
@@ -189,7 +189,7 @@ private fun OnboardingNavigationBar(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
|
||||
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Back button
|
||||
@@ -240,7 +240,7 @@ fun OnboardingProgressIndicator(
|
||||
totalSteps: Int
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(totalSteps) { index ->
|
||||
|
||||
@@ -23,8 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@@ -117,227 +116,201 @@ fun OnboardingSubscriptionContent(
|
||||
)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
WarmGradientBackground(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
|
||||
// Crown header with animation
|
||||
Box(
|
||||
modifier = Modifier.size(180.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Glow effect
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(140.dp)
|
||||
.scale(scale)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Crown icon
|
||||
// Crown header with animation
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
Color(0xFFFF9500)
|
||||
)
|
||||
)
|
||||
),
|
||||
modifier = Modifier.size(180.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.EmojiEvents,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(50.dp),
|
||||
tint = Color.White
|
||||
// Glow effect
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(140.dp)
|
||||
.scale(scale)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Crown icon using OrganicIconContainer
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.EmojiEvents,
|
||||
size = 100.dp,
|
||||
iconSize = 50.dp,
|
||||
gradientColors = listOf(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
Color(0xFFFF9500)
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// PRO badge
|
||||
Surface(
|
||||
shape = RoundedCornerShape(AppRadius.full),
|
||||
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f)
|
||||
) {
|
||||
// PRO badge
|
||||
Surface(
|
||||
shape = RoundedCornerShape(OrganicRadius.full),
|
||||
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AutoAwesome,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_pro),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Black,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.AutoAwesome,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_subtitle),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
// Social proof
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AutoAwesome,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
repeat(5) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_pro),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Black,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.AutoAwesome,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
text = stringResource(Res.string.onboarding_subscription_social_proof),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_subtitle),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
|
||||
// Social proof
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(5) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
// Benefits list
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
benefits.forEach { benefit ->
|
||||
BenefitRow(benefit = benefit)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
// Plan selection
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_social_proof),
|
||||
text = stringResource(Res.string.onboarding_subscription_choose_plan),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
// Yearly plan
|
||||
PlanCard(
|
||||
plan = SubscriptionPlan.YEARLY,
|
||||
isSelected = selectedPlan == SubscriptionPlan.YEARLY,
|
||||
onClick = { selectedPlan = SubscriptionPlan.YEARLY }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
// Monthly plan
|
||||
PlanCard(
|
||||
plan = SubscriptionPlan.MONTHLY,
|
||||
isSelected = selectedPlan == SubscriptionPlan.MONTHLY,
|
||||
onClick = { selectedPlan = SubscriptionPlan.MONTHLY }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
// Start trial button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.onboarding_subscription_start_trial),
|
||||
onClick = {
|
||||
isLoading = true
|
||||
// Simulate subscription flow
|
||||
onSubscribe()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
isLoading = isLoading,
|
||||
icon = Icons.Default.ArrowForward
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
// Continue free button
|
||||
TextButton(onClick = onSkip) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_continue_free),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
|
||||
|
||||
// Legal text
|
||||
Text(
|
||||
text = "7-day free trial, then ${if (selectedPlan == SubscriptionPlan.YEARLY) "$23.99/year" else "$2.99/month"}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
|
||||
// Benefits list
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
) {
|
||||
benefits.forEach { benefit ->
|
||||
BenefitRow(benefit = benefit)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
|
||||
// Plan selection
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_choose_plan),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
|
||||
// Yearly plan
|
||||
PlanCard(
|
||||
plan = SubscriptionPlan.YEARLY,
|
||||
isSelected = selectedPlan == SubscriptionPlan.YEARLY,
|
||||
onClick = { selectedPlan = SubscriptionPlan.YEARLY }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
|
||||
// Monthly plan
|
||||
PlanCard(
|
||||
plan = SubscriptionPlan.MONTHLY,
|
||||
isSelected = selectedPlan == SubscriptionPlan.MONTHLY,
|
||||
onClick = { selectedPlan = SubscriptionPlan.MONTHLY }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
|
||||
// Start trial button
|
||||
Button(
|
||||
onClick = {
|
||||
isLoading = true
|
||||
// Simulate subscription flow
|
||||
onSubscribe()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiary
|
||||
),
|
||||
enabled = !isLoading
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_start_trial),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Icon(Icons.Default.ArrowForward, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
|
||||
// Continue free button
|
||||
TextButton(onClick = onSkip) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_subscription_continue_free),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
text = "Cancel anytime in Settings • No commitment",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.sm))
|
||||
|
||||
// Legal text
|
||||
Text(
|
||||
text = "7-day free trial, then ${if (selectedPlan == SubscriptionPlan.YEARLY) "$23.99/year" else "$2.99/month"}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Cancel anytime in Settings • No commitment",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,28 +320,19 @@ private fun BenefitRow(benefit: SubscriptionBenefit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm),
|
||||
.padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Gradient icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(benefit.gradientColors)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = benefit.icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
// Gradient icon using OrganicIconContainer
|
||||
OrganicIconContainer(
|
||||
icon = benefit.icon,
|
||||
size = 44.dp,
|
||||
iconSize = 24.dp,
|
||||
gradientColors = benefit.gradientColors,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(AppSpacing.md))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.md))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
@@ -402,26 +366,15 @@ private fun PlanCard(
|
||||
) {
|
||||
val isYearly = plan == SubscriptionPlan.YEARLY
|
||||
|
||||
Surface(
|
||||
OrganicCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
shape = RoundedCornerShape(AppRadius.lg),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
border = if (isSelected) {
|
||||
ButtonDefaults.outlinedButtonBorder.copy(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
Color(0xFFFF9500)
|
||||
)
|
||||
),
|
||||
width = 2.dp
|
||||
)
|
||||
} else null
|
||||
accentColor = if (isSelected) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
showBlob = isSelected
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(AppSpacing.lg),
|
||||
modifier = Modifier.padding(OrganicSpacing.lg),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Selection indicator
|
||||
@@ -446,11 +399,11 @@ private fun PlanCard(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(AppSpacing.md))
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.md))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
@@ -466,7 +419,7 @@ private fun PlanCard(
|
||||
|
||||
if (isYearly) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(AppRadius.full),
|
||||
shape = RoundedCornerShape(OrganicRadius.full),
|
||||
color = Color(0xFF34C759)
|
||||
) {
|
||||
Text(
|
||||
@@ -474,7 +427,7 @@ private fun PlanCard(
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp)
|
||||
modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -111,67 +110,62 @@ fun OnboardingValuePropsContent(
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
WarmGradientBackground(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
|
||||
// Feature carousel
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(2f)
|
||||
) { page ->
|
||||
FeatureCard(feature = features[page])
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl))
|
||||
|
||||
// Page indicators
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
repeat(features.size) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(if (index == pagerState.currentPage) 10.dp else 8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (index == pagerState.currentPage) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
|
||||
// Feature carousel
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(2f)
|
||||
) { page ->
|
||||
FeatureCard(feature = features[page])
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
|
||||
|
||||
// Continue button
|
||||
Button(
|
||||
onClick = onContinue,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
) {
|
||||
Text(
|
||||
// Page indicators
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(features.size) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(if (index == pagerState.currentPage) 10.dp else 8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (index == pagerState.currentPage) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Continue button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.onboarding_continue),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
onClick = onContinue,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
icon = Icons.Default.ArrowForward
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Icon(Icons.Default.ArrowForward, contentDescription = null)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,29 +174,20 @@ private fun FeatureCard(feature: FeatureItem) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = AppSpacing.md),
|
||||
.padding(horizontal = OrganicSpacing.md),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
// Icon with gradient background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(feature.gradientColors)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = feature.icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(60.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
// Icon with gradient background using OrganicIconContainer
|
||||
OrganicIconContainer(
|
||||
icon = feature.icon,
|
||||
size = 120.dp,
|
||||
iconSize = 60.dp,
|
||||
gradientColors = feature.gradientColors,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
|
||||
// Title
|
||||
Text(
|
||||
@@ -213,7 +198,7 @@ private fun FeatureCard(feature: FeatureItem) {
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
|
||||
// Description
|
||||
Text(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.example.casera.ui.screens.onboarding
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -11,15 +9,12 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
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.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.OnboardingViewModel
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -56,173 +51,152 @@ fun OnboardingVerifyEmailContent(
|
||||
|
||||
val isLoading = verifyState is ApiResult.Loading
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
WarmGradientBackground(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Header
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Icon with gradient background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MarkEmailRead,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(50.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Title and subtitle
|
||||
// Header
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_verify_email_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
// Icon with OrganicIconContainer
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.MarkEmailRead,
|
||||
size = 100.dp,
|
||||
iconSize = 50.dp,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_verify_email_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
// Title and subtitle
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_verify_email_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
|
||||
// Code input
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = {
|
||||
if (it.length <= 6 && it.all { char -> char.isDigit() }) {
|
||||
code = it
|
||||
localErrorMessage = null
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_verify_email_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"000000",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Pin, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
enabled = !isLoading
|
||||
)
|
||||
}
|
||||
|
||||
// Error message
|
||||
if (localErrorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
Card(
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
|
||||
// Code input
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = {
|
||||
if (it.length <= 6 && it.all { char -> char.isDigit() }) {
|
||||
code = it
|
||||
localErrorMessage = null
|
||||
}
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"000000",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Pin, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
shape = RoundedCornerShape(AppRadius.md)
|
||||
) {
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
// Error message
|
||||
if (localErrorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
OrganicCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
accentColor = MaterialTheme.colorScheme.error,
|
||||
showBlob = false
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(OrganicSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = localErrorMessage ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.md))
|
||||
Row(
|
||||
modifier = Modifier.padding(AppSpacing.md),
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = localErrorMessage ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
text = "Verifying...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
|
||||
|
||||
// Hint text
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_verify_email_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Verify button
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.auth_verify_button),
|
||||
onClick = { viewModel.verifyEmail(code) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = code.length == 6 && !isLoading,
|
||||
isLoading = isLoading
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
if (isLoading) {
|
||||
Spacer(modifier = Modifier.height(AppSpacing.md))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = "Verifying...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.lg))
|
||||
|
||||
// Hint text
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_verify_email_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Verify button
|
||||
Button(
|
||||
onClick = { viewModel.verifyEmail(code) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
enabled = code.length == 6 && !isLoading
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(Res.string.auth_verify_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.example.casera.ui.screens.onboarding
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -11,16 +10,11 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.ui.theme.*
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
@@ -35,132 +29,106 @@ fun OnboardingWelcomeContent(
|
||||
) {
|
||||
var showLoginDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = AppSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
WarmGradientBackground(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Hero section
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = OrganicSpacing.xl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xl)
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
// App icon with shadow
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.shadow(
|
||||
elevation = 20.dp,
|
||||
shape = RoundedCornerShape(AppRadius.xxl),
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
|
||||
)
|
||||
.clip(RoundedCornerShape(AppRadius.xxl))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.align(Alignment.Center),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Welcome text
|
||||
// Hero section
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xl)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_welcome_title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
// App icon with OrganicIconContainer
|
||||
OrganicIconContainer(
|
||||
icon = Icons.Default.Home,
|
||||
size = 120.dp,
|
||||
iconSize = 80.dp,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_welcome_subtitle),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
// Welcome text
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_welcome_title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_welcome_subtitle),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Action buttons
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
) {
|
||||
// Primary CTA - Start Fresh
|
||||
Button(
|
||||
onClick = onStartFresh,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
// Action buttons
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(
|
||||
// Primary CTA - Start Fresh
|
||||
OrganicPrimaryButton(
|
||||
text = stringResource(Res.string.onboarding_start_fresh),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
onClick = onStartFresh,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
icon = Icons.Default.Home
|
||||
)
|
||||
|
||||
// Secondary CTA - Join Existing
|
||||
OutlinedButton(
|
||||
onClick = onJoinExisting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.People,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_join_existing),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
// Returning user login
|
||||
TextButton(
|
||||
onClick = { showLoginDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_already_have_account),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary CTA - Join Existing
|
||||
OutlinedButton(
|
||||
onClick = onJoinExisting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.People,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_join_existing),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
// Returning user login
|
||||
TextButton(
|
||||
onClick = { showLoginDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.onboarding_already_have_account),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
|
||||
}
|
||||
|
||||
// Login dialog
|
||||
@@ -212,7 +180,7 @@ private fun LoginDialog(
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
||||
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
@@ -220,7 +188,7 @@ private fun LoginDialog(
|
||||
label = { Text(stringResource(Res.string.auth_login_username_label)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
@@ -230,7 +198,7 @@ private fun LoginDialog(
|
||||
label = { Text(stringResource(Res.string.auth_login_password_label)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(AppRadius.md),
|
||||
shape = RoundedCornerShape(OrganicRadius.md),
|
||||
visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(),
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
@@ -0,0 +1,745 @@
|
||||
package com.example.casera.ui.theme
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Eco
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.random.Random
|
||||
|
||||
// MARK: - Organic Design System
|
||||
// Warm, natural aesthetic with soft shapes, subtle textures, and flowing layouts
|
||||
|
||||
// MARK: - Organic Shapes
|
||||
|
||||
/**
|
||||
* Soft organic blob shape for backgrounds
|
||||
* Matches iOS OrganicBlobShape
|
||||
*/
|
||||
class OrganicBlobShape(private val variation: Int = 0) : Shape {
|
||||
override fun createOutline(
|
||||
size: Size,
|
||||
layoutDirection: androidx.compose.ui.unit.LayoutDirection,
|
||||
density: androidx.compose.ui.unit.Density
|
||||
): Outline {
|
||||
val path = Path()
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
|
||||
when (variation % 3) {
|
||||
0 -> {
|
||||
// Soft cloud-like blob
|
||||
path.moveTo(w * 0.1f, h * 0.5f)
|
||||
path.cubicTo(
|
||||
w * 0.0f, h * 0.1f,
|
||||
w * 0.25f, h * 0.0f,
|
||||
w * 0.5f, h * 0.05f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.75f, h * 0.1f,
|
||||
w * 1.0f, h * 0.25f,
|
||||
w * 0.95f, h * 0.45f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.9f, h * 0.7f,
|
||||
w * 0.8f, h * 0.95f,
|
||||
w * 0.55f, h * 0.95f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.25f, h * 0.95f,
|
||||
w * 0.05f, h * 0.75f,
|
||||
w * 0.1f, h * 0.5f
|
||||
)
|
||||
}
|
||||
1 -> {
|
||||
// Pebble shape
|
||||
path.moveTo(w * 0.15f, h * 0.4f)
|
||||
path.cubicTo(
|
||||
w * 0.1f, h * 0.15f,
|
||||
w * 0.35f, h * 0.05f,
|
||||
w * 0.6f, h * 0.08f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.85f, h * 0.12f,
|
||||
w * 0.95f, h * 0.35f,
|
||||
w * 0.9f, h * 0.55f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.85f, h * 0.8f,
|
||||
w * 0.65f, h * 0.95f,
|
||||
w * 0.45f, h * 0.92f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.2f, h * 0.88f,
|
||||
w * 0.08f, h * 0.65f,
|
||||
w * 0.15f, h * 0.4f
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Leaf-like shape
|
||||
path.moveTo(w * 0.05f, h * 0.5f)
|
||||
path.cubicTo(
|
||||
w * 0.05f, h * 0.2f,
|
||||
w * 0.25f, h * 0.02f,
|
||||
w * 0.5f, h * 0.02f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.75f, h * 0.02f,
|
||||
w * 0.95f, h * 0.2f,
|
||||
w * 0.95f, h * 0.5f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.95f, h * 0.8f,
|
||||
w * 0.75f, h * 0.98f,
|
||||
w * 0.5f, h * 0.98f
|
||||
)
|
||||
path.cubicTo(
|
||||
w * 0.25f, h * 0.98f,
|
||||
w * 0.05f, h * 0.8f,
|
||||
w * 0.05f, h * 0.5f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
path.close()
|
||||
return Outline.Generic(path)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Super soft rounded rectangle for organic cards
|
||||
* Uses continuous corner radius matching iOS .continuous style
|
||||
*/
|
||||
val OrganicRoundedShape = RoundedCornerShape(28.dp)
|
||||
|
||||
// MARK: - Grain Texture Overlay
|
||||
|
||||
/**
|
||||
* Grain texture overlay for natural feel
|
||||
* Matches iOS GrainTexture
|
||||
*/
|
||||
@Composable
|
||||
fun GrainTexture(
|
||||
modifier: Modifier = Modifier,
|
||||
opacity: Float = 0.03f
|
||||
) {
|
||||
val grainColor = Color.Black.copy(alpha = opacity)
|
||||
|
||||
Canvas(modifier = modifier.fillMaxSize()) {
|
||||
val density = size.width * size.height / 50
|
||||
val random = Random(42) // Fixed seed for consistent pattern
|
||||
|
||||
repeat(density.toInt().coerceAtMost(5000)) {
|
||||
val x = random.nextFloat() * size.width
|
||||
val y = random.nextFloat() * size.height
|
||||
val grainOpacity = random.nextFloat() * 0.7f + 0.3f
|
||||
|
||||
drawCircle(
|
||||
color = grainColor.copy(alpha = grainColor.alpha * grainOpacity),
|
||||
radius = 0.5f,
|
||||
center = Offset(x, y)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Card Background
|
||||
|
||||
/**
|
||||
* Organic card background with subtle accent blob and grain texture
|
||||
* Matches iOS OrganicCardBackground
|
||||
*/
|
||||
@Composable
|
||||
fun OrganicCardBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
accentColor: Color = MaterialTheme.colorScheme.primary,
|
||||
showBlob: Boolean = true,
|
||||
blobVariation: Int = 0
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
// Main card fill
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(OrganicRoundedShape)
|
||||
.background(MaterialTheme.colorScheme.backgroundSecondary)
|
||||
)
|
||||
|
||||
// Subtle accent blob in corner
|
||||
if (showBlob) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(OrganicRoundedShape)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.6f)
|
||||
.fillMaxHeight(0.7f)
|
||||
.offset(x = 80.dp, y = (-20).dp)
|
||||
.align(Alignment.TopEnd)
|
||||
.clip(OrganicBlobShape(blobVariation))
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
accentColor.copy(alpha = 0.08f),
|
||||
accentColor.copy(alpha = 0.02f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.blur(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Grain texture
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(OrganicRoundedShape)
|
||||
) {
|
||||
GrainTexture(opacity = 0.015f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Natural Shadow
|
||||
|
||||
/**
|
||||
* Shadow intensity levels matching iOS NaturalShadow
|
||||
*/
|
||||
enum class ShadowIntensity {
|
||||
Subtle,
|
||||
Medium,
|
||||
Pronounced;
|
||||
|
||||
val elevation: Dp
|
||||
get() = when (this) {
|
||||
Subtle -> 4.dp
|
||||
Medium -> 8.dp
|
||||
Pronounced -> 12.dp
|
||||
}
|
||||
|
||||
val ambientAlpha: Float
|
||||
get() = when (this) {
|
||||
Subtle -> 0.04f
|
||||
Medium -> 0.08f
|
||||
Pronounced -> 0.12f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Natural shadow modifier matching iOS NaturalShadow
|
||||
*/
|
||||
fun Modifier.naturalShadow(
|
||||
intensity: ShadowIntensity = ShadowIntensity.Medium,
|
||||
shape: Shape = OrganicRoundedShape
|
||||
): Modifier = this
|
||||
.shadow(
|
||||
elevation = intensity.elevation,
|
||||
shape = shape,
|
||||
ambientColor = Color.Black.copy(alpha = intensity.ambientAlpha),
|
||||
spotColor = Color.Black.copy(alpha = intensity.ambientAlpha * 0.5f)
|
||||
)
|
||||
|
||||
// MARK: - Organic Icon Container
|
||||
|
||||
/**
|
||||
* Icon with soft organic background and inner glow
|
||||
* Matches iOS OrganicIconContainer
|
||||
*
|
||||
* @param icon The icon to display (ImageVector)
|
||||
* @param modifier Modifier for the container
|
||||
* @param size The size of the container (default 48.dp)
|
||||
* @param iconScale Scale of the icon relative to container (default 0.5f)
|
||||
* @param iconSize Optional explicit icon size (overrides iconScale if provided)
|
||||
* @param backgroundColor Background color for the container
|
||||
* @param iconColor Tint color for the icon
|
||||
* @param containerColor Alias for backgroundColor (for compatibility)
|
||||
* @param iconTint Alias for iconColor (for compatibility)
|
||||
* @param gradientColors Optional custom gradient colors (overrides backgroundColor)
|
||||
* @param contentDescription Accessibility description
|
||||
*/
|
||||
@Composable
|
||||
fun OrganicIconContainer(
|
||||
icon: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 48.dp,
|
||||
iconScale: Float = 0.5f,
|
||||
iconSize: Dp? = null,
|
||||
backgroundColor: Color? = null,
|
||||
iconColor: Color? = null,
|
||||
containerColor: Color? = null,
|
||||
iconTint: Color? = null,
|
||||
gradientColors: List<Color>? = null,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
// Resolve colors with fallback chain: explicit param -> alias -> default
|
||||
val resolvedBackgroundColor = backgroundColor ?: containerColor ?: MaterialTheme.colorScheme.primary
|
||||
val resolvedIconColor = iconColor ?: iconTint ?: MaterialTheme.colorScheme.onPrimary
|
||||
|
||||
val actualGradientColors = gradientColors ?: listOf(
|
||||
resolvedBackgroundColor,
|
||||
resolvedBackgroundColor.copy(alpha = 0.85f)
|
||||
)
|
||||
val actualIconSize = iconSize ?: (size * iconScale)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.naturalShadow(ShadowIntensity.Subtle, CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Soft organic background with radial gradient
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = actualGradientColors,
|
||||
center = Offset(this.size.width * 0.3f, this.size.height * 0.3f),
|
||||
radius = this.size.maxDimension
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Inner glow
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.2f),
|
||||
Color.Transparent
|
||||
),
|
||||
center = Offset(this.size.width * 0.3f, this.size.height * 0.3f),
|
||||
radius = this.size.maxDimension * 0.5f
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Icon
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(actualIconSize),
|
||||
tint = resolvedIconColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Stat Pill
|
||||
|
||||
/**
|
||||
* Stat display pill with icon and label
|
||||
* Matches iOS OrganicStatPill
|
||||
*/
|
||||
@Composable
|
||||
fun OrganicStatPill(
|
||||
icon: ImageVector,
|
||||
value: String,
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.backgroundSecondary,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
.drawBehind {
|
||||
// Border
|
||||
drawRoundRect(
|
||||
color = color.copy(alpha = 0.15f),
|
||||
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx()),
|
||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(50.dp.toPx())
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Icon with soft background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.background(
|
||||
color = color.copy(alpha = 0.12f),
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(13.dp),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||
),
|
||||
color = MaterialTheme.colorScheme.textPrimary
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = 11.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
|
||||
),
|
||||
color = MaterialTheme.colorScheme.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Divider
|
||||
|
||||
/**
|
||||
* Gradient divider that fades at edges
|
||||
* Matches iOS OrganicDivider
|
||||
*/
|
||||
@Composable
|
||||
fun OrganicDivider(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.15f),
|
||||
height: Dp = 1.dp,
|
||||
horizontalPadding: Dp = 0.dp,
|
||||
vertical: Boolean = false
|
||||
) {
|
||||
if (vertical) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(1.dp)
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
color.copy(alpha = 0f),
|
||||
color,
|
||||
color,
|
||||
color.copy(alpha = 0f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(horizontal = horizontalPadding)
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
color.copy(alpha = 0f),
|
||||
color,
|
||||
color,
|
||||
color.copy(alpha = 0f)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Warm Gradient Background
|
||||
|
||||
/**
|
||||
* Screen background with subtle warm gradient and grain
|
||||
* Matches iOS WarmGradientBackground
|
||||
*/
|
||||
@Composable
|
||||
fun WarmGradientBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit = {}
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val backgroundColor = MaterialTheme.colorScheme.backgroundPrimary
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
// Base background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
)
|
||||
|
||||
// Subtle warm gradient overlay
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
primaryColor.copy(alpha = if (isDark) 0.05f else 0.03f),
|
||||
Color.Transparent
|
||||
),
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Grain for natural feel
|
||||
GrainTexture(opacity = 0.02f)
|
||||
|
||||
// Content
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Card Modifier
|
||||
|
||||
/**
|
||||
* Organic card styling modifier
|
||||
* Matches iOS organicCard view modifier
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.organicCard(
|
||||
accentColor: Color = MaterialTheme.colorScheme.primary,
|
||||
showBlob: Boolean = true,
|
||||
shadowIntensity: ShadowIntensity = ShadowIntensity.Medium,
|
||||
blobVariation: Int = 0
|
||||
): Modifier = this
|
||||
.naturalShadow(shadowIntensity)
|
||||
.clip(OrganicRoundedShape)
|
||||
.drawBehind {
|
||||
// Main fill
|
||||
drawRoundRect(
|
||||
color = Color.Transparent, // Background handled by OrganicCardBackground
|
||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(28.dp.toPx())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable wrapper for organic card styling
|
||||
*/
|
||||
@Composable
|
||||
fun OrganicCard(
|
||||
modifier: Modifier = Modifier,
|
||||
accentColor: Color = MaterialTheme.colorScheme.primary,
|
||||
showBlob: Boolean = true,
|
||||
shadowIntensity: ShadowIntensity = ShadowIntensity.Medium,
|
||||
blobVariation: Int = 0,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.naturalShadow(shadowIntensity, OrganicRoundedShape)
|
||||
) {
|
||||
OrganicCardBackground(
|
||||
accentColor = accentColor,
|
||||
showBlob = showBlob,
|
||||
blobVariation = blobVariation
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(OrganicRoundedShape),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Organic Spacing
|
||||
|
||||
/**
|
||||
* Organic spacing constants matching iOS OrganicSpacing
|
||||
* Provides both semantic names (compact, cozy, etc.) and standard names (xs, sm, etc.)
|
||||
*/
|
||||
object OrganicSpacing {
|
||||
// Semantic names (original)
|
||||
val compact = 8.dp
|
||||
val cozy = 20.dp
|
||||
val comfortable = 24.dp
|
||||
val spacious = 32.dp
|
||||
val airy = 40.dp
|
||||
|
||||
// Standard size names (for compatibility)
|
||||
val xs = 4.dp
|
||||
val sm = 8.dp
|
||||
val md = 12.dp
|
||||
val lg = 16.dp
|
||||
val xl = 24.dp
|
||||
val xxl = 32.dp
|
||||
|
||||
// Additional aliases
|
||||
val extraSmall = xs
|
||||
val small = sm
|
||||
val medium = md
|
||||
val large = lg
|
||||
val extraLarge = xl
|
||||
|
||||
// Extended aliases (matching iOS naming)
|
||||
val minimal = 2.dp
|
||||
val generous = 48.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* Organic radius constants for rounded corners
|
||||
* Alias for AppRadius with organic naming
|
||||
*/
|
||||
object OrganicRadius {
|
||||
val xs = 4.dp
|
||||
val sm = 8.dp
|
||||
val md = 12.dp
|
||||
val lg = 16.dp
|
||||
val xl = 20.dp
|
||||
val xxl = 24.dp
|
||||
val full = 50.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* Organic shapes for consistency
|
||||
*/
|
||||
object OrganicShapes {
|
||||
val extraSmall = RoundedCornerShape(OrganicRadius.xs)
|
||||
val small = RoundedCornerShape(OrganicRadius.sm)
|
||||
val medium = RoundedCornerShape(OrganicRadius.md)
|
||||
val large = RoundedCornerShape(OrganicRadius.lg)
|
||||
val extraLarge = RoundedCornerShape(OrganicRadius.xl)
|
||||
}
|
||||
|
||||
// MARK: - Floating Leaf Decoration
|
||||
|
||||
/**
|
||||
* Animated floating leaf decoration
|
||||
* Matches iOS FloatingLeaf
|
||||
*/
|
||||
@Composable
|
||||
fun FloatingLeaf(
|
||||
modifier: Modifier = Modifier,
|
||||
delay: Int = 0,
|
||||
size: Dp = 20.dp,
|
||||
color: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "leaf")
|
||||
|
||||
val rotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 15f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 4000,
|
||||
delayMillis = delay,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "leafRotation"
|
||||
)
|
||||
|
||||
val offsetY by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 8f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 4000,
|
||||
delayMillis = delay,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "leafOffset"
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Eco,
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.rotate(rotation)
|
||||
.offset(y = offsetY.dp),
|
||||
tint = color.copy(alpha = 0.15f)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Organic Button
|
||||
|
||||
/**
|
||||
* Primary button with organic gradient styling
|
||||
*/
|
||||
@Composable
|
||||
fun OrganicPrimaryButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
icon: ImageVector? = null
|
||||
) {
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
androidx.compose.material3.Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = enabled && !isLoading,
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
|
||||
containerColor = primaryColor,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
disabledContainerColor = primaryColor.copy(alpha = 0.5f),
|
||||
disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f)
|
||||
)
|
||||
) {
|
||||
if (isLoading) {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold
|
||||
)
|
||||
)
|
||||
if (icon != null) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user