wip
This commit is contained in:
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user