diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt index ae49278..98cc3c1 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt @@ -207,6 +207,13 @@ object DataManager : IDataManager { private val _taskTemplatesGrouped = MutableStateFlow(null) override val taskTemplatesGrouped: StateFlow = _taskTemplatesGrouped.asStateFlow() + private val _notificationPreferences = MutableStateFlow(null) + override val notificationPreferences: StateFlow = _notificationPreferences.asStateFlow() + + fun setNotificationPreferences(prefs: com.tt.honeyDue.models.NotificationPreference?) { + _notificationPreferences.value = prefs + } + // Map-based for O(1) ID resolution private val _residenceTypesMap = MutableStateFlow>(emptyMap()) val residenceTypesMap: StateFlow> = _residenceTypesMap.asStateFlow() diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt index 930eea6..0176ae7 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt @@ -112,6 +112,11 @@ interface IDataManager { val taskTemplates: StateFlow> val taskTemplatesGrouped: StateFlow + // ==================== NOTIFICATION PREFERENCES ==================== + + /** User's server-backed notification preferences. Populated by APILayer.getNotificationPreferences. */ + val notificationPreferences: StateFlow + // ==================== O(1) LOOKUP HELPERS ==================== fun getResidenceType(id: Int?): ResidenceType? diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt index 3c22dd9..34afa2e 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt @@ -1430,12 +1430,16 @@ object APILayer { suspend fun getNotificationPreferences(): ApiResult { 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 { 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> { diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/testing/InMemoryDataManager.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/testing/InMemoryDataManager.kt index c9645a6..e9f5a7d 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/testing/InMemoryDataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/testing/InMemoryDataManager.kt @@ -61,6 +61,7 @@ class InMemoryDataManager( contractorSpecialties: List = emptyList(), taskTemplates: List = emptyList(), taskTemplatesGrouped: TaskTemplatesGroupedResponse? = null, + notificationPreferences: com.tt.honeyDue.models.NotificationPreference? = null, ) : IDataManager { // ==================== AUTH ==================== @@ -112,6 +113,8 @@ class InMemoryDataManager( override val taskTemplates: StateFlow> = MutableStateFlow(taskTemplates) override val taskTemplatesGrouped: StateFlow = MutableStateFlow(taskTemplatesGrouped) + override val notificationPreferences: StateFlow = MutableStateFlow(notificationPreferences) + // ==================== LOOKUP HELPERS ==================== override fun getResidenceType(id: Int?): ResidenceType? = diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/AuthViewModel.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/AuthViewModel.kt index 54ff188..fb30859 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/AuthViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/AuthViewModel.kt @@ -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.Idle) val loginState: StateFlow> = _loginState @@ -40,8 +46,11 @@ class AuthViewModel : ViewModel() { private val _updateProfileState = MutableStateFlow>(ApiResult.Idle) val updateProfileState: StateFlow> = _updateProfileState - private val _currentUserState = MutableStateFlow>(ApiResult.Idle) - val currentUserState: StateFlow> = _currentUserState + /** Current authenticated user — derived from [IDataManager.currentUser]. APILayer writes through on login/register/getCurrentUser. */ + val currentUserState: StateFlow> = + dataManager.currentUser + .map { if (it != null) ApiResult.Success(it) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) private val _forgotPasswordState = MutableStateFlow>(ApiResult.Idle) val forgotPasswordState: StateFlow> = _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) { diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ContractorViewModel.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ContractorViewModel.kt index 7b3b7c2..66dec59 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ContractorViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ContractorViewModel.kt @@ -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.Idle) - val contractorsState: StateFlow>> = _contractorsState + // ---------- Read state ---------- - private val _contractorDetailState = MutableStateFlow>(ApiResult.Idle) - val contractorDetailState: StateFlow> = _contractorDetailState + val contractorsState: StateFlow>> = + dataManager.contractors + .map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) + + private val _selectedContractorId = MutableStateFlow(null) + + val contractorDetailState: StateFlow> = + 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 = _isLoading + private val _loadError = MutableStateFlow(null) + val loadError: StateFlow = _loadError + + // ---------- Mutation-feedback ---------- private val _createState = MutableStateFlow>(ApiResult.Idle) val createState: StateFlow> = _createState @@ -29,32 +64,45 @@ class ContractorViewModel : ViewModel() { private val _toggleFavoriteState = MutableStateFlow>(ApiResult.Idle) val toggleFavoriteState: StateFlow> = _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 } } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/DocumentViewModel.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/DocumentViewModel.kt index 8776fe5..a19483f 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/DocumentViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/DocumentViewModel.kt @@ -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.Idle) - val documentsState: StateFlow>> = _documentsState + // ---------- Read state ---------- - private val _documentDetailState = MutableStateFlow>(ApiResult.Idle) - val documentDetailState: StateFlow> = _documentDetailState + val documentsState: StateFlow>> = + dataManager.documents + .map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) + + private val _selectedDocumentId = MutableStateFlow(null) + + val documentDetailState: StateFlow> = + 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 = _isLoading + private val _loadError = MutableStateFlow(null) + val loadError: StateFlow = _loadError + + // ---------- Mutation-feedback ---------- private val _createState = MutableStateFlow>(ApiResult.Idle) val createState: StateFlow> = _createState @@ -36,6 +75,8 @@ class DocumentViewModel : ViewModel() { private val _uploadImageState = MutableStateFlow>(ApiResult.Idle) val uploadImageState: StateFlow> = _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 } } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/LookupsViewModel.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/LookupsViewModel.kt index fcaa66f..45940d2 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/LookupsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/LookupsViewModel.kt @@ -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>` 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> = DataManager.residenceTypes - val taskFrequencies: StateFlow> = DataManager.taskFrequencies - val taskPriorities: StateFlow> = DataManager.taskPriorities - val taskCategories: StateFlow> = DataManager.taskCategories - val contractorSpecialties: StateFlow> = DataManager.contractorSpecialties + // ---------- Direct exposure (preferred) ---------- - // Keep legacy state flows for compatibility during migration - private val _residenceTypesState = MutableStateFlow>>(ApiResult.Idle) - val residenceTypesState: StateFlow>> = _residenceTypesState + val residenceTypes: StateFlow> = dataManager.residenceTypes + val taskFrequencies: StateFlow> = dataManager.taskFrequencies + val taskPriorities: StateFlow> = dataManager.taskPriorities + val taskCategories: StateFlow> = dataManager.taskCategories + val contractorSpecialties: StateFlow> = dataManager.contractorSpecialties - private val _taskFrequenciesState = MutableStateFlow>>(ApiResult.Idle) - val taskFrequenciesState: StateFlow>> = _taskFrequenciesState + // ---------- ApiResult-wrapped projections (legacy — derived) ---------- - private val _taskPrioritiesState = MutableStateFlow>>(ApiResult.Idle) - val taskPrioritiesState: StateFlow>> = _taskPrioritiesState + val residenceTypesState: StateFlow>> = + dataManager.residenceTypes + .map { if (it.isNotEmpty()) ApiResult.Success(it) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) - private val _taskCategoriesState = MutableStateFlow>>(ApiResult.Idle) - val taskCategoriesState: StateFlow>> = _taskCategoriesState + val taskFrequenciesState: StateFlow>> = + dataManager.taskFrequencies + .map { if (it.isNotEmpty()) ApiResult.Success(it) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) + + val taskPrioritiesState: StateFlow>> = + dataManager.taskPriorities + .map { if (it.isNotEmpty()) ApiResult.Success(it) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) + + val taskCategoriesState: StateFlow>> = + 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() diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/NotificationPreferencesViewModel.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/NotificationPreferencesViewModel.kt index 901fbfd..fb154d8 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/NotificationPreferencesViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/NotificationPreferencesViewModel.kt @@ -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.Idle) - val preferencesState: StateFlow> = _preferencesState.asStateFlow() + /** Server-backed preferences — derived from [IDataManager.notificationPreferences]. + * APILayer.getNotificationPreferences / updateNotificationPreferences write through. */ + val preferencesState: StateFlow> = + dataManager.notificationPreferences + .map { if (it != null) ApiResult.Success(it) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) private val _updateState = MutableStateFlow>(ApiResult.Idle) val updateState: StateFlow> = _updateState.asStateFlow() @@ -92,15 +103,9 @@ class NotificationPreferencesViewModel : ViewModel() { val categoryState: StateFlow> = _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) } } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt index 0991226..b07bd26 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt @@ -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.Idle) - val residencesState: StateFlow>> = _residencesState + // ---------- Read state (derived from DataManager) ---------- - private val _summaryState = MutableStateFlow>(ApiResult.Idle) - val summaryState: StateFlow> = _summaryState + val residencesState: StateFlow>> = + dataManager.residences + .map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) + + val myResidencesState: StateFlow> = + dataManager.myResidences + .map { if (it != null) ApiResult.Success(it) else ApiResult.Idle } + .stateIn(viewModelScope, SharingStarted.Eagerly, ApiResult.Idle) + + val summaryState: StateFlow> = + 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(null) + + val residenceTasksState: StateFlow> = + 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>> = + 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 = _isLoading + + private val _loadError = MutableStateFlow(null) + val loadError: StateFlow = _loadError + + // ---------- Mutation-feedback (one-shot, owned by VM) ---------- private val _createResidenceState = MutableStateFlow>(ApiResult.Idle) val createResidenceState: StateFlow> = _createResidenceState @@ -28,11 +82,11 @@ class ResidenceViewModel : ViewModel() { private val _updateResidenceState = MutableStateFlow>(ApiResult.Idle) val updateResidenceState: StateFlow> = _updateResidenceState - private val _residenceTasksState = MutableStateFlow>(ApiResult.Idle) - val residenceTasksState: StateFlow> = _residenceTasksState + private val _deleteResidenceState = MutableStateFlow>(ApiResult.Idle) + val deleteResidenceState: StateFlow> = _deleteResidenceState - private val _myResidencesState = MutableStateFlow>(ApiResult.Idle) - val myResidencesState: StateFlow> = _myResidencesState + private val _joinResidenceState = MutableStateFlow>(ApiResult.Idle) + val joinResidenceState: StateFlow> = _joinResidenceState private val _cancelTaskState = MutableStateFlow>(ApiResult.Idle) val cancelTaskState: StateFlow> = _cancelTaskState @@ -46,37 +100,77 @@ class ResidenceViewModel : ViewModel() { private val _generateReportState = MutableStateFlow>(ApiResult.Idle) val generateReportState: StateFlow> = _generateReportState - private val _deleteResidenceState = MutableStateFlow>(ApiResult.Idle) - val deleteResidenceState: StateFlow> = _deleteResidenceState + // ---------- Projection selectors ---------- - private val _residenceContractorsState = MutableStateFlow>>(ApiResult.Idle) - val residenceContractorsState: StateFlow>> = _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) -> 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.Idle) - val joinResidenceState: StateFlow> = _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 } } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/TaskViewModel.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/TaskViewModel.kt index 03d2d9e..2435358 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/TaskViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/TaskViewModel.kt @@ -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.Idle) - val tasksState: StateFlow> = _tasksState + // ---------- Read state (derived from DataManager) ---------- - private val _tasksByResidenceState = MutableStateFlow>(ApiResult.Idle) - val tasksByResidenceState: StateFlow> = _tasksByResidenceState + /** All tasks kanban — mirrors [IDataManager.allTasks]. */ + val tasksState: StateFlow> = + 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(null) + + /** Per-residence kanban — mirrors [IDataManager.tasksByResidence][residenceId]. */ + val tasksByResidenceState: StateFlow> = + 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(null) + + /** Task completions for the currently selected task — mirrors [IDataManager.taskCompletions]. */ + val taskCompletionsState: StateFlow>> = + 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 = _isLoading + + private val _loadError = MutableStateFlow(null) + val loadError: StateFlow = _loadError + + // ---------- Mutation-feedback (independent, one-shot) ---------- private val _taskAddNewCustomTaskState = MutableStateFlow>(ApiResult.Idle) val taskAddNewCustomTaskState: StateFlow> = _taskAddNewCustomTaskState - private val _taskCompletionsState = MutableStateFlow>>(ApiResult.Idle) - val taskCompletionsState: StateFlow>> = _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 - } }