P1: All Kotlin VMs align with DataManager single-source-of-truth

Four broken VMs refactored to derive read-state from IDataManager, three
gaps closed:

1. TaskViewModel: tasksState / tasksByResidenceState / taskCompletionsState
   now derived via .map + .stateIn / combine. isLoading / loadError separated.
2. ResidenceViewModel: residencesState / myResidencesState / summaryState /
   residenceTasksState / residenceContractorsState all derived. 8 mutation
   states retained as independent (legit one-shot feedback).
3. ContractorViewModel: contractorsState / contractorDetailState derived.
   4 mutation states retained.
4. DocumentViewModel: documentsState / documentDetailState derived. 6
   mutation states retained.
5. AuthViewModel: currentUserState now derived from dataManager.currentUser.
   10 other states stay independent (one-shot mutation feedback by design).
6. LookupsViewModel: accepts IDataManager ctor param for test injection
   consistency. Direct-exposure pattern preserved. Legacy ApiResult-wrapped
   states now derived from DataManager instead of manual _xxxState.value =.
7. NotificationPreferencesViewModel: preferencesState derived from new
   IDataManager.notificationPreferences. APILayer writes through on both
   getNotificationPreferences and updateNotificationPreferences.

IDataManager also grew notificationPreferences: StateFlow<NotificationPreference?>.
DataManager, InMemoryDataManager updated. No screen edits needed — screens
consume viewModel.xxxState the same way; the source just switched.

Architecture enforcement test comes in P3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-19 18:42:40 -05:00
parent 2230cde071
commit f0f8dfb68b
11 changed files with 489 additions and 330 deletions

View File

