This commit is contained in:
Trey t
2025-11-07 12:21:48 -06:00
parent 66fe773398
commit 1b777049a8
27 changed files with 2003 additions and 718 deletions

View File

@@ -3,11 +3,13 @@ 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
@@ -32,9 +34,18 @@ 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
@@ -55,12 +66,12 @@ fun App() {
if (hasToken) {
// Fetch current user to check verification status
val authApi = com.mycrib.shared.network.AuthApi()
val authApi = AuthApi()
val token = TokenStorage.getToken()
if (token != null) {
when (val result = authApi.getCurrentUser(token)) {
is com.mycrib.shared.network.ApiResult.Success -> {
is ApiResult.Success -> {
isVerified = result.data.verified
LookupsRepository.initialize()
}
@@ -76,33 +87,34 @@ fun App() {
isCheckingAuth = false
}
if (isCheckingAuth) {
// Show loading screen while checking auth
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 {
!isLoggedIn -> LoginRoute
!isVerified -> VerifyEmailRoute
else -> MainRoute
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
androidx.compose.foundation.layout.Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
androidx.compose.material3.CircularProgressIndicator()
}
}
return
}
val startDestination = when {
!isLoggedIn -> LoginRoute
!isVerified -> VerifyEmailRoute
else -> MainRoute
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
NavHost(
NavHost(
navController = navController,
startDestination = startDestination
) {
@@ -187,6 +199,11 @@ fun App() {
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(
@@ -399,16 +416,20 @@ fun App() {
composable<EditTaskRoute> { backStackEntry ->
val route = backStackEntry.toRoute<EditTaskRoute>()
EditTaskScreen(
task = com.mycrib.shared.models.TaskDetail(
task = TaskDetail(
id = route.taskId,
residence = route.residenceId,
title = route.title,
description = route.description,
category = com.mycrib.shared.models.TaskCategory(route.categoryId, route.categoryName),
frequency = com.mycrib.shared.models.TaskFrequency(route.frequencyId, route.frequencyName, ""),
priority = com.mycrib.shared.models.TaskPriority(route.priorityId, route.priorityName, displayName = route.statusName ?: ""),
category = TaskCategory(route.categoryId, route.categoryName),
frequency = TaskFrequency(
route.frequencyId, route.frequencyName, "",
daySpan = 0,
notifyDays = 0
),
priority = TaskPriority(route.priorityId, route.priorityName, displayName = route.statusName ?: ""),
status = route.statusId?.let {
com.mycrib.shared.models.TaskStatus(it, route.statusName ?: "", displayName = route.statusName ?: "")
TaskStatus(it, route.statusName ?: "", displayName = route.statusName ?: "")
},
dueDate = route.dueDate,
estimatedCost = route.estimatedCost,
@@ -426,7 +447,7 @@ fun App() {
}
composable<ProfileRoute> {
com.mycrib.android.ui.screens.ProfileScreen(
ProfileScreen(
onNavigateBack = {
navController.popBackStack()
},
@@ -443,6 +464,7 @@ fun App() {
)
}
}
}
}

View File

@@ -13,11 +13,12 @@ data class CustomTask (
val description: String? = null,
val category: String,
val priority: String,
val status: String,
val status: String? = null,
@SerialName("due_date") val dueDate: String,
@SerialName("estimated_cost") val estimatedCost: String? = null,
@SerialName("actual_cost") val actualCost: String? = null,
val notes: String? = null,
val archived: Boolean = false,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String,
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
@@ -43,7 +44,6 @@ data class TaskCreateRequest(
val frequency: Int,
@SerialName("interval_days") val intervalDays: Int? = null,
val priority: Int,
val status: Int,
@SerialName("due_date") val dueDate: String,
@SerialName("estimated_cost") val estimatedCost: String? = null
)
@@ -64,6 +64,7 @@ data class TaskDetail(
@SerialName("estimated_cost") val estimatedCost: String? = null,
@SerialName("actual_cost") val actualCost: String? = null,
val notes: String? = null,
val archived: Boolean = false,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String,
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
@@ -78,14 +79,16 @@ data class TasksByResidenceResponse(
val summary: CategorizedTaskSummary,
@SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
@SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
@SerialName("done_tasks") val doneTasks: List<TaskDetail>
@SerialName("done_tasks") val doneTasks: List<TaskDetail>,
@SerialName("archived_tasks") val archivedTasks: List<TaskDetail>
)
@Serializable
data class CategorizedTaskSummary(
val upcoming: Int,
@SerialName("in_progress") val inProgress: Int,
val done: Int
val done: Int,
val archived: Int
)
@Serializable
@@ -94,7 +97,8 @@ data class AllTasksResponse(
val summary: CategorizedTaskSummary,
@SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
@SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
@SerialName("done_tasks") val doneTasks: List<TaskDetail>
@SerialName("done_tasks") val doneTasks: List<TaskDetail>,
@SerialName("archived_tasks") val archivedTasks: List<TaskDetail>
)
@Serializable

View File

@@ -27,6 +27,8 @@ data class TaskFrequency(
val id: Int,
val name: String,
@SerialName("display_name") val displayName: String,
@SerialName("day_span") val daySpan: Int? = null,
@SerialName("notify_days") val notifyDays: Int? = null
)
@Serializable

View File

@@ -10,9 +10,20 @@ expect fun getLocalhostAddress(): String
expect fun createHttpClient(): HttpClient
object ApiClient {
private val BASE_URL = "http://${getLocalhostAddress()}:8000/api"
val httpClient = createHttpClient()
fun getBaseUrl() = BASE_URL
/**
* Get the current base URL based on environment configuration.
* To change environment, update ApiConfig.CURRENT_ENV
*/
fun getBaseUrl(): String = ApiConfig.getBaseUrl()
/**
* Print current environment configuration
*/
init {
println("🌐 API Client initialized")
println("📍 Environment: ${ApiConfig.getEnvironmentName()}")
println("🔗 Base URL: ${getBaseUrl()}")
}
}

View File

@@ -0,0 +1,38 @@
package com.mycrib.shared.network
/**
* API Environment Configuration
*
* To switch between localhost and dev server, simply change the CURRENT_ENV value:
* - Environment.LOCAL for local development
* - Environment.DEV for remote dev server
*/
object ApiConfig {
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
val CURRENT_ENV = Environment.LOCAL
enum class Environment {
LOCAL,
DEV
}
/**
* Get the base URL based on current environment and platform
*/
fun getBaseUrl(): String {
return when (CURRENT_ENV) {
Environment.LOCAL -> "http://${getLocalhostAddress()}:8000/api"
Environment.DEV -> "https://mycrib.treytartt.com/api"
}
}
/**
* Get environment name for logging
*/
fun getEnvironmentName(): String {
return when (CURRENT_ENV) {
Environment.LOCAL -> "Local (${getLocalhostAddress()}:8000)"
Environment.DEV -> "Dev Server (mycrib.treytartt.com)"
}
}
}

View File

@@ -30,7 +30,11 @@ fun AddNewTaskDialog(
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(id = 0, name = "", displayName = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(
id = 0, name = "", displayName = "",
daySpan = 0,
notifyDays = 0
)) }
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "", displayName = "")) }
var showFrequencyDropdown by remember { mutableStateOf(false) }
@@ -270,8 +274,7 @@ fun AddNewTaskDialog(
intervalDays = intervalDays.toIntOrNull(),
priority = priority.id,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null },
status = 9
estimatedCost = estimatedCost.ifBlank { null }
)
)
}

View File

@@ -0,0 +1,364 @@
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.repository.LookupsRepository
import com.mycrib.shared.models.MyResidencesResponse
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
fun AddNewTaskWithResidenceDialog(
residencesResponse: MyResidencesResponse,
onDismiss: () -> Unit,
onCreate: (TaskCreateRequest) -> Unit,
isLoading: Boolean = false,
errorMessage: String? = null
) {
var title by remember { mutableStateOf("") }
var description by remember { mutableStateOf("") }
var intervalDays by remember { mutableStateOf("") }
var dueDate by remember { mutableStateOf("") }
var estimatedCost by remember { mutableStateOf("") }
var selectedResidenceId by remember { mutableStateOf(residencesResponse.residences.firstOrNull()?.id ?: 0) }
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(
id = 0, name = "", displayName = "",
daySpan = 0,
notifyDays = 0
)) }
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "", displayName = "")) }
var showResidenceDropdown by remember { mutableStateOf(false) }
var showFrequencyDropdown by remember { mutableStateOf(false) }
var showPriorityDropdown by remember { mutableStateOf(false) }
var showCategoryDropdown by remember { mutableStateOf(false) }
var titleError by remember { mutableStateOf(false) }
var categoryError by remember { mutableStateOf(false) }
var dueDateError by remember { mutableStateOf(false) }
var residenceError by remember { mutableStateOf(false) }
// Get data from LookupsRepository
val frequencies by LookupsRepository.taskFrequencies.collectAsState()
val priorities by LookupsRepository.taskPriorities.collectAsState()
val categories by LookupsRepository.taskCategories.collectAsState()
// Set defaults when data loads
LaunchedEffect(frequencies) {
if (frequencies.isNotEmpty()) {
frequency = frequencies.first()
}
}
LaunchedEffect(priorities) {
if (priorities.isNotEmpty()) {
priority = priorities.first()
}
}
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add New Task") },
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Residence Selector
ExposedDropdownMenuBox(
expanded = showResidenceDropdown,
onExpandedChange = { showResidenceDropdown = it }
) {
OutlinedTextField(
value = residencesResponse.residences.find { it.id == selectedResidenceId }?.name ?: "",
onValueChange = { },
label = { Text("Property *") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
isError = residenceError,
supportingText = if (residenceError) {
{ Text("Property is required") }
} else null,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showResidenceDropdown) },
readOnly = true,
enabled = residencesResponse.residences.isNotEmpty()
)
ExposedDropdownMenu(
expanded = showResidenceDropdown,
onDismissRequest = { showResidenceDropdown = false }
) {
residencesResponse.residences.forEach { residence ->
DropdownMenuItem(
text = { Text(residence.name) },
onClick = {
selectedResidenceId = residence.id
residenceError = false
showResidenceDropdown = false
}
)
}
}
}
// Title
OutlinedTextField(
value = title,
onValueChange = {
title = it
titleError = false
},
label = { Text("Title *") },
modifier = Modifier.fillMaxWidth(),
isError = titleError,
supportingText = if (titleError) {
{ Text("Title is required") }
} else null,
singleLine = true
)
// Description
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text("Description") },
modifier = Modifier.fillMaxWidth(),
minLines = 2,
maxLines = 4
)
// Category
ExposedDropdownMenuBox(
expanded = showCategoryDropdown,
onExpandedChange = { showCategoryDropdown = it }
) {
OutlinedTextField(
value = categories.find { it == category }?.name ?: "",
onValueChange = { },
label = { Text("Category *") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
isError = categoryError,
supportingText = if (categoryError) {
{ Text("Category is required") }
} else null,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showCategoryDropdown) },
readOnly = false,
enabled = categories.isNotEmpty()
)
ExposedDropdownMenu(
expanded = showCategoryDropdown,
onDismissRequest = { showCategoryDropdown = false }
) {
categories.forEach { cat ->
DropdownMenuItem(
text = { Text(cat.name) },
onClick = {
category = cat
categoryError = false
showCategoryDropdown = false
}
)
}
}
}
// Frequency
ExposedDropdownMenuBox(
expanded = showFrequencyDropdown,
onExpandedChange = { showFrequencyDropdown = it }
) {
OutlinedTextField(
value = frequencies.find { it == frequency }?.displayName ?: "",
onValueChange = { },
label = { Text("Frequency") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) },
enabled = frequencies.isNotEmpty()
)
ExposedDropdownMenu(
expanded = showFrequencyDropdown,
onDismissRequest = { showFrequencyDropdown = false }
) {
frequencies.forEach { freq ->
DropdownMenuItem(
text = { Text(freq.displayName) },
onClick = {
frequency = freq
showFrequencyDropdown = false
// Clear interval days if frequency is "once"
if (freq.name == "once") {
intervalDays = ""
}
}
)
}
}
}
// Interval Days (only for recurring tasks)
if (frequency.name != "once") {
OutlinedTextField(
value = intervalDays,
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
label = { Text("Interval Days (optional)") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
supportingText = { Text("Override default frequency interval") },
singleLine = true
)
}
// Due Date
OutlinedTextField(
value = dueDate,
onValueChange = {
dueDate = it
dueDateError = false
},
label = { Text("Due Date (YYYY-MM-DD) *") },
modifier = Modifier.fillMaxWidth(),
isError = dueDateError,
supportingText = if (dueDateError) {
{ Text("Due date is required (format: YYYY-MM-DD)") }
} else {
{ Text("Format: YYYY-MM-DD") }
},
singleLine = true
)
// Priority
ExposedDropdownMenuBox(
expanded = showPriorityDropdown,
onExpandedChange = { showPriorityDropdown = it }
) {
OutlinedTextField(
value = priorities.find { it.name == priority.name }?.displayName ?: "",
onValueChange = { },
label = { Text("Priority") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) },
enabled = priorities.isNotEmpty()
)
ExposedDropdownMenu(
expanded = showPriorityDropdown,
onDismissRequest = { showPriorityDropdown = false }
) {
priorities.forEach { prio ->
DropdownMenuItem(
text = { Text(prio.displayName) },
onClick = {
priority = prio
showPriorityDropdown = false
}
)
}
}
}
// Estimated Cost
OutlinedTextField(
value = estimatedCost,
onValueChange = { estimatedCost = it },
label = { Text("Estimated Cost") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
prefix = { Text("$") },
singleLine = true
)
// Error message display
if (errorMessage != null) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp)
)
}
}
},
confirmButton = {
Button(
onClick = {
// Validation
var hasError = false
if (selectedResidenceId == 0) {
residenceError = true
hasError = true
}
if (title.isBlank()) {
titleError = true
hasError = true
}
if (dueDate.isBlank() || !isValidDateFormat(dueDate)) {
dueDateError = true
hasError = true
}
if (!hasError) {
onCreate(
TaskCreateRequest(
residence = selectedResidenceId,
title = title,
description = description.ifBlank { null },
category = category.id,
frequency = frequency.id,
intervalDays = intervalDays.toIntOrNull(),
priority = priority.id,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }
)
)
}
},
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text("Create Task")
}
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
// Helper function to validate date format
private fun isValidDateFormat(date: String): Boolean {
val datePattern = Regex("^\\d{4}-\\d{2}-\\d{2}$")
return datePattern.matches(date)
}

