This commit is contained in:
Trey t
2025-11-04 10:51:20 -06:00
parent 78c62cfc52
commit de1c7931e9
21 changed files with 1645 additions and 87 deletions

View File

@@ -0,0 +1,112 @@
package com.mycrib.android.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
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.shared.models.TaskCompletionCreateRequest
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CompleteTaskDialog(
taskId: Int,
taskTitle: String,
onDismiss: () -> Unit,
onComplete: (TaskCompletionCreateRequest) -> Unit
) {
var completedByName by remember { mutableStateOf("") }
var actualCost by remember { mutableStateOf("") }
var notes by remember { mutableStateOf("") }
var rating by remember { mutableStateOf(3) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Complete Task: $taskTitle") },
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedTextField(
value = completedByName,
onValueChange = { completedByName = it },
label = { Text("Completed By (optional)") },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Enter name or leave blank if completed by you") }
)
OutlinedTextField(
value = actualCost,
onValueChange = { actualCost = it },
label = { Text("Actual Cost (optional)") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
prefix = { Text("$") }
)
OutlinedTextField(
value = notes,
onValueChange = { notes = it },
label = { Text("Notes (optional)") },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
maxLines = 5
)
Column {
Text("Rating: $rating out of 5")
Slider(
value = rating.toFloat(),
onValueChange = { rating = it.toInt() },
valueRange = 1f..5f,
steps = 3,
modifier = Modifier.fillMaxWidth()
)
}
}
},
confirmButton = {
Button(
onClick = {
// Get current date in ISO format
val currentDate = getCurrentDateTime()
onComplete(
TaskCompletionCreateRequest(
task = taskId,
completedByName = completedByName.ifBlank { null },
completionDate = currentDate,
actualCost = actualCost.ifBlank { null },
notes = notes.ifBlank { null },
rating = rating
)
)
}
) {
Text("Complete")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
// Helper function to get current date/time in ISO format
@OptIn(ExperimentalTime::class)
private fun getCurrentDateTime(): String {
// This is a simplified version - in production you'd use kotlinx.datetime
val now = Clock.System.now()
return now.toString()
}

View File

@@ -0,0 +1,355 @@
package com.mycrib.android.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.shared.models.ResidenceCreateRequest
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddResidenceScreen(
onNavigateBack: () -> Unit,
onResidenceCreated: () -> Unit,
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
var name by remember { mutableStateOf("") }
var propertyType by remember { mutableStateOf("house") }
var streetAddress by remember { mutableStateOf("") }
var apartmentUnit by remember { mutableStateOf("") }
var city by remember { mutableStateOf("") }
var stateProvince by remember { mutableStateOf("") }
var postalCode by remember { mutableStateOf("") }
var country by remember { mutableStateOf("USA") }
var bedrooms by remember { mutableStateOf("") }
var bathrooms by remember { mutableStateOf("") }
var squareFootage by remember { mutableStateOf("") }
var lotSize by remember { mutableStateOf("") }
var yearBuilt by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
var isPrimary by remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
val createState by viewModel.createResidenceState.collectAsState()
// Validation errors
var nameError by remember { mutableStateOf("") }
var streetAddressError by remember { mutableStateOf("") }
var cityError by remember { mutableStateOf("") }
var stateProvinceError by remember { mutableStateOf("") }
var postalCodeError by remember { mutableStateOf("") }
// Handle create state changes
LaunchedEffect(createState) {
when (createState) {
is ApiResult.Success -> {
viewModel.resetCreateState()
onResidenceCreated()
}
else -> {}
}
}
val propertyTypes = listOf("house", "apartment", "condo", "townhouse", "duplex", "other")
fun validateForm(): Boolean {
var isValid = true
if (name.isBlank()) {
nameError = "Name is required"
isValid = false
} else {
nameError = ""
}
if (streetAddress.isBlank()) {
streetAddressError = "Street address is required"
isValid = false
} else {
streetAddressError = ""
}
if (city.isBlank()) {
cityError = "City is required"
isValid = false
} else {
cityError = ""
}
if (stateProvince.isBlank()) {
stateProvinceError = "State/Province is required"
isValid = false
} else {
stateProvinceError = ""
}
if (postalCode.isBlank()) {
postalCodeError = "Postal code is required"
isValid = false
} else {
postalCodeError = ""
}
return isValid
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Add Residence") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Required fields section
Text(
text = "Required Information",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Property Name *") },
modifier = Modifier.fillMaxWidth(),
isError = nameError.isNotEmpty(),
supportingText = if (nameError.isNotEmpty()) {
{ Text(nameError) }
} else null
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it }
) {
OutlinedTextField(
value = propertyType.replaceFirstChar { it.uppercase() },
onValueChange = {},
readOnly = true,
label = { Text("Property Type *") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
propertyTypes.forEach { type ->
DropdownMenuItem(
text = { Text(type.replaceFirstChar { it.uppercase() }) },
onClick = {
propertyType = type
expanded = false
}
)
}
}
}
OutlinedTextField(
value = streetAddress,
onValueChange = { streetAddress = it },
label = { Text("Street Address *") },
modifier = Modifier.fillMaxWidth(),
isError = streetAddressError.isNotEmpty(),
supportingText = if (streetAddressError.isNotEmpty()) {
{ Text(streetAddressError) }
} else null
)
OutlinedTextField(
value = apartmentUnit,
onValueChange = { apartmentUnit = it },
label = { Text("Apartment/Unit #") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = city,
onValueChange = { city = it },
label = { Text("City *") },
modifier = Modifier.fillMaxWidth(),
isError = cityError.isNotEmpty(),
supportingText = if (cityError.isNotEmpty()) {
{ Text(cityError) }
} else null
)
OutlinedTextField(
value = stateProvince,
onValueChange = { stateProvince = it },
label = { Text("State/Province *") },
modifier = Modifier.fillMaxWidth(),
isError = stateProvinceError.isNotEmpty(),
supportingText = if (stateProvinceError.isNotEmpty()) {
{ Text(stateProvinceError) }
} else null
)
OutlinedTextField(
value = postalCode,
onValueChange = { postalCode = it },
label = { Text("Postal Code *") },
modifier = Modifier.fillMaxWidth(),
isError = postalCodeError.isNotEmpty(),
supportingText = if (postalCodeError.isNotEmpty()) {
{ Text(postalCodeError) }
} else null
)
OutlinedTextField(
value = country,
onValueChange = { country = it },
label = { Text("Country") },
modifier = Modifier.fillMaxWidth()
)
// Optional fields section
Divider()
Text(
text = "Optional Details",
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("Bedrooms") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = bathrooms,
onValueChange = { bathrooms = it },
label = { Text("Bathrooms") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.weight(1f)
)
}
OutlinedTextField(
value = squareFootage,
onValueChange = { squareFootage = it.filter { char -> char.isDigit() } },
label = { Text("Square Footage") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = lotSize,
onValueChange = { lotSize = it },
label = { Text("Lot Size (acres)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = yearBuilt,
onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } },
label = { Text("Year Built") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text("Description") },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
maxLines = 5
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Primary Residence")
Switch(
checked = isPrimary,
onCheckedChange = { isPrimary = it }
)
}
// Error message
if (createState is ApiResult.Error) {
Text(
text = (createState as ApiResult.Error).message,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
// Submit button
Button(
onClick = {
if (validateForm()) {
viewModel.createResidence(
ResidenceCreateRequest(
name = name,
propertyType = propertyType,
streetAddress = streetAddress,
apartmentUnit = apartmentUnit.ifBlank { null },
city = city,
stateProvince = stateProvince,
postalCode = postalCode,
country = country,
bedrooms = bedrooms.toIntOrNull(),
bathrooms = bathrooms.toFloatOrNull(),
squareFootage = squareFootage.toIntOrNull(),
lotSize = lotSize.toFloatOrNull(),
yearBuilt = yearBuilt.toIntOrNull(),
description = description.ifBlank { null },
isPrimary = isPrimary
)
)
}
},
modifier = Modifier.fillMaxWidth(),
enabled = createState !is ApiResult.Loading
) {
if (createState is ApiResult.Loading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text("Create Residence")
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}

View File

@@ -5,18 +5,28 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
onNavigateToResidences: () -> Unit,
onNavigateToTasks: () -> Unit,
onLogout: () -> Unit
onLogout: () -> Unit,
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
val summaryState by viewModel.residenceSummaryState.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadResidenceSummary()
}
Scaffold(
topBar = {
TopAppBar(
@@ -36,6 +46,89 @@ fun HomeScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Summary Card
when (summaryState) {
is ApiResult.Success -> {
val summary = (summaryState as ApiResult.Success).data
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "Overview",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "${summary.residences.size}",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Properties",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "${summary.summary.totalTasks}",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Total Tasks",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "${summary.summary.totalPending}",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Pending",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}
}
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
}
}
// Residences Card
Card(
modifier = Modifier
.fillMaxWidth()
@@ -66,6 +159,7 @@ fun HomeScreen(
}
}
// Tasks Card
Card(
modifier = Modifier
.fillMaxWidth()

View File

@@ -1,19 +1,75 @@
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.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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.ui.components.CompleteTaskDialog
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.android.viewmodel.TaskCompletionViewModel
import com.mycrib.shared.models.Residence
import com.mycrib.shared.models.TaskDetail
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ResidenceDetailScreen(
residenceId: Int,
onNavigateBack: () -> Unit
onNavigateBack: () -> Unit,
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() }
) {
var residenceState by remember { mutableStateOf<ApiResult<Residence>>(ApiResult.Loading) }
val tasksState by residenceViewModel.residenceTasksState.collectAsState()
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
var showCompleteDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
LaunchedEffect(residenceId) {
residenceViewModel.getResidence(residenceId) { result ->
residenceState = result
}
residenceViewModel.loadResidenceTasks(residenceId)
}
// Handle completion success
LaunchedEffect(completionState) {
when (completionState) {
is ApiResult.Success -> {
showCompleteDialog = false
selectedTask = null
taskCompletionViewModel.resetCreateState()
// Reload tasks to show updated data
residenceViewModel.loadResidenceTasks(residenceId)
}
else -> {}
}
}
if (showCompleteDialog && selectedTask != null) {
CompleteTaskDialog(
taskId = selectedTask!!.id,
taskTitle = selectedTask!!.title,
onDismiss = {
showCompleteDialog = false
selectedTask = null
taskCompletionViewModel.resetCreateState()
},
onComplete = { request ->
taskCompletionViewModel.createTaskCompletion(request)
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -26,14 +82,374 @@ fun ResidenceDetailScreen(
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
Text("Residence ID: $residenceId")
Text("Details coming soon!")
when (residenceState) {
is ApiResult.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator()
}
}
is ApiResult.Error -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
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
}
}) {
Text("Retry")
}
}
}
}
is ApiResult.Success -> {
val residence = (residenceState as ApiResult.Success).data
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Property Name
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
)
}
}
}
// Address
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)
}
}
}
// 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")
}
residence.squareFootage?.let {
Text(text = "Square Footage: $it sq ft")
}
residence.lotSize?.let {
Text(text = "Lot Size: $it acres")
}
residence.yearBuilt?.let {
Text(text = "Year Built: $it")
}
}
}
}
// Description
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)
}
}
}
}
// 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")
}
}
}
}
}
// Tasks Section
item {
Divider(modifier = Modifier.padding(vertical = 8.dp))
Text(
text = "Tasks",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
}
when (tasksState) {
is ApiResult.Loading -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator()
}
}
}
is ApiResult.Error -> {
item {
Text(
text = "Error loading tasks: ${(tasksState as ApiResult.Error).message}",
color = MaterialTheme.colorScheme.error
)
}
}
is ApiResult.Success -> {
val taskData = (tasksState as ApiResult.Success).data
if (taskData.tasks.isEmpty()) {
item {
Text("No tasks for this residence yet.")
}
} else {
items(taskData.tasks) { task ->
TaskCard(
task = task,
onCompleteClick = {
selectedTask = task
showCompleteDialog = true
}
)
}
}
}
}
}
}
}
}
}
@Composable
fun TaskCard(
task: TaskDetail,
onCompleteClick: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
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
)
}
// Priority and status badges
Column(horizontalAlignment = androidx.compose.ui.Alignment.End) {
Surface(
color = when (task.priority) {
"urgent" -> MaterialTheme.colorScheme.error
"high" -> MaterialTheme.colorScheme.errorContainer
"medium" -> MaterialTheme.colorScheme.primaryContainer
else -> MaterialTheme.colorScheme.surfaceVariant
},
shape = MaterialTheme.shapes.small
) {
Text(
text = task.priority.uppercase(),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall
)
}
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.description != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = task.description,
style = MaterialTheme.typography.bodyMedium
)
}
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 {
Text(
text = "Est. Cost: $$it",
style = MaterialTheme.typography.bodySmall
)
}
}
// 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
)
task.completions.forEach { completion ->
Spacer(modifier = Modifier.height(8.dp))
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = completion.completionDate.split("T")[0],
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
completion.rating?.let { rating ->
Text(
text = "$rating",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.tertiary
)
}
}
completion.completedByName?.let {
Text(
text = "By: $it",
style = MaterialTheme.typography.bodySmall
)
}
completion.actualCost?.let {
Text(
text = "Cost: $$it",
style = MaterialTheme.typography.bodySmall
)
}
completion.notes?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
// Complete task button
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onCompleteClick,
modifier = Modifier.fillMaxWidth()
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Complete Task")
}
}
}
}

