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())
|
private val _tasksByResidence = MutableStateFlow<Map<Int, TaskColumnsResponse>>(emptyMap())
|
||||||
override val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>> = _tasksByResidence.asStateFlow()
|
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 ====================
|
// ==================== DOCUMENTS ====================
|
||||||
|
|
||||||
private val _documents = MutableStateFlow<List<Document>>(emptyList())
|
private val _documents = MutableStateFlow<List<Document>>(emptyList())
|
||||||
@@ -149,12 +152,21 @@ object DataManager : IDataManager {
|
|||||||
private val _documentsByResidence = MutableStateFlow<Map<Int, List<Document>>>(emptyMap())
|
private val _documentsByResidence = MutableStateFlow<Map<Int, List<Document>>>(emptyMap())
|
||||||
override val documentsByResidence: StateFlow<Map<Int, List<Document>>> = _documentsByResidence.asStateFlow()
|
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 ====================
|
// ==================== CONTRACTORS ====================
|
||||||
// Stores ContractorSummary for list views (lighter weight than full Contractor)
|
// Stores ContractorSummary for list views (lighter weight than full Contractor)
|
||||||
|
|
||||||
private val _contractors = MutableStateFlow<List<ContractorSummary>>(emptyList())
|
private val _contractors = MutableStateFlow<List<ContractorSummary>>(emptyList())
|
||||||
override val contractors: StateFlow<List<ContractorSummary>> = _contractors.asStateFlow()
|
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 ====================
|
// ==================== SUBSCRIPTION ====================
|
||||||
|
|
||||||
private val _subscription = MutableStateFlow<SubscriptionStatus?>(null)
|
private val _subscription = MutableStateFlow<SubscriptionStatus?>(null)
|
||||||
@@ -451,6 +463,11 @@ object DataManager : IDataManager {
|
|||||||
persistToDisk()
|
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.
|
* Filter cached allTasks by residence ID to avoid separate API call.
|
||||||
* Returns null if allTasks not cached.
|
* Returns null if allTasks not cached.
|
||||||
@@ -557,6 +574,12 @@ object DataManager : IDataManager {
|
|||||||
persistToDisk()
|
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.
|
* Add a new document to the cache.
|
||||||
* Caches affected: _documents, _documentsByResidence[residenceId]
|
* Caches affected: _documents, _documentsByResidence[residenceId]
|
||||||
@@ -606,6 +629,16 @@ object DataManager : IDataManager {
|
|||||||
persistToDisk()
|
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) {
|
fun addContractor(contractor: ContractorSummary) {
|
||||||
_contractors.value = _contractors.value + contractor
|
_contractors.value = _contractors.value + contractor
|
||||||
persistToDisk()
|
persistToDisk()
|
||||||
|
|||||||
@@ -65,16 +65,28 @@ interface IDataManager {
|
|||||||
/** Kanban board cache keyed by residence id. */
|
/** Kanban board cache keyed by residence id. */
|
||||||
val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>>
|
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 ====================
|
// ==================== DOCUMENTS ====================
|
||||||
|
|
||||||
val documents: StateFlow<List<Document>>
|
val documents: StateFlow<List<Document>>
|
||||||
|
|
||||||
val documentsByResidence: StateFlow<Map<Int, 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 ====================
|
// ==================== CONTRACTORS ====================
|
||||||
|
|
||||||
val contractors: StateFlow<List<ContractorSummary>>
|
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 ====================
|
// ==================== SUBSCRIPTION ====================
|
||||||
|
|
||||||
/** Observed by [com.tt.honeyDue.ui.screens.ProfileScreen]. */
|
/** Observed by [com.tt.honeyDue.ui.screens.ProfileScreen]. */
|
||||||
|
|||||||
@@ -844,7 +844,11 @@ object APILayer {
|
|||||||
*/
|
*/
|
||||||
suspend fun getTaskCompletions(taskId: Int): ApiResult<List<TaskCompletionResponse>> {
|
suspend fun getTaskCompletions(taskId: Int): ApiResult<List<TaskCompletionResponse>> {
|
||||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
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 ====================
|
// ==================== Document Operations ====================
|
||||||
@@ -903,6 +907,7 @@ object APILayer {
|
|||||||
// Update DataManager on success
|
// Update DataManager on success
|
||||||
if (result is ApiResult.Success) {
|
if (result is ApiResult.Success) {
|
||||||
DataManager.updateDocument(result.data)
|
DataManager.updateDocument(result.data)
|
||||||
|
DataManager.setDocumentDetail(result.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -1069,6 +1074,7 @@ object APILayer {
|
|||||||
// Update the summary in DataManager on success
|
// Update the summary in DataManager on success
|
||||||
if (result is ApiResult.Success) {
|
if (result is ApiResult.Success) {
|
||||||
DataManager.updateContractor(result.data)
|
DataManager.updateContractor(result.data)
|
||||||
|
DataManager.setContractorDetail(result.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -1127,6 +1133,7 @@ object APILayer {
|
|||||||
if (!forceRefresh && DataManager.isCacheValid(DataManager.contractorsCacheTime)) {
|
if (!forceRefresh && DataManager.isCacheValid(DataManager.contractorsCacheTime)) {
|
||||||
val cachedContractors = DataManager.contractors.value
|
val cachedContractors = DataManager.contractors.value
|
||||||
val filtered = cachedContractors.filter { it.residenceId == residenceId }
|
val filtered = cachedContractors.filter { it.residenceId == residenceId }
|
||||||
|
DataManager.setContractorsForResidence(residenceId, filtered)
|
||||||
return ApiResult.Success(filtered)
|
return ApiResult.Success(filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1138,6 +1145,7 @@ object APILayer {
|
|||||||
DataManager.setContractors(result.data)
|
DataManager.setContractors(result.data)
|
||||||
// Now filter from the fresh cache
|
// Now filter from the fresh cache
|
||||||
val filtered = result.data.filter { it.residenceId == residenceId }
|
val filtered = result.data.filter { it.residenceId == residenceId }
|
||||||
|
DataManager.setContractorsForResidence(residenceId, filtered)
|
||||||
return ApiResult.Success(filtered)
|
return ApiResult.Success(filtered)
|
||||||
}
|
}
|
||||||
return result as ApiResult<List<ContractorSummary>>
|
return result as ApiResult<List<ContractorSummary>>
|
||||||
@@ -1145,6 +1153,7 @@ object APILayer {
|
|||||||
|
|
||||||
// Fallback: filter from cache
|
// Fallback: filter from cache
|
||||||
val filtered = DataManager.contractors.value.filter { it.residenceId == residenceId }
|
val filtered = DataManager.contractors.value.filter { it.residenceId == residenceId }
|
||||||
|
DataManager.setContractorsForResidence(residenceId, filtered)
|
||||||
return ApiResult.Success(filtered)
|
return ApiResult.Success(filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,13 @@ object FixtureDataManager {
|
|||||||
},
|
},
|
||||||
documents = Fixtures.documents,
|
documents = Fixtures.documents,
|
||||||
documentsByResidence = Fixtures.documentsByResidence,
|
documentsByResidence = Fixtures.documentsByResidence,
|
||||||
|
documentDetail = Fixtures.documents.associateBy { it.id ?: 0 }.filterKeys { it != 0 },
|
||||||
contractors = Fixtures.contractorSummaries,
|
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,
|
subscription = Fixtures.premiumSubscription,
|
||||||
upgradeTriggers = emptyMap(),
|
upgradeTriggers = emptyMap(),
|
||||||
featureBenefits = Fixtures.featureBenefits,
|
featureBenefits = Fixtures.featureBenefits,
|
||||||
|
|||||||
@@ -43,9 +43,13 @@ class InMemoryDataManager(
|
|||||||
residenceSummaries: Map<Int, ResidenceSummaryResponse> = emptyMap(),
|
residenceSummaries: Map<Int, ResidenceSummaryResponse> = emptyMap(),
|
||||||
allTasks: TaskColumnsResponse? = null,
|
allTasks: TaskColumnsResponse? = null,
|
||||||
tasksByResidence: Map<Int, TaskColumnsResponse> = emptyMap(),
|
tasksByResidence: Map<Int, TaskColumnsResponse> = emptyMap(),
|
||||||
|
taskCompletions: Map<Int, List<com.tt.honeyDue.models.TaskCompletionResponse>> = emptyMap(),
|
||||||
documents: List<Document> = emptyList(),
|
documents: List<Document> = emptyList(),
|
||||||
documentsByResidence: Map<Int, List<Document>> = emptyMap(),
|
documentsByResidence: Map<Int, List<Document>> = emptyMap(),
|
||||||
|
documentDetail: Map<Int, Document> = emptyMap(),
|
||||||
contractors: List<ContractorSummary> = emptyList(),
|
contractors: List<ContractorSummary> = emptyList(),
|
||||||
|
contractorsByResidence: Map<Int, List<ContractorSummary>> = emptyMap(),
|
||||||
|
contractorDetail: Map<Int, com.tt.honeyDue.models.Contractor> = emptyMap(),
|
||||||
subscription: SubscriptionStatus? = null,
|
subscription: SubscriptionStatus? = null,
|
||||||
upgradeTriggers: Map<String, UpgradeTriggerData> = emptyMap(),
|
upgradeTriggers: Map<String, UpgradeTriggerData> = emptyMap(),
|
||||||
featureBenefits: List<FeatureBenefit> = emptyList(),
|
featureBenefits: List<FeatureBenefit> = emptyList(),
|
||||||
@@ -74,15 +78,19 @@ class InMemoryDataManager(
|
|||||||
|
|
||||||
override val allTasks: StateFlow<TaskColumnsResponse?> = MutableStateFlow(allTasks)
|
override val allTasks: StateFlow<TaskColumnsResponse?> = MutableStateFlow(allTasks)
|
||||||
override val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>> = MutableStateFlow(tasksByResidence)
|
override val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>> = MutableStateFlow(tasksByResidence)
|
||||||
|
override val taskCompletions: StateFlow<Map<Int, List<com.tt.honeyDue.models.TaskCompletionResponse>>> = MutableStateFlow(taskCompletions)
|
||||||
|
|
||||||
// ==================== DOCUMENTS ====================
|
// ==================== DOCUMENTS ====================
|
||||||
|
|
||||||
override val documents: StateFlow<List<Document>> = MutableStateFlow(documents)
|
override val documents: StateFlow<List<Document>> = MutableStateFlow(documents)
|
||||||
override val documentsByResidence: StateFlow<Map<Int, List<Document>>> = MutableStateFlow(documentsByResidence)
|
override val documentsByResidence: StateFlow<Map<Int, List<Document>>> = MutableStateFlow(documentsByResidence)
|
||||||
|
override val documentDetail: StateFlow<Map<Int, Document>> = MutableStateFlow(documentDetail)
|
||||||
|
|
||||||
// ==================== CONTRACTORS ====================
|
// ==================== CONTRACTORS ====================
|
||||||
|
|
||||||
override val contractors: StateFlow<List<ContractorSummary>> = MutableStateFlow(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 ====================
|
// ==================== SUBSCRIPTION ====================
|
||||||
|
|
||||||
|
|||||||