Files
honeyDueKMP/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt
Trey t dd5e050025 Make residence address fields optional (only name required)
Updated Kotlin models, Android UI, and iOS UI to make all address fields
optional for residences. Only the residence name is now required.

Changes:
- Kotlin: Made propertyType, streetAddress, city, stateProvince, postalCode,
  country nullable in Residence, ResidenceSummary, ResidenceWithTasks models
- Kotlin: Updated navigation routes to handle nullable address fields
- Android: Updated ResidenceFormScreen and ResidenceDetailScreen to handle nulls
- iOS: Updated ResidenceFormView validation to only check name field
- iOS: Updated PropertyHeaderCard and ResidenceCard to use optional binding
  for address field displays

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 23:03:45 -06:00

580 lines
25 KiB
Kotlin

package com.example.mycrib
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.mycrib.android.ui.screens.AddResidenceScreen
import com.mycrib.android.ui.screens.EditResidenceScreen
import com.mycrib.android.ui.screens.EditTaskScreen
import com.mycrib.android.ui.screens.ForgotPasswordScreen
import com.mycrib.android.ui.screens.HomeScreen
import com.mycrib.android.ui.screens.LoginScreen
import com.mycrib.android.ui.screens.RegisterScreen
import com.mycrib.android.ui.screens.ResetPasswordScreen
import com.mycrib.android.ui.screens.ResidenceDetailScreen
import com.mycrib.android.ui.screens.ResidencesScreen
import com.mycrib.android.ui.screens.TasksScreen
import com.mycrib.android.ui.screens.VerifyEmailScreen
import com.mycrib.android.ui.screens.VerifyResetCodeScreen
import com.mycrib.android.viewmodel.PasswordResetViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import com.mycrib.android.ui.screens.MainScreen
import com.mycrib.android.ui.screens.ProfileScreen
import com.mycrib.android.ui.theme.MyCribTheme
import com.mycrib.navigation.*
import com.mycrib.repository.LookupsRepository
import com.mycrib.shared.models.Residence
import com.mycrib.shared.models.TaskCategory
import com.mycrib.shared.models.TaskDetail
import com.mycrib.shared.models.TaskFrequency
import com.mycrib.shared.models.TaskPriority
import com.mycrib.shared.models.TaskStatus
import com.mycrib.shared.network.ApiResult
import com.mycrib.shared.network.AuthApi
import com.mycrib.storage.TokenStorage
import mycrib.composeapp.generated.resources.Res
import mycrib.composeapp.generated.resources.compose_multiplatform
@Composable
@Preview
fun App(
deepLinkResetToken: String? = null,
onClearDeepLinkToken: () -> Unit = {}
) {
var isLoggedIn by remember { mutableStateOf(TokenStorage.hasToken()) }
var isVerified by remember { mutableStateOf(false) }
var isCheckingAuth by remember { mutableStateOf(true) }
val navController = rememberNavController()
// Check for stored token and verification status on app start
LaunchedEffect(Unit) {
val hasToken = TokenStorage.hasToken()
isLoggedIn = hasToken
if (hasToken) {
// Fetch current user to check verification status
val authApi = AuthApi()
val token = TokenStorage.getToken()
if (token != null) {
when (val result = authApi.getCurrentUser(token)) {
is ApiResult.Success -> {
isVerified = result.data.verified
LookupsRepository.initialize()
}
else -> {
// If fetching user fails, clear token and logout
TokenStorage.clearToken()
isLoggedIn = false
}
}
}
}
isCheckingAuth = false
}
MyCribTheme {
if (isCheckingAuth) {
// Show loading screen while checking auth
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
return@MyCribTheme
}
val startDestination = when {
deepLinkResetToken != null -> ForgotPasswordRoute
!isLoggedIn -> LoginRoute
!isVerified -> VerifyEmailRoute
else -> MainRoute
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
NavHost(
navController = navController,
startDestination = startDestination
) {
composable<LoginRoute> {
LoginScreen(
onLoginSuccess = { user ->
isLoggedIn = true
isVerified = user.verified
// Initialize lookups after successful login
LookupsRepository.initialize()
// Check if user is verified
if (user.verified) {
navController.navigate(MainRoute) {
popUpTo<LoginRoute> { inclusive = true }
}
} else {
navController.navigate(VerifyEmailRoute) {
popUpTo<LoginRoute> { inclusive = true }
}
}
},
onNavigateToRegister = {
navController.navigate(RegisterRoute)
},
onNavigateToForgotPassword = {
navController.navigate(ForgotPasswordRoute)
}
)
}
composable<RegisterRoute> {
RegisterScreen(
onRegisterSuccess = {
isLoggedIn = true
isVerified = false
// Initialize lookups after successful registration
LookupsRepository.initialize()
navController.navigate(VerifyEmailRoute) {
popUpTo<RegisterRoute> { inclusive = true }
}
},
onNavigateBack = {
navController.popBackStack()
}
)
}
composable<ForgotPasswordRoute> { backStackEntry ->
// Create shared ViewModel for all password reset screens
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<ForgotPasswordRoute>()
}
val passwordResetViewModel: PasswordResetViewModel = viewModel(parentEntry) {
PasswordResetViewModel(deepLinkToken = deepLinkResetToken)
}
ForgotPasswordScreen(
onNavigateBack = {
// Clear deep link token when navigating back to login
onClearDeepLinkToken()
navController.popBackStack()
},
onNavigateToVerify = {
navController.navigate(VerifyResetCodeRoute)
},
onNavigateToReset = {
navController.navigate(ResetPasswordRoute)
},
viewModel = passwordResetViewModel
)
}
composable<VerifyResetCodeRoute> { backStackEntry ->
// Use shared ViewModel from ForgotPasswordRoute
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<ForgotPasswordRoute>()
}
val passwordResetViewModel: PasswordResetViewModel = viewModel(parentEntry) { PasswordResetViewModel() }
VerifyResetCodeScreen(
onNavigateBack = {
navController.popBackStack()
},
onNavigateToReset = {
navController.navigate(ResetPasswordRoute)
},
viewModel = passwordResetViewModel
)
}
composable<ResetPasswordRoute> { backStackEntry ->
// Use shared ViewModel from ForgotPasswordRoute
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<ForgotPasswordRoute>()
}
val passwordResetViewModel: PasswordResetViewModel = viewModel(parentEntry) { PasswordResetViewModel() }
ResetPasswordScreen(
onPasswordResetSuccess = {
// Clear deep link token and navigate back to login after successful password reset
onClearDeepLinkToken()
navController.navigate(LoginRoute) {
popUpTo<ForgotPasswordRoute> { inclusive = true }
}
},
onNavigateBack = {
navController.popBackStack()
},
viewModel = passwordResetViewModel
)
}
composable<VerifyEmailRoute> {
VerifyEmailScreen(
onVerifySuccess = {
isVerified = true
navController.navigate(MainRoute) {
popUpTo<VerifyEmailRoute> { inclusive = true }
}
},
onLogout = {
// Clear token and lookups on logout
TokenStorage.clearToken()
LookupsRepository.clear()
isLoggedIn = false
isVerified = false
navController.navigate(LoginRoute) {
popUpTo<VerifyEmailRoute> { inclusive = true }
}
}
)
}
composable<MainRoute> {
MainScreen(
onLogout = {
// Clear token and lookups on logout
TokenStorage.clearToken()
LookupsRepository.clear()
isLoggedIn = false
isVerified = false
navController.navigate(LoginRoute) {
popUpTo<MainRoute> { inclusive = true }
}
},
onResidenceClick = { residenceId ->
navController.navigate(ResidenceDetailRoute(residenceId))
},
onAddResidence = {
navController.navigate(AddResidenceRoute)
},
onAddTask = {
// Tasks are added from within a residence
// Navigate to first residence or show message if no residences exist
// For now, this will be handled by the UI showing "add a property first"
},
onNavigateToEditResidence = { residence ->
navController.navigate(
EditResidenceRoute(
residenceId = residence.id,
name = residence.name,
propertyType = residence.propertyType?.toInt(),
streetAddress = residence.streetAddress,
apartmentUnit = residence.apartmentUnit,
city = residence.city,
stateProvince = residence.stateProvince,
postalCode = residence.postalCode,
country = residence.country,
bedrooms = residence.bedrooms,
bathrooms = residence.bathrooms,
squareFootage = residence.squareFootage,
lotSize = residence.lotSize,
yearBuilt = residence.yearBuilt,
description = residence.description,
isPrimary = residence.isPrimary,
ownerUserName = residence.ownerUsername,
createdAt = residence.createdAt,
updatedAt = residence.updatedAt,
owner = residence.owner
)
)
},
onNavigateToEditTask = { task ->
navController.navigate(
EditTaskRoute(
taskId = task.id,
residenceId = task.residence,
title = task.title,
description = task.description,
categoryId = task.category.id,
categoryName = task.category.name,
frequencyId = task.frequency.id,
frequencyName = task.frequency.name,
priorityId = task.priority.id,
priorityName = task.priority.name,
statusId = task.status?.id,
statusName = task.status?.name,
dueDate = task.dueDate,
estimatedCost = task.estimatedCost?.toString(),
createdAt = task.createdAt,
updatedAt = task.updatedAt
)
)
}
)
}
composable<HomeRoute> {
HomeScreen(
onNavigateToResidences = {
navController.navigate(MainRoute)
},
onNavigateToTasks = {
navController.navigate(TasksRoute)
},
onLogout = {
// Clear token and lookups on logout
TokenStorage.clearToken()
LookupsRepository.clear()
isLoggedIn = false
isVerified = false
navController.navigate(LoginRoute) {
popUpTo<HomeRoute> { inclusive = true }
}
}
)
}
composable<ResidencesRoute> { backStackEntry ->
// Get refresh flag from saved state (set when returning from add/edit)
val shouldRefresh = backStackEntry.savedStateHandle.get<Boolean>("refresh") ?: false
ResidencesScreen(
onResidenceClick = { residenceId ->
navController.navigate(ResidenceDetailRoute(residenceId))
},
onAddResidence = {
navController.navigate(AddResidenceRoute)
},
onNavigateToProfile = {
navController.navigate(ProfileRoute)
},
shouldRefresh = shouldRefresh,
onLogout = {
// Clear token and lookups on logout
TokenStorage.clearToken()
LookupsRepository.clear()
isLoggedIn = false
isVerified = false
navController.navigate(LoginRoute) {
popUpTo<HomeRoute> { inclusive = true }
}
}
)
}
composable<AddResidenceRoute> {
AddResidenceScreen(
onNavigateBack = {
navController.popBackStack()
},
onResidenceCreated = {
// Set refresh flag before navigating back
navController.previousBackStackEntry?.savedStateHandle?.set("refresh", true)
navController.popBackStack()
}
)
}
composable<EditResidenceRoute> { backStackEntry ->
val route = backStackEntry.toRoute<EditResidenceRoute>()
EditResidenceScreen(
residence = Residence(
id = route.residenceId,
name = route.name,
propertyType = route.propertyType.toString(), // Will be fetched from lookups
streetAddress = route.streetAddress,
apartmentUnit = route.apartmentUnit,
city = route.city,
stateProvince = route.stateProvince,
postalCode = route.postalCode,
country = route.country,
bedrooms = route.bedrooms,
bathrooms = route.bathrooms,
squareFootage = route.squareFootage,
lotSize = route.lotSize,
yearBuilt = route.yearBuilt,
description = route.description,
purchaseDate = null,
purchasePrice = null,
isPrimary = route.isPrimary,
ownerUsername = route.ownerUserName,
owner = route.owner,
createdAt = route.createdAt,
updatedAt = route.updatedAt
),
onNavigateBack = {
navController.popBackStack()
},
onResidenceUpdated = {
// Set refresh flag before navigating back
navController.previousBackStackEntry?.savedStateHandle?.set("refresh", true)
navController.popBackStack()
}
)
}
composable<TasksRoute> {
TasksScreen(
onNavigateBack = {
navController.popBackStack()
}
)
}
composable<ResidenceDetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<ResidenceDetailRoute>()
ResidenceDetailScreen(
residenceId = route.residenceId,
onNavigateBack = {
navController.popBackStack()
},
onNavigateToEditResidence = { residence ->
navController.navigate(
EditResidenceRoute(
residenceId = residence.id,
name = residence.name,
propertyType = residence.propertyType?.toInt(),
streetAddress = residence.streetAddress,
apartmentUnit = residence.apartmentUnit,
city = residence.city,
stateProvince = residence.stateProvince,
postalCode = residence.postalCode,
country = residence.country,
bedrooms = residence.bedrooms,
bathrooms = residence.bathrooms,
squareFootage = residence.squareFootage,
lotSize = residence.lotSize,
yearBuilt = residence.yearBuilt,
description = residence.description,
isPrimary = residence.isPrimary,
ownerUserName = residence.ownerUsername,
createdAt = residence.createdAt,
updatedAt = residence.updatedAt,
owner = residence.owner
)
)
},
onNavigateToEditTask = { task ->
navController.navigate(
EditTaskRoute(
taskId = task.id,
residenceId = task.residence,
title = task.title,
description = task.description,
categoryId = task.category.id,
categoryName = task.category.name,
frequencyId = task.frequency.id,
frequencyName = task.frequency.name,
priorityId = task.priority.id,
priorityName = task.priority.name,
statusId = task.status?.id,
statusName = task.status?.name,
dueDate = task.dueDate,
estimatedCost = task.estimatedCost?.toString(),
createdAt = task.createdAt,
updatedAt = task.updatedAt
)
)
}
)
}
composable<EditTaskRoute> { backStackEntry ->
val route = backStackEntry.toRoute<EditTaskRoute>()
EditTaskScreen(
task = TaskDetail(
id = route.taskId,
residence = route.residenceId,
title = route.title,
description = route.description,
category = TaskCategory(route.categoryId, route.categoryName),
frequency = TaskFrequency(
route.frequencyId, route.frequencyName, "", route.frequencyName,
daySpan = 0,
notifyDays = 0
),
priority = TaskPriority(route.priorityId, route.priorityName, displayName = route.statusName ?: ""),
status = route.statusId?.let {
TaskStatus(it, route.statusName ?: "", displayName = route.statusName ?: "")
},
dueDate = route.dueDate,
estimatedCost = route.estimatedCost?.toDoubleOrNull(),
createdAt = route.createdAt,
updatedAt = route.updatedAt,
nextScheduledDate = null,
showCompletedButton = false,
completions = emptyList()
),
onNavigateBack = { navController.popBackStack() },
onTaskUpdated = { navController.popBackStack() }
)
}
composable<ProfileRoute> {
ProfileScreen(
onNavigateBack = {
navController.popBackStack()
},
onLogout = {
// Clear token and lookups on logout
TokenStorage.clearToken()
LookupsRepository.clear()
isLoggedIn = false
isVerified = false
navController.navigate(LoginRoute) {
popUpTo<ProfileRoute> { inclusive = true }
}
}
)
}
}
}
}
/*
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
}
}
*/
}