View File

@@ -10,69 +10,113 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ResidencesScreen(
onNavigateBack: () -> Unit,
onResidenceClick: (Int) -> Unit
onResidenceClick: (Int) -> Unit,
onAddResidence: () -> Unit,
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
// TODO: Load residences from API
val residences = remember { emptyList<String>() }
val residencesState by viewModel.residencesState.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadResidences()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Residences") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* TODO: Add residence */ }) {
IconButton(onClick = onAddResidence) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
)
}
) { paddingValues ->
if (residences.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Text("No residences yet. Add one to get started!")
when (residencesState) {
is ApiResult.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator()
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(residences) { residence ->
Card(
is ApiResult.Error -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
Text(
text = "Error: ${(residencesState as ApiResult.Error).message}",
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { viewModel.loadResidences() }) {
Text("Retry")
}
}
}
}
is ApiResult.Success -> {
val residences = (residencesState as ApiResult.Success).data
if (residences.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.clickable { onResidenceClick(0) }
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "Residence Name",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "Address",
style = MaterialTheme.typography.bodyMedium
)
Text("No residences yet. Add one to get started!")
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(residences) { residence ->
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onResidenceClick(residence.id) }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = residence.name ?: "Unnamed Property",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "${residence.streetAddress}, ${residence.city}, ${residence.stateProvince}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (residence.propertyType != null) {
Text(
text = residence.propertyType.replaceFirstChar { it.uppercase() },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
}

View File

@@ -1,18 +1,30 @@
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.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.TaskViewModel
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TasksScreen(
onNavigateBack: () -> Unit
onNavigateBack: () -> Unit,
viewModel: TaskViewModel = viewModel { TaskViewModel() }
) {
val tasksState by viewModel.tasksState.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadTasks()
}
Scaffold(
topBar = {
TopAppBar(
@@ -21,17 +33,132 @@ fun TasksScreen(
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* TODO: Add task */ }) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Text("Tasks coming soon!")
when (tasksState) {
is ApiResult.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator()
}
}
is ApiResult.Error -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
Text(
text = "Error: ${(tasksState as ApiResult.Error).message}",
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { viewModel.loadTasks() }) {
Text("Retry")
}
}
}
}
is ApiResult.Success -> {
val tasks = (tasksState as ApiResult.Success).data
if (tasks.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Text("No tasks yet. Add one to get started!")
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(tasks) { task ->
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = task.title,
style = MaterialTheme.typography.titleMedium
)
// Priority badge
Surface(
color = when (task.priority) {
"urgent" -> MaterialTheme.colorScheme.error
"high" -> MaterialTheme.colorScheme.errorContainer
"medium" -> MaterialTheme.colorScheme.primaryContainer
else -> MaterialTheme.colorScheme.surfaceVariant
},
shape = MaterialTheme.shapes.small
) {
Text(
text = task.priority?.uppercase() ?: "LOW",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall
)
}
}
Spacer(modifier = Modifier.height(4.dp))
if (task.description != null) {
Text(
text = task.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Status: ${task.status?.replaceFirstChar { it.uppercase() }}",
style = MaterialTheme.typography.bodySmall
)
if (task.dueDate != null) {
Text(
text = "Due: ${task.dueDate}",
style = MaterialTheme.typography.bodySmall,
color = if (task.isOverdue == true)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}
}
}
}