This commit is contained in:
Trey t
2025-11-04 23:11:18 -06:00
parent 177e588944
commit 025fcf677a
15 changed files with 1382 additions and 473 deletions

View File

@@ -9,7 +9,11 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.mycrib.repository.LookupsRepository
import com.mycrib.shared.models.TaskCategory
import com.mycrib.shared.models.TaskCreateRequest
import com.mycrib.shared.models.TaskFrequency
import com.mycrib.shared.models.TaskPriority
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -20,13 +24,15 @@ fun AddNewTaskDialog(
) {
var title by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
var category by remember { mutableStateOf("") }
var frequency by remember { mutableStateOf("once") }
var intervalDays by remember { mutableStateOf("") }
var priority by remember { mutableStateOf("medium") }
var dueDate by remember { mutableStateOf("") }
var estimatedCost by remember { mutableStateOf("") }
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(id = 0, name = "", displayName = "")) }
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "", displayName = "")) }
var showFrequencyDropdown by remember { mutableStateOf(false) }
var showPriorityDropdown by remember { mutableStateOf(false) }
var showCategoryDropdown by remember { mutableStateOf(false) }
@@ -35,38 +41,23 @@ fun AddNewTaskDialog(
var categoryError by remember { mutableStateOf(false) }
var dueDateError by remember { mutableStateOf(false) }
val frequencies = listOf(
"once" to "One Time",
"daily" to "Daily",
"weekly" to "Weekly",
"biweekly" to "Bi-Weekly",
"monthly" to "Monthly",
"quarterly" to "Quarterly",
"semiannually" to "Semi-Annually",
"annually" to "Annually"
)
// Get data from LookupsRepository
val frequencies by LookupsRepository.taskFrequencies.collectAsState()
val priorities by LookupsRepository.taskPriorities.collectAsState()
val categories by LookupsRepository.taskCategories.collectAsState()
val priorities = listOf(
"low" to "Low",
"medium" to "Medium",
"high" to "High",
"urgent" to "Urgent"
)
// Set defaults when data loads
LaunchedEffect(frequencies) {
if (frequencies.isNotEmpty()) {
frequency = frequencies.first()
}
}
val categories = listOf(
"Plumbing",
"Electrical",
"HVAC",
"Landscaping",
"Painting",
"Roofing",
"Flooring",
"Appliances",
"General Maintenance",
"Cleaning",
"Inspection",
"Other"
)
LaunchedEffect(priorities) {
if (priorities.isNotEmpty()) {
priority = priorities.first()
}
}
AlertDialog(
onDismissRequest = onDismiss,
@@ -110,11 +101,8 @@ fun AddNewTaskDialog(
onExpandedChange = { showCategoryDropdown = it }
) {
OutlinedTextField(
value = category,
onValueChange = {
category = it
categoryError = false
},
value = categories.find { it == category }?.name ?: "",
onValueChange = { },
label = { Text("Category *") },
modifier = Modifier
.fillMaxWidth()
@@ -124,7 +112,8 @@ fun AddNewTaskDialog(
{ Text("Category is required") }
} else null,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showCategoryDropdown) },
readOnly = false
readOnly = false,
enabled = categories.isNotEmpty()
)
ExposedDropdownMenu(
expanded = showCategoryDropdown,
@@ -132,7 +121,7 @@ fun AddNewTaskDialog(
) {
categories.forEach { cat ->
DropdownMenuItem(
text = { Text(cat) },
text = { Text(cat.name) },
onClick = {
category = cat
categoryError = false
@@ -149,27 +138,28 @@ fun AddNewTaskDialog(
onExpandedChange = { showFrequencyDropdown = it }
) {
OutlinedTextField(
value = frequencies.find { it.first == frequency }?.second ?: "One Time",
value = frequencies.find { it == frequency }?.displayName ?: "",
onValueChange = { },
label = { Text("Frequency") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) }
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) },
enabled = frequencies.isNotEmpty()
)
ExposedDropdownMenu(
expanded = showFrequencyDropdown,
onDismissRequest = { showFrequencyDropdown = false }
) {
frequencies.forEach { (key, label) ->
frequencies.forEach { freq ->
DropdownMenuItem(
text = { Text(label) },
text = { Text(freq.displayName) },
onClick = {
frequency = key
frequency = freq
showFrequencyDropdown = false
// Clear interval days if frequency is "once"
if (key == "once") {
if (freq.name == "once") {
intervalDays = ""
}
}
@@ -179,7 +169,7 @@ fun AddNewTaskDialog(
}
// Interval Days (only for recurring tasks)
if (frequency != "once") {
if (frequency.name != "once") {
OutlinedTextField(
value = intervalDays,
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
@@ -215,24 +205,25 @@ fun AddNewTaskDialog(
onExpandedChange = { showPriorityDropdown = it }
) {
OutlinedTextField(
value = priorities.find { it.first == priority }?.second ?: "Medium",
value = priorities.find { it.name == priority.name }?.displayName ?: "",
onValueChange = { },
label = { Text("Priority") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) }
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) },
enabled = priorities.isNotEmpty()
)
ExposedDropdownMenu(
expanded = showPriorityDropdown,
onDismissRequest = { showPriorityDropdown = false }
) {
priorities.forEach { (key, label) ->
priorities.forEach { prio ->
DropdownMenuItem(
text = { Text(label) },
text = { Text(prio.displayName) },
onClick = {
priority = key
priority = prio
showPriorityDropdown = false
}
)
@@ -257,14 +248,12 @@ fun AddNewTaskDialog(
onClick = {
// Validation
var hasError = false
if (title.isBlank()) {
titleError = true
hasError = true
}
if (category.isBlank()) {
categoryError = true
hasError = true
}
if (dueDate.isBlank() || !isValidDateFormat(dueDate)) {
dueDateError = true
hasError = true
@@ -276,10 +265,10 @@ fun AddNewTaskDialog(
residence = residenceId,
title = title,
description = description.ifBlank { null },
category = category,
frequency = frequency,
category = category.id,
frequency = frequency.id,
intervalDays = intervalDays.toIntOrNull(),
priority = priority,
priority = priority.id,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }
)

View File

@@ -13,7 +13,9 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.repository.LookupsRepository
import com.mycrib.shared.models.ResidenceCreateRequest
import com.mycrib.shared.models.ResidenceType
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@@ -24,7 +26,7 @@ fun AddResidenceScreen(
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
var name by remember { mutableStateOf("") }
var propertyType by remember { mutableStateOf("house") }
var propertyType by remember { mutableStateOf(ResidenceType(0, "placeHolder")) }
var streetAddress by remember { mutableStateOf("") }
var apartmentUnit by remember { mutableStateOf("") }
var city by remember { mutableStateOf("") }
@@ -41,6 +43,7 @@ fun AddResidenceScreen(
var expanded by remember { mutableStateOf(false) }
val createState by viewModel.createResidenceState.collectAsState()
val propertyTypes by LookupsRepository.residenceTypes.collectAsState()
// Validation errors
var nameError by remember { mutableStateOf("") }
@@ -60,7 +63,12 @@ fun AddResidenceScreen(
}
}
val propertyTypes = listOf("house", "apartment", "condo", "townhouse", "duplex", "other")
// Set default property type if not set and types are loaded
LaunchedEffect(propertyTypes) {
if (propertyTypes.isNotEmpty()) {
propertyType = propertyTypes.first()
}
}
fun validateForm(): Boolean {
var isValid = true
@@ -146,14 +154,15 @@ fun AddResidenceScreen(
onExpandedChange = { expanded = it }
) {
OutlinedTextField(
value = propertyType.replaceFirstChar { it.uppercase() },
value = propertyTypes.find { it.name == propertyType.name }?.name?.replaceFirstChar { it.uppercase() } ?: "",
onValueChange = {},
readOnly = true,
label = { Text("Property Type *") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
.menuAnchor(),
enabled = propertyTypes.isNotEmpty()
)
ExposedDropdownMenu(
expanded = expanded,
@@ -161,7 +170,7 @@ fun AddResidenceScreen(
) {
propertyTypes.forEach { type ->
DropdownMenuItem(
text = { Text(type.replaceFirstChar { it.uppercase() }) },
text = { Text(type.name.replaceFirstChar { it.uppercase() }) },
onClick = {
propertyType = type
expanded = false
@@ -318,7 +327,7 @@ fun AddResidenceScreen(
viewModel.createResidence(
ResidenceCreateRequest(
name = name,
propertyType = propertyType,
propertyType = propertyType.id,
streetAddress = streetAddress,
apartmentUnit = apartmentUnit.ifBlank { null },
city = city,

View File

@@ -1,10 +1,16 @@
package com.mycrib.android.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.*
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.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -38,69 +44,128 @@ fun LoginScreen(
val isLoading = loginState is ApiResult.Loading
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
Text(
text = "myCrib",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(bottom = 32.dp)
)
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation()
)
if (errorMessage.isNotEmpty()) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(top = 8.dp)
)
}
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
viewModel.login(username, password)
},
modifier = Modifier.fillMaxWidth(),
enabled = username.isNotEmpty() && password.isNotEmpty()
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
// App Logo/Icon
Icon(
Icons.Default.Home,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
} else {
Text("Login")
Text(
text = "myCrib",
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "Manage your properties with ease",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
leadingIcon = {
Icon(Icons.Default.Lock, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp)
)
if (errorMessage.isNotEmpty()) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodySmall
)
}
}
Button(
onClick = {
viewModel.login(username, password)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = username.isNotEmpty() && password.isNotEmpty(),
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Text(
"Sign In",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
TextButton(
onClick = onNavigateToRegister,
modifier = Modifier.fillMaxWidth()
) {
Text(
"Don't have an account? Register",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = onNavigateToRegister) {
Text("Don't have an account? Register")
}
}
}

View File

@@ -1,17 +1,20 @@
package com.mycrib.android.ui.screens
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.ArrowBack
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.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.AuthViewModel
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@@ -39,16 +42,18 @@ fun RegisterScreen(
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Register") },
title = { Text("Create Account", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { paddingValues ->
@@ -56,59 +61,103 @@ fun RegisterScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
.verticalScroll(rememberScrollState())
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Spacer(modifier = Modifier.height(8.dp))
Icon(
Icons.Default.PersonAdd,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "Join myCrib",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Start managing your properties today",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
leadingIcon = {
Icon(Icons.Default.Email, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
leadingIcon = {
Icon(Icons.Default.Lock, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation()
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = confirmPassword,
onValueChange = { confirmPassword = it },
label = { Text("Confirm Password") },
leadingIcon = {
Icon(Icons.Default.Lock, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation()
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp)
)
if (errorMessage.isNotEmpty()) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(top = 8.dp)
)
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
@@ -123,19 +172,29 @@ fun RegisterScreen(
}
}
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = username.isNotEmpty() && email.isNotEmpty() &&
password.isNotEmpty() && !isLoading
password.isNotEmpty() && !isLoading,
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Text("Register")
Text(
"Create Account",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}

View File

@@ -3,13 +3,14 @@ package com.mycrib.android.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.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.ui.components.AddNewTaskDialog
@@ -33,10 +34,10 @@ fun ResidenceDetailScreen(
var residenceState by remember { mutableStateOf<ApiResult<Residence>>(ApiResult.Loading) }
val tasksState by residenceViewModel.residenceTasksState.collectAsState()
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
val taskAddNewTaskState by taskViewModel.taskAddNewTaskState.collectAsState()
var showCompleteDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
var showNewTaskDialog by remember { mutableStateOf(false) }
LaunchedEffect(residenceId) {
@@ -53,7 +54,18 @@ fun ResidenceDetailScreen(
showCompleteDialog = false
selectedTask = null
taskCompletionViewModel.resetCreateState()
// Reload tasks to show updated data
residenceViewModel.loadResidenceTasks(residenceId)
}
else -> {}
}
}
LaunchedEffect(taskAddNewTaskState) {
when (taskAddNewTaskState) {
is ApiResult.Success -> {
showCompleteDialog = false
selectedTask = null
taskCompletionViewModel.resetCreateState()
residenceViewModel.loadResidenceTasks(residenceId)
}
else -> {}
@@ -65,20 +77,16 @@ fun ResidenceDetailScreen(
taskId = selectedTask!!.id,
taskTitle = selectedTask!!.title,
onDismiss = {
showCompleteDialog = false
selectedTask = null
taskCompletionViewModel.resetCreateState()
},
onComplete = { request, images ->
if (images.isNotEmpty()) {
// Use the method that supports images
taskCompletionViewModel.createTaskCompletionWithImages(
request = request,
images = images.map { it.bytes },
imageFileNames = images.map { it.fileName }
)
} else {
// Use the regular method without images
taskCompletionViewModel.createTaskCompletion(request)
}
}
@@ -91,22 +99,33 @@ fun ResidenceDetailScreen(
onDismiss = {
showNewTaskDialog = false
}, onCreate = { request ->
showNewTaskDialog = false
val newTask = taskViewModel.createNewTask(request)
residenceViewModel.loadResidenceTasks(residenceId)
showNewTaskDialog = false
taskViewModel.createNewTask(request)
})
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Residence Details") },
title = { Text("Property Details", fontWeight = FontWeight.Bold) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { showNewTaskDialog = true },
containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(16.dp)
) {
Icon(Icons.Default.Add, contentDescription = "Add Task")
}
}
) { paddingValues ->
when (residenceState) {
@@ -115,7 +134,7 @@ fun ResidenceDetailScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
@@ -125,19 +144,30 @@ fun ResidenceDetailScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.Error,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.error
)
Text(
text = "Error: ${(residenceState as ApiResult.Error).message}",
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
residenceViewModel.getResidence(residenceId) { result ->
residenceState = result
}
}) {
Button(
onClick = {
residenceViewModel.getResidence(residenceId) { result ->
residenceState = result
}
},
shape = RoundedCornerShape(12.dp)
) {
Text("Retry")
}
}
@@ -148,89 +178,103 @@ fun ResidenceDetailScreen(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Property Name
// Property Header Card
item {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = residence.name,
style = MaterialTheme.typography.headlineMedium
)
Text(
text = residence.propertyType.replaceFirstChar { it.uppercase() },
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = residence.name,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}
}
// Address
// Address Card
item {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Address",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = residence.streetAddress)
if (residence.apartmentUnit != null) {
Text(text = "Unit: ${residence.apartmentUnit}")
}
Text(text = "${residence.city}, ${residence.stateProvince} ${residence.postalCode}")
Text(text = residence.country)
InfoCard(
icon = Icons.Default.LocationOn,
title = "Address"
) {
Text(text = residence.streetAddress)
if (residence.apartmentUnit != null) {
Text(text = "Unit: ${residence.apartmentUnit}")
}
Text(text = "${residence.city}, ${residence.stateProvince} ${residence.postalCode}")
Text(text = residence.country)
}
}
// Property Details
item {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Property Details",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
residence.bedrooms?.let {
Text(text = "Bedrooms: $it")
}
residence.bathrooms?.let {
Text(text = "Bathrooms: $it")
// Property Details Card
if (residence.bedrooms != null || residence.bathrooms != null ||
residence.squareFootage != null || residence.yearBuilt != null) {
item {
InfoCard(
icon = Icons.Default.Info,
title = "Property Details"
) {
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(12.dp))
residence.squareFootage?.let {
Text(text = "Square Footage: $it sq ft")
DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft")
}
residence.lotSize?.let {
Text(text = "Lot Size: $it acres")
DetailRow(Icons.Default.Landscape, "Lot Size", "$it acres")
}
residence.yearBuilt?.let {
Text(text = "Year Built: $it")
DetailRow(Icons.Default.CalendarToday, "Year Built", "$it")
}
}
}
}
// Description
// Description Card
if (residence.description != null) {
item {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Description",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = residence.description)
}
InfoCard(
icon = Icons.Default.Description,
title = "Description"
) {
Text(
text = residence.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@@ -238,45 +282,41 @@ fun ResidenceDetailScreen(
// Purchase Information
if (residence.purchaseDate != null || residence.purchasePrice != null) {
item {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Purchase Information",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
residence.purchaseDate?.let {
Text(text = "Purchase Date: $it")
}
residence.purchasePrice?.let {
Text(text = "Purchase Price: $$it")
}
InfoCard(
icon = Icons.Default.AttachMoney,
title = "Purchase Information"
) {
residence.purchaseDate?.let {
DetailRow(Icons.Default.Event, "Purchase Date", it)
}
residence.purchasePrice?.let {
DetailRow(Icons.Default.Payment, "Purchase Price", "$$it")
}
}
}
}
// Tasks Section
// Tasks Section Header
item {
Divider(modifier = Modifier.padding(vertical = 8.dp))
Text(
text = "Tasks",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
Button(
onClick = { showNewTaskDialog = true },
modifier = Modifier.fillMaxWidth()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Add,
Icons.Default.Assignment,
contentDescription = null,
modifier = Modifier.size(18.dp)
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Add New Task")
Text(
text = "Tasks",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
@@ -287,7 +327,7 @@ fun ResidenceDetailScreen(
modifier = Modifier
.fillMaxWidth()
.height(100.dp),
contentAlignment = androidx.compose.ui.Alignment.Center
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
@@ -295,17 +335,53 @@ fun ResidenceDetailScreen(
}
is ApiResult.Error -> {
item {
Text(
text = "Error loading tasks: ${(tasksState as ApiResult.Error).message}",
color = MaterialTheme.colorScheme.error
)
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Error loading tasks: ${(tasksState as ApiResult.Error).message}",
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp)
)
}
}
}
is ApiResult.Success -> {
val taskData = (tasksState as ApiResult.Success).data
if (taskData.tasks.isEmpty()) {
item {
Text("No tasks for this residence yet.")
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.Assignment,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"No tasks yet",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
)
Text(
"Add a task to get started",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
} else {
items(taskData.tasks) { task ->
@@ -326,165 +402,314 @@ fun ResidenceDetailScreen(
}
}
@Composable
private fun InfoCard(
icon: androidx.compose.ui.graphics.vector.ImageVector,
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(4.dp))
content()
}
}
}
@Composable
private fun PropertyDetailItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
value: String,
label: String
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Text(
text = value,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
private fun DetailRow(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
value: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "$label: ",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun TaskCard(
task: TaskDetail,
onCompleteClick: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = task.title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = task.category,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(8.dp)
) {
Text(
text = task.category.name,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
// Priority and status badges
Column(horizontalAlignment = androidx.compose.ui.Alignment.End) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Surface(
color = when (task.priority) {
color = when (task.priority.name.lowercase()) {
"urgent" -> MaterialTheme.colorScheme.error
"high" -> MaterialTheme.colorScheme.errorContainer
"medium" -> MaterialTheme.colorScheme.primaryContainer
else -> MaterialTheme.colorScheme.surfaceVariant
},
shape = MaterialTheme.shapes.small
shape = RoundedCornerShape(10.dp)
) {
Text(
text = task.priority.uppercase(),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall
text = task.priority.name.uppercase(),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(4.dp))
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.small
) {
Text(
text = task.status.uppercase(),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall
)
if (task.status != null) {
Surface(
color = MaterialTheme.colorScheme.tertiaryContainer,
shape = RoundedCornerShape(10.dp)
) {
Text(
text = task.status.name.uppercase(),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}
}
if (task.description != null) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(12.dp))
Text(
text = task.description,
style = MaterialTheme.typography.bodyMedium
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
task.nextScheduledDate?.let {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Next Due Date: $it",
style = MaterialTheme.typography.bodySmall
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.CalendarToday,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = task.nextScheduledDate ?: task.dueDate,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold
)
task.estimatedCost?.let {
Text(
text = "Est. Cost: $$it",
style = MaterialTheme.typography.bodySmall
)
}
}
} ?: run {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Due: ${task.dueDate}",
style = MaterialTheme.typography.bodySmall
)
task.estimatedCost?.let {
task.estimatedCost?.let {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.AttachMoney,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.tertiary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Est. Cost: $$it",
style = MaterialTheme.typography.bodySmall
text = "$$it",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.tertiary
)
}
}
}
// Show completions
if (task.completions.isNotEmpty()) {
Divider(modifier = Modifier.padding(vertical = 8.dp))
Text(
text = "Completions (${task.completions.size})",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(12.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Completions (${task.completions.size})",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary
)
}
task.completions.forEach { completion ->
Spacer(modifier = Modifier.height(8.dp))
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small
Spacer(modifier = Modifier.height(12.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = completion.completionDate.split("T")[0],
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
completion.rating?.let { rating ->
Text(
text = "$rating",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.tertiary
)
Surface(
color = MaterialTheme.colorScheme.tertiaryContainer,
shape = RoundedCornerShape(8.dp)
) {
Text(
text = "$rating",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}
completion.completedByName?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "By: $it",
style = MaterialTheme.typography.bodySmall
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium
)
}
completion.actualCost?.let {
Text(
text = "Cost: $$it",
style = MaterialTheme.typography.bodySmall
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary,
fontWeight = FontWeight.Medium
)
}
completion.notes?.let {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(8.dp))
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
@@ -498,18 +723,23 @@ fun TaskCard(
// Show complete task button based on API logic
if (task.showCompletedButton) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onCompleteClick,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Complete Task")
Text(
"Complete Task",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold
)
}
}
}

View File

@@ -4,17 +4,18 @@ 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.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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.AuthViewModel
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.shared.network.ApiResult
import com.mycrib.shared.network.AuthApi
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -33,16 +34,30 @@ fun ResidencesScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text("Residences") },
title = {
Text(
"My Properties",
fontWeight = FontWeight.Bold
)
},
actions = {
IconButton(onClick = onAddResidence) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
IconButton(onClick = onLogout) {
Icon(Icons.Default.ExitToApp, contentDescription = "Logout")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = onAddResidence,
containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(16.dp)
) {
Icon(Icons.Default.Add, contentDescription = "Add Property")
}
}
) { paddingValues ->
when (myResidencesState) {
@@ -51,7 +66,7 @@ fun ResidencesScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
@@ -61,15 +76,27 @@ fun ResidencesScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
Icons.Default.Error,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.error
)
Text(
text = "Error: ${(myResidencesState as ApiResult.Error).message}",
color = MaterialTheme.colorScheme.error
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { viewModel.loadMyResidences() }) {
Button(
onClick = { viewModel.loadMyResidences() },
shape = RoundedCornerShape(12.dp)
) {
Text("Retry")
}
}
@@ -82,9 +109,29 @@ fun ResidencesScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
contentAlignment = Alignment.Center
) {
Text("No residences yet. Add one to get started!")
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
Icons.Default.Home,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Text(
"No properties yet",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
Text(
"Add your first property to get started!",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
@@ -92,7 +139,7 @@ fun ResidencesScreen(
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Summary Card
item {
@@ -100,129 +147,146 @@ fun ResidencesScreen(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
),
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Summary",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "${response.summary.totalResidences}",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Properties",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Column {
Text(
text = "${response.summary.totalTasks}",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Total Tasks",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Icon(
Icons.Default.Dashboard,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Overview",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceEvenly
) {
Column {
Text(
text = "${response.summary.tasksDueNextWeek}",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Due This Week",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Column {
Text(
text = "${response.summary.tasksDueNextMonth}",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Due This Month",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
StatItem(
icon = Icons.Default.Home,
value = "${response.summary.totalResidences}",
label = "Properties"
)
StatItem(
icon = Icons.Default.Assignment,
value = "${response.summary.totalTasks}",
label = "Total Tasks"
)
}
HorizontalDivider(
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
StatItem(
icon = Icons.Default.CalendarToday,
value = "${response.summary.tasksDueNextWeek}",
label = "Due This Week"
)
StatItem(
icon = Icons.Default.Event,
value = "${response.summary.tasksDueNextMonth}",
label = "Due This Month"
)
}
}
}
}
// Properties Header
item {
Text(
text = "Your Properties",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp)
)
}
// Residences
items(response.residences) { residence ->
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onResidenceClick(residence.id) }
.clickable { onResidenceClick(residence.id) },
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.padding(20.dp)
) {
Text(
text = residence.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "${residence.streetAddress}, ${residence.city}, ${residence.stateProvince}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = residence.propertyType.replaceFirstChar { it.uppercase() },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
// Task Summary
Spacer(modifier = Modifier.height(8.dp))
Divider()
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Tasks: ${residence.taskSummary.total}",
style = MaterialTheme.typography.bodySmall
Column(modifier = Modifier.weight(1f)) {
Text(
text = residence.name,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${residence.streetAddress}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${residence.city}, ${residence.stateProvince}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
TaskStatChip(
icon = Icons.Default.Assignment,
value = "${residence.taskSummary.total}",
label = "Tasks",
color = MaterialTheme.colorScheme.primary
)
Text(
text = "Completed: ${residence.taskSummary.completed}",
style = MaterialTheme.typography.bodySmall,
TaskStatChip(
icon = Icons.Default.CheckCircle,
value = "${residence.taskSummary.completed}",
label = "Done",
color = MaterialTheme.colorScheme.tertiary
)
Text(
text = "Pending: ${residence.taskSummary.pending}",
style = MaterialTheme.typography.bodySmall,
TaskStatChip(
icon = Icons.Default.Schedule,
value = "${residence.taskSummary.pending}",
label = "Pending",
color = MaterialTheme.colorScheme.error
)
}
@@ -235,3 +299,64 @@ fun ResidencesScreen(
}
}
}
@Composable
private fun StatItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
value: String,
label: String
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = value,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
)
}
}
@Composable
private fun TaskStatChip(
icon: androidx.compose.ui.graphics.vector.ImageVector,
value: String,
label: String,
color: androidx.compose.ui.graphics.Color
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = color
)
Text(
text = "$value",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = color
)
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}