This commit is contained in:
Trey t
2025-11-04 15:41:03 -06:00
parent f2ade0a1e2
commit 219eaa69ee
17 changed files with 637 additions and 92 deletions

View File

@@ -53,6 +53,17 @@ kotlin {
iosMain.dependencies { iosMain.dependencies {
implementation(libs.ktor.client.darwin) implementation(libs.ktor.client.darwin)
} }
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
implementation(libs.ktor.client.cio)
}
jsMain.dependencies {
implementation(libs.ktor.client.js)
}
wasmJsMain.dependencies {
implementation(libs.ktor.client.js)
}
commonMain.dependencies { commonMain.dependencies {
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.content.negotiation)
@@ -74,10 +85,6 @@ kotlin {
commonTest.dependencies { commonTest.dependencies {
implementation(libs.kotlin.test) implementation(libs.kotlin.test)
} }
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
}
} }
} }

View File

@@ -1,3 +1,27 @@
package com.mycrib.shared.network package com.mycrib.shared.network
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
actual fun getLocalhostAddress(): String = "10.0.2.2" actual fun getLocalhostAddress(): String = "10.0.2.2"
actual fun createHttpClient(): HttpClient {
return HttpClient(OkHttp) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
}

View File

@@ -106,6 +106,14 @@ fun App() {
}, },
onAddResidence = { onAddResidence = {
navController.navigate(AddResidenceRoute) navController.navigate(AddResidenceRoute)
},
onLogout = {
// Clear token on logout
com.mycrib.storage.TokenStorage.clearToken()
isLoggedIn = false
navController.navigate(LoginRoute) {
popUpTo<HomeRoute> { inclusive = true }
}
} }
) )
} }

View File

@@ -20,6 +20,7 @@ data class Task(
val notes: String?, val notes: String?,
@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("show_completed_button") val showCompletedButton: Boolean = false,
@SerialName("days_until_due") val daysUntilDue: Int? = null, @SerialName("days_until_due") val daysUntilDue: Int? = null,
@SerialName("is_overdue") val isOverdue: Boolean? = null, @SerialName("is_overdue") val isOverdue: Boolean? = null,
@SerialName("last_completion") val lastCompletion: LastCompletion? = null @SerialName("last_completion") val lastCompletion: LastCompletion? = null
@@ -39,11 +40,12 @@ data class TaskCreateRequest(
val title: String, val title: String,
val description: String? = null, val description: String? = null,
val category: String, val category: String,
val priority: String, val frequency: String = "once",
@SerialName("interval_days") val intervalDays: Int? = null,
val priority: String = "medium",
val status: String = "pending", val status: String = "pending",
@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
val notes: String? = null
) )
@Serializable @Serializable
@@ -65,6 +67,7 @@ data class TaskDetail(
@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, @SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
val completions: List<TaskCompletion> val completions: List<TaskCompletion>
) )

View File

