Rebrand from Casera/MyCrib to honeyDue
Total rebrand across KMM project: - Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations) - Gradle: rootProject.name, namespace, applicationId - Android: manifest, strings.xml (all languages), widget resources - iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig - iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc. - Swift source: all class/struct/enum renames - Deep links: casera:// -> honeydue://, .casera -> .honeydue - App icons replaced with honeyDue honeycomb icon - Domains: casera.treytartt.com -> honeyDue.treytartt.com - Bundle IDs: com.tt.casera -> com.tt.honeyDue - Database table names preserved Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.AppleSignInRequest
|
||||
import com.tt.honeyDue.models.AppleSignInResponse
|
||||
import com.tt.honeyDue.models.GoogleSignInRequest
|
||||
import com.tt.honeyDue.models.GoogleSignInResponse
|
||||
import com.tt.honeyDue.models.AuthResponse
|
||||
import com.tt.honeyDue.models.ForgotPasswordRequest
|
||||
import com.tt.honeyDue.models.ForgotPasswordResponse
|
||||
import com.tt.honeyDue.models.LoginRequest
|
||||
import com.tt.honeyDue.models.RegisterRequest
|
||||
import com.tt.honeyDue.models.ResetPasswordRequest
|
||||
import com.tt.honeyDue.models.ResetPasswordResponse
|
||||
import com.tt.honeyDue.models.Residence
|
||||
import com.tt.honeyDue.models.User
|
||||
import com.tt.honeyDue.models.VerifyEmailRequest
|
||||
import com.tt.honeyDue.models.VerifyEmailResponse
|
||||
import com.tt.honeyDue.models.VerifyResetCodeRequest
|
||||
import com.tt.honeyDue.models.VerifyResetCodeResponse
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AuthViewModel : ViewModel() {
|
||||
|
||||
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
|
||||
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
|
||||
|
||||
private val _registerState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
|
||||
val registerState: StateFlow<ApiResult<AuthResponse>> = _registerState
|
||||
|
||||
private val _verifyEmailState = MutableStateFlow<ApiResult<VerifyEmailResponse>>(ApiResult.Idle)
|
||||
val verifyEmailState: StateFlow<ApiResult<VerifyEmailResponse>> = _verifyEmailState
|
||||
|
||||
private val _updateProfileState = MutableStateFlow<ApiResult<User>>(ApiResult.Idle)
|
||||
val updateProfileState: StateFlow<ApiResult<User>> = _updateProfileState
|
||||
|
||||
private val _currentUserState = MutableStateFlow<ApiResult<User>>(ApiResult.Idle)
|
||||
val currentUserState: StateFlow<ApiResult<User>> = _currentUserState
|
||||
|
||||
private val _forgotPasswordState = MutableStateFlow<ApiResult<ForgotPasswordResponse>>(ApiResult.Idle)
|
||||
val forgotPasswordState: StateFlow<ApiResult<ForgotPasswordResponse>> = _forgotPasswordState
|
||||
|
||||
private val _verifyResetCodeState = MutableStateFlow<ApiResult<VerifyResetCodeResponse>>(ApiResult.Idle)
|
||||
val verifyResetCodeState: StateFlow<ApiResult<VerifyResetCodeResponse>> = _verifyResetCodeState
|
||||
|
||||
private val _resetPasswordState = MutableStateFlow<ApiResult<ResetPasswordResponse>>(ApiResult.Idle)
|
||||
val resetPasswordState: StateFlow<ApiResult<ResetPasswordResponse>> = _resetPasswordState
|
||||
|
||||
private val _appleSignInState = MutableStateFlow<ApiResult<AppleSignInResponse>>(ApiResult.Idle)
|
||||
val appleSignInState: StateFlow<ApiResult<AppleSignInResponse>> = _appleSignInState
|
||||
|
||||
private val _googleSignInState = MutableStateFlow<ApiResult<GoogleSignInResponse>>(ApiResult.Idle)
|
||||
val googleSignInState: StateFlow<ApiResult<GoogleSignInResponse>> = _googleSignInState
|
||||
|
||||
fun login(username: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
_loginState.value = ApiResult.Loading
|
||||
val result = APILayer.login(LoginRequest(username, password))
|
||||
// APILayer.login already stores token in DataManager
|
||||
_loginState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun register(username: String, email: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
_registerState.value = ApiResult.Loading
|
||||
val result = APILayer.register(
|
||||
RegisterRequest(
|
||||
username = username,
|
||||
email = email,
|
||||
password = password
|
||||
)
|
||||
)
|
||||
_registerState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Store token in DataManager for future API calls
|
||||
DataManager.setAuthToken(result.data.token)
|
||||
DataManager.setCurrentUser(result.data.user)
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetRegisterState() {
|
||||
_registerState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun verifyEmail(code: String) {
|
||||
viewModelScope.launch {
|
||||
_verifyEmailState.value = ApiResult.Loading
|
||||
val token = DataManager.authToken.value ?: run {
|
||||
_verifyEmailState.value = ApiResult.Error("Not authenticated")
|
||||
return@launch
|
||||
}
|
||||
val result = APILayer.verifyEmail(token, VerifyEmailRequest(code = code))
|
||||
_verifyEmailState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetVerifyEmailState() {
|
||||
_verifyEmailState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun updateProfile(firstName: String?, lastName: String?, email: String?) {
|
||||
viewModelScope.launch {
|
||||
_updateProfileState.value = ApiResult.Loading
|
||||
val token = DataManager.authToken.value ?: run {
|
||||
_updateProfileState.value = ApiResult.Error("Not authenticated")
|
||||
return@launch
|
||||
}
|
||||
val result = APILayer.updateProfile(
|
||||
token,
|
||||
com.tt.honeyDue.models.UpdateProfileRequest(
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
email = email
|
||||
)
|
||||
)
|
||||
_updateProfileState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetUpdateProfileState() {
|
||||
_updateProfileState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun getCurrentUser(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_currentUserState.value = ApiResult.Loading
|
||||
val result = APILayer.getCurrentUser(forceRefresh = forceRefresh)
|
||||
_currentUserState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCurrentUserState() {
|
||||
_currentUserState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun forgotPassword(email: String) {
|
||||
viewModelScope.launch {
|
||||
_forgotPasswordState.value = ApiResult.Loading
|
||||
val result = APILayer.forgotPassword(ForgotPasswordRequest(email = email))
|
||||
_forgotPasswordState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetForgotPasswordState() {
|
||||
_forgotPasswordState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun verifyResetCode(email: String, code: String) {
|
||||
viewModelScope.launch {
|
||||
_verifyResetCodeState.value = ApiResult.Loading
|
||||
val result = APILayer.verifyResetCode(VerifyResetCodeRequest(email = email, code = code))
|
||||
_verifyResetCodeState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetVerifyResetCodeState() {
|
||||
_verifyResetCodeState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetPassword(resetToken: String, newPassword: String, confirmPassword: String) {
|
||||
viewModelScope.launch {
|
||||
_resetPasswordState.value = ApiResult.Loading
|
||||
// Note: confirmPassword is for UI validation only, not sent to API
|
||||
val result = APILayer.resetPassword(
|
||||
ResetPasswordRequest(
|
||||
resetToken = resetToken,
|
||||
newPassword = newPassword
|
||||
)
|
||||
)
|
||||
_resetPasswordState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetResetPasswordState() {
|
||||
_resetPasswordState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun appleSignIn(
|
||||
idToken: String,
|
||||
userId: String,
|
||||
email: String?,
|
||||
firstName: String?,
|
||||
lastName: String?
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_appleSignInState.value = ApiResult.Loading
|
||||
val result = APILayer.appleSignIn(
|
||||
AppleSignInRequest(
|
||||
idToken = idToken,
|
||||
userId = userId,
|
||||
email = email,
|
||||
firstName = firstName,
|
||||
lastName = lastName
|
||||
)
|
||||
)
|
||||
// APILayer.appleSignIn already stores token in DataManager
|
||||
_appleSignInState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetAppleSignInState() {
|
||||
_appleSignInState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun googleSignIn(idToken: String) {
|
||||
viewModelScope.launch {
|
||||
_googleSignInState.value = ApiResult.Loading
|
||||
val result = APILayer.googleSignIn(
|
||||
GoogleSignInRequest(idToken = idToken)
|
||||
)
|
||||
// APILayer.googleSignIn already stores token in DataManager
|
||||
_googleSignInState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetGoogleSignInState() {
|
||||
_googleSignInState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
// APILayer.logout clears DataManager
|
||||
APILayer.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.*
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ContractorViewModel : ViewModel() {
|
||||
|
||||
private val _contractorsState = MutableStateFlow<ApiResult<List<ContractorSummary>>>(ApiResult.Idle)
|
||||
val contractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _contractorsState
|
||||
|
||||
private val _contractorDetailState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
||||
val contractorDetailState: StateFlow<ApiResult<Contractor>> = _contractorDetailState
|
||||
|
||||
private val _createState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
||||
val createState: StateFlow<ApiResult<Contractor>> = _createState
|
||||
|
||||
private val _updateState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
||||
val updateState: StateFlow<ApiResult<Contractor>> = _updateState
|
||||
|
||||
private val _deleteState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val deleteState: StateFlow<ApiResult<Unit>> = _deleteState
|
||||
|
||||
private val _toggleFavoriteState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
||||
val toggleFavoriteState: StateFlow<ApiResult<Contractor>> = _toggleFavoriteState
|
||||
|
||||
fun loadContractors(
|
||||
specialty: String? = null,
|
||||
isFavorite: Boolean? = null,
|
||||
isActive: Boolean? = null,
|
||||
search: String? = null,
|
||||
forceRefresh: Boolean = false
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_contractorsState.value = ApiResult.Loading
|
||||
_contractorsState.value = APILayer.getContractors(
|
||||
specialty = specialty,
|
||||
isFavorite = isFavorite,
|
||||
isActive = isActive,
|
||||
search = search,
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadContractorDetail(id: Int) {
|
||||
viewModelScope.launch {
|
||||
_contractorDetailState.value = ApiResult.Loading
|
||||
_contractorDetailState.value = APILayer.getContractor(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun createContractor(request: ContractorCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_createState.value = ApiResult.Loading
|
||||
_createState.value = APILayer.createContractor(request)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContractor(id: Int, request: ContractorUpdateRequest) {
|
||||
viewModelScope.launch {
|
||||
_updateState.value = ApiResult.Loading
|
||||
_updateState.value = APILayer.updateContractor(id, request)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteContractor(id: Int) {
|
||||
viewModelScope.launch {
|
||||
_deleteState.value = ApiResult.Loading
|
||||
_deleteState.value = APILayer.deleteContractor(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFavorite(id: Int) {
|
||||
viewModelScope.launch {
|
||||
_toggleFavoriteState.value = ApiResult.Loading
|
||||
_toggleFavoriteState.value = APILayer.toggleFavorite(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCreateState() {
|
||||
_createState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUpdateState() {
|
||||
_updateState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetDeleteState() {
|
||||
_deleteState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetToggleFavoriteState() {
|
||||
_toggleFavoriteState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.*
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.util.ImageCompressor
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DocumentViewModel : ViewModel() {
|
||||
|
||||
private val _documentsState = MutableStateFlow<ApiResult<List<Document>>>(ApiResult.Idle)
|
||||
val documentsState: StateFlow<ApiResult<List<Document>>> = _documentsState
|
||||
|
||||
private val _documentDetailState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
||||
val documentDetailState: StateFlow<ApiResult<Document>> = _documentDetailState
|
||||
|
||||
private val _createState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
||||
val createState: StateFlow<ApiResult<Document>> = _createState
|
||||
|
||||
private val _updateState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
||||
val updateState: StateFlow<ApiResult<Document>> = _updateState
|
||||
|
||||
private val _deleteState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val deleteState: StateFlow<ApiResult<Unit>> = _deleteState
|
||||
|
||||
private val _downloadState = MutableStateFlow<ApiResult<ByteArray>>(ApiResult.Idle)
|
||||
val downloadState: StateFlow<ApiResult<ByteArray>> = _downloadState
|
||||
|
||||
private val _deleteImageState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
||||
val deleteImageState: StateFlow<ApiResult<Document>> = _deleteImageState
|
||||
|
||||
private val _uploadImageState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
||||
val uploadImageState: StateFlow<ApiResult<Document>> = _uploadImageState
|
||||
|
||||
fun loadDocuments(
|
||||
residenceId: Int? = null,
|
||||
documentType: String? = null,
|
||||
category: String? = null,
|
||||
contractorId: Int? = null,
|
||||
isActive: Boolean? = null,
|
||||
expiringSoon: Int? = null,
|
||||
tags: String? = null,
|
||||
search: String? = null,
|
||||
forceRefresh: Boolean = false
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_documentsState.value = ApiResult.Loading
|
||||
_documentsState.value = APILayer.getDocuments(
|
||||
residenceId = residenceId,
|
||||
documentType = documentType,
|
||||
category = category,
|
||||
contractorId = contractorId,
|
||||
isActive = isActive,
|
||||
expiringSoon = expiringSoon,
|
||||
tags = tags,
|
||||
search = search,
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all documents without any filters - filtering is done client-side.
|
||||
* This reduces API calls when switching tabs or applying filters.
|
||||
*/
|
||||
fun loadAllDocuments(
|
||||
residenceId: Int? = null,
|
||||
forceRefresh: Boolean = false
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_documentsState.value = ApiResult.Loading
|
||||
_documentsState.value = APILayer.getDocuments(
|
||||
residenceId = residenceId,
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDocumentDetail(id: Int) {
|
||||
viewModelScope.launch {
|
||||
_documentDetailState.value = ApiResult.Loading
|
||||
_documentDetailState.value = APILayer.getDocument(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun createDocument(
|
||||
title: String,
|
||||
documentType: String,
|
||||
residenceId: Int,
|
||||
description: String? = null,
|
||||
category: String? = null,
|
||||
tags: String? = null,
|
||||
notes: String? = null,
|
||||
contractorId: Int? = null,
|
||||
isActive: Boolean = true,
|
||||
// Warranty-specific fields
|
||||
itemName: String? = null,
|
||||
modelNumber: String? = null,
|
||||
serialNumber: String? = null,
|
||||
provider: String? = null,
|
||||
providerContact: String? = null,
|
||||
claimPhone: String? = null,
|
||||
claimEmail: String? = null,
|
||||
claimWebsite: String? = null,
|
||||
purchaseDate: String? = null,
|
||||
startDate: String? = null,
|
||||
endDate: String? = null,
|
||||
// Images
|
||||
images: List<com.tt.honeyDue.platform.ImageData> = emptyList()
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_createState.value = ApiResult.Loading
|
||||
// Compress images and convert to ByteArrays
|
||||
val fileBytesList = if (images.isNotEmpty()) {
|
||||
images.map { ImageCompressor.compressImage(it) }
|
||||
} else null
|
||||
|
||||
val fileNamesList = if (images.isNotEmpty()) {
|
||||
images.mapIndexed { index, image ->
|
||||
// Always use .jpg extension since we compress to JPEG
|
||||
val baseName = image.fileName.ifBlank { "image_$index" }
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) ||
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) {
|
||||
baseName
|
||||
} else {
|
||||
// Remove any existing extension and add .jpg
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg"
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
val mimeTypesList = if (images.isNotEmpty()) {
|
||||
images.map { "image/jpeg" }
|
||||
} else null
|
||||
|
||||
_createState.value = APILayer.createDocument(
|
||||
title = title,
|
||||
documentType = documentType,
|
||||
residenceId = residenceId,
|
||||
description = description,
|
||||
category = category,
|
||||
tags = tags,
|
||||
notes = notes,
|
||||
contractorId = contractorId,
|
||||
isActive = isActive,
|
||||
itemName = itemName,
|
||||
modelNumber = modelNumber,
|
||||
serialNumber = serialNumber,
|
||||
provider = provider,
|
||||
providerContact = providerContact,
|
||||
claimPhone = claimPhone,
|
||||
claimEmail = claimEmail,
|
||||
claimWebsite = claimWebsite,
|
||||
purchaseDate = purchaseDate,
|
||||
startDate = startDate,
|
||||
endDate = endDate,
|
||||
fileBytes = null,
|
||||
fileName = null,
|
||||
mimeType = null,
|
||||
fileBytesList = fileBytesList,
|
||||
fileNamesList = fileNamesList,
|
||||
mimeTypesList = mimeTypesList
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDocument(
|
||||
id: Int,
|
||||
title: String,
|
||||
documentType: String,
|
||||
description: String? = null,
|
||||
category: String? = null,
|
||||
tags: String? = null,
|
||||
notes: String? = null,
|
||||
contractorId: Int? = null,
|
||||
isActive: Boolean = true,
|
||||
// Warranty-specific fields
|
||||
itemName: String? = null,
|
||||
modelNumber: String? = null,
|
||||
serialNumber: String? = null,
|
||||
provider: String? = null,
|
||||
providerContact: String? = null,
|
||||
claimPhone: String? = null,
|
||||
claimEmail: String? = null,
|
||||
claimWebsite: String? = null,
|
||||
purchaseDate: String? = null,
|
||||
startDate: String? = null,
|
||||
endDate: String? = null,
|
||||
// Images
|
||||
images: List<com.tt.honeyDue.platform.ImageData> = emptyList()
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_updateState.value = ApiResult.Loading
|
||||
// First, update the document metadata
|
||||
val updateResult = APILayer.updateDocument(
|
||||
id = id,
|
||||
title = title,
|
||||
documentType = documentType,
|
||||
description = description,
|
||||
category = category,
|
||||
tags = tags,
|
||||
notes = notes,
|
||||
contractorId = contractorId,
|
||||
isActive = isActive,
|
||||
itemName = itemName,
|
||||
modelNumber = modelNumber,
|
||||
serialNumber = serialNumber,
|
||||
provider = provider,
|
||||
providerContact = providerContact,
|
||||
claimPhone = claimPhone,
|
||||
claimEmail = claimEmail,
|
||||
claimWebsite = claimWebsite,
|
||||
purchaseDate = purchaseDate,
|
||||
startDate = startDate,
|
||||
endDate = endDate
|
||||
)
|
||||
|
||||
// If update succeeded and there are new images, upload them
|
||||
if (updateResult is ApiResult.Success && images.isNotEmpty()) {
|
||||
var uploadFailed = false
|
||||
for ((index, image) in images.withIndex()) {
|
||||
// Compress the image
|
||||
val compressedBytes = ImageCompressor.compressImage(image)
|
||||
|
||||
// Determine filename with .jpg extension
|
||||
val fileName = if (image.fileName.isNotBlank()) {
|
||||
val baseName = image.fileName
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) ||
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) {
|
||||
baseName
|
||||
} else {
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg"
|
||||
}
|
||||
} else {
|
||||
"image_$index.jpg"
|
||||
}
|
||||
|
||||
val uploadResult = APILayer.uploadDocumentImage(
|
||||
documentId = id,
|
||||
imageBytes = compressedBytes,
|
||||
fileName = fileName,
|
||||
mimeType = "image/jpeg"
|
||||
)
|
||||
|
||||
if (uploadResult is ApiResult.Error) {
|
||||
uploadFailed = true
|
||||
_updateState.value = ApiResult.Error(
|
||||
"Document updated but failed to upload image: ${uploadResult.message}",
|
||||
uploadResult.code
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If all uploads succeeded, use the last uploaded document (has all images)
|
||||
if (!uploadFailed) {
|
||||
_updateState.value = updateResult
|
||||
}
|
||||
} else {
|
||||
_updateState.value = updateResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDocument(id: Int) {
|
||||
viewModelScope.launch {
|
||||
_deleteState.value = ApiResult.Loading
|
||||
_deleteState.value = APILayer.deleteDocument(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadDocument(url: String) {
|
||||
viewModelScope.launch {
|
||||
_downloadState.value = ApiResult.Loading
|
||||
_downloadState.value = APILayer.downloadDocument(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCreateState() {
|
||||
_createState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUpdateState() {
|
||||
_updateState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetDeleteState() {
|
||||
_deleteState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetDownloadState() {
|
||||
_downloadState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun deleteDocumentImage(documentId: Int, imageId: Int) {
|
||||
viewModelScope.launch {
|
||||
_deleteImageState.value = ApiResult.Loading
|
||||
_deleteImageState.value = APILayer.deleteDocumentImage(documentId, imageId)
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadDocumentImage(
|
||||
documentId: Int,
|
||||
imageData: com.tt.honeyDue.platform.ImageData,
|
||||
caption: String? = null
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_uploadImageState.value = ApiResult.Loading
|
||||
val compressedBytes = ImageCompressor.compressImage(imageData)
|
||||
val fileName = if (imageData.fileName.isNotBlank()) {
|
||||
val baseName = imageData.fileName
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) ||
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) {
|
||||
baseName
|
||||
} else {
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg"
|
||||
}
|
||||
} else {
|
||||
"image.jpg"
|
||||
}
|
||||
_uploadImageState.value = APILayer.uploadDocumentImage(
|
||||
documentId = documentId,
|
||||
imageBytes = compressedBytes,
|
||||
fileName = fileName,
|
||||
mimeType = "image/jpeg",
|
||||
caption = caption
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetDeleteImageState() {
|
||||
_deleteImageState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUploadImageState() {
|
||||
_uploadImageState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.*
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* ViewModel for lookup data.
|
||||
* Now uses DataManager as the single source of truth for all lookups.
|
||||
* Lookups are loaded once via APILayer.initializeLookups() after login.
|
||||
*/
|
||||
class LookupsViewModel : ViewModel() {
|
||||
|
||||
// Expose DataManager's lookup StateFlows directly
|
||||
val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes
|
||||
val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies
|
||||
val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities
|
||||
val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories
|
||||
val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties
|
||||
|
||||
// Keep legacy state flows for compatibility during migration
|
||||
private val _residenceTypesState = MutableStateFlow<ApiResult<List<ResidenceType>>>(ApiResult.Idle)
|
||||
val residenceTypesState: StateFlow<ApiResult<List<ResidenceType>>> = _residenceTypesState
|
||||
|
||||
private val _taskFrequenciesState = MutableStateFlow<ApiResult<List<TaskFrequency>>>(ApiResult.Idle)
|
||||
val taskFrequenciesState: StateFlow<ApiResult<List<TaskFrequency>>> = _taskFrequenciesState
|
||||
|
||||
private val _taskPrioritiesState = MutableStateFlow<ApiResult<List<TaskPriority>>>(ApiResult.Idle)
|
||||
val taskPrioritiesState: StateFlow<ApiResult<List<TaskPriority>>> = _taskPrioritiesState
|
||||
|
||||
private val _taskCategoriesState = MutableStateFlow<ApiResult<List<TaskCategory>>>(ApiResult.Idle)
|
||||
val taskCategoriesState: StateFlow<ApiResult<List<TaskCategory>>> = _taskCategoriesState
|
||||
|
||||
fun loadResidenceTypes() {
|
||||
viewModelScope.launch {
|
||||
val cached = DataManager.residenceTypes.value
|
||||
if (cached.isNotEmpty()) {
|
||||
_residenceTypesState.value = ApiResult.Success(cached)
|
||||
return@launch
|
||||
}
|
||||
_residenceTypesState.value = ApiResult.Loading
|
||||
val result = APILayer.getResidenceTypes()
|
||||
_residenceTypesState.value = result
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTaskFrequencies() {
|
||||
viewModelScope.launch {
|
||||
val cached = DataManager.taskFrequencies.value
|
||||
if (cached.isNotEmpty()) {
|
||||
_taskFrequenciesState.value = ApiResult.Success(cached)
|
||||
return@launch
|
||||
}
|
||||
_taskFrequenciesState.value = ApiResult.Loading
|
||||
val result = APILayer.getTaskFrequencies()
|
||||
_taskFrequenciesState.value = result
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTaskPriorities() {
|
||||
viewModelScope.launch {
|
||||
val cached = DataManager.taskPriorities.value
|
||||
if (cached.isNotEmpty()) {
|
||||
_taskPrioritiesState.value = ApiResult.Success(cached)
|
||||
return@launch
|
||||
}
|
||||
_taskPrioritiesState.value = ApiResult.Loading
|
||||
val result = APILayer.getTaskPriorities()
|
||||
_taskPrioritiesState.value = result
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTaskCategories() {
|
||||
viewModelScope.launch {
|
||||
val cached = DataManager.taskCategories.value
|
||||
if (cached.isNotEmpty()) {
|
||||
_taskCategoriesState.value = ApiResult.Success(cached)
|
||||
return@launch
|
||||
}
|
||||
_taskCategoriesState.value = ApiResult.Loading
|
||||
val result = APILayer.getTaskCategories()
|
||||
_taskCategoriesState.value = result
|
||||
}
|
||||
}
|
||||
|
||||
// Load all lookups at once
|
||||
fun loadAllLookups() {
|
||||
loadResidenceTypes()
|
||||
loadTaskFrequencies()
|
||||
loadTaskPriorities()
|
||||
loadTaskCategories()
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.NotificationPreference
|
||||
import com.tt.honeyDue.models.UpdateNotificationPreferencesRequest
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NotificationPreferencesViewModel : ViewModel() {
|
||||
|
||||
private val _preferencesState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
|
||||
val preferencesState: StateFlow<ApiResult<NotificationPreference>> = _preferencesState
|
||||
|
||||
private val _updateState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
|
||||
val updateState: StateFlow<ApiResult<NotificationPreference>> = _updateState
|
||||
|
||||
fun loadPreferences() {
|
||||
viewModelScope.launch {
|
||||
_preferencesState.value = ApiResult.Loading
|
||||
val result = APILayer.getNotificationPreferences()
|
||||
_preferencesState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePreference(
|
||||
taskDueSoon: Boolean? = null,
|
||||
taskOverdue: Boolean? = null,
|
||||
taskCompleted: Boolean? = null,
|
||||
taskAssigned: Boolean? = null,
|
||||
residenceShared: Boolean? = null,
|
||||
warrantyExpiring: Boolean? = null,
|
||||
dailyDigest: Boolean? = null,
|
||||
emailTaskCompleted: Boolean? = null,
|
||||
taskDueSoonHour: Int? = null,
|
||||
taskOverdueHour: Int? = null,
|
||||
warrantyExpiringHour: Int? = null,
|
||||
dailyDigestHour: Int? = null
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_updateState.value = ApiResult.Loading
|
||||
val request = UpdateNotificationPreferencesRequest(
|
||||
taskDueSoon = taskDueSoon,
|
||||
taskOverdue = taskOverdue,
|
||||
taskCompleted = taskCompleted,
|
||||
taskAssigned = taskAssigned,
|
||||
residenceShared = residenceShared,
|
||||
warrantyExpiring = warrantyExpiring,
|
||||
dailyDigest = dailyDigest,
|
||||
emailTaskCompleted = emailTaskCompleted,
|
||||
taskDueSoonHour = taskDueSoonHour,
|
||||
taskOverdueHour = taskOverdueHour,
|
||||
warrantyExpiringHour = warrantyExpiringHour,
|
||||
dailyDigestHour = dailyDigestHour
|
||||
)
|
||||
val result = APILayer.updateNotificationPreferences(request)
|
||||
_updateState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Update the preferences state with the new values
|
||||
_preferencesState.value = ApiResult.Success(result.data)
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetUpdateState() {
|
||||
_updateState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.AuthResponse
|
||||
import com.tt.honeyDue.models.LoginRequest
|
||||
import com.tt.honeyDue.models.RegisterRequest
|
||||
import com.tt.honeyDue.models.ResidenceCreateRequest
|
||||
import com.tt.honeyDue.models.TaskCreateRequest
|
||||
import com.tt.honeyDue.models.TaskTemplate
|
||||
import com.tt.honeyDue.models.VerifyEmailRequest
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.repository.LookupsRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* User's intent during onboarding
|
||||
*/
|
||||
enum class OnboardingIntent {
|
||||
UNKNOWN,
|
||||
START_FRESH, // Creating a new residence
|
||||
JOIN_EXISTING // Joining with a share code
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps in the onboarding flow
|
||||
*/
|
||||
enum class OnboardingStep {
|
||||
WELCOME,
|
||||
VALUE_PROPS,
|
||||
NAME_RESIDENCE,
|
||||
CREATE_ACCOUNT,
|
||||
VERIFY_EMAIL,
|
||||
JOIN_RESIDENCE,
|
||||
RESIDENCE_LOCATION,
|
||||
FIRST_TASK,
|
||||
SUBSCRIPTION_UPSELL
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewModel for managing the onboarding flow state
|
||||
*/
|
||||
class OnboardingViewModel : ViewModel() {
|
||||
|
||||
private val _currentStep = MutableStateFlow(OnboardingStep.WELCOME)
|
||||
val currentStep: StateFlow<OnboardingStep> = _currentStep
|
||||
|
||||
private val _userIntent = MutableStateFlow(OnboardingIntent.UNKNOWN)
|
||||
val userIntent: StateFlow<OnboardingIntent> = _userIntent
|
||||
|
||||
private val _residenceName = MutableStateFlow("")
|
||||
val residenceName: StateFlow<String> = _residenceName
|
||||
|
||||
private val _shareCode = MutableStateFlow("")
|
||||
val shareCode: StateFlow<String> = _shareCode
|
||||
|
||||
// Registration state
|
||||
private val _registerState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
|
||||
val registerState: StateFlow<ApiResult<AuthResponse>> = _registerState
|
||||
|
||||
// Login state (for returning users)
|
||||
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
|
||||
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
|
||||
|
||||
// Email verification state
|
||||
private val _verifyEmailState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val verifyEmailState: StateFlow<ApiResult<Unit>> = _verifyEmailState
|
||||
|
||||
// Residence creation state
|
||||
private val _createResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val createResidenceState: StateFlow<ApiResult<Unit>> = _createResidenceState
|
||||
|
||||
// Join residence state
|
||||
private val _joinResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val joinResidenceState: StateFlow<ApiResult<Unit>> = _joinResidenceState
|
||||
|
||||
// Task creation state
|
||||
private val _createTasksState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val createTasksState: StateFlow<ApiResult<Unit>> = _createTasksState
|
||||
|
||||
// Regional templates state
|
||||
private val _regionalTemplates = MutableStateFlow<ApiResult<List<TaskTemplate>>>(ApiResult.Idle)
|
||||
val regionalTemplates: StateFlow<ApiResult<List<TaskTemplate>>> = _regionalTemplates
|
||||
|
||||
// ZIP code entered during location step (persisted on residence)
|
||||
private val _postalCode = MutableStateFlow("")
|
||||
val postalCode: StateFlow<String> = _postalCode
|
||||
|
||||
// Whether onboarding is complete
|
||||
private val _isComplete = MutableStateFlow(false)
|
||||
val isComplete: StateFlow<Boolean> = _isComplete
|
||||
|
||||
fun setUserIntent(intent: OnboardingIntent) {
|
||||
_userIntent.value = intent
|
||||
}
|
||||
|
||||
fun setResidenceName(name: String) {
|
||||
_residenceName.value = name
|
||||
}
|
||||
|
||||
fun setShareCode(code: String) {
|
||||
_shareCode.value = code
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next step in the flow
|
||||
* Flow: Welcome → Features → Name Residence → Create Account → Verify → Tasks → Upsell
|
||||
*/
|
||||
fun nextStep() {
|
||||
_currentStep.value = when (_currentStep.value) {
|
||||
OnboardingStep.WELCOME -> {
|
||||
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
|
||||
OnboardingStep.CREATE_ACCOUNT
|
||||
} else {
|
||||
OnboardingStep.VALUE_PROPS
|
||||
}
|
||||
}
|
||||
OnboardingStep.VALUE_PROPS -> OnboardingStep.NAME_RESIDENCE
|
||||
OnboardingStep.NAME_RESIDENCE -> OnboardingStep.CREATE_ACCOUNT
|
||||
OnboardingStep.CREATE_ACCOUNT -> OnboardingStep.VERIFY_EMAIL
|
||||
OnboardingStep.VERIFY_EMAIL -> {
|
||||
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
|
||||
OnboardingStep.JOIN_RESIDENCE
|
||||
} else {
|
||||
OnboardingStep.RESIDENCE_LOCATION
|
||||
}
|
||||
}
|
||||
OnboardingStep.JOIN_RESIDENCE -> OnboardingStep.SUBSCRIPTION_UPSELL
|
||||
OnboardingStep.RESIDENCE_LOCATION -> OnboardingStep.FIRST_TASK
|
||||
OnboardingStep.FIRST_TASK -> OnboardingStep.SUBSCRIPTION_UPSELL
|
||||
OnboardingStep.SUBSCRIPTION_UPSELL -> {
|
||||
completeOnboarding()
|
||||
OnboardingStep.SUBSCRIPTION_UPSELL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to a specific step
|
||||
*/
|
||||
fun goToStep(step: OnboardingStep) {
|
||||
_currentStep.value = step
|
||||
}
|
||||
|
||||
/**
|
||||
* Go back to the previous step
|
||||
*/
|
||||
fun previousStep() {
|
||||
_currentStep.value = when (_currentStep.value) {
|
||||
OnboardingStep.VALUE_PROPS -> OnboardingStep.WELCOME
|
||||
OnboardingStep.NAME_RESIDENCE -> OnboardingStep.VALUE_PROPS
|
||||
OnboardingStep.CREATE_ACCOUNT -> {
|
||||
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
|
||||
OnboardingStep.WELCOME
|
||||
} else {
|
||||
OnboardingStep.NAME_RESIDENCE
|
||||
}
|
||||
}
|
||||
OnboardingStep.VERIFY_EMAIL -> OnboardingStep.CREATE_ACCOUNT
|
||||
else -> _currentStep.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the current step (for skippable screens)
|
||||
*/
|
||||
fun skipStep() {
|
||||
when (_currentStep.value) {
|
||||
OnboardingStep.VALUE_PROPS,
|
||||
OnboardingStep.JOIN_RESIDENCE,
|
||||
OnboardingStep.RESIDENCE_LOCATION,
|
||||
OnboardingStep.FIRST_TASK -> nextStep()
|
||||
OnboardingStep.SUBSCRIPTION_UPSELL -> completeOnboarding()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*/
|
||||
fun register(username: String, email: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
_registerState.value = ApiResult.Loading
|
||||
val result = APILayer.register(
|
||||
RegisterRequest(
|
||||
username = username,
|
||||
email = email,
|
||||
password = password
|
||||
)
|
||||
)
|
||||
_registerState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
DataManager.setAuthToken(result.data.token)
|
||||
DataManager.setCurrentUser(result.data.user)
|
||||
LookupsRepository.initialize()
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login an existing user
|
||||
*/
|
||||
fun login(username: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
_loginState.value = ApiResult.Loading
|
||||
val result = APILayer.login(LoginRequest(username, password))
|
||||
_loginState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
LookupsRepository.initialize()
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify email with 6-digit code
|
||||
*/
|
||||
fun verifyEmail(code: String) {
|
||||
viewModelScope.launch {
|
||||
_verifyEmailState.value = ApiResult.Loading
|
||||
val token = DataManager.authToken.value ?: run {
|
||||
_verifyEmailState.value = ApiResult.Error("Not authenticated")
|
||||
return@launch
|
||||
}
|
||||
val result = APILayer.verifyEmail(token, VerifyEmailRequest(code = code))
|
||||
_verifyEmailState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(Unit)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the residence with the name from onboarding
|
||||
*/
|
||||
fun createResidence() {
|
||||
viewModelScope.launch {
|
||||
if (_residenceName.value.isBlank()) {
|
||||
_createResidenceState.value = ApiResult.Success(Unit)
|
||||
return@launch
|
||||
}
|
||||
|
||||
_createResidenceState.value = ApiResult.Loading
|
||||
|
||||
val result = APILayer.createResidence(
|
||||
ResidenceCreateRequest(
|
||||
name = _residenceName.value,
|
||||
propertyTypeId = null,
|
||||
streetAddress = null,
|
||||
apartmentUnit = null,
|
||||
city = null,
|
||||
stateProvince = null,
|
||||
postalCode = _postalCode.value.takeIf { it.isNotBlank() },
|
||||
country = null,
|
||||
bedrooms = null,
|
||||
bathrooms = null,
|
||||
squareFootage = null,
|
||||
lotSize = null,
|
||||
yearBuilt = null,
|
||||
description = null,
|
||||
purchaseDate = null,
|
||||
purchasePrice = null,
|
||||
isPrimary = true
|
||||
)
|
||||
)
|
||||
|
||||
_createResidenceState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(Unit)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Failed to create residence")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join an existing residence with a share code
|
||||
*/
|
||||
fun joinResidence(code: String) {
|
||||
viewModelScope.launch {
|
||||
_joinResidenceState.value = ApiResult.Loading
|
||||
val result = APILayer.joinWithCode(code)
|
||||
_joinResidenceState.value = when (result) {
|
||||
is ApiResult.Success -> ApiResult.Success(Unit)
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Failed to join residence")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create selected tasks during onboarding
|
||||
*/
|
||||
fun createTasks(taskRequests: List<TaskCreateRequest>) {
|
||||
viewModelScope.launch {
|
||||
if (taskRequests.isEmpty()) {
|
||||
_createTasksState.value = ApiResult.Success(Unit)
|
||||
return@launch
|
||||
}
|
||||
|
||||
_createTasksState.value = ApiResult.Loading
|
||||
|
||||
var successCount = 0
|
||||
for (request in taskRequests) {
|
||||
val result = APILayer.createTask(request)
|
||||
if (result is ApiResult.Success) {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
_createTasksState.value = if (successCount > 0) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Failed to create tasks")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load regional templates by ZIP code (backend resolves ZIP → state → climate zone).
|
||||
* Also stores the ZIP code for later use when creating the residence.
|
||||
*/
|
||||
fun loadRegionalTemplates(zip: String) {
|
||||
_postalCode.value = zip
|
||||
viewModelScope.launch {
|
||||
_regionalTemplates.value = ApiResult.Loading
|
||||
_regionalTemplates.value = APILayer.getRegionalTemplates(zip = zip)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark onboarding as complete
|
||||
*/
|
||||
fun completeOnboarding() {
|
||||
_isComplete.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all state (useful for testing)
|
||||
*/
|
||||
fun reset() {
|
||||
_currentStep.value = OnboardingStep.WELCOME
|
||||
_userIntent.value = OnboardingIntent.UNKNOWN
|
||||
_residenceName.value = ""
|
||||
_shareCode.value = ""
|
||||
_registerState.value = ApiResult.Idle
|
||||
_loginState.value = ApiResult.Idle
|
||||
_verifyEmailState.value = ApiResult.Idle
|
||||
_createResidenceState.value = ApiResult.Idle
|
||||
_joinResidenceState.value = ApiResult.Idle
|
||||
_createTasksState.value = ApiResult.Idle
|
||||
_regionalTemplates.value = ApiResult.Idle
|
||||
_postalCode.value = ""
|
||||
_isComplete.value = false
|
||||
}
|
||||
|
||||
fun resetRegisterState() {
|
||||
_registerState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetLoginState() {
|
||||
_loginState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetVerifyEmailState() {
|
||||
_verifyEmailState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.*
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class PasswordResetStep {
|
||||
REQUEST_CODE,
|
||||
VERIFY_CODE,
|
||||
RESET_PASSWORD,
|
||||
LOGGING_IN,
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
class PasswordResetViewModel(
|
||||
private val deepLinkToken: String? = null
|
||||
) : ViewModel() {
|
||||
|
||||
private val _forgotPasswordState = MutableStateFlow<ApiResult<ForgotPasswordResponse>>(ApiResult.Idle)
|
||||
val forgotPasswordState: StateFlow<ApiResult<ForgotPasswordResponse>> = _forgotPasswordState
|
||||
|
||||
private val _verifyCodeState = MutableStateFlow<ApiResult<VerifyResetCodeResponse>>(ApiResult.Idle)
|
||||
val verifyCodeState: StateFlow<ApiResult<VerifyResetCodeResponse>> = _verifyCodeState
|
||||
|
||||
private val _resetPasswordState = MutableStateFlow<ApiResult<ResetPasswordResponse>>(ApiResult.Idle)
|
||||
val resetPasswordState: StateFlow<ApiResult<ResetPasswordResponse>> = _resetPasswordState
|
||||
|
||||
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
|
||||
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
|
||||
|
||||
// Callback for successful login after password reset
|
||||
var onLoginSuccess: ((Boolean) -> Unit)? = null
|
||||
|
||||
private val _currentStep = MutableStateFlow(
|
||||
if (deepLinkToken != null) PasswordResetStep.RESET_PASSWORD else PasswordResetStep.REQUEST_CODE
|
||||
)
|
||||
val currentStep: StateFlow<PasswordResetStep> = _currentStep
|
||||
|
||||
private val _resetToken = MutableStateFlow(deepLinkToken)
|
||||
val resetToken: StateFlow<String?> = _resetToken
|
||||
|
||||
private val _email = MutableStateFlow("")
|
||||
val email: StateFlow<String> = _email
|
||||
|
||||
fun setEmail(email: String) {
|
||||
_email.value = email
|
||||
}
|
||||
|
||||
fun requestPasswordReset(email: String) {
|
||||
viewModelScope.launch {
|
||||
_forgotPasswordState.value = ApiResult.Loading
|
||||
val result = APILayer.forgotPassword(ForgotPasswordRequest(email))
|
||||
_forgotPasswordState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
_email.value = email
|
||||
// Move to next step after short delay
|
||||
kotlinx.coroutines.delay(1500)
|
||||
_currentStep.value = PasswordResetStep.VERIFY_CODE
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyResetCode(email: String, code: String) {
|
||||
viewModelScope.launch {
|
||||
_verifyCodeState.value = ApiResult.Loading
|
||||
val result = APILayer.verifyResetCode(VerifyResetCodeRequest(email, code))
|
||||
_verifyCodeState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
_resetToken.value = result.data.resetToken
|
||||
// Move to next step after short delay
|
||||
kotlinx.coroutines.delay(1500)
|
||||
_currentStep.value = PasswordResetStep.RESET_PASSWORD
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _newPassword: String = ""
|
||||
|
||||
fun resetPassword(newPassword: String, confirmPassword: String) {
|
||||
val token = _resetToken.value
|
||||
if (token == null) {
|
||||
_resetPasswordState.value = ApiResult.Error("Invalid reset token. Please start over.")
|
||||
return
|
||||
}
|
||||
|
||||
_newPassword = newPassword
|
||||
|
||||
viewModelScope.launch {
|
||||
_resetPasswordState.value = ApiResult.Loading
|
||||
// Note: confirmPassword is for UI validation only, not sent to API
|
||||
val result = APILayer.resetPassword(
|
||||
ResetPasswordRequest(
|
||||
resetToken = token,
|
||||
newPassword = newPassword
|
||||
)
|
||||
)
|
||||
_resetPasswordState.value = when (result) {
|
||||
is ApiResult.Success -> {
|
||||
// Password reset successful - now auto-login
|
||||
_currentStep.value = PasswordResetStep.LOGGING_IN
|
||||
autoLogin()
|
||||
ApiResult.Success(result.data)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
else -> ApiResult.Error("Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun autoLogin() {
|
||||
val username = _email.value
|
||||
|
||||
if (username.isEmpty()) {
|
||||
// If we don't have the email (e.g., deep link flow), fall back to manual login
|
||||
_currentStep.value = PasswordResetStep.SUCCESS
|
||||
return
|
||||
}
|
||||
|
||||
_loginState.value = ApiResult.Loading
|
||||
|
||||
val loginResult = APILayer.login(LoginRequest(username = username, password = _newPassword))
|
||||
|
||||
when (loginResult) {
|
||||
is ApiResult.Success -> {
|
||||
val isVerified = loginResult.data.user.verified
|
||||
// Initialize lookups
|
||||
APILayer.initializeLookups()
|
||||
_loginState.value = loginResult
|
||||
// Call the login success callback
|
||||
onLoginSuccess?.invoke(isVerified)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
// Auto-login failed, fall back to manual login
|
||||
println("Auto-login failed: ${loginResult.message}")
|
||||
_loginState.value = ApiResult.Idle
|
||||
_currentStep.value = PasswordResetStep.SUCCESS
|
||||
}
|
||||
else -> {
|
||||
_currentStep.value = PasswordResetStep.SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToNextStep() {
|
||||
_currentStep.value = when (_currentStep.value) {
|
||||
PasswordResetStep.REQUEST_CODE -> PasswordResetStep.VERIFY_CODE
|
||||
PasswordResetStep.VERIFY_CODE -> PasswordResetStep.RESET_PASSWORD
|
||||
PasswordResetStep.RESET_PASSWORD -> PasswordResetStep.LOGGING_IN
|
||||
PasswordResetStep.LOGGING_IN -> PasswordResetStep.SUCCESS
|
||||
PasswordResetStep.SUCCESS -> PasswordResetStep.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToPreviousStep() {
|
||||
_currentStep.value = when (_currentStep.value) {
|
||||
PasswordResetStep.REQUEST_CODE -> PasswordResetStep.REQUEST_CODE
|
||||
PasswordResetStep.VERIFY_CODE -> PasswordResetStep.REQUEST_CODE
|
||||
PasswordResetStep.RESET_PASSWORD -> PasswordResetStep.VERIFY_CODE
|
||||
PasswordResetStep.LOGGING_IN -> PasswordResetStep.LOGGING_IN // Can't go back while logging in
|
||||
PasswordResetStep.SUCCESS -> PasswordResetStep.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
fun resetForgotPasswordState() {
|
||||
_forgotPasswordState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetVerifyCodeState() {
|
||||
_verifyCodeState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetResetPasswordState() {
|
||||
_resetPasswordState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetAll() {
|
||||
_email.value = ""
|
||||
_resetToken.value = null
|
||||
_forgotPasswordState.value = ApiResult.Idle
|
||||
_verifyCodeState.value = ApiResult.Idle
|
||||
_resetPasswordState.value = ApiResult.Idle
|
||||
_currentStep.value = PasswordResetStep.REQUEST_CODE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.Residence
|
||||
import com.tt.honeyDue.models.ResidenceCreateRequest
|
||||
import com.tt.honeyDue.models.TotalSummary
|
||||
import com.tt.honeyDue.models.MyResidencesResponse
|
||||
import com.tt.honeyDue.models.TaskColumnsResponse
|
||||
import com.tt.honeyDue.models.ContractorSummary
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ResidenceViewModel : ViewModel() {
|
||||
|
||||
private val _residencesState = MutableStateFlow<ApiResult<List<Residence>>>(ApiResult.Idle)
|
||||
val residencesState: StateFlow<ApiResult<List<Residence>>> = _residencesState
|
||||
|
||||
private val _summaryState = MutableStateFlow<ApiResult<TotalSummary>>(ApiResult.Idle)
|
||||
val summaryState: StateFlow<ApiResult<TotalSummary>> = _summaryState
|
||||
|
||||
private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
||||
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
|
||||
|
||||
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
||||
val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState
|
||||
|
||||
private val _residenceTasksState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
||||
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _residenceTasksState
|
||||
|
||||
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Idle)
|
||||
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
|
||||
|
||||
private val _cancelTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
|
||||
val cancelTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _cancelTaskState
|
||||
|
||||
private val _uncancelTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
|
||||
val uncancelTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _uncancelTaskState
|
||||
|
||||
private val _updateTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
|
||||
val updateTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _updateTaskState
|
||||
|
||||
private val _generateReportState = MutableStateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>>(ApiResult.Idle)
|
||||
val generateReportState: StateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>> = _generateReportState
|
||||
|
||||
private val _deleteResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val deleteResidenceState: StateFlow<ApiResult<Unit>> = _deleteResidenceState
|
||||
|
||||
private val _residenceContractorsState = MutableStateFlow<ApiResult<List<ContractorSummary>>>(ApiResult.Idle)
|
||||
val residenceContractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _residenceContractorsState
|
||||
|
||||
/**
|
||||
* Load residences from cache. If cache is empty or force refresh is requested,
|
||||
* fetch from API and update cache.
|
||||
*/
|
||||
fun loadResidences(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_residencesState.value = ApiResult.Loading
|
||||
_residencesState.value = APILayer.getResidences(forceRefresh = forceRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSummary(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_summaryState.value = ApiResult.Loading
|
||||
_summaryState.value = APILayer.getSummary(forceRefresh = forceRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
fun getResidence(id: Int, onResult: (ApiResult<Residence>) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val result = APILayer.getResidence(id)
|
||||
onResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun createResidence(request: ResidenceCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_createResidenceState.value = ApiResult.Loading
|
||||
_createResidenceState.value = APILayer.createResidence(request)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetResidenceTasksState() {
|
||||
_residenceTasksState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun loadResidenceTasks(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_residenceTasksState.value = ApiResult.Loading
|
||||
_residenceTasksState.value = APILayer.getTasksByResidence(residenceId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateResidence(residenceId: Int, request: ResidenceCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_updateResidenceState.value = ApiResult.Loading
|
||||
_updateResidenceState.value = APILayer.updateResidence(residenceId, request)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCreateState() {
|
||||
_createResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUpdateState() {
|
||||
_updateResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun loadMyResidences(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_myResidencesState.value = ApiResult.Loading
|
||||
_myResidencesState.value = APILayer.getMyResidences(forceRefresh = forceRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelTask(taskId: Int) {
|
||||
viewModelScope.launch {
|
||||
_cancelTaskState.value = ApiResult.Loading
|
||||
_cancelTaskState.value = APILayer.cancelTask(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
fun uncancelTask(taskId: Int) {
|
||||
viewModelScope.launch {
|
||||
_uncancelTaskState.value = ApiResult.Loading
|
||||
_uncancelTaskState.value = APILayer.uncancelTask(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTask(taskId: Int, request: com.tt.honeyDue.models.TaskCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_updateTaskState.value = ApiResult.Loading
|
||||
_updateTaskState.value = APILayer.updateTask(taskId, request)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCancelTaskState() {
|
||||
_cancelTaskState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUncancelTaskState() {
|
||||
_uncancelTaskState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun resetUpdateTaskState() {
|
||||
_updateTaskState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun generateTasksReport(residenceId: Int, email: String? = null) {
|
||||
viewModelScope.launch {
|
||||
_generateReportState.value = ApiResult.Loading
|
||||
_generateReportState.value = APILayer.generateTasksReport(residenceId, email)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetGenerateReportState() {
|
||||
_generateReportState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun deleteResidence(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_deleteResidenceState.value = ApiResult.Loading
|
||||
_deleteResidenceState.value = APILayer.deleteResidence(residenceId)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetDeleteResidenceState() {
|
||||
_deleteResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
private val _joinResidenceState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>>(ApiResult.Idle)
|
||||
val joinResidenceState: StateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>> = _joinResidenceState
|
||||
|
||||
fun joinWithCode(code: String) {
|
||||
viewModelScope.launch {
|
||||
_joinResidenceState.value = ApiResult.Loading
|
||||
_joinResidenceState.value = APILayer.joinWithCode(code)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetJoinResidenceState() {
|
||||
_joinResidenceState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun loadResidenceContractors(residenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
_residenceContractorsState.value = ApiResult.Loading
|
||||
_residenceContractorsState.value = APILayer.getContractorsByResidence(residenceId)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetResidenceContractorsState() {
|
||||
_residenceContractorsState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.TaskCompletionCreateRequest
|
||||
import com.tt.honeyDue.models.TaskCompletionResponse
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import com.tt.honeyDue.util.ImageCompressor
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TaskCompletionViewModel : ViewModel() {
|
||||
|
||||
private val _createCompletionState = MutableStateFlow<ApiResult<TaskCompletionResponse>>(ApiResult.Idle)
|
||||
val createCompletionState: StateFlow<ApiResult<TaskCompletionResponse>> = _createCompletionState
|
||||
|
||||
fun createTaskCompletion(request: TaskCompletionCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_createCompletionState.value = ApiResult.Loading
|
||||
// Use APILayer which handles DataManager updates and summary refresh
|
||||
_createCompletionState.value = APILayer.createTaskCompletion(request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create task completion with images.
|
||||
*
|
||||
* @param request The completion request data
|
||||
* @param images List of ImageData (from platform-specific image pickers)
|
||||
*/
|
||||
fun createTaskCompletionWithImages(
|
||||
request: TaskCompletionCreateRequest,
|
||||
images: List<com.tt.honeyDue.platform.ImageData> = emptyList()
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_createCompletionState.value = ApiResult.Loading
|
||||
|
||||
// Compress images and prepare for upload
|
||||
val compressedImages = images.map { ImageCompressor.compressImage(it) }
|
||||
val imageFileNames = images.mapIndexed { index, image ->
|
||||
// Always use .jpg extension since we compress to JPEG
|
||||
val baseName = image.fileName.ifBlank { "completion_$index" }
|
||||
if (baseName.endsWith(".jpg", ignoreCase = true) ||
|
||||
baseName.endsWith(".jpeg", ignoreCase = true)) {
|
||||
baseName
|
||||
} else {
|
||||
// Remove any existing extension and add .jpg
|
||||
baseName.substringBeforeLast('.', baseName) + ".jpg"
|
||||
}
|
||||
}
|
||||
|
||||
// Use APILayer which handles DataManager updates and summary refresh
|
||||
_createCompletionState.value = APILayer.createTaskCompletionWithImages(
|
||||
request = request,
|
||||
images = compressedImages,
|
||||
imageFileNames = imageFileNames
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCreateState() {
|
||||
_createCompletionState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.tt.honeyDue.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.models.TaskColumnsResponse
|
||||
import com.tt.honeyDue.models.CustomTask
|
||||
import com.tt.honeyDue.models.TaskCreateRequest
|
||||
import com.tt.honeyDue.models.TaskCompletionResponse
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TaskViewModel : ViewModel() {
|
||||
|
||||
private val _tasksState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
||||
val tasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksState
|
||||
|
||||
private val _tasksByResidenceState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
||||
val tasksByResidenceState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksByResidenceState
|
||||
|
||||
private val _taskAddNewCustomTaskState = MutableStateFlow<ApiResult<CustomTask>>(ApiResult.Idle)
|
||||
val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState
|
||||
|
||||
private val _taskCompletionsState = MutableStateFlow<ApiResult<List<TaskCompletionResponse>>>(ApiResult.Idle)
|
||||
val taskCompletionsState: StateFlow<ApiResult<List<TaskCompletionResponse>>> = _taskCompletionsState
|
||||
|
||||
fun loadTasks(forceRefresh: Boolean = false) {
|
||||
println("TaskViewModel: loadTasks called")
|
||||
viewModelScope.launch {
|
||||
_tasksState.value = ApiResult.Loading
|
||||
_tasksState.value = APILayer.getTasks(forceRefresh = forceRefresh)
|
||||
println("TaskViewModel: loadTasks result: ${_tasksState.value}")
|
||||
}
|
||||
}
|
||||
|
||||
fun loadTasksByResidence(residenceId: Int, forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_tasksByResidenceState.value = ApiResult.Loading
|
||||
_tasksByResidenceState.value = APILayer.getTasksByResidence(
|
||||
residenceId = residenceId,
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewTask(request: TaskCreateRequest) {
|
||||
println("TaskViewModel: createNewTask called with $request")
|
||||
viewModelScope.launch {
|
||||
println("TaskViewModel: Setting state to Loading")
|
||||
_taskAddNewCustomTaskState.value = ApiResult.Loading
|
||||
val result = APILayer.createTask(request)
|
||||
println("TaskViewModel: API result: $result")
|
||||
_taskAddNewCustomTaskState.value = result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun resetAddTaskState() {
|
||||
_taskAddNewCustomTaskState.value = ApiResult.Idle
|
||||
}
|
||||
|
||||
fun updateTask(taskId: Int, request: TaskCreateRequest, onComplete: (Boolean) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
when (val result = APILayer.updateTask(taskId, request)) {
|
||||
is ApiResult.Success -> {
|
||||
onComplete(true)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
onComplete(false)
|
||||
}
|
||||
else -> {
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
when (val result = APILayer.cancelTask(taskId)) {
|
||||
is ApiResult.Success -> {
|
||||
onComplete(true)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
onComplete(false)
|
||||
}
|
||||
else -> {
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uncancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
when (val result = APILayer.uncancelTask(taskId)) {
|
||||
is ApiResult.Success -> {
|
||||
onComplete(true)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
onComplete(false)
|
||||
}
|
||||
else -> {
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
when (val result = APILayer.markInProgress(taskId)) {
|
||||
is ApiResult.Success -> {
|
||||
onComplete(true)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
onComplete(false)
|
||||
}
|
||||
else -> {
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun archiveTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
when (val result = APILayer.archiveTask(taskId)) {
|
||||
is ApiResult.Success -> {
|
||||
onComplete(true)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
onComplete(false)
|
||||
}
|
||||
else -> {
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unarchiveTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
when (val result = APILayer.unarchiveTask(taskId)) {
|
||||
is ApiResult.Success -> {
|
||||
onComplete(true)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
onComplete(false)
|
||||
}
|
||||
else -> {
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load completions for a specific task
|
||||
*/
|
||||
fun loadTaskCompletions(taskId: Int) {
|
||||
viewModelScope.launch {
|
||||
_taskCompletionsState.value = ApiResult.Loading
|
||||
_taskCompletionsState.value = APILayer.getTaskCompletions(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset task completions state
|
||||
*/
|
||||
fun resetTaskCompletionsState() {
|
||||
_taskCompletionsState.value = ApiResult.Idle
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user