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:
Trey t
2026-03-07 06:33:57 -06:00
parent 9c574c4343
commit 1e2adf7660
450 changed files with 1730 additions and 1788 deletions
@@ -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()
}
}
@@ -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
}
}