@@ -7,24 +7,12 @@ import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
expect fun getLocalhostAddress(): String expect fun getLocalhostAddress(): String
expect fun createHttpClient(): HttpClient
object ApiClient { object ApiClient {
private val BASE_URL = "http://${getLocalhostAddress()}:8000/api" private val BASE_URL = "http://${getLocalhostAddress()}:8000/api"
val httpClient = HttpClient { val httpClient = createHttpClient()
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
fun getBaseUrl() = BASE_URL fun getBaseUrl() = BASE_URL
} }

View File

@@ -0,0 +1,305 @@
package com.mycrib.android.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.mycrib.shared.models.TaskCreateRequest
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddNewTaskDialog(
residenceId: Int,
onDismiss: () -> Unit,
onCreate: (TaskCreateRequest) -> Unit
) {
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 showFrequencyDropdown by remember { mutableStateOf(false) }
var showPriorityDropdown by remember { mutableStateOf(false) }
var showCategoryDropdown by remember { mutableStateOf(false) }
var titleError by remember { mutableStateOf(false) }
var categoryError by remember { mutableStateOf(false) }
var dueDateError by remember { mutableStateOf(false) }
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"
)
val priorities = listOf(
"low" to "Low",
"medium" to "Medium",
"high" to "High",
"urgent" to "Urgent"
)
val categories = listOf(
"Plumbing",
"Electrical",
"HVAC",
"Landscaping",
"Painting",
"Roofing",
"Flooring",
"Appliances",
"General Maintenance",
"Cleaning",
"Inspection",
"Other"
)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add New Task") },
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Title
OutlinedTextField(
value = title,
onValueChange = {
title = it
titleError = false
},
label = { Text("Title *") },
modifier = Modifier.fillMaxWidth(),
isError = titleError,
supportingText = if (titleError) {
{ Text("Title is required") }
} else null,
singleLine = true
)
// Description
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text("Description") },
modifier = Modifier.fillMaxWidth(),
minLines = 2,
maxLines = 4
)
// Category
ExposedDropdownMenuBox(
expanded = showCategoryDropdown,
onExpandedChange = { showCategoryDropdown = it }
) {
OutlinedTextField(
value = category,
onValueChange = {
category = it
categoryError = false
},
label = { Text("Category *") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
isError = categoryError,
supportingText = if (categoryError) {
{ Text("Category is required") }
} else null,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showCategoryDropdown) },
readOnly = false
)
ExposedDropdownMenu(
expanded = showCategoryDropdown,
onDismissRequest = { showCategoryDropdown = false }
) {
categories.forEach { cat ->
DropdownMenuItem(
text = { Text(cat) },
onClick = {
category = cat
categoryError = false
showCategoryDropdown = false
}
)
}
}
}
// Frequency
ExposedDropdownMenuBox(
expanded = showFrequencyDropdown,
onExpandedChange = { showFrequencyDropdown = it }
) {
OutlinedTextField(
value = frequencies.find { it.first == frequency }?.second ?: "One Time",
onValueChange = { },
label = { Text("Frequency") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) }
)
ExposedDropdownMenu(
expanded = showFrequencyDropdown,
onDismissRequest = { showFrequencyDropdown = false }
) {
frequencies.forEach { (key, label) ->
DropdownMenuItem(
text = { Text(label) },
onClick = {
frequency = key
showFrequencyDropdown = false
// Clear interval days if frequency is "once"
if (key == "once") {
intervalDays = ""
}
}
)
}
}
}
// Interval Days (only for recurring tasks)
if (frequency != "once") {
OutlinedTextField(
value = intervalDays,
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
label = { Text("Interval Days (optional)") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
supportingText = { Text("Override default frequency interval") },
singleLine = true
)
}
// Due Date
OutlinedTextField(
value = dueDate,
onValueChange = {
dueDate = it
dueDateError = false
},
label = { Text("Due Date (YYYY-MM-DD) *") },
modifier = Modifier.fillMaxWidth(),
isError = dueDateError,
supportingText = if (dueDateError) {
{ Text("Due date is required (format: YYYY-MM-DD)") }
} else {
{ Text("Format: YYYY-MM-DD") }
},
singleLine = true
)
// Priority
ExposedDropdownMenuBox(
expanded = showPriorityDropdown,
onExpandedChange = { showPriorityDropdown = it }
) {
OutlinedTextField(
value = priorities.find { it.first == priority }?.second ?: "Medium",
onValueChange = { },
label = { Text("Priority") },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) }
)
ExposedDropdownMenu(
expanded = showPriorityDropdown,
onDismissRequest = { showPriorityDropdown = false }
) {
priorities.forEach { (key, label) ->
DropdownMenuItem(
text = { Text(label) },
onClick = {
priority = key
showPriorityDropdown = false
}
)
}
}
}
// Estimated Cost
OutlinedTextField(
value = estimatedCost,
onValueChange = { estimatedCost = it },
label = { Text("Estimated Cost") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
prefix = { Text("$") },
singleLine = true
)
}
},
confirmButton = {
Button(
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
}
if (!hasError) {
onCreate(
TaskCreateRequest(
residence = residenceId,
title = title,
description = description.ifBlank { null },
category = category,
frequency = frequency,
intervalDays = intervalDays.toIntOrNull(),
priority = priority,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }
)
)
}
}
) {
Text("Create Task")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
// Helper function to validate date format
private fun isValidDateFormat(date: String): Boolean {
val datePattern = Regex("^\\d{4}-\\d{2}-\\d{2}$")
return datePattern.matches(date)
}

View File

@@ -9,12 +9,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RegisterScreen( fun RegisterScreen(
onRegisterSuccess: () -> Unit, onRegisterSuccess: () -> Unit,
onNavigateBack: () -> Unit onNavigateBack: () -> Unit,
viewModel: AuthViewModel = viewModel { AuthViewModel() }
) { ) {
var username by remember { mutableStateOf("") } var username by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") } var email by remember { mutableStateOf("") }
@@ -23,6 +28,18 @@ fun RegisterScreen(
var errorMessage by remember { mutableStateOf("") } var errorMessage by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(false) }
val createState by viewModel.registerState.collectAsState()
LaunchedEffect(createState) {
when (createState) {
is ApiResult.Success -> {
viewModel.resetRegisterState()
onRegisterSuccess()
}
else -> {}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -102,8 +119,7 @@ fun RegisterScreen(
else -> { else -> {
isLoading = true isLoading = true
errorMessage = "" errorMessage = ""
// TODO: Call API viewModel.register(username, email, password)
onRegisterSuccess()
} }
} }
}, },

View File

@@ -12,9 +12,11 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.ui.components.AddNewTaskDialog
import com.mycrib.android.ui.components.CompleteTaskDialog import com.mycrib.android.ui.components.CompleteTaskDialog
import com.mycrib.android.viewmodel.ResidenceViewModel import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.android.viewmodel.TaskCompletionViewModel import com.mycrib.android.viewmodel.TaskCompletionViewModel
import com.mycrib.android.viewmodel.TaskViewModel
import com.mycrib.shared.models.Residence import com.mycrib.shared.models.Residence
import com.mycrib.shared.models.TaskDetail import com.mycrib.shared.models.TaskDetail
import com.mycrib.shared.network.ApiResult import com.mycrib.shared.network.ApiResult
@@ -25,7 +27,8 @@ fun ResidenceDetailScreen(
residenceId: Int, residenceId: Int,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }, residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() } taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() },
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
) { ) {
var residenceState by remember { mutableStateOf<ApiResult<Residence>>(ApiResult.Loading) } var residenceState by remember { mutableStateOf<ApiResult<Residence>>(ApiResult.Loading) }
val tasksState by residenceViewModel.residenceTasksState.collectAsState() val tasksState by residenceViewModel.residenceTasksState.collectAsState()
@@ -34,6 +37,8 @@ fun ResidenceDetailScreen(
var showCompleteDialog by remember { mutableStateOf(false) } var showCompleteDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) } var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
var showNewTaskDialog by remember { mutableStateOf(false) }
LaunchedEffect(residenceId) { LaunchedEffect(residenceId) {
residenceViewModel.getResidence(residenceId) { result -> residenceViewModel.getResidence(residenceId) { result ->
residenceState = result residenceState = result
@@ -70,6 +75,18 @@ fun ResidenceDetailScreen(
) )
} }
if (showNewTaskDialog) {
AddNewTaskDialog(
residenceId,
onDismiss = {
showNewTaskDialog = false
}, onCreate = { request ->
showNewTaskDialog = false
val newTask = taskViewModel.createNewTask(request)
residenceViewModel.loadResidenceTasks(residenceId)
})
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -238,6 +255,19 @@ fun ResidenceDetailScreen(
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Button(
onClick = { showNewTaskDialog = true },
modifier = Modifier.fillMaxWidth()
) {
Icon(
Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Add New Task")
}
} }
when (tasksState) { when (tasksState) {
@@ -354,6 +384,24 @@ fun TaskCard(
) )
} }
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
)
task.estimatedCost?.let {
Text(
text = "Est. Cost: $$it",
style = MaterialTheme.typography.bodySmall
)
}
}
} ?: run {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -370,8 +418,9 @@ fun TaskCard(
) )
} }
} }
}
if (!task.frequency.equals("once")) {
// Show completions // Show completions
if (task.completions.isNotEmpty()) { if (task.completions.isNotEmpty()) {
Divider(modifier = Modifier.padding(vertical = 8.dp)) Divider(modifier = Modifier.padding(vertical = 8.dp))
@@ -437,7 +486,8 @@ fun TaskCard(
} }
} }
// Complete task button // Show complete task button based on API logic
if (task.showCompletedButton) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Button( Button(
onClick = onCompleteClick, onClick = onCompleteClick,

View File

@@ -11,14 +11,17 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.AuthViewModel
import com.mycrib.android.viewmodel.ResidenceViewModel import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.shared.network.ApiResult import com.mycrib.shared.network.ApiResult
import com.mycrib.shared.network.AuthApi
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ResidencesScreen( fun ResidencesScreen(
onResidenceClick: (Int) -> Unit, onResidenceClick: (Int) -> Unit,
onAddResidence: () -> Unit, onAddResidence: () -> Unit,
onLogout: () -> Unit,
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() } viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) { ) {
val myResidencesState by viewModel.myResidencesState.collectAsState() val myResidencesState by viewModel.myResidencesState.collectAsState()
@@ -35,6 +38,9 @@ fun ResidencesScreen(
IconButton(onClick = onAddResidence) { IconButton(onClick = onAddResidence) {
Icon(Icons.Default.Add, contentDescription = "Add") Icon(Icons.Default.Add, contentDescription = "Add")
} }
IconButton(onClick = onLogout) {
Icon(Icons.Default.ExitToApp, contentDescription = "Logout")
}
} }
) )
} }