View File

@@ -372,7 +372,11 @@ fun TaskCardPreview() {
description = "Remove all debris from gutters and downspouts",
category = TaskCategory(id = 1, name = "maintenance", description = ""),
priority = TaskPriority(id = 2, name = "medium", displayName = "Medium", description = ""),
frequency = TaskFrequency(id = 1, name = "monthly", displayName = "Monthly"),
frequency = TaskFrequency(
id = 1, name = "monthly", displayName = "Monthly",
daySpan = 0,
notifyDays = 0
),
status = TaskStatus(id = 1, name = "pending", displayName = "Pending", description = ""),
dueDate = "2024-12-15",
estimatedCost = "150.00",

View File

@@ -0,0 +1,211 @@
package com.mycrib.android.ui.components.task
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mycrib.shared.models.AllTasksResponse
import com.mycrib.shared.models.TaskDetail
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TaskKanbanView(
upcomingTasks: List<TaskDetail>,
inProgressTasks: List<TaskDetail>,
doneTasks: List<TaskDetail>,
archivedTasks: List<TaskDetail>,
onCompleteTask: (TaskDetail) -> Unit,
onEditTask: (TaskDetail) -> Unit,
onCancelTask: ((TaskDetail) -> Unit)?,
onUncancelTask: ((TaskDetail) -> Unit)?,
onMarkInProgress: ((TaskDetail) -> Unit)?
) {
val pagerState = rememberPagerState(pageCount = { 4 })
Column(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
pageSpacing = 16.dp,
contentPadding = PaddingValues(horizontal = 16.dp)
) { page ->
when (page) {
0 -> TaskColumn(
title = "Upcoming",
icon = Icons.Default.CalendarToday,
color = MaterialTheme.colorScheme.primary,
count = upcomingTasks.size,
tasks = upcomingTasks,
onCompleteTask = onCompleteTask,
onEditTask = onEditTask,
onCancelTask = onCancelTask,
onUncancelTask = onUncancelTask,
onMarkInProgress = onMarkInProgress
)
1 -> TaskColumn(
title = "In Progress",
icon = Icons.Default.PlayCircle,
color = MaterialTheme.colorScheme.tertiary,
count = inProgressTasks.size,
tasks = inProgressTasks,
onCompleteTask = onCompleteTask,
onEditTask = onEditTask,
onCancelTask = onCancelTask,
onUncancelTask = onUncancelTask,
onMarkInProgress = null
)
2 -> TaskColumn(
title = "Done",
icon = Icons.Default.CheckCircle,
color = MaterialTheme.colorScheme.secondary,
count = doneTasks.size,
tasks = doneTasks,
onCompleteTask = null,
onEditTask = onEditTask,
onCancelTask = null,
onUncancelTask = null,
onMarkInProgress = null
)
3 -> TaskColumn(
title = "Archived",
icon = Icons.Default.Archive,
color = MaterialTheme.colorScheme.outline,
count = archivedTasks.size,
tasks = archivedTasks,
onCompleteTask = null,
onEditTask = onEditTask,
onCancelTask = null,
onUncancelTask = null,
onMarkInProgress = null
)
}
}
}
}
@Composable
private fun TaskColumn(
title: String,
icon: ImageVector,
color: Color,
count: Int,
tasks: List<TaskDetail>,
onCompleteTask: ((TaskDetail) -> Unit)?,
onEditTask: (TaskDetail) -> Unit,
onCancelTask: ((TaskDetail) -> Unit)?,
onUncancelTask: ((TaskDetail) -> Unit)?,
onMarkInProgress: ((TaskDetail) -> Unit)?
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(12.dp)
)
) {
// Header
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color,
modifier = Modifier.size(24.dp)
)
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = color
)
}
Surface(
color = color,
shape = CircleShape
) {
Text(
text = count.toString(),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.surface
)
}
}
// Tasks List
if (tasks.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color.copy(alpha = 0.3f),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "No tasks",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(tasks, key = { it.id }) { task ->
TaskCard(
task = task,
onCompleteClick = if (onCompleteTask != null) {
{ onCompleteTask(task) }
} else null,
onEditClick = { onEditTask(task) },
onCancelClick = if (onCancelTask != null) {
{ onCancelTask(task) }
} else null,
onUncancelClick = if (onUncancelTask != null) {
{ onUncancelTask(task) }
} else null,
onMarkInProgressClick = if (onMarkInProgress != null) {
{ onMarkInProgress(task) }
} else null
)
}
}
}
}
}

