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:
@@ -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()
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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? =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user