View File

@@ -2,8 +2,10 @@ package com.mycrib.android.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.mycrib.shared.models.AuthResponse
import com.mycrib.shared.models.LoginRequest import com.mycrib.shared.models.LoginRequest
import com.mycrib.shared.models.RegisterRequest import com.mycrib.shared.models.RegisterRequest
import com.mycrib.shared.models.Residence
import com.mycrib.shared.network.ApiResult import com.mycrib.shared.network.ApiResult
import com.mycrib.shared.network.AuthApi import com.mycrib.shared.network.AuthApi
import com.mycrib.storage.TokenStorage import com.mycrib.storage.TokenStorage
@@ -17,6 +19,9 @@ class AuthViewModel : ViewModel() {
private val _loginState = MutableStateFlow<ApiResult<String>>(ApiResult.Loading) private val _loginState = MutableStateFlow<ApiResult<String>>(ApiResult.Loading)
val loginState: StateFlow<ApiResult<String>> = _loginState val loginState: StateFlow<ApiResult<String>> = _loginState
private val _registerState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Loading)
val registerState: StateFlow<ApiResult<AuthResponse>> = _registerState
fun login(username: String, password: String) { fun login(username: String, password: String) {
viewModelScope.launch { viewModelScope.launch {
_loginState.value = ApiResult.Loading _loginState.value = ApiResult.Loading
@@ -35,7 +40,7 @@ class AuthViewModel : ViewModel() {
fun register(username: String, email: String, password: String) { fun register(username: String, email: String, password: String) {
viewModelScope.launch { viewModelScope.launch {
_loginState.value = ApiResult.Loading _registerState.value = ApiResult.Loading
val result = authApi.register( val result = authApi.register(
RegisterRequest( RegisterRequest(
username = username, username = username,
@@ -43,11 +48,11 @@ class AuthViewModel : ViewModel() {
password = password password = password
) )
) )
_loginState.value = when (result) { _registerState.value = when (result) {
is ApiResult.Success -> { is ApiResult.Success -> {
// Store token for future API calls // Store token for future API calls
TokenStorage.saveToken(result.data.token) TokenStorage.saveToken(result.data.token)
ApiResult.Success(result.data.token) ApiResult.Success(result.data)
} }
is ApiResult.Error -> result is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error") else -> ApiResult.Error("Unknown error")
@@ -55,6 +60,10 @@ class AuthViewModel : ViewModel() {
} }
} }
fun resetRegisterState() {
_registerState.value = ApiResult.Loading
}
fun logout() { fun logout() {
viewModelScope.launch { viewModelScope.launch {
val token = TokenStorage.getToken() val token = TokenStorage.getToken()

View File

@@ -2,7 +2,10 @@ package com.mycrib.android.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.mycrib.shared.models.ResidenceCreateRequest
import com.mycrib.shared.models.Task import com.mycrib.shared.models.Task
import com.mycrib.shared.models.TaskCreateRequest
import com.mycrib.shared.models.TaskDetail
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.TaskApi import com.mycrib.shared.network.TaskApi
@@ -20,6 +23,9 @@ class TaskViewModel : ViewModel() {
private val _tasksByResidenceState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading) private val _tasksByResidenceState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading)
val tasksByResidenceState: StateFlow<ApiResult<TasksByResidenceResponse>> = _tasksByResidenceState val tasksByResidenceState: StateFlow<ApiResult<TasksByResidenceResponse>> = _tasksByResidenceState
private val _taskAddNewTaskState = MutableStateFlow<ApiResult<Task>>(ApiResult.Loading)
val taskAddNewTaskState: StateFlow<ApiResult<Task>> = _taskAddNewTaskState
fun loadTasks() { fun loadTasks() {
viewModelScope.launch { viewModelScope.launch {
_tasksState.value = ApiResult.Loading _tasksState.value = ApiResult.Loading
@@ -43,4 +49,20 @@ 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)
}
}
}
fun resetCreateTaskState() {
_taskAddNewTaskState.value = ApiResult.Loading
}
} }

View File

@@ -1,3 +1,33 @@
package com.mycrib.shared.network package com.mycrib.shared.network
import io.ktor.client.*
import io.ktor.client.engine.darwin.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
actual fun getLocalhostAddress(): String = "127.0.0.1" actual fun getLocalhostAddress(): String = "127.0.0.1"
actual fun createHttpClient(): HttpClient {
return HttpClient(Darwin) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
engine {
configureRequest {
setAllowsCellularAccess(true)
}
}
}
}

View File

@@ -1,3 +1,27 @@
package com.mycrib.shared.network package com.mycrib.shared.network
import io.ktor.client.*
import io.ktor.client.engine.js.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
actual fun getLocalhostAddress(): String = "127.0.0.1" actual fun getLocalhostAddress(): String = "127.0.0.1"
actual fun createHttpClient(): HttpClient {
return HttpClient(Js) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
}

View File

@@ -1,3 +1,27 @@
package com.mycrib.shared.network package com.mycrib.shared.network
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
actual fun getLocalhostAddress(): String = "127.0.0.1" actual fun getLocalhostAddress(): String = "127.0.0.1"
actual fun createHttpClient(): HttpClient {
return HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
}

View File

@@ -1,3 +1,27 @@
package com.mycrib.shared.network package com.mycrib.shared.network
import io.ktor.client.*
import io.ktor.client.engine.js.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
actual fun getLocalhostAddress(): String = "127.0.0.1" actual fun getLocalhostAddress(): String = "127.0.0.1"
actual fun createHttpClient(): HttpClient {
return HttpClient(Js) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
}

View File

@@ -10,3 +10,6 @@ org.gradle.caching=true
#Android #Android
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.useAndroidX=true android.useAndroidX=true
kotlin.native.binary.objcDisposeOnMain=false

View File

@@ -40,6 +40,8 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }