wip
This commit is contained in:
@@ -29,6 +29,7 @@ import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import com.mycrib.navigation.*
|
||||
import com.mycrib.repository.LookupsRepository
|
||||
|
||||
import mycrib.composeapp.generated.resources.Res
|
||||
import mycrib.composeapp.generated.resources.compose_multiplatform
|
||||
@@ -39,9 +40,12 @@ fun App() {
|
||||
var isLoggedIn by remember { mutableStateOf(com.mycrib.storage.TokenStorage.hasToken()) }
|
||||
val navController = rememberNavController()
|
||||
|
||||
// Check for stored token on app start
|
||||
// Check for stored token on app start and initialize lookups if logged in
|
||||
LaunchedEffect(Unit) {
|
||||
isLoggedIn = com.mycrib.storage.TokenStorage.hasToken()
|
||||
if (isLoggedIn) {
|
||||
LookupsRepository.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
@@ -56,6 +60,8 @@ fun App() {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
isLoggedIn = true
|
||||
// Initialize lookups after successful login
|
||||
LookupsRepository.initialize()
|
||||
navController.navigate(ResidencesRoute) {
|
||||
popUpTo<LoginRoute> { inclusive = true }
|
||||
}
|
||||
@@ -70,6 +76,8 @@ fun App() {
|
||||
RegisterScreen(
|
||||
onRegisterSuccess = {
|
||||
isLoggedIn = true
|
||||
// Initialize lookups after successful registration
|
||||
LookupsRepository.initialize()
|
||||
navController.navigate(ResidencesRoute) {
|
||||
popUpTo<RegisterRoute> { inclusive = true }
|
||||
}
|
||||
@@ -89,8 +97,9 @@ fun App() {
|
||||
navController.navigate(TasksRoute)
|
||||
},
|
||||
onLogout = {
|
||||
// Clear token on logout
|
||||
// Clear token and lookups on logout
|
||||
com.mycrib.storage.TokenStorage.clearToken()
|
||||
LookupsRepository.clear()
|
||||
isLoggedIn = false
|
||||
navController.navigate(LoginRoute) {
|
||||
popUpTo<HomeRoute> { inclusive = true }
|
||||
@@ -108,8 +117,9 @@ fun App() {
|
||||
navController.navigate(AddResidenceRoute)
|
||||
},
|
||||
onLogout = {
|
||||
// Clear token on logout
|
||||
// Clear token and lookups on logout
|
||||
com.mycrib.storage.TokenStorage.clearToken()
|
||||
LookupsRepository.clear()
|
||||
isLoggedIn = false
|
||||
navController.navigate(LoginRoute) {
|
||||
popUpTo<HomeRoute> { inclusive = true }
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.mycrib.shared.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ResidenceTypeResponse(
|
||||
val count: Int,
|
||||
val results: List<ResidenceType>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResidenceType(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskFrequencyResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskFrequency>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskFrequency(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskPriorityResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskPriority>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskPriority(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskStatusResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskStatus>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskStatus(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCategoryResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskCategory>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCategory(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String? = null
|
||||
)
|
||||
@@ -32,7 +32,7 @@ data class Residence(
|
||||
@Serializable
|
||||
data class ResidenceCreateRequest(
|
||||
val name: String,
|
||||
@SerialName("property_type") val propertyType: String,
|
||||
@SerialName("property_type") val propertyType: Int,
|
||||
@SerialName("street_address") val streetAddress: String,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String,
|
||||
|
||||
@@ -39,11 +39,11 @@ data class TaskCreateRequest(
|
||||
val residence: Int,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
val category: String,
|
||||
val frequency: String = "once",
|
||||
val category: Int,
|
||||
val frequency: Int,
|
||||
@SerialName("interval_days") val intervalDays: Int? = null,
|
||||
val priority: String = "medium",
|
||||
val status: String = "pending",
|
||||
val priority: Int,
|
||||
val status: Int = 9,
|
||||
@SerialName("due_date") val dueDate: String,
|
||||
@SerialName("estimated_cost") val estimatedCost: String? = null
|
||||
)
|
||||
@@ -52,14 +52,14 @@ data class TaskCreateRequest(
|
||||
data class TaskDetail(
|
||||
val id: Int,
|
||||
val residence: Int,
|
||||
@SerialName("created_by") val createdBy: Int,
|
||||
@SerialName("created_by_username") val createdByUsername: String,
|
||||
//@SerialName("created_by") val createdBy: Int,
|
||||
//@SerialName("created_by_username") val createdByUsername: String,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val category: String,
|
||||
val priority: String,
|
||||
val frequency: String,
|
||||
val status: String,
|
||||
val category: TaskCategory,
|
||||
val priority: TaskPriority,
|
||||
val frequency: TaskFrequency,
|
||||
val status: TaskStatus?,
|
||||
@SerialName("due_date") val dueDate: String,
|
||||
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
||||
@SerialName("actual_cost") val actualCost: String? = null,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.mycrib.shared.network
|
||||
|
||||
import com.mycrib.shared.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getResidenceTypes(token: String): ApiResult<ResidenceTypeResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residence-types/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch residence types", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTaskFrequencies(token: String): ApiResult<TaskFrequencyResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-frequencies/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task frequencies", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTaskPriorities(token: String): ApiResult<TaskPriorityResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-priorities/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task priorities", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTaskStatuses(token: String): ApiResult<TaskStatusResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-statuses/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task statuses", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTaskCategories(token: String): ApiResult<TaskCategoryResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-categories/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task categories", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.mycrib.repository
|
||||
|
||||
import com.mycrib.shared.models.*
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
import com.mycrib.shared.network.LookupsApi
|
||||
import com.mycrib.storage.TokenStorage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Singleton repository for managing lookup data across the entire app.
|
||||
* Fetches data once on initialization and caches it for the app session.
|
||||
*/
|
||||
object LookupsRepository {
|
||||
private val lookupsApi = LookupsApi()
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
private val _residenceTypes = MutableStateFlow<List<ResidenceType>>(emptyList())
|
||||
val residenceTypes: StateFlow<List<ResidenceType>> = _residenceTypes
|
||||
|
||||
private val _taskFrequencies = MutableStateFlow<List<TaskFrequency>>(emptyList())
|
||||
val taskFrequencies: StateFlow<List<TaskFrequency>> = _taskFrequencies
|
||||
|
||||
private val _taskPriorities = MutableStateFlow<List<TaskPriority>>(emptyList())
|
||||
val taskPriorities: StateFlow<List<TaskPriority>> = _taskPriorities
|
||||
|
||||
private val _taskStatuses = MutableStateFlow<List<TaskStatus>>(emptyList())
|
||||
val taskStatuses: StateFlow<List<TaskStatus>> = _taskStatuses
|
||||
|
||||
private val _taskCategories = MutableStateFlow<List<TaskCategory>>(emptyList())
|
||||
val taskCategories: StateFlow<List<TaskCategory>> = _taskCategories
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading: StateFlow<Boolean> = _isLoading
|
||||
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
val isInitialized: StateFlow<Boolean> = _isInitialized
|
||||
|
||||
/**
|
||||
* Load all lookups from the API.
|
||||
* This should be called once when the user logs in.
|
||||
*/
|
||||
fun initialize() {
|
||||
// Only initialize once per app session
|
||||
if (_isInitialized.value) {
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
_isLoading.value = true
|
||||
val token = TokenStorage.getToken()
|
||||
|
||||
if (token != null) {
|
||||
// Load all lookups in parallel
|
||||
launch {
|
||||
when (val result = lookupsApi.getResidenceTypes(token)) {
|
||||
is ApiResult.Success -> _residenceTypes.value = result.data.results
|
||||
else -> {} // Keep empty list on error
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
when (val result = lookupsApi.getTaskFrequencies(token)) {
|
||||
is ApiResult.Success -> _taskFrequencies.value = result.data.results
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
when (val result = lookupsApi.getTaskPriorities(token)) {
|
||||
is ApiResult.Success -> _taskPriorities.value = result.data.results
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
when (val result = lookupsApi.getTaskStatuses(token)) {
|
||||
is ApiResult.Success -> _taskStatuses.value = result.data.results
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
when (val result = lookupsApi.getTaskCategories(token)) {
|
||||
is ApiResult.Success -> _taskCategories.value = result.data.results
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isInitialized.value = true
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data.
|
||||
* This should be called when the user logs out.
|
||||
*/
|
||||
fun clear() {
|
||||
_residenceTypes.value = emptyList()
|
||||
_taskFrequencies.value = emptyList()
|
||||
_taskPriorities.value = emptyList()
|
||||
_taskStatuses.value = emptyList()
|
||||
_taskCategories.value = emptyList()
|
||||
_isInitialized.value = false
|
||||
_isLoading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh all lookups from the API.
|
||||
*/
|
||||
fun refresh() {
|
||||
_isInitialized.value = false
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,11 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mycrib.repository.LookupsRepository
|
||||
import com.mycrib.shared.models.TaskCategory
|
||||
import com.mycrib.shared.models.TaskCreateRequest
|
||||
import com.mycrib.shared.models.TaskFrequency
|
||||
import com.mycrib.shared.models.TaskPriority
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -20,13 +24,15 @@ fun AddNewTaskDialog(
|
||||
) {
|
||||
var title by remember { mutableStateOf("") }
|
||||
var description by remember { mutableStateOf("") }
|
||||
var category by remember { mutableStateOf("") }
|
||||
var frequency by remember { mutableStateOf("once") }
|
||||
var intervalDays by remember { mutableStateOf("") }
|
||||
var priority by remember { mutableStateOf("medium") }
|
||||
var dueDate by remember { mutableStateOf("") }
|
||||
var estimatedCost by remember { mutableStateOf("") }
|
||||
|
||||
|
||||
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
|
||||
var frequency by remember { mutableStateOf(TaskFrequency(id = 0, name = "", displayName = "")) }
|
||||
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "", displayName = "")) }
|
||||
|
||||
var showFrequencyDropdown by remember { mutableStateOf(false) }
|
||||
var showPriorityDropdown by remember { mutableStateOf(false) }
|
||||
var showCategoryDropdown by remember { mutableStateOf(false) }
|
||||
@@ -35,38 +41,23 @@ fun AddNewTaskDialog(
|
||||
var categoryError by remember { mutableStateOf(false) }
|
||||
var dueDateError by remember { mutableStateOf(false) }
|
||||
|
||||
val frequencies = listOf(
|
||||
"once" to "One Time",
|
||||
"daily" to "Daily",
|
||||
"weekly" to "Weekly",
|
||||
"biweekly" to "Bi-Weekly",
|
||||
"monthly" to "Monthly",
|
||||
"quarterly" to "Quarterly",
|
||||
"semiannually" to "Semi-Annually",
|
||||
"annually" to "Annually"
|
||||
)
|
||||
// Get data from LookupsRepository
|
||||
val frequencies by LookupsRepository.taskFrequencies.collectAsState()
|
||||
val priorities by LookupsRepository.taskPriorities.collectAsState()
|
||||
val categories by LookupsRepository.taskCategories.collectAsState()
|
||||
|
||||
val priorities = listOf(
|
||||
"low" to "Low",
|
||||
"medium" to "Medium",
|
||||
"high" to "High",
|
||||
"urgent" to "Urgent"
|
||||
)
|
||||
// Set defaults when data loads
|
||||
LaunchedEffect(frequencies) {
|
||||
if (frequencies.isNotEmpty()) {
|
||||
frequency = frequencies.first()
|
||||
}
|
||||
}
|
||||
|
||||
val categories = listOf(
|
||||
"Plumbing",
|
||||
"Electrical",
|
||||
"HVAC",
|
||||
"Landscaping",
|
||||
"Painting",
|
||||
"Roofing",
|
||||
"Flooring",
|
||||
"Appliances",
|
||||
"General Maintenance",
|
||||
"Cleaning",
|
||||
"Inspection",
|
||||
"Other"
|
||||
)
|
||||
LaunchedEffect(priorities) {
|
||||
if (priorities.isNotEmpty()) {
|
||||
priority = priorities.first()
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -110,11 +101,8 @@ fun AddNewTaskDialog(
|
||||
onExpandedChange = { showCategoryDropdown = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = category,
|
||||
onValueChange = {
|
||||
category = it
|
||||
categoryError = false
|
||||
},
|
||||
value = categories.find { it == category }?.name ?: "",
|
||||
onValueChange = { },
|
||||
label = { Text("Category *") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -124,7 +112,8 @@ fun AddNewTaskDialog(
|
||||
{ Text("Category is required") }
|
||||
} else null,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showCategoryDropdown) },
|
||||
readOnly = false
|
||||
readOnly = false,
|
||||
enabled = categories.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = showCategoryDropdown,
|
||||
@@ -132,7 +121,7 @@ fun AddNewTaskDialog(
|
||||
) {
|
||||
categories.forEach { cat ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(cat) },
|
||||
text = { Text(cat.name) },
|
||||
onClick = {
|
||||
category = cat
|
||||
categoryError = false
|
||||
@@ -149,27 +138,28 @@ fun AddNewTaskDialog(
|
||||
onExpandedChange = { showFrequencyDropdown = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = frequencies.find { it.first == frequency }?.second ?: "One Time",
|
||||
value = frequencies.find { it == frequency }?.displayName ?: "",
|
||||
onValueChange = { },
|
||||
label = { Text("Frequency") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) }
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) },
|
||||
enabled = frequencies.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = showFrequencyDropdown,
|
||||
onDismissRequest = { showFrequencyDropdown = false }
|
||||
) {
|
||||
frequencies.forEach { (key, label) ->
|
||||
frequencies.forEach { freq ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
text = { Text(freq.displayName) },
|
||||
onClick = {
|
||||
frequency = key
|
||||
frequency = freq
|
||||
showFrequencyDropdown = false
|
||||
// Clear interval days if frequency is "once"
|
||||
if (key == "once") {
|
||||
if (freq.name == "once") {
|
||||
intervalDays = ""
|
||||
}
|
||||
}
|
||||
@@ -179,7 +169,7 @@ fun AddNewTaskDialog(
|
||||
}
|
||||
|
||||
// Interval Days (only for recurring tasks)
|
||||
if (frequency != "once") {
|
||||
if (frequency.name != "once") {
|
||||
OutlinedTextField(
|
||||
value = intervalDays,
|
||||
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
|
||||
@@ -215,24 +205,25 @@ fun AddNewTaskDialog(
|
||||
onExpandedChange = { showPriorityDropdown = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = priorities.find { it.first == priority }?.second ?: "Medium",
|
||||
value = priorities.find { it.name == priority.name }?.displayName ?: "",
|
||||
onValueChange = { },
|
||||
label = { Text("Priority") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) }
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) },
|
||||
enabled = priorities.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = showPriorityDropdown,
|
||||
onDismissRequest = { showPriorityDropdown = false }
|
||||
) {
|
||||
priorities.forEach { (key, label) ->
|
||||
priorities.forEach { prio ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
text = { Text(prio.displayName) },
|
||||
onClick = {
|
||||
priority = key
|
||||
priority = prio
|
||||
showPriorityDropdown = false
|
||||
}
|
||||
)
|
||||
@@ -257,14 +248,12 @@ fun AddNewTaskDialog(
|
||||
onClick = {
|
||||
// Validation
|
||||
var hasError = false
|
||||
|
||||
if (title.isBlank()) {
|
||||
titleError = true
|
||||
hasError = true
|
||||
}
|
||||
if (category.isBlank()) {
|
||||
categoryError = true
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if (dueDate.isBlank() || !isValidDateFormat(dueDate)) {
|
||||
dueDateError = true
|
||||
hasError = true
|
||||
@@ -276,10 +265,10 @@ fun AddNewTaskDialog(
|
||||
residence = residenceId,
|
||||
title = title,
|
||||
description = description.ifBlank { null },
|
||||
category = category,
|
||||
frequency = frequency,
|
||||
category = category.id,
|
||||
frequency = frequency.id,
|
||||
intervalDays = intervalDays.toIntOrNull(),
|
||||
priority = priority,
|
||||
priority = priority.id,
|
||||
dueDate = dueDate,
|
||||
estimatedCost = estimatedCost.ifBlank { null }
|
||||
)
|
||||
|
||||
@@ -13,7 +13,9 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||
import com.mycrib.repository.LookupsRepository
|
||||
import com.mycrib.shared.models.ResidenceCreateRequest
|
||||
import com.mycrib.shared.models.ResidenceType
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -24,7 +26,7 @@ fun AddResidenceScreen(
|
||||
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var propertyType by remember { mutableStateOf("house") }
|
||||
var propertyType by remember { mutableStateOf(ResidenceType(0, "placeHolder")) }
|
||||
var streetAddress by remember { mutableStateOf("") }
|
||||
var apartmentUnit by remember { mutableStateOf("") }
|
||||
var city by remember { mutableStateOf("") }
|
||||
@@ -41,6 +43,7 @@ fun AddResidenceScreen(
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val createState by viewModel.createResidenceState.collectAsState()
|
||||
val propertyTypes by LookupsRepository.residenceTypes.collectAsState()
|
||||
|
||||
// Validation errors
|
||||
var nameError by remember { mutableStateOf("") }
|
||||
@@ -60,7 +63,12 @@ fun AddResidenceScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val propertyTypes = listOf("house", "apartment", "condo", "townhouse", "duplex", "other")
|
||||
// Set default property type if not set and types are loaded
|
||||
LaunchedEffect(propertyTypes) {
|
||||
if (propertyTypes.isNotEmpty()) {
|
||||
propertyType = propertyTypes.first()
|
||||
}
|
||||
}
|
||||
|
||||
fun validateForm(): Boolean {
|
||||
var isValid = true
|
||||
@@ -146,14 +154,15 @@ fun AddResidenceScreen(
|
||||
onExpandedChange = { expanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = propertyType.replaceFirstChar { it.uppercase() },
|
||||
value = propertyTypes.find { it.name == propertyType.name }?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Property Type *") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor()
|
||||
.menuAnchor(),
|
||||
enabled = propertyTypes.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
@@ -161,7 +170,7 @@ fun AddResidenceScreen(
|
||||
) {
|
||||
propertyTypes.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(type.replaceFirstChar { it.uppercase() }) },
|
||||
text = { Text(type.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
propertyType = type
|
||||
expanded = false
|
||||
@@ -318,7 +327,7 @@ fun AddResidenceScreen(
|
||||
viewModel.createResidence(
|
||||
ResidenceCreateRequest(
|
||||
name = name,
|
||||
propertyType = propertyType,
|
||||
propertyType = propertyType.id,
|
||||
streetAddress = streetAddress,
|
||||
apartmentUnit = apartmentUnit.ifBlank { null },
|
||||
city = city,
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.mycrib.android.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -38,69 +44,128 @@ fun LoginScreen(
|
||||
|
||||
val isLoading = loginState is ApiResult.Loading
|
||||
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "myCrib",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.login(username, password)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = username.isNotEmpty() && password.isNotEmpty()
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
// App Logo/Icon
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
Text("Login")
|
||||
|
||||
Text(
|
||||
text = "myCrib",
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Manage your properties with ease",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.login(username, password)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = username.isNotEmpty() && password.isNotEmpty(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
"Sign In",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onNavigateToRegister,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Don't have an account? Register",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
TextButton(onClick = onNavigateToRegister) {
|
||||
Text("Don't have an account? Register")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package com.mycrib.android.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mycrib.android.viewmodel.AuthViewModel
|
||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -39,16 +42,18 @@ fun RegisterScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Register") },
|
||||
title = { Text("Create Account", fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -56,59 +61,103 @@ fun RegisterScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Icon(
|
||||
Icons.Default.PersonAdd,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Join myCrib",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Start managing your properties today",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("Email") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text("Confirm Password") },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -123,19 +172,29 @@ fun RegisterScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = username.isNotEmpty() && email.isNotEmpty() &&
|
||||
password.isNotEmpty() && !isLoading
|
||||
password.isNotEmpty() && !isLoading,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text("Register")
|
||||
Text(
|
||||
"Create Account",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ package com.mycrib.android.ui.screens
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mycrib.android.ui.components.AddNewTaskDialog
|
||||
@@ -33,10 +34,10 @@ fun ResidenceDetailScreen(
|
||||
var residenceState by remember { mutableStateOf<ApiResult<Residence>>(ApiResult.Loading) }
|
||||
val tasksState by residenceViewModel.residenceTasksState.collectAsState()
|
||||
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
|
||||
val taskAddNewTaskState by taskViewModel.taskAddNewTaskState.collectAsState()
|
||||
|
||||
var showCompleteDialog by remember { mutableStateOf(false) }
|
||||
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
|
||||
|
||||
var showNewTaskDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(residenceId) {
|
||||
@@ -53,7 +54,18 @@ fun ResidenceDetailScreen(
|
||||
showCompleteDialog = false
|
||||
selectedTask = null
|
||||
taskCompletionViewModel.resetCreateState()
|
||||
// Reload tasks to show updated data
|
||||
residenceViewModel.loadResidenceTasks(residenceId)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(taskAddNewTaskState) {
|
||||
when (taskAddNewTaskState) {
|
||||
is ApiResult.Success -> {
|
||||
showCompleteDialog = false
|
||||
selectedTask = null
|
||||
taskCompletionViewModel.resetCreateState()
|
||||
residenceViewModel.loadResidenceTasks(residenceId)
|
||||
}
|
||||
else -> {}
|
||||
@@ -65,20 +77,16 @@ fun ResidenceDetailScreen(
|
||||
taskId = selectedTask!!.id,
|
||||
taskTitle = selectedTask!!.title,
|
||||
onDismiss = {
|
||||
showCompleteDialog = false
|
||||
selectedTask = null
|
||||
taskCompletionViewModel.resetCreateState()
|
||||
},
|
||||
onComplete = { request, images ->
|
||||
if (images.isNotEmpty()) {
|
||||
// Use the method that supports images
|
||||
taskCompletionViewModel.createTaskCompletionWithImages(
|
||||
request = request,
|
||||
images = images.map { it.bytes },
|
||||
imageFileNames = images.map { it.fileName }
|
||||
)
|
||||
} else {
|
||||
// Use the regular method without images
|
||||
taskCompletionViewModel.createTaskCompletion(request)
|
||||
}
|
||||
}
|
||||
@@ -91,22 +99,33 @@ fun ResidenceDetailScreen(
|
||||
onDismiss = {
|
||||
showNewTaskDialog = false
|
||||
}, onCreate = { request ->
|
||||
showNewTaskDialog = false
|
||||
val newTask = taskViewModel.createNewTask(request)
|
||||
residenceViewModel.loadResidenceTasks(residenceId)
|
||||
showNewTaskDialog = false
|
||||
taskViewModel.createNewTask(request)
|
||||
})
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Residence Details") },
|
||||
title = { Text("Property Details", fontWeight = FontWeight.Bold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { showNewTaskDialog = true },
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add Task")
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
when (residenceState) {
|
||||
@@ -115,7 +134,7 @@ fun ResidenceDetailScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
@@ -125,19 +144,30 @@ fun ResidenceDetailScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = "Error: ${(residenceState as ApiResult.Error).message}",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = {
|
||||
residenceViewModel.getResidence(residenceId) { result ->
|
||||
residenceState = result
|
||||
}
|
||||
}) {
|
||||
Button(
|
||||
onClick = {
|
||||
residenceViewModel.getResidence(residenceId) { result ->
|
||||
residenceState = result
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
@@ -148,89 +178,103 @@ fun ResidenceDetailScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Property Name
|
||||
// Property Header Card
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = residence.name,
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
Text(
|
||||
text = residence.propertyType.replaceFirstChar { it.uppercase() },
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = residence.name,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Address
|
||||
// Address Card
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Address",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = residence.streetAddress)
|
||||
if (residence.apartmentUnit != null) {
|
||||
Text(text = "Unit: ${residence.apartmentUnit}")
|
||||
}
|
||||
Text(text = "${residence.city}, ${residence.stateProvince} ${residence.postalCode}")
|
||||
Text(text = residence.country)
|
||||
InfoCard(
|
||||
icon = Icons.Default.LocationOn,
|
||||
title = "Address"
|
||||
) {
|
||||
Text(text = residence.streetAddress)
|
||||
if (residence.apartmentUnit != null) {
|
||||
Text(text = "Unit: ${residence.apartmentUnit}")
|
||||
}
|
||||
Text(text = "${residence.city}, ${residence.stateProvince} ${residence.postalCode}")
|
||||
Text(text = residence.country)
|
||||
}
|
||||
}
|
||||
|
||||
// Property Details
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Property Details",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
residence.bedrooms?.let {
|
||||
Text(text = "Bedrooms: $it")
|
||||
}
|
||||
residence.bathrooms?.let {
|
||||
Text(text = "Bathrooms: $it")
|
||||
// Property Details Card
|
||||
if (residence.bedrooms != null || residence.bathrooms != null ||
|
||||
residence.squareFootage != null || residence.yearBuilt != null) {
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.Info,
|
||||
title = "Property Details"
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
residence.bedrooms?.let {
|
||||
PropertyDetailItem(Icons.Default.Bed, "$it", "Bedrooms")
|
||||
}
|
||||
residence.bathrooms?.let {
|
||||
PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
residence.squareFootage?.let {
|
||||
Text(text = "Square Footage: $it sq ft")
|
||||
DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft")
|
||||
}
|
||||
residence.lotSize?.let {
|
||||
Text(text = "Lot Size: $it acres")
|
||||
DetailRow(Icons.Default.Landscape, "Lot Size", "$it acres")
|
||||
}
|
||||
residence.yearBuilt?.let {
|
||||
Text(text = "Year Built: $it")
|
||||
DetailRow(Icons.Default.CalendarToday, "Year Built", "$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Description
|
||||
// Description Card
|
||||
if (residence.description != null) {
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Description",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = residence.description)
|
||||
}
|
||||
InfoCard(
|
||||
icon = Icons.Default.Description,
|
||||
title = "Description"
|
||||
) {
|
||||
Text(
|
||||
text = residence.description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,45 +282,41 @@ fun ResidenceDetailScreen(
|
||||
// Purchase Information
|
||||
if (residence.purchaseDate != null || residence.purchasePrice != null) {
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Purchase Information",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
residence.purchaseDate?.let {
|
||||
Text(text = "Purchase Date: $it")
|
||||
}
|
||||
residence.purchasePrice?.let {
|
||||
Text(text = "Purchase Price: $$it")
|
||||
}
|
||||
InfoCard(
|
||||
icon = Icons.Default.AttachMoney,
|
||||
title = "Purchase Information"
|
||||
) {
|
||||
residence.purchaseDate?.let {
|
||||
DetailRow(Icons.Default.Event, "Purchase Date", it)
|
||||
}
|
||||
residence.purchasePrice?.let {
|
||||
DetailRow(Icons.Default.Payment, "Purchase Price", "$$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks Section
|
||||
// Tasks Section Header
|
||||
item {
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
Text(
|
||||
text = "Tasks",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { showNewTaskDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
Icons.Default.Assignment,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Add New Task")
|
||||
Text(
|
||||
text = "Tasks",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +327,7 @@ fun ResidenceDetailScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
@@ -295,17 +335,53 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
item {
|
||||
Text(
|
||||
text = "Error loading tasks: ${(tasksState as ApiResult.Error).message}",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Error loading tasks: ${(tasksState as ApiResult.Error).message}",
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ApiResult.Success -> {
|
||||
val taskData = (tasksState as ApiResult.Success).data
|
||||
if (taskData.tasks.isEmpty()) {
|
||||
item {
|
||||
Text("No tasks for this residence yet.")
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Assignment,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
"No tasks yet",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Add a task to get started",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(taskData.tasks) { task ->
|
||||
@@ -326,165 +402,314 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PropertyDetailItem(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
value: String,
|
||||
label: String
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
label: String,
|
||||
value: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "$label: ",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TaskCard(
|
||||
task: TaskDetail,
|
||||
onCompleteClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = task.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = task.category,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = task.category.name,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Priority and status badges
|
||||
Column(horizontalAlignment = androidx.compose.ui.Alignment.End) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Surface(
|
||||
color = when (task.priority) {
|
||||
color = when (task.priority.name.lowercase()) {
|
||||
"urgent" -> MaterialTheme.colorScheme.error
|
||||
"high" -> MaterialTheme.colorScheme.errorContainer
|
||||
"medium" -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = task.priority.uppercase(),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
text = task.priority.name.uppercase(),
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = task.status.uppercase(),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
if (task.status != null) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = task.status.name.uppercase(),
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (task.description != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = task.description,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
task.nextScheduledDate?.let {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Next Due Date: $it",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.CalendarToday,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = task.nextScheduledDate ?: task.dueDate,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
task.estimatedCost?.let {
|
||||
Text(
|
||||
text = "Est. Cost: $$it",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Due: ${task.dueDate}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
task.estimatedCost?.let {
|
||||
task.estimatedCost?.let {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.AttachMoney,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "Est. Cost: $$it",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
text = "$$it",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show completions
|
||||
if (task.completions.isNotEmpty()) {
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
Text(
|
||||
text = "Completions (${task.completions.size})",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Completions (${task.completions.size})",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
|
||||
task.completions.forEach { completion ->
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.small
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = completion.completionDate.split("T")[0],
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
completion.rating?.let { rating ->
|
||||
Text(
|
||||
text = "$rating★",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$rating★",
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion.completedByName?.let {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "By: $it",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
completion.actualCost?.let {
|
||||
Text(
|
||||
text = "Cost: $$it",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
completion.notes?.let {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -498,18 +723,23 @@ fun TaskCard(
|
||||
|
||||
// Show complete task button based on API logic
|
||||
if (task.showCompletedButton) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onCompleteClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Complete Task")
|
||||
Text(
|
||||
"Complete Task",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,18 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mycrib.android.viewmodel.AuthViewModel
|
||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
import com.mycrib.shared.network.AuthApi
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -33,16 +34,30 @@ fun ResidencesScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Residences") },
|
||||
title = {
|
||||
Text(
|
||||
"My Properties",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onAddResidence) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add")
|
||||
}
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(Icons.Default.ExitToApp, contentDescription = "Logout")
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = onAddResidence,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add Property")
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
when (myResidencesState) {
|
||||
@@ -51,7 +66,7 @@ fun ResidencesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
@@ -61,15 +76,27 @@ fun ResidencesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = "Error: ${(myResidencesState as ApiResult.Error).message}",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = { viewModel.loadMyResidences() }) {
|
||||
Button(
|
||||
onClick = { viewModel.loadMyResidences() },
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
@@ -82,9 +109,29 @@ fun ResidencesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("No residences yet. Add one to get started!")
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
"No properties yet",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Add your first property to get started!",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
@@ -92,7 +139,7 @@ fun ResidencesScreen(
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Summary Card
|
||||
item {
|
||||
@@ -100,129 +147,146 @@ fun ResidencesScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Summary",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "${response.summary.totalResidences}",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = "Properties",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "${response.summary.totalTasks}",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = "Total Tasks",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.Dashboard,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Overview",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "${response.summary.tasksDueNextWeek}",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = "Due This Week",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "${response.summary.tasksDueNextMonth}",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = "Due This Month",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
StatItem(
|
||||
icon = Icons.Default.Home,
|
||||
value = "${response.summary.totalResidences}",
|
||||
label = "Properties"
|
||||
)
|
||||
StatItem(
|
||||
icon = Icons.Default.Assignment,
|
||||
value = "${response.summary.totalTasks}",
|
||||
label = "Total Tasks"
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
StatItem(
|
||||
icon = Icons.Default.CalendarToday,
|
||||
value = "${response.summary.tasksDueNextWeek}",
|
||||
label = "Due This Week"
|
||||
)
|
||||
StatItem(
|
||||
icon = Icons.Default.Event,
|
||||
value = "${response.summary.tasksDueNextMonth}",
|
||||
label = "Due This Month"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties Header
|
||||
item {
|
||||
Text(
|
||||
text = "Your Properties",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Residences
|
||||
items(response.residences) { residence ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onResidenceClick(residence.id) }
|
||||
.clickable { onResidenceClick(residence.id) },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = residence.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "${residence.streetAddress}, ${residence.city}, ${residence.stateProvince}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = residence.propertyType.replaceFirstChar { it.uppercase() },
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
// Task Summary
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Divider()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Tasks: ${residence.taskSummary.total}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = residence.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${residence.streetAddress}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${residence.city}, ${residence.stateProvince}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
TaskStatChip(
|
||||
icon = Icons.Default.Assignment,
|
||||
value = "${residence.taskSummary.total}",
|
||||
label = "Tasks",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "Completed: ${residence.taskSummary.completed}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
TaskStatChip(
|
||||
icon = Icons.Default.CheckCircle,
|
||||
value = "${residence.taskSummary.completed}",
|
||||
label = "Done",
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Text(
|
||||
text = "Pending: ${residence.taskSummary.pending}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
TaskStatChip(
|
||||
icon = Icons.Default.Schedule,
|
||||
value = "${residence.taskSummary.pending}",
|
||||
label = "Pending",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
@@ -235,3 +299,64 @@ fun ResidencesScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatItem(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
value: String,
|
||||
label: String
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TaskStatChip(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
value: String,
|
||||
label: String,
|
||||
color: androidx.compose.ui.graphics.Color
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = color
|
||||
)
|
||||
Text(
|
||||
text = "$value",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.mycrib.android.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mycrib.shared.models.*
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
import com.mycrib.shared.network.LookupsApi
|
||||
import com.mycrib.storage.TokenStorage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LookupsViewModel : ViewModel() {
|
||||
private val lookupsApi = LookupsApi()
|
||||
|
||||
private val _residenceTypesState = MutableStateFlow<ApiResult<ResidenceTypeResponse>>(ApiResult.Loading)
|
||||
val residenceTypesState: StateFlow<ApiResult<ResidenceTypeResponse>> = _residenceTypesState
|
||||
|
||||
private val _taskFrequenciesState = MutableStateFlow<ApiResult<TaskFrequencyResponse>>(ApiResult.Loading)
|
||||
val taskFrequenciesState: StateFlow<ApiResult<TaskFrequencyResponse>> = _taskFrequenciesState
|
||||
|
||||
private val _taskPrioritiesState = MutableStateFlow<ApiResult<TaskPriorityResponse>>(ApiResult.Loading)
|
||||
val taskPrioritiesState: StateFlow<ApiResult<TaskPriorityResponse>> = _taskPrioritiesState
|
||||
|
||||
private val _taskStatusesState = MutableStateFlow<ApiResult<TaskStatusResponse>>(ApiResult.Loading)
|
||||
val taskStatusesState: StateFlow<ApiResult<TaskStatusResponse>> = _taskStatusesState
|
||||
|
||||
private val _taskCategoriesState = MutableStateFlow<ApiResult<TaskCategoryResponse>>(ApiResult.Loading)
|
||||
val taskCategoriesState: StateFlow<ApiResult<TaskCategoryResponse>> = _taskCategoriesState
|
||||
|
||||
// Cache flags to avoid refetching
|
||||
private var residenceTypesFetched = false
|
||||
private var taskFrequenciesFetched = false
|
||||
private var taskPrioritiesFetched = false
|
||||
private var taskStatusesFetched = false
|
||||
private var taskCategoriesFetched = false
|
||||
|
||||
fun loadResidenceTypes() {
|
||||
if (residenceTypesFetched && _residenceTypesState.value is ApiResult.Success) {
|
||||
return // Already loaded
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_residenceTypesState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_residenceTypesState.value = lookupsApi.getResidenceTypes(token)
|
||||
if (_residenceTypesState.value is ApiResult.Success) {
|
||||
residenceTypesFetched = true
|
||||
}
|
||||
} else {
|
||||
_residenceTypesState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTaskFrequencies() {
|
||||
if (taskFrequenciesFetched && _taskFrequenciesState.value is ApiResult.Success) {
|
||||
return // Already loaded
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_taskFrequenciesState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_taskFrequenciesState.value = lookupsApi.getTaskFrequencies(token)
|
||||
if (_taskFrequenciesState.value is ApiResult.Success) {
|
||||
taskFrequenciesFetched = true
|
||||
}
|
||||
} else {
|
||||
_taskFrequenciesState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTaskPriorities() {
|
||||
if (taskPrioritiesFetched && _taskPrioritiesState.value is ApiResult.Success) {
|
||||
return // Already loaded
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_taskPrioritiesState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_taskPrioritiesState.value = lookupsApi.getTaskPriorities(token)
|
||||
if (_taskPrioritiesState.value is ApiResult.Success) {
|
||||
taskPrioritiesFetched = true
|
||||
}
|
||||
} else {
|
||||
_taskPrioritiesState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTaskStatuses() {
|
||||
if (taskStatusesFetched && _taskStatusesState.value is ApiResult.Success) {
|
||||
return // Already loaded
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_taskStatusesState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_taskStatusesState.value = lookupsApi.getTaskStatuses(token)
|
||||
if (_taskStatusesState.value is ApiResult.Success) {
|
||||
taskStatusesFetched = true
|
||||
}
|
||||
} else {
|
||||
_taskStatusesState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTaskCategories() {
|
||||
if (taskCategoriesFetched && _taskCategoriesState.value is ApiResult.Success) {
|
||||
return // Already loaded
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_taskCategoriesState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_taskCategoriesState.value = lookupsApi.getTaskCategories(token)
|
||||
if (_taskCategoriesState.value is ApiResult.Success) {
|
||||
taskCategoriesFetched = true
|
||||
}
|
||||
} else {
|
||||
_taskCategoriesState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all lookups at once
|
||||
fun loadAllLookups() {
|
||||
loadResidenceTypes()
|
||||
loadTaskFrequencies()
|
||||
loadTaskPriorities()
|
||||
loadTaskStatuses()
|
||||
loadTaskCategories()
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,10 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetResidenceTasksState() {
|
||||
_residenceTasksState.value = ApiResult.Loading
|
||||
}
|
||||
|
||||
fun loadResidenceTasks(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_residenceTasksState.value = ApiResult.Loading
|
||||
|
||||
@@ -53,16 +53,16 @@ class TaskViewModel : ViewModel() {
|
||||
fun createNewTask(request: TaskCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_taskAddNewTaskState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_taskAddNewTaskState.value = taskApi.createTask(token, request)
|
||||
} else {
|
||||
_taskAddNewTaskState.value = ApiResult.Error("Not authenticated", 401)
|
||||
try {
|
||||
_taskAddNewTaskState.value = taskApi.createTask(TokenStorage.getToken()!!, request)
|
||||
} catch (e: Exception) {
|
||||
_taskAddNewTaskState.value = ApiResult.Error(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCreateTaskState() {
|
||||
_taskAddNewTaskState.value = ApiResult.Loading
|
||||
|
||||
fun resetAddTaskState() {
|
||||
_taskAddNewTaskState.value = ApiResult.Loading // or ApiResult.Idle if you have it
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user