wip
This commit is contained in:
@@ -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() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,6 @@ fun EditTaskScreen(
|
||||
category = selectedCategory!!.id,
|
||||
frequency = selectedFrequency!!.id,
|
||||
priority = selectedPriority!!.id,
|
||||
status = selectedStatus!!.id,
|
||||
dueDate = dueDate,
|
||||
estimatedCost = estimatedCost.ifBlank { null }
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user