Files
honeyDueKMP/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt
T
Trey t 1884853e4b android: ResidenceViewModel.residenceTasksState derives from _allTasks
Same screen contract, but the data flows from DataManager.allTasks
through a combine(_allTasks, _currentResidenceId) into the existing
StateFlow. No per-residence network call needed; the upstream
getTasks() refresh propagates and the screen re-renders.

Eliminates the gitea#2 race window on Android — same fix as the iOS
TaskViewModel commit. Both platforms now react to _allTasks changes
without manual refresh.
2026-05-01 18:34:08 -07:00

282 lines
11 KiB
Kotlin

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.TaskColumnsResponse
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
/**
* 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() {
// ---------- Read state (derived from DataManager) ----------
val residencesState: StateFlow<ApiResult<List<Residence>>> =
dataManager.residences
.map { list -> if (list.isNotEmpty()) ApiResult.Success(list) else ApiResult.Idle }
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
dataManager.residences.value
.takeIf { it.isNotEmpty() }
?.let { ApiResult.Success(it) } ?: ApiResult.Idle,
)
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> =
dataManager.myResidences
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
dataManager.myResidences.value
?.let { ApiResult.Success(it) } ?: ApiResult.Idle,
)
val summaryState: StateFlow<ApiResult<TotalSummary>> =
dataManager.totalSummary
.map { if (it != null) ApiResult.Success(it) else ApiResult.Idle }
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
dataManager.totalSummary.value
?.let { ApiResult.Success(it) } ?: ApiResult.Idle,
)
/** Drives the residence-scoped projections. */
private val _selectedResidenceId = MutableStateFlow<Int?>(null)
/// Residence-scoped kanban derived from `DataManager.allTasks` filtered
/// by `_selectedResidenceId`. Single source of truth — eliminates the
/// gitea#2 race window where the per-residence cache slot could be
/// empty while `_allTasks` was populated. The per-residence cache
/// (`tasksByResidence`) was deleted in cec521b.
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> =
combine(_selectedResidenceId, DataManager.allTasks) { id, all ->
when {
id == null -> ApiResult.Idle
all == null -> ApiResult.Loading
else -> {
val filtered = DataManager.getTasksForResidence(id)
if (filtered != null) ApiResult.Success(filtered) else ApiResult.Loading
}
}
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
_selectedResidenceId.value?.let { id ->
DataManager.getTasksForResidence(id)?.let { ApiResult.Success(it) }
} ?: 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,
_selectedResidenceId.value?.let { id ->
dataManager.contractorsByResidence.value[id]?.let { ApiResult.Success(it) }
} ?: 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
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState
private val _deleteResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
val deleteResidenceState: StateFlow<ApiResult<Unit>> = _deleteResidenceState
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
private val _uncancelTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
val uncancelTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _uncancelTaskState
private val _updateTaskState = MutableStateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>>(ApiResult.Idle)
val updateTaskState: StateFlow<ApiResult<com.tt.honeyDue.models.CustomTask>> = _updateTaskState
private val _generateReportState = MutableStateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>>(ApiResult.Idle)
val generateReportState: StateFlow<ApiResult<com.tt.honeyDue.network.GenerateReportResponse>> = _generateReportState
// ---------- Projection selectors ----------
fun selectResidence(residenceId: Int?) {
_selectedResidenceId.value = residenceId
}
// ---------- Load methods (write-through to DataManager) ----------
fun loadResidences(forceRefresh: Boolean = false) {
viewModelScope.launch {
_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 {
_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 {
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
_createResidenceState.value = APILayer.createResidence(request)
}
}
fun updateResidence(residenceId: Int, request: ResidenceCreateRequest) {
viewModelScope.launch {
_updateResidenceState.value = ApiResult.Loading
_updateResidenceState.value = APILayer.updateResidence(residenceId, request)
}
}
fun resetCreateState() { _createResidenceState.value = ApiResult.Idle }
fun resetUpdateState() { _updateResidenceState.value = ApiResult.Idle }
fun cancelTask(taskId: Int) {
viewModelScope.launch {
_cancelTaskState.value = ApiResult.Loading
_cancelTaskState.value = APILayer.cancelTask(taskId)
}
}
fun uncancelTask(taskId: Int) {
viewModelScope.launch {
_uncancelTaskState.value = ApiResult.Loading
_uncancelTaskState.value = APILayer.uncancelTask(taskId)
}
}
fun updateTask(taskId: Int, request: com.tt.honeyDue.models.TaskCreateRequest) {
viewModelScope.launch {
_updateTaskState.value = ApiResult.Loading
_updateTaskState.value = APILayer.updateTask(taskId, request)
}
}
fun resetCancelTaskState() { _cancelTaskState.value = ApiResult.Idle }
fun resetUncancelTaskState() { _uncancelTaskState.value = ApiResult.Idle }
fun resetUpdateTaskState() { _updateTaskState.value = ApiResult.Idle }
fun generateTasksReport(residenceId: Int, email: String? = null) {
viewModelScope.launch {
_generateReportState.value = ApiResult.Loading
_generateReportState.value = APILayer.generateTasksReport(residenceId, email)
}
}
fun resetGenerateReportState() { _generateReportState.value = ApiResult.Idle }
fun deleteResidence(residenceId: Int) {
viewModelScope.launch {
_deleteResidenceState.value = ApiResult.Loading
_deleteResidenceState.value = APILayer.deleteResidence(residenceId)
}
}
fun resetDeleteResidenceState() { _deleteResidenceState.value = ApiResult.Idle }
fun joinWithCode(code: String) {
viewModelScope.launch {
_joinResidenceState.value = ApiResult.Loading
_joinResidenceState.value = APILayer.joinWithCode(code)
}
}
fun resetJoinResidenceState() { _joinResidenceState.value = ApiResult.Idle }
}