View File

@@ -12,8 +12,11 @@ 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.AddNewTaskWithResidenceDialog
import com.mycrib.android.ui.components.CompleteTaskDialog
import com.mycrib.android.ui.components.task.TaskCard
import com.mycrib.android.ui.components.task.TaskKanbanView
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.android.viewmodel.TaskCompletionViewModel
import com.mycrib.android.viewmodel.TaskViewModel
import com.mycrib.shared.models.TaskDetail
@@ -23,18 +26,23 @@ import com.mycrib.shared.network.ApiResult
@Composable
fun AllTasksScreen(
onNavigateToEditTask: (TaskDetail) -> Unit,
onAddTask: () -> Unit = {},
viewModel: TaskViewModel = viewModel { TaskViewModel() },
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() }
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() },
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
val tasksState by viewModel.tasksState.collectAsState()
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
var showInProgressTasks by remember { mutableStateOf(false) }
var showDoneTasks by remember { mutableStateOf(false) }
val myResidencesState by residenceViewModel.myResidencesState.collectAsState()
val createTaskState by viewModel.taskAddNewCustomTaskState.collectAsState()
var showCompleteDialog by remember { mutableStateOf(false) }
var showNewTaskDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
LaunchedEffect(Unit) {
viewModel.loadTasks()
residenceViewModel.loadMyResidences()
}
// Handle completion success
@@ -50,6 +58,28 @@ fun AllTasksScreen(
}
}
// Handle task creation success
LaunchedEffect(createTaskState) {
println("AllTasksScreen: createTaskState changed to $createTaskState")
when (createTaskState) {
is ApiResult.Success -> {
println("AllTasksScreen: Task created successfully, closing dialog and reloading tasks")
showNewTaskDialog = false
viewModel.resetAddTaskState()
viewModel.loadTasks()
}
is ApiResult.Error -> {
println("AllTasksScreen: Task creation error: ${(createTaskState as ApiResult.Error).message}")
}
is ApiResult.Loading -> {
println("AllTasksScreen: Task creation loading")
}
else -> {
println("AllTasksScreen: Task creation idle")
}
}
}
Scaffold(
topBar = {
TopAppBar(
@@ -59,6 +89,18 @@ fun AllTasksScreen(
fontWeight = FontWeight.Bold
)
},
actions = {
IconButton(
onClick = { showNewTaskDialog = true },
enabled = myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
) {
Icon(
Icons.Default.Add,
contentDescription = "Add Task"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
@@ -109,7 +151,8 @@ fun AllTasksScreen(
val taskData = (tasksState as ApiResult.Success).data
val hasNoTasks = taskData.upcomingTasks.isEmpty() &&
taskData.inProgressTasks.isEmpty() &&
taskData.doneTasks.isEmpty()
taskData.doneTasks.isEmpty() &&
taskData.archivedTasks.isEmpty()
if (hasNoTasks) {
Box(
@@ -120,211 +163,92 @@ fun AllTasksScreen(
) {
Column(
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(24.dp)
) {
Icon(
Icons.Default.CheckCircle,
Icons.Default.Assignment,
contentDescription = null,
modifier = Modifier.size(64.dp),
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Text(
"No tasks yet",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
Text(
"Add a task to a residence to get started",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
"Create your first task to get started",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 96.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Task summary pills
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { showNewTaskDialog = true },
modifier = Modifier
.fillMaxWidth(0.7f)
.height(56.dp),
enabled = myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
) {
TaskSummaryPill(
count = taskData.summary.upcoming,
label = "Upcoming",
color = MaterialTheme.colorScheme.primary
)
TaskSummaryPill(
count = taskData.summary.inProgress,
label = "In Progress",
color = MaterialTheme.colorScheme.tertiary
)
TaskSummaryPill(
count = taskData.summary.done,
label = "Done",
color = MaterialTheme.colorScheme.secondary
)
}
}
// Upcoming tasks header
if (taskData.upcomingTasks.isNotEmpty()) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Icon(
Icons.Default.CalendarToday,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Icon(Icons.Default.Add, contentDescription = null)
Text(
text = "Upcoming (${taskData.upcomingTasks.size})",
"Add Task",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp)
fontWeight = FontWeight.Bold
)
}
}
if (myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isEmpty()) {
Text(
"Add a property first from the Residences tab",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
// Upcoming tasks
items(taskData.upcomingTasks) { task ->
TaskCard(
task = task,
onCompleteClick = {
selectedTask = task
showCompleteDialog = true
},
onEditClick = { onNavigateToEditTask(task) },
onCancelClick = { /* TODO */ },
onUncancelClick = null,
onMarkInProgressClick = {
viewModel.markInProgress(task.id) { success ->
if (success) {
viewModel.loadTasks()
}
}
}
)
}
// In Progress section (collapsible)
if (taskData.inProgressTasks.isNotEmpty()) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
onClick = { showInProgressTasks = !showInProgressTasks }
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Icon(
Icons.Default.PlayArrow,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary
)
Text(
text = "In Progress (${taskData.inProgressTasks.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
Icon(
if (showInProgressTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = if (showInProgressTasks) "Collapse" else "Expand"
)
}
} else {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
TaskKanbanView(
upcomingTasks = taskData.upcomingTasks,
inProgressTasks = taskData.inProgressTasks,
doneTasks = taskData.doneTasks,
archivedTasks = taskData.archivedTasks,
onCompleteTask = { task ->
selectedTask = task
showCompleteDialog = true
},
onEditTask = { task ->
onNavigateToEditTask(task)
},
onCancelTask = { task ->
// viewModel.cancelTask(task.id) { _ ->
// viewModel.loadTasks()
// }
},
onUncancelTask = { task ->
// viewModel.uncancelTask(task.id) { _ ->
// viewModel.loadTasks()
// }
},
onMarkInProgress = { task ->
viewModel.markInProgress(task.id) { success ->
if (success) {
viewModel.loadTasks()
}
}
}
if (showInProgressTasks) {
items(taskData.inProgressTasks) { task ->
TaskCard(
task = task,
onCompleteClick = {
selectedTask = task
showCompleteDialog = true
},
onEditClick = { onNavigateToEditTask(task) },
onCancelClick = { /* TODO */ },
onUncancelClick = null,
onMarkInProgressClick = null
)
}
}
}
// Done section (collapsible)
if (taskData.doneTasks.isNotEmpty()) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
onClick = { showDoneTasks = !showDoneTasks }
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
Text(
text = "Done (${taskData.doneTasks.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
Icon(
if (showDoneTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = if (showDoneTasks) "Collapse" else "Expand"
)
}
}
}
if (showDoneTasks) {
items(taskData.doneTasks) { task ->
TaskCard(
task = task,
onCompleteClick = null,
onEditClick = { onNavigateToEditTask(task) },
onCancelClick = null,
onUncancelClick = null,
onMarkInProgressClick = null
)
}
}
}
)
}
}
}
@@ -355,34 +279,22 @@ fun AllTasksScreen(
}
)
}
}
@Composable
private fun TaskSummaryPill(
count: Int,
label: String,
color: androidx.compose.ui.graphics.Color
) {
Surface(
color = color.copy(alpha = 0.1f),
shape = MaterialTheme.shapes.small
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Text(
text = count.toString(),
style = MaterialTheme.typography.labelLarge,
color = color,
fontWeight = FontWeight.Bold
)
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = color
)
}
if (showNewTaskDialog && myResidencesState is ApiResult.Success) {
AddNewTaskWithResidenceDialog(
residencesResponse = (myResidencesState as ApiResult.Success).data,
onDismiss = {
showNewTaskDialog = false
viewModel.resetAddTaskState()
},
onCreate = { taskRequest ->
println("AllTasksScreen: onCreate called with request: $taskRequest")
viewModel.createNewTask(taskRequest)
},
isLoading = createTaskState is ApiResult.Loading,
errorMessage = if (createTaskState is ApiResult.Error) {
(createTaskState as ApiResult.Error).message
} else null
)
}
}

View File

@@ -300,7 +300,6 @@ fun EditTaskScreen(
category = selectedCategory!!.id,
frequency = selectedFrequency!!.id,
priority = selectedPriority!!.id,
status = selectedStatus!!.id,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }
)

View File

@@ -22,7 +22,8 @@ fun MainScreen(
onResidenceClick: (Int) -> Unit,
onAddResidence: () -> Unit,
onNavigateToEditResidence: (Residence) -> Unit,
onNavigateToEditTask: (com.mycrib.shared.models.TaskDetail) -> Unit
onNavigateToEditTask: (com.mycrib.shared.models.TaskDetail) -> Unit,
onAddTask: () -> Unit
) {
var selectedTab by remember { mutableStateOf(0) }
val navController = rememberNavController()
@@ -112,7 +113,8 @@ fun MainScreen(
composable<MainTabTasksRoute> {
Box(modifier = Modifier.fillMaxSize()) {
AllTasksScreen(
onNavigateToEditTask = onNavigateToEditTask
onNavigateToEditTask = onNavigateToEditTask,
onAddTask = onAddTask
)
}
}

View File

@@ -20,6 +20,7 @@ import com.mycrib.android.ui.components.common.InfoCard
import com.mycrib.android.ui.components.residence.PropertyDetailItem
import com.mycrib.android.ui.components.residence.DetailRow
import com.mycrib.android.ui.components.task.TaskCard
import com.mycrib.android.ui.components.task.TaskKanbanView
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.android.viewmodel.TaskCompletionViewModel
import com.mycrib.android.viewmodel.TaskViewModel
@@ -48,8 +49,6 @@ fun ResidenceDetailScreen(
var showCompleteDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
var showNewTaskDialog by remember { mutableStateOf(false) }
var showInProgressTasks by remember { mutableStateOf(false) }
var showDoneTasks by remember { mutableStateOf(false) }
LaunchedEffect(residenceId) {
residenceViewModel.getResidence(residenceId) { result ->
@@ -394,7 +393,7 @@ fun ResidenceDetailScreen(
}
is ApiResult.Success -> {
val taskData = (tasksState as ApiResult.Success).data
if (taskData.upcomingTasks.isEmpty() && taskData.inProgressTasks.isEmpty() && taskData.doneTasks.isEmpty()) {
if (taskData.upcomingTasks.isEmpty() && taskData.inProgressTasks.isEmpty() && taskData.doneTasks.isEmpty() && taskData.archivedTasks.isEmpty()) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
@@ -427,137 +426,38 @@ fun ResidenceDetailScreen(
}
}
} else {
// Upcoming tasks section
items(taskData.upcomingTasks) { task ->
TaskCard(
task = task,
onCompleteClick = {
selectedTask = task
showCompleteDialog = true
},
onEditClick = {
onNavigateToEditTask(task)
},
onCancelClick = {
residenceViewModel.cancelTask(task.id)
},
onUncancelClick = null,
onMarkInProgressClick = {
taskViewModel.markInProgress(task.id) { success ->
if (success) {
residenceViewModel.loadResidenceTasks(residenceId)
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(500.dp)
) {
TaskKanbanView(
upcomingTasks = taskData.upcomingTasks,
inProgressTasks = taskData.inProgressTasks,
doneTasks = taskData.doneTasks,
archivedTasks = taskData.archivedTasks,
onCompleteTask = { task ->
selectedTask = task
showCompleteDialog = true
},
onEditTask = { task ->
onNavigateToEditTask(task)
},
onCancelTask = { task ->
residenceViewModel.cancelTask(task.id)
},
onUncancelTask = { task ->
residenceViewModel.uncancelTask(task.id)
},
onMarkInProgress = { task ->
taskViewModel.markInProgress(task.id) { success ->
if (success) {
residenceViewModel.loadResidenceTasks(residenceId)
}
}
}
}
)
}
// In Progress tasks section
if (taskData.inProgressTasks.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { showInProgressTasks = !showInProgressTasks }
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.PlayCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "In Progress (${taskData.inProgressTasks.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary
)
}
Icon(
if (showInProgressTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (showInProgressTasks) {
items(taskData.inProgressTasks) { task ->
TaskCard(
task = task,
onCompleteClick = {
selectedTask = task
showCompleteDialog = true
},
onEditClick = {
onNavigateToEditTask(task)
},
onCancelClick = {
residenceViewModel.cancelTask(task.id)
},
onUncancelClick = null,
onMarkInProgressClick = null
)
}
}
}
// Done tasks section
if (taskData.doneTasks.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { showDoneTasks = !showDoneTasks }
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Done (${taskData.doneTasks.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
Icon(
if (showDoneTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (showDoneTasks) {
items(taskData.doneTasks) { task ->
TaskCard(
task = task,
onCompleteClick = null,
onEditClick = {
onNavigateToEditTask(task)
},
onCancelClick = null,
onUncancelClick = {
residenceViewModel.uncancelTask(task.id)
},
onMarkInProgressClick = null
)
}
)
}
}
}

View File

@@ -57,14 +57,30 @@ fun ResidencesScreen(
)
},
floatingActionButton = {
FloatingActionButton(
onClick = onAddResidence,
containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(16.dp)
) {
Icon(Icons.Default.Add, contentDescription = "Add Property")
// Only show FAB when there are properties
val hasResidences = myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
if (hasResidences) {
Box(modifier = Modifier.padding(bottom = 80.dp)) {
FloatingActionButton(
onClick = onAddResidence,
containerColor = MaterialTheme.colorScheme.primary,
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 8.dp,
pressedElevation = 12.dp
)
) {
Icon(
Icons.Default.Add,
contentDescription = "Add Property",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}
},
floatingActionButtonPosition = FabPosition.End
) { paddingValues ->
when (myResidencesState) {
is ApiResult.Idle, is ApiResult.Loading -> {
@@ -119,7 +135,8 @@ fun ResidencesScreen(
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(24.dp)
) {
Icon(
Icons.Default.Home,
@@ -137,6 +154,26 @@ fun ResidencesScreen(
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onAddResidence,
modifier = Modifier
.fillMaxWidth(0.7f)
.height(56.dp),
shape = RoundedCornerShape(12.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Add, contentDescription = null)
Text(
"Add Property",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
} else {

View File

@@ -20,6 +20,7 @@ import com.mycrib.shared.network.ApiResult
@Composable
fun TasksScreen(
onNavigateBack: () -> Unit,
onAddTask: () -> Unit = {},
viewModel: TaskViewModel = viewModel { TaskViewModel() },
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() }
) {
@@ -55,14 +56,10 @@ fun TasksScreen(
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* TODO: Add task */ }) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
)
}
},
// No FAB on Tasks screen - tasks are added from within residences
) { paddingValues ->
when (tasksState) {
is ApiResult.Idle, is ApiResult.Loading -> {
@@ -107,7 +104,33 @@ fun TasksScreen(
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
Text("No tasks yet. Add one to get started!")
Column(
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(24.dp)
) {
Icon(
Icons.Default.Assignment,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Text(
"No tasks yet",
style = MaterialTheme.typography.headlineSmall,
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold
)
Text(
"Tasks are created from your properties.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"Go to Residences tab to add a property, then add tasks to it!",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
}
} else {
LazyColumn(

View File

@@ -44,10 +44,13 @@ fun VerifyEmailScreen(
errorMessage = (verifyState as ApiResult.Error).message
isLoading = false
}
is ApiResult.Idle, is ApiResult.Loading -> {
is ApiResult.Loading -> {
isLoading = true
errorMessage = ""
}
is ApiResult.Idle -> {
// Do nothing - initial state, no loading indicator needed
}
else -> {}
}
}

View File

@@ -5,16 +5,88 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
// Bright color palette
private val BrightBlue = Color(0xFF007AFF)
private val BrightGreen = Color(0xFF34C759)
private val BrightOrange = Color(0xFFFF9500)
private val BrightRed = Color(0xFFFF3B30)
private val BrightPurple = Color(0xFFAF52DE)
private val BrightTeal = Color(0xFF5AC8FA)
// Light variations for containers
private val LightBlue = Color(0xFFE3F2FD)
private val LightGreen = Color(0xFFE8F5E9)
private val LightOrange = Color(0xFFFFF3E0)
// Dark variations
private val DarkBlue = Color(0xFF0A84FF)
private val DarkGreen = Color(0xFF30D158)
private val DarkOrange = Color(0xFFFF9F0A)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC6),
tertiary = Color(0xFF3700B3)
primary = DarkBlue,
onPrimary = Color.White,
primaryContainer = Color(0xFF003D75),
onPrimaryContainer = Color(0xFFD0E4FF),
secondary = DarkGreen,
onSecondary = Color.White,
secondaryContainer = Color(0xFF1B5E20),
onSecondaryContainer = Color(0xFFB9F6CA),
tertiary = DarkOrange,
onTertiary = Color.White,
tertiaryContainer = Color(0xFF663C00),
onTertiaryContainer = Color(0xFFFFE0B2),
error = BrightRed,
onError = Color.White,
errorContainer = Color(0xFF93000A),
onErrorContainer = Color(0xFFFFDAD6),
background = Color(0xFF1C1B1F),
onBackground = Color(0xFFE6E1E5),
surface = Color(0xFF1C1B1F),
onSurface = Color(0xFFE6E1E5),
surfaceVariant = Color(0xFF49454F),
onSurfaceVariant = Color(0xFFCAC4D0),
outline = Color(0xFF938F99),
outlineVariant = Color(0xFF49454F)
)
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC6),
tertiary = Color(0xFF3700B3)
primary = BrightBlue,
onPrimary = Color.White,
primaryContainer = LightBlue,
onPrimaryContainer = Color(0xFF001D35),
secondary = BrightGreen,
onSecondary = Color.White,
secondaryContainer = LightGreen,
onSecondaryContainer = Color(0xFF002106),
tertiary = BrightOrange,
onTertiary = Color.White,
tertiaryContainer = LightOrange,
onTertiaryContainer = Color(0xFF2B1700),
error = BrightRed,
onError = Color.White,
errorContainer = Color(0xFFFFDAD6),
onErrorContainer = Color(0xFF410002),
background = Color(0xFFFFFBFE),
onBackground = Color(0xFF1C1B1F),
surface = Color(0xFFFFFBFE),
onSurface = Color(0xFF1C1B1F),
surfaceVariant = Color(0xFFE7E0EC),
onSurfaceVariant = Color(0xFF49454F),
outline = Color(0xFF79747E),
outlineVariant = Color(0xFFCAC4D0)
)
@Composable

View File

@@ -26,11 +26,14 @@ class TaskViewModel : ViewModel() {
val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState
fun loadTasks() {
println("TaskViewModel: loadTasks called")
viewModelScope.launch {
_tasksState.value = ApiResult.Loading
val token = TokenStorage.getToken()
if (token != null) {
_tasksState.value = taskApi.getTasks(token)
val result = taskApi.getTasks(token)
println("TaskViewModel: loadTasks result: $result")
_tasksState.value = result
} else {
_tasksState.value = ApiResult.Error("Not authenticated", 401)
}
@@ -50,11 +53,17 @@ class TaskViewModel : ViewModel() {
}
fun createNewTask(request: TaskCreateRequest) {
println("TaskViewModel: createNewTask called with $request")
viewModelScope.launch {
println("TaskViewModel: Setting state to Loading")
_taskAddNewCustomTaskState.value = ApiResult.Loading
try {
_taskAddNewCustomTaskState.value = taskApi.createTask(TokenStorage.getToken()!!, request)
val result = taskApi.createTask(TokenStorage.getToken()!!, request)
println("TaskViewModel: API result: $result")
_taskAddNewCustomTaskState.value = result
} catch (e: Exception) {
println("TaskViewModel: Exception: ${e.message}")
e.printStackTrace()
_taskAddNewCustomTaskState.value = ApiResult.Error(e.message ?: "Unknown error")
}
}