wip
This commit is contained in:
@@ -64,12 +64,12 @@ kotlin {
|
|||||||
implementation(compose.components.uiToolingPreview)
|
implementation(compose.components.uiToolingPreview)
|
||||||
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||||
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.ktor.client.logging)
|
implementation(libs.ktor.client.logging)
|
||||||
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.9.1")
|
|
||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(compose.components.resources)
|
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import com.mycrib.navigation.*
|
||||||
|
|
||||||
import mycrib.composeapp.generated.resources.Res
|
import mycrib.composeapp.generated.resources.Res
|
||||||
import mycrib.composeapp.generated.resources.compose_multiplatform
|
import mycrib.composeapp.generated.resources.compose_multiplatform
|
||||||
@@ -48,28 +50,28 @@ fun App() {
|
|||||||
) {
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = if (isLoggedIn) "residences" else "login"
|
startDestination = if (isLoggedIn) ResidencesRoute else LoginRoute
|
||||||
) {
|
) {
|
||||||
composable("login") {
|
composable<LoginRoute> {
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
onLoginSuccess = {
|
onLoginSuccess = {
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
navController.navigate("residences") {
|
navController.navigate(ResidencesRoute) {
|
||||||
popUpTo("login") { inclusive = true }
|
popUpTo<LoginRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigateToRegister = {
|
onNavigateToRegister = {
|
||||||
navController.navigate("register")
|
navController.navigate(RegisterRoute)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("register") {
|
composable<RegisterRoute> {
|
||||||
RegisterScreen(
|
RegisterScreen(
|
||||||
onRegisterSuccess = {
|
onRegisterSuccess = {
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
navController.navigate("residences") {
|
navController.navigate(ResidencesRoute) {
|
||||||
popUpTo("register") { inclusive = true }
|
popUpTo<RegisterRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
@@ -78,37 +80,37 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("home") {
|
composable<HomeRoute> {
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
onNavigateToResidences = {
|
onNavigateToResidences = {
|
||||||
navController.navigate("residences")
|
navController.navigate(ResidencesRoute)
|
||||||
},
|
},
|
||||||
onNavigateToTasks = {
|
onNavigateToTasks = {
|
||||||
navController.navigate("tasks")
|
navController.navigate(TasksRoute)
|
||||||
},
|
},
|
||||||
onLogout = {
|
onLogout = {
|
||||||
// Clear token on logout
|
// Clear token on logout
|
||||||
com.mycrib.storage.TokenStorage.clearToken()
|
com.mycrib.storage.TokenStorage.clearToken()
|
||||||
isLoggedIn = false
|
isLoggedIn = false
|
||||||
navController.navigate("login") {
|
navController.navigate(LoginRoute) {
|
||||||
popUpTo("home") { inclusive = true }
|
popUpTo<HomeRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("residences") {
|
composable<ResidencesRoute> {
|
||||||
ResidencesScreen(
|
ResidencesScreen(
|
||||||
onResidenceClick = { residenceId ->
|
onResidenceClick = { residenceId ->
|
||||||
navController.navigate("residence_detail/$residenceId")
|
navController.navigate(ResidenceDetailRoute(residenceId))
|
||||||
},
|
},
|
||||||
onAddResidence = {
|
onAddResidence = {
|
||||||
navController.navigate("add_residence")
|
navController.navigate(AddResidenceRoute)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("add_residence") {
|
composable<AddResidenceRoute> {
|
||||||
AddResidenceScreen(
|
AddResidenceScreen(
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
@@ -119,7 +121,7 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("tasks") {
|
composable<TasksRoute> {
|
||||||
TasksScreen(
|
TasksScreen(
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
@@ -127,22 +129,14 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable("residence_detail/{residenceId}") { backStackEntry ->
|
composable<ResidenceDetailRoute> { backStackEntry ->
|
||||||
// val residenceId = backStackEntry.arguments?.getString("residenceId")?.toIntOrNull()
|
val route = backStackEntry.toRoute<ResidenceDetailRoute>()
|
||||||
val residenceId = backStackEntry.arguments
|
ResidenceDetailScreen(
|
||||||
?.get("residenceId")
|
residenceId = route.residenceId,
|
||||||
?.toString()
|
onNavigateBack = {
|
||||||
?.toIntOrNull()
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
if (residenceId != null) {
|
|
||||||
ResidenceDetailScreen(
|
|
||||||
residenceId = residenceId,
|
|
||||||
onNavigateBack = {
|
|
||||||
navController.popBackStack()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,5 +91,49 @@ data class OverallSummary(
|
|||||||
@SerialName("total_residences") val totalResidences: Int,
|
@SerialName("total_residences") val totalResidences: Int,
|
||||||
@SerialName("total_tasks") val totalTasks: Int,
|
@SerialName("total_tasks") val totalTasks: Int,
|
||||||
@SerialName("total_completed") val totalCompleted: Int,
|
@SerialName("total_completed") val totalCompleted: Int,
|
||||||
@SerialName("total_pending") val totalPending: Int
|
@SerialName("total_pending") val totalPending: Int,
|
||||||
|
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int,
|
||||||
|
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ResidenceWithTasks(
|
||||||
|
val id: Int,
|
||||||
|
val owner: Int,
|
||||||
|
@SerialName("owner_username") val ownerUsername: String,
|
||||||
|
val name: String,
|
||||||
|
@SerialName("property_type") val propertyType: String,
|
||||||
|
@SerialName("street_address") val streetAddress: String,
|
||||||
|
@SerialName("apartment_unit") val apartmentUnit: String?,
|
||||||
|
val city: String,
|
||||||
|
@SerialName("state_province") val stateProvince: String,
|
||||||
|
@SerialName("postal_code") val postalCode: String,
|
||||||
|
val country: String,
|
||||||
|
val bedrooms: Int?,
|
||||||
|
val bathrooms: Float?,
|
||||||
|
@SerialName("square_footage") val squareFootage: Int?,
|
||||||
|
@SerialName("lot_size") val lotSize: Float?,
|
||||||
|
@SerialName("year_built") val yearBuilt: Int?,
|
||||||
|
val description: String?,
|
||||||
|
@SerialName("purchase_date") val purchaseDate: String?,
|
||||||
|
@SerialName("purchase_price") val purchasePrice: String?,
|
||||||
|
@SerialName("is_primary") val isPrimary: Boolean,
|
||||||
|
@SerialName("task_summary") val taskSummary: TaskSummary,
|
||||||
|
val tasks: List<TaskDetail>,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MyResidencesSummary(
|
||||||
|
@SerialName("total_residences") val totalResidences: Int,
|
||||||
|
@SerialName("total_tasks") val totalTasks: Int,
|
||||||
|
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int,
|
||||||
|
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MyResidencesResponse(
|
||||||
|
val summary: MyResidencesSummary,
|
||||||
|
val residences: List<ResidenceWithTasks>
|
||||||
)
|
)
|
||||||
@@ -56,6 +56,7 @@ data class TaskDetail(
|
|||||||
val description: String?,
|
val description: String?,
|
||||||
val category: String,
|
val category: String,
|
||||||
val priority: String,
|
val priority: String,
|
||||||
|
val frequency: String,
|
||||||
val status: String,
|
val status: String,
|
||||||
@SerialName("due_date") val dueDate: String,
|
@SerialName("due_date") val dueDate: String,
|
||||||
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
||||||
@@ -63,6 +64,7 @@ data class TaskDetail(
|
|||||||
val notes: String? = null,
|
val notes: String? = null,
|
||||||
@SerialName("created_at") val createdAt: String,
|
@SerialName("created_at") val createdAt: String,
|
||||||
@SerialName("updated_at") val updatedAt: String,
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
|
||||||
val completions: List<TaskCompletion>
|
val completions: List<TaskCompletion>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.mycrib.navigation
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object LoginRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object RegisterRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object HomeRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object ResidencesRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object AddResidenceRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ResidenceDetailRoute(val residenceId: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object TasksRoute
|
||||||
@@ -109,6 +109,22 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMyResidences(token: String): ApiResult<MyResidencesResponse> {
|
||||||
|
return try {
|
||||||
|
val response = client.get("$baseUrl/residences/my-residences/") {
|
||||||
|
header("Authorization", "Token $token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
ApiResult.Success(response.body())
|
||||||
|
} else {
|
||||||
|
ApiResult.Error("Failed to fetch my residences", response.status.value)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.mycrib.shared.models.TaskCompletionCreateRequest
|
import com.mycrib.shared.models.TaskCompletionCreateRequest
|
||||||
import kotlin.time.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -106,7 +106,6 @@ fun CompleteTaskDialog(
|
|||||||
// Helper function to get current date/time in ISO format
|
// Helper function to get current date/time in ISO format
|
||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
private fun getCurrentDateTime(): String {
|
private fun getCurrentDateTime(): String {
|
||||||
// This is a simplified version - in production you'd use kotlinx.datetime
|
val now = kotlin.time.Clock.System.now()
|
||||||
val now = Clock.System.now()
|
|
||||||
return now.toString()
|
return now.toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,84 +371,86 @@ fun TaskCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show completions
|
if (!task.frequency.equals("once")) {
|
||||||
if (task.completions.isNotEmpty()) {
|
// Show completions
|
||||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
if (task.completions.isNotEmpty()) {
|
||||||
Text(
|
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
text = "Completions (${task.completions.size})",
|
Text(
|
||||||
style = MaterialTheme.typography.titleSmall,
|
text = "Completions (${task.completions.size})",
|
||||||
color = MaterialTheme.colorScheme.primary
|
style = MaterialTheme.typography.titleSmall,
|
||||||
)
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
task.completions.forEach { completion ->
|
task.completions.forEach { completion ->
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = completion.completionDate.split("T")[0],
|
modifier = Modifier.fillMaxWidth(),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
color = MaterialTheme.colorScheme.primary
|
) {
|
||||||
)
|
|
||||||
completion.rating?.let { rating ->
|
|
||||||
Text(
|
Text(
|
||||||
text = "$rating★",
|
text = completion.completionDate.split("T")[0],
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.tertiary
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
completion.rating?.let { rating ->
|
||||||
|
Text(
|
||||||
|
text = "$rating★",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completion.completedByName?.let {
|
||||||
|
Text(
|
||||||
|
text = "By: $it",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
completion.completedByName?.let {
|
completion.actualCost?.let {
|
||||||
Text(
|
Text(
|
||||||
text = "By: $it",
|
text = "Cost: $$it",
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
completion.actualCost?.let {
|
completion.notes?.let {
|
||||||
Text(
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
text = "Cost: $$it",
|
Text(
|
||||||
style = MaterialTheme.typography.bodySmall
|
text = it,
|
||||||
)
|
style = MaterialTheme.typography.bodySmall,
|
||||||
}
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
completion.notes?.let {
|
}
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Complete task button
|
// Complete task button
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = onCompleteClick,
|
onClick = onCompleteClick,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CheckCircle,
|
Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text("Complete Task")
|
Text("Complete Task")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ fun ResidencesScreen(
|
|||||||
onAddResidence: () -> Unit,
|
onAddResidence: () -> Unit,
|
||||||
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
||||||
) {
|
) {
|
||||||
val residencesState by viewModel.residencesState.collectAsState()
|
val myResidencesState by viewModel.myResidencesState.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadResidences()
|
viewModel.loadMyResidences()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -39,7 +39,7 @@ fun ResidencesScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
when (residencesState) {
|
when (myResidencesState) {
|
||||||
is ApiResult.Loading -> {
|
is ApiResult.Loading -> {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -59,19 +59,19 @@ fun ResidencesScreen(
|
|||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) {
|
||||||
Text(
|
Text(
|
||||||
text = "Error: ${(residencesState as ApiResult.Error).message}",
|
text = "Error: ${(myResidencesState as ApiResult.Error).message}",
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Button(onClick = { viewModel.loadResidences() }) {
|
Button(onClick = { viewModel.loadMyResidences() }) {
|
||||||
Text("Retry")
|
Text("Retry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
val residences = (residencesState as ApiResult.Success).data
|
val response = (myResidencesState as ApiResult.Success).data
|
||||||
if (residences.isEmpty()) {
|
if (response.residences.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -86,9 +86,92 @@ fun ResidencesScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues),
|
.padding(paddingValues),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
items(residences) { residence ->
|
// Summary Card
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(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
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Residences
|
||||||
|
items(response.residences) { residence ->
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -100,7 +183,7 @@ fun ResidencesScreen(
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = residence.name ?: "Unnamed Property",
|
text = residence.name,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -108,11 +191,33 @@ fun ResidencesScreen(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
if (residence.propertyType != null) {
|
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
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = residence.propertyType.replaceFirstChar { it.uppercase() },
|
text = "Tasks: ${residence.taskSummary.total}",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Completed: ${residence.taskSummary.completed}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Pending: ${residence.taskSummary.pending}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.mycrib.shared.models.Residence
|
import com.mycrib.shared.models.Residence
|
||||||
import com.mycrib.shared.models.ResidenceCreateRequest
|
import com.mycrib.shared.models.ResidenceCreateRequest
|
||||||
import com.mycrib.shared.models.ResidenceSummaryResponse
|
import com.mycrib.shared.models.ResidenceSummaryResponse
|
||||||
|
import com.mycrib.shared.models.MyResidencesResponse
|
||||||
import com.mycrib.shared.models.TasksByResidenceResponse
|
import com.mycrib.shared.models.TasksByResidenceResponse
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
import com.mycrib.shared.network.ResidenceApi
|
import com.mycrib.shared.network.ResidenceApi
|
||||||
@@ -30,6 +31,9 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
private val _residenceTasksState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading)
|
private val _residenceTasksState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading)
|
||||||
val residenceTasksState: StateFlow<ApiResult<TasksByResidenceResponse>> = _residenceTasksState
|
val residenceTasksState: StateFlow<ApiResult<TasksByResidenceResponse>> = _residenceTasksState
|
||||||
|
|
||||||
|
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Loading)
|
||||||
|
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
|
||||||
|
|
||||||
fun loadResidences() {
|
fun loadResidences() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_residencesState.value = ApiResult.Loading
|
_residencesState.value = ApiResult.Loading
|
||||||
@@ -93,4 +97,16 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
fun resetCreateState() {
|
fun resetCreateState() {
|
||||||
_createResidenceState.value = ApiResult.Loading
|
_createResidenceState.value = ApiResult.Loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadMyResidences() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_myResidencesState.value = ApiResult.Loading
|
||||||
|
val token = TokenStorage.getToken()
|
||||||
|
if (token != null) {
|
||||||
|
_myResidencesState.value = residenceApi.getMyResidences(token)
|
||||||
|
} else {
|
||||||
|
_myResidencesState.value = ApiResult.Error("Not authenticated", 401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ androidx-appcompat = "1.7.1"
|
|||||||
androidx-core = "1.17.0"
|
androidx-core = "1.17.0"
|
||||||
androidx-espresso = "3.7.0"
|
androidx-espresso = "3.7.0"
|
||||||
androidx-lifecycle = "2.9.5"
|
androidx-lifecycle = "2.9.5"
|
||||||
|
androidx-navigation = "2.9.1"
|
||||||
androidx-testExt = "1.3.0"
|
androidx-testExt = "1.3.0"
|
||||||
composeHotReload = "1.0.0-rc02"
|
composeHotReload = "1.0.0-rc02"
|
||||||
composeMultiplatform = "1.9.1"
|
composeMultiplatform = "1.9.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.2.20"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
|
kotlinx-datetime = "0.6.0"
|
||||||
ktor = "3.3.1"
|
ktor = "3.3.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -27,7 +29,9 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a
|
|||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||||
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
|
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||||
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user