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>
This commit is contained in:
Trey T
2026-04-19 18:31:06 -05:00
parent f83e89bee3
commit 2230cde071
55 changed files with 69 additions and 1 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -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()

View File

@@ -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]. */

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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 ====================