@@ -207,6 +207,13 @@ object DataManager : IDataManager {
private val _taskTemplatesGrouped = MutableStateFlow<TaskTemplatesGroupedResponse?>(null)
override val taskTemplatesGrouped: StateFlow<TaskTemplatesGroupedResponse?> = _taskTemplatesGrouped.asStateFlow()
private val _notificationPreferences = MutableStateFlow<com.tt.honeyDue.models.NotificationPreference?>(null)
override val notificationPreferences: StateFlow<com.tt.honeyDue.models.NotificationPreference?> = _notificationPreferences.asStateFlow()
fun setNotificationPreferences(prefs: com.tt.honeyDue.models.NotificationPreference?) {
_notificationPreferences.value = prefs
}
// Map-based for O(1) ID resolution
private val _residenceTypesMap = MutableStateFlow<Map<Int, ResidenceType>>(emptyMap())
val residenceTypesMap: StateFlow<Map<Int, ResidenceType>> = _residenceTypesMap.asStateFlow()

View File

@@ -112,6 +112,11 @@ interface IDataManager {
val taskTemplates: StateFlow<List<TaskTemplate>>
val taskTemplatesGrouped: StateFlow<TaskTemplatesGroupedResponse?>
// ==================== NOTIFICATION PREFERENCES ====================
/** User's server-backed notification preferences. Populated by APILayer.getNotificationPreferences. */
val notificationPreferences: StateFlow<com.tt.honeyDue.models.NotificationPreference?>
// ==================== O(1) LOOKUP HELPERS ====================
fun getResidenceType(id: Int?): ResidenceType?

View File

@@ -1430,12 +1430,16 @@ object APILayer {
suspend fun getNotificationPreferences(): ApiResult<NotificationPreference> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
return notificationApi.getNotificationPreferences(token)
val result = notificationApi.getNotificationPreferences(token)
if (result is ApiResult.Success) DataManager.setNotificationPreferences(result.data)
return result
}
suspend fun updateNotificationPreferences(request: UpdateNotificationPreferencesRequest): ApiResult<NotificationPreference> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
return notificationApi.updateNotificationPreferences(token, request)
val result = notificationApi.updateNotificationPreferences(token, request)
if (result is ApiResult.Success) DataManager.setNotificationPreferences(result.data)
return result
}
suspend fun getNotificationHistory(): ApiResult<List<Notification>> {

View File

@@ -61,6 +61,7 @@ class InMemoryDataManager(
contractorSpecialties: List<ContractorSpecialty> = emptyList(),
taskTemplates: List<TaskTemplate> = emptyList(),
taskTemplatesGrouped: TaskTemplatesGroupedResponse? = null,
notificationPreferences: com.tt.honeyDue.models.NotificationPreference? = null,
) : IDataManager {
// ==================== AUTH ====================
@@ -112,6 +113,8 @@ class InMemoryDataManager(
override val taskTemplates: StateFlow<List<TaskTemplate>> = MutableStateFlow(taskTemplates)
override val taskTemplatesGrouped: StateFlow<TaskTemplatesGroupedResponse?> = MutableStateFlow(taskTemplatesGrouped)
override val notificationPreferences: StateFlow<com.tt.honeyDue.models.NotificationPreference?> = MutableStateFlow(notificationPreferences)
// ==================== LOOKUP HELPERS ====================
override fun getResidenceType(id: Int?): ResidenceType? =

View File

@@ -3,6 +3,7 @@ package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.IDataManager
import com.tt.honeyDue.models.AppleSignInRequest
import com.tt.honeyDue.models.AppleSignInResponse
import com.tt.honeyDue.models.GoogleSignInRequest
@@ -23,10 +24,15 @@ 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.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class AuthViewModel : ViewModel() {
class AuthViewModel(
private val dataManager: IDataManager = DataManager,
) : ViewModel() {
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
@@ -40,8 +46,11 @@ class AuthViewModel : ViewModel() {
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
/** Current authenticated user — derived from [IDataManager.currentUser]. APILayer writes through on login/register/getCurrentUser. */
val currentUserState: StateFlow<ApiResult<User>> =
dataManager.currentUser
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
private val _forgotPasswordState = MutableStateFlow<ApiResult<ForgotPasswordResponse>>(ApiResult.Idle)
val forgotPasswordState: StateFlow<ApiResult<ForgotPasswordResponse>> = _forgotPasswordState
@@ -149,19 +158,18 @@ class AuthViewModel : ViewModel() {
}
fun getCurrentUser(forceRefresh: Boolean = false) {
// Fire the API call; APILayer writes through to DataManager.setCurrentUser
// on success. [currentUserState] is a derived flow so it re-emits
// automatically. No local state mutation needed.
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")
}
APILayer.getCurrentUser(forceRefresh = forceRefresh)
}
}
/** No-op — [currentUserState] is derived from DataManager and can't be
* locally reset. To clear, call [DataManager.setCurrentUser] with null. */
fun resetCurrentUserState() {
_currentUserState.value = ApiResult.Idle
// intentionally empty
}
fun forgotPassword(email: String) {

View File

@@ -2,20 +2,55 @@ package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.IDataManager
import com.tt.honeyDue.models.*
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ContractorViewModel : ViewModel() {
/**
* ContractorViewModel — read-state derived from [IDataManager].
*
* Reads ([contractorsState], [contractorDetailState]) reactively mirror
* [IDataManager.contractors] / [IDataManager.contractorDetail] so the
* VM reflects any DataManager update instantly (API success write,
* fixture seed, etc.). Mutations (create/update/delete/toggleFavorite)
* remain owned by the VM as one-shot [ApiResult] fields.
*/
class ContractorViewModel(
private val dataManager: IDataManager = DataManager,
) : ViewModel() {
private val _contractorsState = MutableStateFlow<ApiResult<List<ContractorSummary>>>(ApiResult.Idle)
val contractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _contractorsState
// ---------- Read state ----------
private val _contractorDetailState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
val contractorDetailState: StateFlow<ApiResult<Contractor>> = _contractorDetailState
val contractorsState: StateFlow<ApiResult<List<ContractorSummary>>> =
dataManager.contractors
.map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
private val _selectedContractorId = MutableStateFlow<Int?>(null)
val contractorDetailState: StateFlow<ApiResult<Contractor>> =
combine(_selectedContractorId, dataManager.contractorDetail) { id, map ->
if (id == null) ApiResult.Idle
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
// ---------- Loading / error ----------
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _loadError = MutableStateFlow<String?>(null)
val loadError: StateFlow<String?> = _loadError
// ---------- Mutation-feedback ----------
private val _createState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
val createState: StateFlow<ApiResult<Contractor>> = _createState
@@ -29,32 +64,45 @@ class ContractorViewModel : ViewModel() {
private val _toggleFavoriteState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
val toggleFavoriteState: StateFlow<ApiResult<Contractor>> = _toggleFavoriteState
// ---------- Loaders (write-through to DataManager) ----------
fun loadContractors(
specialty: String? = null,
isFavorite: Boolean? = null,
isActive: Boolean? = null,
search: String? = null,
forceRefresh: Boolean = false
forceRefresh: Boolean = false,
) {
viewModelScope.launch {
_contractorsState.value = ApiResult.Loading
_contractorsState.value = APILayer.getContractors(
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getContractors(
specialty = specialty,
isFavorite = isFavorite,
isActive = isActive,
search = search,
forceRefresh = forceRefresh
)
forceRefresh = forceRefresh,
) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun loadContractorDetail(id: Int) {
viewModelScope.launch {
_contractorDetailState.value = ApiResult.Loading
_contractorDetailState.value = APILayer.getContractor(id)
_selectedContractorId.value = id
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getContractor(id) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun selectContractor(id: Int?) {
_selectedContractorId.value = id
}
// ---------- Mutations ----------
fun createContractor(request: ContractorCreateRequest) {
viewModelScope.launch {
_createState.value = ApiResult.Loading
@@ -83,19 +131,8 @@ class ContractorViewModel : ViewModel() {
}
}
fun resetCreateState() {
_createState.value = ApiResult.Idle
}
fun resetUpdateState() {
_updateState.value = ApiResult.Idle
}
fun resetDeleteState() {
_deleteState.value = ApiResult.Idle
}
fun resetToggleFavoriteState() {
_toggleFavoriteState.value = ApiResult.Idle
}
fun resetCreateState() { _createState.value = ApiResult.Idle }
fun resetUpdateState() { _updateState.value = ApiResult.Idle }
fun resetDeleteState() { _deleteState.value = ApiResult.Idle }
fun resetToggleFavoriteState() { _toggleFavoriteState.value = ApiResult.Idle }
}

View File

@@ -2,21 +2,60 @@ package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.IDataManager
import com.tt.honeyDue.models.*
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.util.ImageCompressor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class DocumentViewModel : ViewModel() {
/**
* DocumentViewModel — read-state derived from [IDataManager].
*
* [documentsState] and [documentDetailState] are reactive projections of
* [IDataManager.documents] / [IDataManager.documentDetail]. Mutation-
* feedback fields (create/update/delete/download/deleteImage/uploadImage)
* remain independent [MutableStateFlow]s — they're one-shot results, not
* cached data.
*/
class DocumentViewModel(
private val dataManager: IDataManager = DataManager,
) : ViewModel() {
private val _documentsState = MutableStateFlow<ApiResult<List<Document>>>(ApiResult.Idle)
val documentsState: StateFlow<ApiResult<List<Document>>> = _documentsState
// ---------- Read state ----------
private val _documentDetailState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
val documentDetailState: StateFlow<ApiResult<Document>> = _documentDetailState
val documentsState: StateFlow<ApiResult<List<Document>>> =
dataManager.documents
.map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
private val _selectedDocumentId = MutableStateFlow<Int?>(null)
val documentDetailState: StateFlow<ApiResult<Document>> =
combine(_selectedDocumentId, dataManager.documentDetail) { id, map ->
if (id == null) ApiResult.Idle
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
fun selectDocument(id: Int?) {
_selectedDocumentId.value = id
}
// ---------- Loading / error ----------
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _loadError = MutableStateFlow<String?>(null)
val loadError: StateFlow<String?> = _loadError
// ---------- Mutation-feedback ----------
private val _createState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
val createState: StateFlow<ApiResult<Document>> = _createState
@@ -36,6 +75,8 @@ class DocumentViewModel : ViewModel() {
private val _uploadImageState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
val uploadImageState: StateFlow<ApiResult<Document>> = _uploadImageState
// ---------- Loaders (write-through to DataManager) ----------
fun loadDocuments(
residenceId: Int? = null,
documentType: String? = null,
@@ -48,8 +89,9 @@ class DocumentViewModel : ViewModel() {
forceRefresh: Boolean = false
) {
viewModelScope.launch {
_documentsState.value = ApiResult.Loading
_documentsState.value = APILayer.getDocuments(
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getDocuments(
residenceId = residenceId,
documentType = documentType,
category = category,
@@ -59,7 +101,8 @@ class DocumentViewModel : ViewModel() {
tags = tags,
search = search,
forceRefresh = forceRefresh
)
) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
@@ -72,18 +115,23 @@ class DocumentViewModel : ViewModel() {
forceRefresh: Boolean = false
) {
viewModelScope.launch {
_documentsState.value = ApiResult.Loading
_documentsState.value = APILayer.getDocuments(
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getDocuments(
residenceId = residenceId,
forceRefresh = forceRefresh
)
) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun loadDocumentDetail(id: Int) {
viewModelScope.launch {
_documentDetailState.value = ApiResult.Loading
_documentDetailState.value = APILayer.getDocument(id)
_selectedDocumentId.value = id
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getDocument(id) as? ApiResult.Error)?.message
_isLoading.value = false
}
}

View File

@@ -3,93 +3,88 @@ package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.IDataManager
import com.tt.honeyDue.models.*
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
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.
* LookupsViewModel — already the template for reactive DataManager
* derivation. Extended to accept [IDataManager] as a constructor param
* so test doubles can be injected identically to every other VM.
*
* Direct StateFlow exposure (no wrapping) for the happy path:
* screens observing [residenceTypes] etc. see the live [IDataManager]
* value instantly.
*
* Legacy `*State: StateFlow<ApiResult<T>>` fields are kept for screens
* still coded against the `ApiResult` pattern — they're now derived
* from DataManager via `.map + .stateIn` so they emit `Success` as
* soon as data is present, bypassing the old `_xxxState.value =`
* write pattern.
*/
class LookupsViewModel : ViewModel() {
class LookupsViewModel(
private val dataManager: IDataManager = DataManager,
) : 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
// ---------- Direct exposure (preferred) ----------
// Keep legacy state flows for compatibility during migration
private val _residenceTypesState = MutableStateFlow<ApiResult<List<ResidenceType>>>(ApiResult.Idle)
val residenceTypesState: StateFlow<ApiResult<List<ResidenceType>>> = _residenceTypesState
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
private val _taskFrequenciesState = MutableStateFlow<ApiResult<List<TaskFrequency>>>(ApiResult.Idle)
val taskFrequenciesState: StateFlow<ApiResult<List<TaskFrequency>>> = _taskFrequenciesState
// ---------- ApiResult-wrapped projections (legacy — derived) ----------
private val _taskPrioritiesState = MutableStateFlow<ApiResult<List<TaskPriority>>>(ApiResult.Idle)
val taskPrioritiesState: StateFlow<ApiResult<List<TaskPriority>>> = _taskPrioritiesState
val residenceTypesState: StateFlow<ApiResult<List<ResidenceType>>> =
dataManager.residenceTypes
.map { if (it.isNotEmpty()) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
private val _taskCategoriesState = MutableStateFlow<ApiResult<List<TaskCategory>>>(ApiResult.Idle)
val taskCategoriesState: StateFlow<ApiResult<List<TaskCategory>>> = _taskCategoriesState
val taskFrequenciesState: StateFlow<ApiResult<List<TaskFrequency>>> =
dataManager.taskFrequencies
.map { if (it.isNotEmpty()) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
val taskPrioritiesState: StateFlow<ApiResult<List<TaskPriority>>> =
dataManager.taskPriorities
.map { if (it.isNotEmpty()) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
val taskCategoriesState: StateFlow<ApiResult<List<TaskCategory>>> =
dataManager.taskCategories
.map { if (it.isNotEmpty()) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
// ---------- Load methods (write-through) ----------
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
}
if (dataManager.residenceTypes.value.isNotEmpty()) return
viewModelScope.launch { APILayer.getResidenceTypes() }
}
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
}
if (dataManager.taskFrequencies.value.isNotEmpty()) return
viewModelScope.launch { APILayer.getTaskFrequencies() }
}
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
}
if (dataManager.taskPriorities.value.isNotEmpty()) return
viewModelScope.launch { APILayer.getTaskPriorities() }
}
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
}
if (dataManager.taskCategories.value.isNotEmpty()) return
viewModelScope.launch { APILayer.getTaskCategories() }
}
// Load all lookups at once
fun loadAllLookups() {
loadResidenceTypes()
loadTaskFrequencies()

View File

@@ -2,13 +2,18 @@ package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.IDataManager
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 com.tt.honeyDue.network.ApiResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
@@ -73,10 +78,16 @@ class NotificationCategoriesController(
* alongside the new per-category local toggles driven by
* [NotificationCategoriesController].
*/
class NotificationPreferencesViewModel : ViewModel() {
class NotificationPreferencesViewModel(
private val dataManager: IDataManager = DataManager,
) : ViewModel() {
private val _preferencesState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
val preferencesState: StateFlow<ApiResult<NotificationPreference>> = _preferencesState.asStateFlow()
/** Server-backed preferences — derived from [IDataManager.notificationPreferences].
* APILayer.getNotificationPreferences / updateNotificationPreferences write through. */
val preferencesState: StateFlow<ApiResult<NotificationPreference>> =
dataManager.notificationPreferences
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
private val _updateState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
val updateState: StateFlow<ApiResult<NotificationPreference>> = _updateState.asStateFlow()
@@ -92,15 +103,9 @@ class NotificationPreferencesViewModel : ViewModel() {
val categoryState: StateFlow<Map<String, Boolean>> = _categoryState.asStateFlow()
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")
}
}
// Fire the API call; APILayer writes to DataManager.setNotificationPreferences
// on success and [preferencesState] re-emits automatically.
viewModelScope.launch { APILayer.getNotificationPreferences() }
}
fun updatePreference(
@@ -133,15 +138,10 @@ class NotificationPreferencesViewModel : ViewModel() {
warrantyExpiringHour = warrantyExpiringHour,
dailyDigestHour = dailyDigestHour,
)
val result = APILayer.updateNotificationPreferences(request)
_updateState.value = when (result) {
is ApiResult.Success -> {
_preferencesState.value = ApiResult.Success(result.data)
ApiResult.Success(result.data)
}
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
// APILayer.updateNotificationPreferences writes through to
// DataManager.setNotificationPreferences on success, so
// [preferencesState] re-emits automatically.
_updateState.value = APILayer.updateNotificationPreferences(request)
}
}

View File

@@ -2,25 +2,79 @@ package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.IDataManager
import com.tt.honeyDue.models.ContractorSummary
import com.tt.honeyDue.models.MyResidencesResponse
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.models.TotalSummary
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ResidenceViewModel : ViewModel() {
/**
* ResidenceViewModel — read-state derived from [IDataManager].
*
* All list/detail reads (`residencesState`, `myResidencesState`,
* `summaryState`, `residenceTasksState`, `residenceContractorsState`)
* are reactive projections of [IDataManager] StateFlows. Mutation
* feedback (create/update/delete/join/cancel/uncancel/updateTask/
* generateReport) remains owned by the VM as one-shot [ApiResult]
* fields — they track API operation outcomes, not cached data.
*/
class ResidenceViewModel(
private val dataManager: IDataManager = DataManager,
) : ViewModel() {
private val _residencesState = MutableStateFlow<ApiResult<List<Residence>>>(ApiResult.Idle)
val residencesState: StateFlow<ApiResult<List<Residence>>> = _residencesState
// ---------- Read state (derived from DataManager) ----------
private val _summaryState = MutableStateFlow<ApiResult<TotalSummary>>(ApiResult.Idle)
val summaryState: StateFlow<ApiResult<TotalSummary>> = _summaryState
val residencesState: StateFlow<ApiResult<List<Residence>>> =
dataManager.residences
.map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> =
dataManager.myResidences
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
val summaryState: StateFlow<ApiResult<TotalSummary>> =
dataManager.totalSummary
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
/** Drives the residence-scoped projections. */
private val _selectedResidenceId = MutableStateFlow<Int?>(null)
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> =
combine(_selectedResidenceId, dataManager.tasksByResidence) { id, map ->
if (id == null) ApiResult.Idle
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
val residenceContractorsState: StateFlow<ApiResult<List<ContractorSummary>>> =
combine(_selectedResidenceId, dataManager.contractorsByResidence) { id, map ->
if (id == null) ApiResult.Idle
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
// ---------- Loading / error feedback ----------
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _loadError = MutableStateFlow<String?>(null)
val loadError: StateFlow<String?> = _loadError
// ---------- Mutation-feedback (one-shot, owned by VM) ----------
private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
@@ -28,11 +82,11 @@ class ResidenceViewModel : ViewModel() {
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 _deleteResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
val deleteResidenceState: StateFlow<ApiResult<Unit>> = _deleteResidenceState
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Idle)
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
private val _joinResidenceState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>>(ApiResult.Idle)
val joinResidenceState: StateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>> = _joinResidenceState
private val _cancelTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
val cancelTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _cancelTaskState
@@ -46,37 +100,77 @@ class ResidenceViewModel : ViewModel() {
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
// ---------- Projection selectors ----------
private val _residenceContractorsState = MutableStateFlow<ApiResult<List<ContractorSummary>>>(ApiResult.Idle)
val residenceContractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _residenceContractorsState
fun selectResidence(residenceId: Int?) {
_selectedResidenceId.value = residenceId
}
// ---------- Load methods (write-through to DataManager) ----------
/**
* 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)
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getResidences(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun loadSummary(forceRefresh: Boolean = false) {
viewModelScope.launch {
_summaryState.value = ApiResult.Loading
_summaryState.value = APILayer.getSummary(forceRefresh = forceRefresh)
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getSummary(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun getResidence(id: Int, onResult: (ApiResult<Residence>) -> Unit) {
viewModelScope.launch {
val result = APILayer.getResidence(id)
onResult(result)
onResult(APILayer.getResidence(id))
}
}
fun loadMyResidences(forceRefresh: Boolean = false) {
viewModelScope.launch {
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getMyResidences(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun loadResidenceTasks(residenceId: Int) {
viewModelScope.launch {
_selectedResidenceId.value = residenceId
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getTasksByResidence(residenceId) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun loadResidenceContractors(residenceId: Int) {
viewModelScope.launch {
_selectedResidenceId.value = residenceId
_isLoading.value = true
_loadError.value = null
_loadError.value = (APILayer.getContractorsByResidence(residenceId) as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun resetResidenceTasksState() {
_selectedResidenceId.value = null
}
fun resetResidenceContractorsState() {
_selectedResidenceId.value = null
}
// ---------- Mutations ----------
fun createResidence(request: ResidenceCreateRequest) {
viewModelScope.launch {
_createResidenceState.value = ApiResult.Loading
@@ -84,17 +178,6 @@ class ResidenceViewModel : ViewModel() {
}
}
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
@@ -102,20 +185,8 @@ class ResidenceViewModel : ViewModel() {
}
}
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 resetCreateState() { _createResidenceState.value = ApiResult.Idle }
fun resetUpdateState() { _updateResidenceState.value = ApiResult.Idle }
fun cancelTask(taskId: Int) {
viewModelScope.launch {
@@ -138,17 +209,9 @@ class ResidenceViewModel : ViewModel() {
}
}
fun resetCancelTaskState() {
_cancelTaskState.value = ApiResult.Idle
}
fun resetUncancelTaskState() {
_uncancelTaskState.value = ApiResult.Idle
}
fun resetUpdateTaskState() {
_updateTaskState.value = ApiResult.Idle
}
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 {
@@ -157,9 +220,7 @@ class ResidenceViewModel : ViewModel() {
}
}
fun resetGenerateReportState() {
_generateReportState.value = ApiResult.Idle
}
fun resetGenerateReportState() { _generateReportState.value = ApiResult.Idle }
fun deleteResidence(residenceId: Int) {
viewModelScope.launch {
@@ -168,12 +229,7 @@ class ResidenceViewModel : ViewModel() {
}
}
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 resetDeleteResidenceState() { _deleteResidenceState.value = ApiResult.Idle }
fun joinWithCode(code: String) {
viewModelScope.launch {
@@ -182,18 +238,5 @@ class ResidenceViewModel : ViewModel() {
}
}
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
}
fun resetJoinResidenceState() { _joinResidenceState.value = ApiResult.Idle }
}

View File

@@ -2,175 +2,184 @@ package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.models.TaskColumnsResponse
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.IDataManager
import com.tt.honeyDue.models.CustomTask
import com.tt.honeyDue.models.TaskCreateRequest
import com.tt.honeyDue.models.TaskColumnsResponse
import com.tt.honeyDue.models.TaskCompletionResponse
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.models.TaskCreateRequest
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class TaskViewModel : ViewModel() {
/**
* TaskViewModel — derives all read-state from [IDataManager].
*
* The VM no longer owns `_tasksState` / `_tasksByResidenceState` /
* `_taskCompletionsState` `MutableStateFlow` fields. Instead:
*
* * [tasksState] is a derived flow over [IDataManager.allTasks] — whenever
* DataManager is updated (APILayer success write, fixture seed, etc.),
* the VM re-emits automatically. Screens rendering against the fixture
* in snapshot tests see populated data immediately.
* * [tasksByResidenceState] is parameterised by [selectedResidenceId]
* which callers set via [selectResidence]. The projection pulls the
* current selected-residence tasks out of [IDataManager.tasksByResidence].
* * [taskCompletionsState] is parameterised by [selectedTaskId].
*
* Loading / error state for the read-states is tracked separately on
* [isLoading] and [loadError]. Mutation-feedback fields ([taskAddNewCustomTaskState])
* remain owned by the VM — they track one-shot mutation outcomes, not
* cached data.
*/
class TaskViewModel(
private val dataManager: IDataManager = DataManager,
) : ViewModel() {
private val _tasksState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
val tasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksState
// ---------- Read state (derived from DataManager) ----------
private val _tasksByResidenceState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
val tasksByResidenceState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksByResidenceState
/** All tasks kanban — mirrors [IDataManager.allTasks]. */
val tasksState: StateFlow<ApiResult<TaskColumnsResponse>> =
dataManager.allTasks
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
/** Drives the [tasksByResidenceState] projection key. */
private val _selectedResidenceId = MutableStateFlow<Int?>(null)
/** Per-residence kanban — mirrors [IDataManager.tasksByResidence][residenceId]. */
val tasksByResidenceState: StateFlow<ApiResult<TaskColumnsResponse>> =
combine(_selectedResidenceId, dataManager.tasksByResidence) { id, map ->
if (id == null) ApiResult.Idle
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
/** Drives the [taskCompletionsState] projection key. */
private val _selectedTaskId = MutableStateFlow<Int?>(null)
/** Task completions for the currently selected task — mirrors [IDataManager.taskCompletions]. */
val taskCompletionsState: StateFlow<ApiResult<List<TaskCompletionResponse>>> =
combine(_selectedTaskId, dataManager.taskCompletions) { id, map ->
if (id == null) ApiResult.Idle
else map[id]?.let { ApiResult.Success(it) } ?: ApiResult.Idle
}.stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle)
// ---------- Loading / error feedback ----------
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _loadError = MutableStateFlow<String?>(null)
val loadError: StateFlow<String?> = _loadError
// ---------- Mutation-feedback (independent, one-shot) ----------
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
// ---------- Projection selectors ----------
fun selectResidence(residenceId: Int?) {
_selectedResidenceId.value = residenceId
}
fun selectTask(taskId: Int?) {
_selectedTaskId.value = taskId
}
// ---------- Load methods (write-through to DataManager) ----------
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}")
_isLoading.value = true
_loadError.value = null
val result = APILayer.getTasks(forceRefresh = forceRefresh)
_loadError.value = (result as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun loadTasksByResidence(residenceId: Int, forceRefresh: Boolean = false) {
viewModelScope.launch {
_tasksByResidenceState.value = ApiResult.Loading
_tasksByResidenceState.value = APILayer.getTasksByResidence(
_selectedResidenceId.value = residenceId
_isLoading.value = true
_loadError.value = null
val result = APILayer.getTasksByResidence(
residenceId = residenceId,
forceRefresh = forceRefresh
forceRefresh = forceRefresh,
)
_loadError.value = (result as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun loadTaskCompletions(taskId: Int) {
viewModelScope.launch {
_selectedTaskId.value = taskId
_isLoading.value = true
_loadError.value = null
val result = APILayer.getTaskCompletions(taskId)
_loadError.value = (result as? ApiResult.Error)?.message
_isLoading.value = false
}
}
fun resetTaskCompletionsState() {
_selectedTaskId.value = null
}
// ---------- Mutations ----------
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
_taskAddNewCustomTaskState.value = APILayer.createTask(request)
}
}
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)
}
}
onComplete(APILayer.updateTask(taskId, request) is ApiResult.Success)
}
}
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)
}
}
onComplete(APILayer.cancelTask(taskId) is ApiResult.Success)
}
}
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)
}
}
onComplete(APILayer.uncancelTask(taskId) is ApiResult.Success)
}
}
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)
}
}
onComplete(APILayer.markInProgress(taskId) is ApiResult.Success)
}
}
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)
}
}
onComplete(APILayer.archiveTask(taskId) is ApiResult.Success)
}
}
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)
}
}
onComplete(APILayer.unarchiveTask(taskId) is ApiResult.Success)
}
}
/**
* 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
}
}