P0: IDataManager coverage gaps — contractorDetail/documentDetail/taskCompletions/contractorsByResidence
Adds 4 new StateFlow members to IDataManager + DataManager + InMemoryDataManager + FixtureDataManager: - contractorDetail: Map<Int, Contractor> — cached detail fetches - documentDetail: Map<Int, Document> - taskCompletions: Map<Int, List<TaskCompletionResponse>> - contractorsByResidence: Map<Int, List<ContractorSummary>> APILayer now writes to these on successful detail/per-residence fetches: - getTaskCompletions -> setTaskCompletions - getDocument -> setDocumentDetail - getContractor -> setContractorDetail - getContractorsByResidence -> setContractorsForResidence Fixture populated() seeds contractorDetail + contractorsByResidence. Populated taskCompletions is empty (Fixtures doesn't define any completions yet). Foundation for P1 — VMs can now derive every read-state from DataManager reactively instead of owning independent MutableStateFlow fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 43 KiB |
@@ -141,6 +141,9 @@ object DataManager : IDataManager {
|
||||
private val _tasksByResidence = MutableStateFlow<Map<Int, TaskColumnsResponse>>(emptyMap())
|
||||
override val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>> = _tasksByResidence.asStateFlow()
|
||||
|
||||
private val _taskCompletions = MutableStateFlow<Map<Int, List<com.tt.honeyDue.models.TaskCompletionResponse>>>(emptyMap())
|
||||
override val taskCompletions: StateFlow<Map<Int, List<com.tt.honeyDue.models.TaskCompletionResponse>>> = _taskCompletions.asStateFlow()
|
||||
|
||||
// ==================== DOCUMENTS ====================
|
||||
|
||||
private val _documents = MutableStateFlow<List<Document>>(emptyList())
|
||||
@@ -149,12 +152,21 @@ object DataManager : IDataManager {
|
||||
private val _documentsByResidence = MutableStateFlow<Map<Int, List<Document>>>(emptyMap())
|
||||
override val documentsByResidence: StateFlow<Map<Int, List<Document>>> = _documentsByResidence.asStateFlow()
|
||||
|
||||
private val _documentDetail = MutableStateFlow<Map<Int, Document>>(emptyMap())
|
||||
override val documentDetail: StateFlow<Map<Int, Document>> = _documentDetail.asStateFlow()
|
||||
|
||||
// ==================== CONTRACTORS ====================
|
||||
// Stores ContractorSummary for list views (lighter weight than full Contractor)
|
||||
|
||||
private val _contractors = MutableStateFlow<List<ContractorSummary>>(emptyList())
|
||||
override val contractors: StateFlow<List<ContractorSummary>> = _contractors.asStateFlow()
|
||||
|
||||
private val _contractorsByResidence = MutableStateFlow<Map<Int, List<ContractorSummary>>>(emptyMap())
|
||||
override val contractorsByResidence: StateFlow<Map<Int, List<ContractorSummary>>> = _contractorsByResidence.asStateFlow()
|
||||
|
||||
private val _contractorDetail = MutableStateFlow<Map<Int, com.tt.honeyDue.models.Contractor>>(emptyMap())
|
||||
override val contractorDetail: StateFlow<Map<Int, com.tt.honeyDue.models.Contractor>> = _contractorDetail.asStateFlow()
|
||||
|
||||
// ==================== SUBSCRIPTION ====================
|
||||
|
||||
private val _subscription = MutableStateFlow<SubscriptionStatus?>(null)
|
||||
@@ -451,6 +463,11 @@ object DataManager : IDataManager {
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
/** Populate the per-task completion cache (used by TaskViewModel's derived flow). */
|
||||
fun setTaskCompletions(taskId: Int, completions: List<com.tt.honeyDue.models.TaskCompletionResponse>) {
|
||||
_taskCompletions.value = _taskCompletions.value + (taskId to completions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter cached allTasks by residence ID to avoid separate API call.
|
||||
* Returns null if allTasks not cached.
|
||||
@@ -557,6 +574,12 @@ object DataManager : IDataManager {
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
/** Populate the per-document detail cache (used by DocumentViewModel's derived flow). */
|
||||
fun setDocumentDetail(document: Document) {
|
||||
val id = document.id ?: return
|
||||
_documentDetail.value = _documentDetail.value + (id to document)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new document to the cache.
|
||||
* Caches affected: _documents, _documentsByResidence[residenceId]
|
||||
@@ -606,6 +629,16 @@ object DataManager : IDataManager {
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
/** Populate the per-residence contractor cache. */
|
||||
fun setContractorsForResidence(residenceId: Int, contractors: List<ContractorSummary>) {
|
||||
_contractorsByResidence.value = _contractorsByResidence.value + (residenceId to contractors)
|
||||
}
|
||||
|
||||
/** Populate the per-contractor detail cache (used by ContractorViewModel's derived flow). */
|
||||
fun setContractorDetail(contractor: com.tt.honeyDue.models.Contractor) {
|
||||
_contractorDetail.value = _contractorDetail.value + (contractor.id to contractor)
|
||||
}
|
||||
|
||||
fun addContractor(contractor: ContractorSummary) {
|
||||
_contractors.value = _contractors.value + contractor
|
||||
persistToDisk()
|
||||
|
||||
@@ -65,16 +65,28 @@ interface IDataManager {
|
||||
/** Kanban board cache keyed by residence id. */
|
||||
val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>>
|
||||
|
||||
/** Task completions keyed by task id. Populated by APILayer.getTaskCompletions. */
|
||||
val taskCompletions: StateFlow<Map<Int, List<com.tt.honeyDue.models.TaskCompletionResponse>>>
|
||||
|
||||
// ==================== DOCUMENTS ====================
|
||||
|
||||
val documents: StateFlow<List<Document>>
|
||||
|
||||
val documentsByResidence: StateFlow<Map<Int, List<Document>>>
|
||||
|
||||
/** Document detail (full Document with user+images) cached by id. Populated by APILayer.getDocument. */
|
||||
val documentDetail: StateFlow<Map<Int, Document>>
|
||||
|
||||
// ==================== CONTRACTORS ====================
|
||||
|
||||
val contractors: StateFlow<List<ContractorSummary>>
|
||||
|
||||
/** Contractor list per residence id (from residence-scoped fetches). */
|
||||
val contractorsByResidence: StateFlow<Map<Int, List<ContractorSummary>>>
|
||||
|
||||
/** Contractor detail (full Contractor with user association) cached by id. Populated by APILayer.getContractor. */
|
||||
val contractorDetail: StateFlow<Map<Int, com.tt.honeyDue.models.Contractor>>
|
||||
|
||||
// ==================== SUBSCRIPTION ====================
|
||||
|
||||
/** Observed by [com.tt.honeyDue.ui.screens.ProfileScreen]. */
|
||||
|
||||
@@ -844,7 +844,11 @@ object APILayer {
|
||||
*/
|
||||
suspend fun getTaskCompletions(taskId: Int): ApiResult<List<TaskCompletionResponse>> {
|
||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
return taskApi.getTaskCompletions(token, taskId)
|
||||
val result = taskApi.getTaskCompletions(token, taskId)
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.setTaskCompletions(taskId, result.data)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==================== Document Operations ====================
|
||||
@@ -903,6 +907,7 @@ object APILayer {
|
||||
// Update DataManager on success
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.updateDocument(result.data)
|
||||
DataManager.setDocumentDetail(result.data)
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -1069,6 +1074,7 @@ object APILayer {
|
||||
// Update the summary in DataManager on success
|
||||
if (result is ApiResult.Success) {
|
||||
DataManager.updateContractor(result.data)
|
||||
DataManager.setContractorDetail(result.data)
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -1127,6 +1133,7 @@ object APILayer {
|
||||
if (!forceRefresh && DataManager.isCacheValid(DataManager.contractorsCacheTime)) {
|
||||
val cachedContractors = DataManager.contractors.value
|
||||
val filtered = cachedContractors.filter { it.residenceId == residenceId }
|
||||
DataManager.setContractorsForResidence(residenceId, filtered)
|
||||
return ApiResult.Success(filtered)
|
||||
}
|
||||
|
||||
@@ -1138,6 +1145,7 @@ object APILayer {
|
||||
DataManager.setContractors(result.data)
|
||||
// Now filter from the fresh cache
|
||||
val filtered = result.data.filter { it.residenceId == residenceId }
|
||||
DataManager.setContractorsForResidence(residenceId, filtered)
|
||||
return ApiResult.Success(filtered)
|
||||
}
|
||||
return result as ApiResult<List<ContractorSummary>>
|
||||
@@ -1145,6 +1153,7 @@ object APILayer {
|
||||
|
||||
// Fallback: filter from cache
|
||||
val filtered = DataManager.contractors.value.filter { it.residenceId == residenceId }
|
||||
DataManager.setContractorsForResidence(residenceId, filtered)
|
||||
return ApiResult.Success(filtered)
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,13 @@ object FixtureDataManager {
|
||||
},
|
||||
documents = Fixtures.documents,
|
||||
documentsByResidence = Fixtures.documentsByResidence,
|
||||
documentDetail = Fixtures.documents.associateBy { it.id ?: 0 }.filterKeys { it != 0 },
|
||||
contractors = Fixtures.contractorSummaries,
|
||||
contractorsByResidence = Fixtures.residences.associate { r ->
|
||||
r.id to Fixtures.contractorSummaries.filter { it.residenceId == r.id }
|
||||
},
|
||||
contractorDetail = Fixtures.contractors.associateBy { it.id },
|
||||
taskCompletions = emptyMap(), // Fixtures doesn't define task completions; leave empty
|
||||
subscription = Fixtures.premiumSubscription,
|
||||
upgradeTriggers = emptyMap(),
|
||||
featureBenefits = Fixtures.featureBenefits,
|
||||
|
||||
@@ -43,9 +43,13 @@ class InMemoryDataManager(
|
||||
residenceSummaries: Map<Int, ResidenceSummaryResponse> = emptyMap(),
|
||||
allTasks: TaskColumnsResponse? = null,
|
||||
tasksByResidence: Map<Int, TaskColumnsResponse> = emptyMap(),
|
||||
taskCompletions: Map<Int, List<com.tt.honeyDue.models.TaskCompletionResponse>> = emptyMap(),
|
||||
documents: List<Document> = emptyList(),
|
||||
documentsByResidence: Map<Int, List<Document>> = emptyMap(),
|
||||
documentDetail: Map<Int, Document> = emptyMap(),
|
||||
contractors: List<ContractorSummary> = emptyList(),
|
||||
contractorsByResidence: Map<Int, List<ContractorSummary>> = emptyMap(),
|
||||
contractorDetail: Map<Int, com.tt.honeyDue.models.Contractor> = emptyMap(),
|
||||
subscription: SubscriptionStatus? = null,
|
||||
upgradeTriggers: Map<String, UpgradeTriggerData> = emptyMap(),
|
||||
featureBenefits: List<FeatureBenefit> = emptyList(),
|
||||
@@ -74,15 +78,19 @@ class InMemoryDataManager(
|
||||
|
||||
override val allTasks: StateFlow<TaskColumnsResponse?> = MutableStateFlow(allTasks)
|
||||
override val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>> = MutableStateFlow(tasksByResidence)
|
||||
override val taskCompletions: StateFlow<Map<Int, List<com.tt.honeyDue.models.TaskCompletionResponse>>> = MutableStateFlow(taskCompletions)
|
||||
|
||||
// ==================== DOCUMENTS ====================
|
||||
|
||||
override val documents: StateFlow<List<Document>> = MutableStateFlow(documents)
|
||||
override val documentsByResidence: StateFlow<Map<Int, List<Document>>> = MutableStateFlow(documentsByResidence)
|
||||
override val documentDetail: StateFlow<Map<Int, Document>> = MutableStateFlow(documentDetail)
|
||||
|
||||
// ==================== CONTRACTORS ====================
|
||||
|
||||
override val contractors: StateFlow<List<ContractorSummary>> = MutableStateFlow(contractors)
|
||||
override val contractorsByResidence: StateFlow<Map<Int, List<ContractorSummary>>> = MutableStateFlow(contractorsByResidence)
|
||||
override val contractorDetail: StateFlow<Map<Int, com.tt.honeyDue.models.Contractor>> = MutableStateFlow(contractorDetail)
|
||||
|
||||
// ==================== SUBSCRIPTION ====================
|
||||
|
||||
|
||||