This commit is contained in:
Trey t
2025-11-04 11:42:00 -06:00
parent de1c7931e9
commit ba27ddda71
11 changed files with 323 additions and 117 deletions

View File

@@ -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)

View File

@@ -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()
}
)
}
} }
} }
} }

View File

@@ -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>
) )

View File

@@ -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>
) )

View File

@@ -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

View File

@@ -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

View File

@@ -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()
} }

View File

@@ -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")
}
} }
} }
} }

View File

@@ -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
) )
} }
} }

View File

@@ -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)
}
}
}
} }

View File

@@ -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" }