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)
|
private val _taskTemplatesGrouped = MutableStateFlow<TaskTemplatesGroupedResponse?>(null)
|
||||||
override val taskTemplatesGrouped: StateFlow<TaskTemplatesGroupedResponse?> = _taskTemplatesGrouped.asStateFlow()
|
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
|
// Map-based for O(1) ID resolution
|
||||||
private val _residenceTypesMap = MutableStateFlow<Map<Int, ResidenceType>>(emptyMap())
|
private val _residenceTypesMap = MutableStateFlow<Map<Int, ResidenceType>>(emptyMap())
|
||||||
val residenceTypesMap: StateFlow<Map<Int, ResidenceType>> = _residenceTypesMap.asStateFlow()
|
val residenceTypesMap: StateFlow<Map<Int, ResidenceType>> = _residenceTypesMap.asStateFlow()
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ interface IDataManager {
|
|||||||
val taskTemplates: StateFlow<List<TaskTemplate>>
|
val taskTemplates: StateFlow<List<TaskTemplate>>
|
||||||
val taskTemplatesGrouped: StateFlow<TaskTemplatesGroupedResponse?>
|
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 ====================
|
// ==================== O(1) LOOKUP HELPERS ====================
|
||||||
|
|
||||||
fun getResidenceType(id: Int?): ResidenceType?
|
fun getResidenceType(id: Int?): ResidenceType?
|
||||||
|
|||||||
@@ -1430,12 +1430,16 @@ object APILayer {
|
|||||||
|
|
||||||
suspend fun getNotificationPreferences(): ApiResult<NotificationPreference> {
|
suspend fun getNotificationPreferences(): ApiResult<NotificationPreference> {
|
||||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
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> {
|
suspend fun updateNotificationPreferences(request: UpdateNotificationPreferencesRequest): ApiResult<NotificationPreference> {
|
||||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
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>> {
|
suspend fun getNotificationHistory(): ApiResult<List<Notification>> {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class InMemoryDataManager(
|
|||||||
contractorSpecialties: List<ContractorSpecialty> = emptyList(),
|
contractorSpecialties: List<ContractorSpecialty> = emptyList(),
|
||||||
taskTemplates: List<TaskTemplate> = emptyList(),
|
taskTemplates: List<TaskTemplate> = emptyList(),
|
||||||
taskTemplatesGrouped: TaskTemplatesGroupedResponse? = null,
|
taskTemplatesGrouped: TaskTemplatesGroupedResponse? = null,
|
||||||
|
notificationPreferences: com.tt.honeyDue.models.NotificationPreference? = null,
|
||||||
) : IDataManager {
|
) : IDataManager {
|
||||||
|
|
||||||
// ==================== AUTH ====================
|
// ==================== AUTH ====================
|
||||||
@@ -112,6 +113,8 @@ class InMemoryDataManager(
|
|||||||
override val taskTemplates: StateFlow<List<TaskTemplate>> = MutableStateFlow(taskTemplates)
|
override val taskTemplates: StateFlow<List<TaskTemplate>> = MutableStateFlow(taskTemplates)
|
||||||
override val taskTemplatesGrouped: StateFlow<TaskTemplatesGroupedResponse?> = MutableStateFlow(taskTemplatesGrouped)
|
override val taskTemplatesGrouped: StateFlow<TaskTemplatesGroupedResponse?> = MutableStateFlow(taskTemplatesGrouped)
|
||||||
|
|
||||||
|
override val notificationPreferences: StateFlow<com.tt.honeyDue.models.NotificationPreference?> = MutableStateFlow(notificationPreferences)
|
||||||
|
|
||||||
// ==================== LOOKUP HELPERS ====================
|
// ==================== LOOKUP HELPERS ====================
|
||||||
|
|
||||||
override fun getResidenceType(id: Int?): ResidenceType? =
|
override fun getResidenceType(id: Int?): ResidenceType? =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.tt.honeyDue.viewmodel
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.tt.honeyDue.data.DataManager
|
import com.tt.honeyDue.data.DataManager
|
||||||
|
import com.tt.honeyDue.data.IDataManager
|
||||||
import com.tt.honeyDue.models.AppleSignInRequest
|
import com.tt.honeyDue.models.AppleSignInRequest
|
||||||
import com.tt.honeyDue.models.AppleSignInResponse
|
import com.tt.honeyDue.models.AppleSignInResponse
|
||||||
import com.tt.honeyDue.models.GoogleSignInRequest
|
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.ApiResult
|
||||||
import com.tt.honeyDue.network.APILayer
|
import com.tt.honeyDue.network.APILayer
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
class AuthViewModel(
|
||||||
|
private val dataManager: IDataManager = DataManager,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
|
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
|
||||||
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
|
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
|
||||||
@@ -40,8 +46,11 @@ class AuthViewModel : ViewModel() {
|
|||||||
private val _updateProfileState = MutableStateFlow<ApiResult<User>>(ApiResult.Idle)
|
private val _updateProfileState = MutableStateFlow<ApiResult<User>>(ApiResult.Idle)
|
||||||
val updateProfileState: StateFlow<ApiResult<User>> = _updateProfileState
|
val updateProfileState: StateFlow<ApiResult<User>> = _updateProfileState
|
||||||
|
|
||||||
private val _currentUserState = MutableStateFlow<ApiResult<User>>(ApiResult.Idle)
|
/** Current authenticated user — derived from [IDataManager.currentUser]. APILayer writes through on login/register/getCurrentUser. */
|
||||||
val currentUserState: StateFlow<ApiResult<User>> = _currentUserState
|
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)
|
private val _forgotPasswordState = MutableStateFlow<ApiResult<ForgotPasswordResponse>>(ApiResult.Idle)
|
||||||
val forgotPasswordState: StateFlow<ApiResult<ForgotPasswordResponse>> = _forgotPasswordState
|
val forgotPasswordState: StateFlow<ApiResult<ForgotPasswordResponse>> = _forgotPasswordState
|
||||||
@@ -149,19 +158,18 @@ class AuthViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentUser(forceRefresh: Boolean = false) {
|
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 {
|
viewModelScope.launch {
|
||||||
_currentUserState.value = ApiResult.Loading
|
APILayer.getCurrentUser(forceRefresh = forceRefresh)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** No-op — [currentUserState] is derived from DataManager and can't be
|
||||||
|
* locally reset. To clear, call [DataManager.setCurrentUser] with null. */
|
||||||
fun resetCurrentUserState() {
|
fun resetCurrentUserState() {
|
||||||
_currentUserState.value = ApiResult.Idle
|
// intentionally empty
|
||||||
}
|
}
|
||||||
|
|
||||||
fun forgotPassword(email: String) {
|
fun forgotPassword(email: String) {
|
||||||
|
|||||||
@@ -2,20 +2,55 @@ package com.tt.honeyDue.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.models.*
|
||||||
import com.tt.honeyDue.network.ApiResult
|
|
||||||
import com.tt.honeyDue.network.APILayer
|
import com.tt.honeyDue.network.APILayer
|
||||||
|
import com.tt.honeyDue.network.ApiResult
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
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)
|
// ---------- Read state ----------
|
||||||
val contractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _contractorsState
|
|
||||||
|
|
||||||
private val _contractorDetailState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
val contractorsState: StateFlow<ApiResult<List<ContractorSummary>>> =
|
||||||
val contractorDetailState: StateFlow<ApiResult<Contractor>> = _contractorDetailState
|
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)
|
private val _createState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
||||||
val createState: StateFlow<ApiResult<Contractor>> = _createState
|
val createState: StateFlow<ApiResult<Contractor>> = _createState
|
||||||
@@ -29,32 +64,45 @@ class ContractorViewModel : ViewModel() {
|
|||||||
private val _toggleFavoriteState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
private val _toggleFavoriteState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
|
||||||
val toggleFavoriteState: StateFlow<ApiResult<Contractor>> = _toggleFavoriteState
|
val toggleFavoriteState: StateFlow<ApiResult<Contractor>> = _toggleFavoriteState
|
||||||
|
|
||||||
|
// ---------- Loaders (write-through to DataManager) ----------
|
||||||
|
|
||||||
fun loadContractors(
|
fun loadContractors(
|
||||||
specialty: String? = null,
|
specialty: String? = null,
|
||||||
isFavorite: Boolean? = null,
|
isFavorite: Boolean? = null,
|
||||||
isActive: Boolean? = null,
|
isActive: Boolean? = null,
|
||||||
search: String? = null,
|
search: String? = null,
|
||||||
forceRefresh: Boolean = false
|
forceRefresh: Boolean = false,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_contractorsState.value = ApiResult.Loading
|
_isLoading.value = true
|
||||||
_contractorsState.value = APILayer.getContractors(
|
_loadError.value = null
|
||||||
|
_loadError.value = (APILayer.getContractors(
|
||||||
specialty = specialty,
|
specialty = specialty,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
isActive = isActive,
|
isActive = isActive,
|
||||||
search = search,
|
search = search,
|
||||||
forceRefresh = forceRefresh
|
forceRefresh = forceRefresh,
|
||||||
)
|
) as? ApiResult.Error)?.message
|
||||||
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadContractorDetail(id: Int) {
|
fun loadContractorDetail(id: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_contractorDetailState.value = ApiResult.Loading
|
_selectedContractorId.value = id
|
||||||
_contractorDetailState.value = APILayer.getContractor(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) {
|
fun createContractor(request: ContractorCreateRequest) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_createState.value = ApiResult.Loading
|
_createState.value = ApiResult.Loading
|
||||||
@@ -83,19 +131,8 @@ class ContractorViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetCreateState() {
|
fun resetCreateState() { _createState.value = ApiResult.Idle }
|
||||||
_createState.value = ApiResult.Idle
|
fun resetUpdateState() { _updateState.value = ApiResult.Idle }
|
||||||
}
|
fun resetDeleteState() { _deleteState.value = ApiResult.Idle }
|
||||||
|
fun resetToggleFavoriteState() { _toggleFavoriteState.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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.models.*
|
||||||
import com.tt.honeyDue.network.ApiResult
|
|
||||||
import com.tt.honeyDue.network.APILayer
|
import com.tt.honeyDue.network.APILayer
|
||||||
|
import com.tt.honeyDue.network.ApiResult
|
||||||
import com.tt.honeyDue.util.ImageCompressor
|
import com.tt.honeyDue.util.ImageCompressor
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
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)
|
// ---------- Read state ----------
|
||||||
val documentsState: StateFlow<ApiResult<List<Document>>> = _documentsState
|
|
||||||
|
|
||||||
private val _documentDetailState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
val documentsState: StateFlow<ApiResult<List<Document>>> =
|
||||||
val documentDetailState: StateFlow<ApiResult<Document>> = _documentDetailState
|
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)
|
private val _createState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
||||||
val createState: StateFlow<ApiResult<Document>> = _createState
|
val createState: StateFlow<ApiResult<Document>> = _createState
|
||||||
@@ -36,6 +75,8 @@ class DocumentViewModel : ViewModel() {
|
|||||||
private val _uploadImageState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
private val _uploadImageState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
|
||||||
val uploadImageState: StateFlow<ApiResult<Document>> = _uploadImageState
|
val uploadImageState: StateFlow<ApiResult<Document>> = _uploadImageState
|
||||||
|
|
||||||
|
// ---------- Loaders (write-through to DataManager) ----------
|
||||||
|
|
||||||
fun loadDocuments(
|
fun loadDocuments(
|
||||||
residenceId: Int? = null,
|
residenceId: Int? = null,
|
||||||
documentType: String? = null,
|
documentType: String? = null,
|
||||||
@@ -48,8 +89,9 @@ class DocumentViewModel : ViewModel() {
|
|||||||
forceRefresh: Boolean = false
|
forceRefresh: Boolean = false
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_documentsState.value = ApiResult.Loading
|
_isLoading.value = true
|
||||||
_documentsState.value = APILayer.getDocuments(
|
_loadError.value = null
|
||||||
|
_loadError.value = (APILayer.getDocuments(
|
||||||
residenceId = residenceId,
|
residenceId = residenceId,
|
||||||
documentType = documentType,
|
documentType = documentType,
|
||||||
category = category,
|
category = category,
|
||||||
@@ -59,7 +101,8 @@ class DocumentViewModel : ViewModel() {
|
|||||||
tags = tags,
|
tags = tags,
|
||||||
search = search,
|
search = search,
|
||||||
forceRefresh = forceRefresh
|
forceRefresh = forceRefresh
|
||||||
)
|
) as? ApiResult.Error)?.message
|
||||||
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,18 +115,23 @@ class DocumentViewModel : ViewModel() {
|
|||||||
forceRefresh: Boolean = false
|
forceRefresh: Boolean = false
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_documentsState.value = ApiResult.Loading
|
_isLoading.value = true
|
||||||
_documentsState.value = APILayer.getDocuments(
|
_loadError.value = null
|
||||||
|
_loadError.value = (APILayer.getDocuments(
|
||||||
residenceId = residenceId,
|
residenceId = residenceId,
|
||||||
forceRefresh = forceRefresh
|
forceRefresh = forceRefresh
|
||||||
)
|
) as? ApiResult.Error)?.message
|
||||||
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDocumentDetail(id: Int) {
|
fun loadDocumentDetail(id: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_documentDetailState.value = ApiResult.Loading
|
_selectedDocumentId.value = id
|
||||||
_documentDetailState.value = APILayer.getDocument(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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.tt.honeyDue.data.DataManager
|
import com.tt.honeyDue.data.DataManager
|
||||||
|
import com.tt.honeyDue.data.IDataManager
|
||||||
import com.tt.honeyDue.models.*
|
import com.tt.honeyDue.models.*
|
||||||
import com.tt.honeyDue.network.ApiResult
|
|
||||||
import com.tt.honeyDue.network.APILayer
|
import com.tt.honeyDue.network.APILayer
|
||||||
|
import com.tt.honeyDue.network.ApiResult
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel for lookup data.
|
* LookupsViewModel — already the template for reactive DataManager
|
||||||
* Now uses DataManager as the single source of truth for all lookups.
|
* derivation. Extended to accept [IDataManager] as a constructor param
|
||||||
* Lookups are loaded once via APILayer.initializeLookups() after login.
|
* 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
|
// ---------- Direct exposure (preferred) ----------
|
||||||
val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes
|
|
||||||
val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies
|
|
||||||
val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities
|
|
||||||
val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories
|
|
||||||
val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties
|
|
||||||
|
|
||||||
// Keep legacy state flows for compatibility during migration
|
val residenceTypes: StateFlow<List<ResidenceType>> = dataManager.residenceTypes
|
||||||
private val _residenceTypesState = MutableStateFlow<ApiResult<List<ResidenceType>>>(ApiResult.Idle)
|
val taskFrequencies: StateFlow<List<TaskFrequency>> = dataManager.taskFrequencies
|
||||||
val residenceTypesState: StateFlow<ApiResult<List<ResidenceType>>> = _residenceTypesState
|
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)
|
// ---------- ApiResult-wrapped projections (legacy — derived) ----------
|
||||||
val taskFrequenciesState: StateFlow<ApiResult<List<TaskFrequency>>> = _taskFrequenciesState
|
|
||||||
|
|
||||||
private val _taskPrioritiesState = MutableStateFlow<ApiResult<List<TaskPriority>>>(ApiResult.Idle)
|
val residenceTypesState: StateFlow<ApiResult<List<ResidenceType>>> =
|
||||||
val taskPrioritiesState: StateFlow<ApiResult<List<TaskPriority>>> = _taskPrioritiesState
|
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 taskFrequenciesState: StateFlow<ApiResult<List<TaskFrequency>>> =
|
||||||
val taskCategoriesState: StateFlow<ApiResult<List<TaskCategory>>> = _taskCategoriesState
|
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() {
|
fun loadResidenceTypes() {
|
||||||
viewModelScope.launch {
|
if (dataManager.residenceTypes.value.isNotEmpty()) return
|
||||||
val cached = DataManager.residenceTypes.value
|
viewModelScope.launch { APILayer.getResidenceTypes() }
|
||||||
if (cached.isNotEmpty()) {
|
|
||||||
_residenceTypesState.value = ApiResult.Success(cached)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
_residenceTypesState.value = ApiResult.Loading
|
|
||||||
val result = APILayer.getResidenceTypes()
|
|
||||||
_residenceTypesState.value = result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadTaskFrequencies() {
|
fun loadTaskFrequencies() {
|
||||||
viewModelScope.launch {
|
if (dataManager.taskFrequencies.value.isNotEmpty()) return
|
||||||
val cached = DataManager.taskFrequencies.value
|
viewModelScope.launch { APILayer.getTaskFrequencies() }
|
||||||
if (cached.isNotEmpty()) {
|
|
||||||
_taskFrequenciesState.value = ApiResult.Success(cached)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
_taskFrequenciesState.value = ApiResult.Loading
|
|
||||||
val result = APILayer.getTaskFrequencies()
|
|
||||||
_taskFrequenciesState.value = result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadTaskPriorities() {
|
fun loadTaskPriorities() {
|
||||||
viewModelScope.launch {
|
if (dataManager.taskPriorities.value.isNotEmpty()) return
|
||||||
val cached = DataManager.taskPriorities.value
|
viewModelScope.launch { APILayer.getTaskPriorities() }
|
||||||
if (cached.isNotEmpty()) {
|
|
||||||
_taskPrioritiesState.value = ApiResult.Success(cached)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
_taskPrioritiesState.value = ApiResult.Loading
|
|
||||||
val result = APILayer.getTaskPriorities()
|
|
||||||
_taskPrioritiesState.value = result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadTaskCategories() {
|
fun loadTaskCategories() {
|
||||||
viewModelScope.launch {
|
if (dataManager.taskCategories.value.isNotEmpty()) return
|
||||||
val cached = DataManager.taskCategories.value
|
viewModelScope.launch { APILayer.getTaskCategories() }
|
||||||
if (cached.isNotEmpty()) {
|
|
||||||
_taskCategoriesState.value = ApiResult.Success(cached)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
_taskCategoriesState.value = ApiResult.Loading
|
|
||||||
val result = APILayer.getTaskCategories()
|
|
||||||
_taskCategoriesState.value = result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load all lookups at once
|
|
||||||
fun loadAllLookups() {
|
fun loadAllLookups() {
|
||||||
loadResidenceTypes()
|
loadResidenceTypes()
|
||||||
loadTaskFrequencies()
|
loadTaskFrequencies()
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ package com.tt.honeyDue.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.NotificationPreference
|
||||||
import com.tt.honeyDue.models.UpdateNotificationPreferencesRequest
|
import com.tt.honeyDue.models.UpdateNotificationPreferencesRequest
|
||||||
import com.tt.honeyDue.network.ApiResult
|
|
||||||
import com.tt.honeyDue.network.APILayer
|
import com.tt.honeyDue.network.APILayer
|
||||||
|
import com.tt.honeyDue.network.ApiResult
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,10 +78,16 @@ class NotificationCategoriesController(
|
|||||||
* alongside the new per-category local toggles driven by
|
* alongside the new per-category local toggles driven by
|
||||||
* [NotificationCategoriesController].
|
* [NotificationCategoriesController].
|
||||||
*/
|
*/
|
||||||
class NotificationPreferencesViewModel : ViewModel() {
|
class NotificationPreferencesViewModel(
|
||||||
|
private val dataManager: IDataManager = DataManager,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _preferencesState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
|
/** Server-backed preferences — derived from [IDataManager.notificationPreferences].
|
||||||
val preferencesState: StateFlow<ApiResult<NotificationPreference>> = _preferencesState.asStateFlow()
|
* 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)
|
private val _updateState = MutableStateFlow<ApiResult<NotificationPreference>>(ApiResult.Idle)
|
||||||
val updateState: StateFlow<ApiResult<NotificationPreference>> = _updateState.asStateFlow()
|
val updateState: StateFlow<ApiResult<NotificationPreference>> = _updateState.asStateFlow()
|
||||||
@@ -92,15 +103,9 @@ class NotificationPreferencesViewModel : ViewModel() {
|
|||||||
val categoryState: StateFlow<Map<String, Boolean>> = _categoryState.asStateFlow()
|
val categoryState: StateFlow<Map<String, Boolean>> = _categoryState.asStateFlow()
|
||||||
|
|
||||||
fun loadPreferences() {
|
fun loadPreferences() {
|
||||||
viewModelScope.launch {
|
// Fire the API call; APILayer writes to DataManager.setNotificationPreferences
|
||||||
_preferencesState.value = ApiResult.Loading
|
// on success and [preferencesState] re-emits automatically.
|
||||||
val result = APILayer.getNotificationPreferences()
|
viewModelScope.launch { APILayer.getNotificationPreferences() }
|
||||||
_preferencesState.value = when (result) {
|
|
||||||
is ApiResult.Success -> ApiResult.Success(result.data)
|
|
||||||
is ApiResult.Error -> result
|
|
||||||
else -> ApiResult.Error("Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePreference(
|
fun updatePreference(
|
||||||
@@ -133,15 +138,10 @@ class NotificationPreferencesViewModel : ViewModel() {
|
|||||||
warrantyExpiringHour = warrantyExpiringHour,
|
warrantyExpiringHour = warrantyExpiringHour,
|
||||||
dailyDigestHour = dailyDigestHour,
|
dailyDigestHour = dailyDigestHour,
|
||||||
)
|
)
|
||||||
val result = APILayer.updateNotificationPreferences(request)
|
// APILayer.updateNotificationPreferences writes through to
|
||||||
_updateState.value = when (result) {
|
// DataManager.setNotificationPreferences on success, so
|
||||||
is ApiResult.Success -> {
|
// [preferencesState] re-emits automatically.
|
||||||
_preferencesState.value = ApiResult.Success(result.data)
|
_updateState.value = APILayer.updateNotificationPreferences(request)
|
||||||
ApiResult.Success(result.data)
|
|
||||||
}
|
|
||||||
is ApiResult.Error -> result
|
|
||||||
else -> ApiResult.Error("Unknown error")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,79 @@ package com.tt.honeyDue.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.Residence
|
||||||
import com.tt.honeyDue.models.ResidenceCreateRequest
|
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.TaskColumnsResponse
|
||||||
import com.tt.honeyDue.models.ContractorSummary
|
import com.tt.honeyDue.models.TotalSummary
|
||||||
import com.tt.honeyDue.network.ApiResult
|
|
||||||
import com.tt.honeyDue.network.APILayer
|
import com.tt.honeyDue.network.APILayer
|
||||||
|
import com.tt.honeyDue.network.ApiResult
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
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)
|
// ---------- Read state (derived from DataManager) ----------
|
||||||
val residencesState: StateFlow<ApiResult<List<Residence>>> = _residencesState
|
|
||||||
|
|
||||||
private val _summaryState = MutableStateFlow<ApiResult<TotalSummary>>(ApiResult.Idle)
|
val residencesState: StateFlow<ApiResult<List<Residence>>> =
|
||||||
val summaryState: StateFlow<ApiResult<TotalSummary>> = _summaryState
|
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)
|
private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
||||||
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
|
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
|
||||||
@@ -28,11 +82,11 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
||||||
val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState
|
val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState
|
||||||
|
|
||||||
private val _residenceTasksState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
private val _deleteResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||||
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _residenceTasksState
|
val deleteResidenceState: StateFlow<ApiResult<Unit>> = _deleteResidenceState
|
||||||
|
|
||||||
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Idle)
|
private val _joinResidenceState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>>(ApiResult.Idle)
|
||||||
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
|
val joinResidenceState: StateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>> = _joinResidenceState
|
||||||
|
|
||||||
private val _cancelTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
|
private val _cancelTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
|
||||||
val cancelTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _cancelTaskState
|
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)
|
private val _generateReportState = MutableStateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>>(ApiResult.Idle)
|
||||||
val generateReportState: StateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>> = _generateReportState
|
val generateReportState: StateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>> = _generateReportState
|
||||||
|
|
||||||
private val _deleteResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
// ---------- Projection selectors ----------
|
||||||
val deleteResidenceState: StateFlow<ApiResult<Unit>> = _deleteResidenceState
|
|
||||||
|
|
||||||
private val _residenceContractorsState = MutableStateFlow<ApiResult<List<ContractorSummary>>>(ApiResult.Idle)
|
fun selectResidence(residenceId: Int?) {
|
||||||
val residenceContractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _residenceContractorsState
|
_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) {
|
fun loadResidences(forceRefresh: Boolean = false) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_residencesState.value = ApiResult.Loading
|
_isLoading.value = true
|
||||||
_residencesState.value = APILayer.getResidences(forceRefresh = forceRefresh)
|
_loadError.value = null
|
||||||
|
_loadError.value = (APILayer.getResidences(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
|
||||||
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadSummary(forceRefresh: Boolean = false) {
|
fun loadSummary(forceRefresh: Boolean = false) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_summaryState.value = ApiResult.Loading
|
_isLoading.value = true
|
||||||
_summaryState.value = APILayer.getSummary(forceRefresh = forceRefresh)
|
_loadError.value = null
|
||||||
|
_loadError.value = (APILayer.getSummary(forceRefresh = forceRefresh) as? ApiResult.Error)?.message
|
||||||
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getResidence(id: Int, onResult: (ApiResult<Residence>) -> Unit) {
|
fun getResidence(id: Int, onResult: (ApiResult<Residence>) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = APILayer.getResidence(id)
|
onResult(APILayer.getResidence(id))
|
||||||
onResult(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fun createResidence(request: ResidenceCreateRequest) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_createResidenceState.value = ApiResult.Loading
|
_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) {
|
fun updateResidence(residenceId: Int, request: ResidenceCreateRequest) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_updateResidenceState.value = ApiResult.Loading
|
_updateResidenceState.value = ApiResult.Loading
|
||||||
@@ -102,20 +185,8 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetCreateState() {
|
fun resetCreateState() { _createResidenceState.value = ApiResult.Idle }
|
||||||
_createResidenceState.value = ApiResult.Idle
|
fun resetUpdateState() { _updateResidenceState.value = ApiResult.Idle }
|
||||||
}
|
|
||||||
|
|
||||||
fun resetUpdateState() {
|
|
||||||
_updateResidenceState.value = ApiResult.Idle
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadMyResidences(forceRefresh: Boolean = false) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_myResidencesState.value = ApiResult.Loading
|
|
||||||
_myResidencesState.value = APILayer.getMyResidences(forceRefresh = forceRefresh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelTask(taskId: Int) {
|
fun cancelTask(taskId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -138,17 +209,9 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetCancelTaskState() {
|
fun resetCancelTaskState() { _cancelTaskState.value = ApiResult.Idle }
|
||||||
_cancelTaskState.value = ApiResult.Idle
|
fun resetUncancelTaskState() { _uncancelTaskState.value = ApiResult.Idle }
|
||||||
}
|
fun resetUpdateTaskState() { _updateTaskState.value = ApiResult.Idle }
|
||||||
|
|
||||||
fun resetUncancelTaskState() {
|
|
||||||
_uncancelTaskState.value = ApiResult.Idle
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetUpdateTaskState() {
|
|
||||||
_updateTaskState.value = ApiResult.Idle
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateTasksReport(residenceId: Int, email: String? = null) {
|
fun generateTasksReport(residenceId: Int, email: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -157,9 +220,7 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetGenerateReportState() {
|
fun resetGenerateReportState() { _generateReportState.value = ApiResult.Idle }
|
||||||
_generateReportState.value = ApiResult.Idle
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteResidence(residenceId: Int) {
|
fun deleteResidence(residenceId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -168,12 +229,7 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetDeleteResidenceState() {
|
fun resetDeleteResidenceState() { _deleteResidenceState.value = ApiResult.Idle }
|
||||||
_deleteResidenceState.value = ApiResult.Idle
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _joinResidenceState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>>(ApiResult.Idle)
|
|
||||||
val joinResidenceState: StateFlow<ApiResult<com.tt.honeyDue.models.JoinResidenceResponse>> = _joinResidenceState
|
|
||||||
|
|
||||||
fun joinWithCode(code: String) {
|
fun joinWithCode(code: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -182,18 +238,5 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetJoinResidenceState() {
|
fun resetJoinResidenceState() { _joinResidenceState.value = ApiResult.Idle }
|
||||||
_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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,175 +2,184 @@ package com.tt.honeyDue.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.CustomTask
|
||||||
import com.tt.honeyDue.models.TaskCreateRequest
|
import com.tt.honeyDue.models.TaskColumnsResponse
|
||||||
import com.tt.honeyDue.models.TaskCompletionResponse
|
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.APILayer
|
||||||
|
import com.tt.honeyDue.network.ApiResult
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
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)
|
// ---------- Read state (derived from DataManager) ----------
|
||||||
val tasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksState
|
|
||||||
|
|
||||||
private val _tasksByResidenceState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
/** All tasks kanban — mirrors [IDataManager.allTasks]. */
|
||||||
val tasksByResidenceState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksByResidenceState
|
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)
|
private val _taskAddNewCustomTaskState = MutableStateFlow<ApiResult<CustomTask>>(ApiResult.Idle)
|
||||||
val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState
|
val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState
|
||||||
|
|
||||||
private val _taskCompletionsState = MutableStateFlow<ApiResult<List<TaskCompletionResponse>>>(ApiResult.Idle)
|
// ---------- Projection selectors ----------
|
||||||
val taskCompletionsState: StateFlow<ApiResult<List<TaskCompletionResponse>>> = _taskCompletionsState
|
|
||||||
|
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) {
|
fun loadTasks(forceRefresh: Boolean = false) {
|
||||||
println("TaskViewModel: loadTasks called")
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_tasksState.value = ApiResult.Loading
|
_isLoading.value = true
|
||||||
_tasksState.value = APILayer.getTasks(forceRefresh = forceRefresh)
|
_loadError.value = null
|
||||||
println("TaskViewModel: loadTasks result: ${_tasksState.value}")
|
val result = APILayer.getTasks(forceRefresh = forceRefresh)
|
||||||
|
_loadError.value = (result as? ApiResult.Error)?.message
|
||||||
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadTasksByResidence(residenceId: Int, forceRefresh: Boolean = false) {
|
fun loadTasksByResidence(residenceId: Int, forceRefresh: Boolean = false) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_tasksByResidenceState.value = ApiResult.Loading
|
_selectedResidenceId.value = residenceId
|
||||||
_tasksByResidenceState.value = APILayer.getTasksByResidence(
|
_isLoading.value = true
|
||||||
|
_loadError.value = null
|
||||||
|
val result = APILayer.getTasksByResidence(
|
||||||
residenceId = residenceId,
|
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) {
|
fun createNewTask(request: TaskCreateRequest) {
|
||||||
println("TaskViewModel: createNewTask called with $request")
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
println("TaskViewModel: Setting state to Loading")
|
|
||||||
_taskAddNewCustomTaskState.value = ApiResult.Loading
|
_taskAddNewCustomTaskState.value = ApiResult.Loading
|
||||||
val result = APILayer.createTask(request)
|
_taskAddNewCustomTaskState.value = APILayer.createTask(request)
|
||||||
println("TaskViewModel: API result: $result")
|
|
||||||
_taskAddNewCustomTaskState.value = result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun resetAddTaskState() {
|
fun resetAddTaskState() {
|
||||||
_taskAddNewCustomTaskState.value = ApiResult.Idle
|
_taskAddNewCustomTaskState.value = ApiResult.Idle
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTask(taskId: Int, request: TaskCreateRequest, onComplete: (Boolean) -> Unit) {
|
fun updateTask(taskId: Int, request: TaskCreateRequest, onComplete: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = APILayer.updateTask(taskId, request)) {
|
onComplete(APILayer.updateTask(taskId, request) is ApiResult.Success)
|
||||||
is ApiResult.Success -> {
|
|
||||||
onComplete(true)
|
|
||||||
}
|
|
||||||
is ApiResult.Error -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
fun cancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = APILayer.cancelTask(taskId)) {
|
onComplete(APILayer.cancelTask(taskId) is ApiResult.Success)
|
||||||
is ApiResult.Success -> {
|
|
||||||
onComplete(true)
|
|
||||||
}
|
|
||||||
is ApiResult.Error -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uncancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
fun uncancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = APILayer.uncancelTask(taskId)) {
|
onComplete(APILayer.uncancelTask(taskId) is ApiResult.Success)
|
||||||
is ApiResult.Success -> {
|
|
||||||
onComplete(true)
|
|
||||||
}
|
|
||||||
is ApiResult.Error -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) {
|
fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = APILayer.markInProgress(taskId)) {
|
onComplete(APILayer.markInProgress(taskId) is ApiResult.Success)
|
||||||
is ApiResult.Success -> {
|
|
||||||
onComplete(true)
|
|
||||||
}
|
|
||||||
is ApiResult.Error -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun archiveTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
fun archiveTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = APILayer.archiveTask(taskId)) {
|
onComplete(APILayer.archiveTask(taskId) is ApiResult.Success)
|
||||||
is ApiResult.Success -> {
|
|
||||||
onComplete(true)
|
|
||||||
}
|
|
||||||
is ApiResult.Error -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unarchiveTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
fun unarchiveTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = APILayer.unarchiveTask(taskId)) {
|
onComplete(APILayer.unarchiveTask(taskId) is ApiResult.Success)
|
||||||
is ApiResult.Success -> {
|
|
||||||
onComplete(true)
|
|
||||||
}
|
|
||||||
is ApiResult.Error -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load completions for a specific task
|
|
||||||
*/
|
|
||||||
fun loadTaskCompletions(taskId: Int) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_taskCompletionsState.value = ApiResult.Loading
|
|
||||||
_taskCompletionsState.value = APILayer.getTaskCompletions(taskId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset task completions state
|
|
||||||
*/
|
|
||||||
fun resetTaskCompletionsState() {
|
|
||||||
_taskCompletionsState.value = ApiResult.Idle
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user