1884853e4b
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.
282 lines
11 KiB
Kotlin
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 }
|
|
}
|