Skip to content

Commit 69cd771

Browse files
committed
Org unit selection and usage fixed in the app
1 parent ef523f9 commit 69cd771

14 files changed

Lines changed: 589 additions & 121 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ Run on a device or emulator:
8989

9090
| Validation | Unsaved Changes |
9191
|:---:|:---:|
92-
| ![Validation](assets/screenshots/09_validation.png) | ![Unsaved](assets/screenshots/10_unsaved_changes.png) |
92+
| ![Validation](assets/screensho<br/>ts/09_validation.png) | ![Unsaved](assets/screenshots/10_unsaved_changes.png) |
9393

9494
### Tracker Programs
95-
| Enrollment List | Tracker Dashboard |
96-
|:---:|:---:|
95+
| Enrollment List | Tracker Dashboard<br/><br/> |
96+
|:---:|:---------------------------------------------------------:|
9797
| ![Enrollments](assets/screenshots/11_tracker_enrollments.png) | ![Dashboard](assets/screenshots/12_tracker_dashboard.png) |
9898

9999
| Events Summary | Events Table |

app/src/main/java/com/ash/simpledataentry/data/repositoryImpl/DataEntryRepositoryImpl.kt

Lines changed: 120 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -479,38 +479,142 @@ class DataEntryRepositoryImpl @Inject constructor(
479479

480480
override suspend fun getUserOrgUnit(datasetId: String): OrganisationUnit {
481481
return withContext(Dispatchers.IO) {
482-
val orgUnits = d2.organisationUnitModule().organisationUnits()
482+
val orgUnits = getUserOrgUnits(datasetId)
483+
orgUnits.firstOrNull()
484+
?: throw Exception("No organization units available for data capture")
485+
}
486+
}
487+
488+
override suspend fun getUserOrgUnits(datasetId: String): List<OrganisationUnit> {
489+
return withContext(Dispatchers.IO) {
490+
val scopedOrgUnits = d2.organisationUnitModule().organisationUnits()
483491
.byOrganisationUnitScope(org.hisp.dhis.android.core.organisationunit.OrganisationUnit.Scope.SCOPE_DATA_CAPTURE)
484492
.blockingGet()
485493

486-
if (orgUnits.isEmpty()) {
487-
throw Exception("No organization units available for data capture")
494+
if (scopedOrgUnits.isEmpty()) {
495+
return@withContext emptyList()
488496
}
489497

490-
OrganisationUnit(
491-
id = orgUnits.first().uid(),
492-
name = orgUnits.first().displayName() ?: orgUnits.first().uid(),
493-
path = orgUnits.first().displayNamePath()?.joinToString(" / ")
494-
)
498+
val targetType = resolveTargetType(datasetId)
499+
val attachedOrgUnits = getAttachedOrgUnits(datasetId, targetType)
500+
501+
val attachedIds = attachedOrgUnits.map { it.uid() }.toSet()
502+
val intersected = scopedOrgUnits.filter { it.uid() in attachedIds }
503+
504+
val relevantOrgUnits = if (attachedIds.isNotEmpty()) intersected else scopedOrgUnits
505+
506+
mapAndSortOrgUnits(relevantOrgUnits)
495507
}
496508
}
497509

498-
override suspend fun getUserOrgUnits(datasetId: String): List<OrganisationUnit> {
510+
override suspend fun getScopedOrgUnits(): List<OrganisationUnit> {
499511
return withContext(Dispatchers.IO) {
500-
val orgUnits = d2.organisationUnitModule().organisationUnits()
512+
val scopedOrgUnits = d2.organisationUnitModule().organisationUnits()
501513
.byOrganisationUnitScope(org.hisp.dhis.android.core.organisationunit.OrganisationUnit.Scope.SCOPE_DATA_CAPTURE)
502514
.blockingGet()
515+
mapAndSortOrgUnits(scopedOrgUnits)
516+
}
517+
}
503518

504-
orgUnits.map { orgUnit ->
505-
OrganisationUnit(
506-
id = orgUnit.uid(),
507-
name = orgUnit.displayName() ?: orgUnit.uid(),
508-
path = orgUnit.displayNamePath()?.joinToString(" / ")
509-
)
519+
override suspend fun getOrgUnitsAttachedToDataSets(datasetIds: List<String>): Set<String> {
520+
if (datasetIds.isEmpty()) return emptySet()
521+
return withContext(Dispatchers.IO) {
522+
d2.organisationUnitModule().organisationUnits()
523+
.byDataSetUids(datasetIds)
524+
.blockingGet()
525+
.map { it.uid() }
526+
.toSet()
527+
}
528+
}
529+
530+
override suspend fun expandOrgUnitSelection(targetId: String, orgUnitId: String): Set<String> {
531+
return withContext(Dispatchers.IO) {
532+
val targetType = resolveTargetType(targetId)
533+
534+
val scopedOrgUnits = d2.organisationUnitModule().organisationUnits()
535+
.byOrganisationUnitScope(org.hisp.dhis.android.core.organisationunit.OrganisationUnit.Scope.SCOPE_DATA_CAPTURE)
536+
.blockingGet()
537+
if (scopedOrgUnits.isEmpty()) {
538+
return@withContext emptySet()
539+
}
540+
541+
val attachedOrgUnits = getAttachedOrgUnits(targetId, targetType)
542+
val attachedIds = attachedOrgUnits.map { it.uid() }.toSet()
543+
val scopedIds = scopedOrgUnits.map { it.uid() }.toSet()
544+
val allowedIds = scopedIds.intersect(attachedIds).ifEmpty { scopedIds }
545+
546+
val selected = d2.organisationUnitModule().organisationUnits()
547+
.uid(orgUnitId)
548+
.blockingGet()
549+
?: return@withContext emptySet()
550+
551+
val selectedPath = selected.path()
552+
val descendants = if (!selectedPath.isNullOrBlank()) {
553+
d2.organisationUnitModule().organisationUnits()
554+
.byPath().like("$selectedPath/%")
555+
.blockingGet()
556+
} else {
557+
emptyList()
510558
}
559+
560+
val expandedIds = buildSet {
561+
add(orgUnitId)
562+
descendants.forEach { add(it.uid()) }
563+
}
564+
565+
expandedIds.intersect(allowedIds)
511566
}
512567
}
513568

569+
private enum class TargetType {
570+
DATASET,
571+
PROGRAM
572+
}
573+
574+
private fun resolveTargetType(targetId: String): TargetType {
575+
val isDataset = d2.dataSetModule().dataSets()
576+
.uid(targetId)
577+
.blockingGet() != null
578+
if (isDataset) {
579+
return TargetType.DATASET
580+
}
581+
582+
val isProgram = d2.programModule().programs()
583+
.uid(targetId)
584+
.blockingGet() != null
585+
return if (isProgram) TargetType.PROGRAM else TargetType.DATASET
586+
}
587+
588+
private fun getAttachedOrgUnits(targetId: String, targetType: TargetType): List<org.hisp.dhis.android.core.organisationunit.OrganisationUnit> {
589+
val repository = d2.organisationUnitModule().organisationUnits()
590+
return when (targetType) {
591+
TargetType.DATASET -> repository
592+
.byDataSetUids(listOf(targetId))
593+
.blockingGet()
594+
TargetType.PROGRAM -> repository
595+
.byProgramUids(listOf(targetId))
596+
.blockingGet()
597+
}
598+
}
599+
600+
private fun mapAndSortOrgUnits(
601+
orgUnits: List<org.hisp.dhis.android.core.organisationunit.OrganisationUnit>
602+
): List<OrganisationUnit> {
603+
return orgUnits.map { orgUnit ->
604+
OrganisationUnit(
605+
id = orgUnit.uid(),
606+
name = orgUnit.displayName() ?: orgUnit.uid(),
607+
path = orgUnit.displayNamePath()?.joinToString(" / "),
608+
uidPath = orgUnit.path(),
609+
parentId = orgUnit.parent()?.uid(),
610+
level = orgUnit.level()
611+
)
612+
}.sortedWith(
613+
compareBy<OrganisationUnit> { it.path ?: it.name }
614+
.thenBy { it.name }
615+
)
616+
}
617+
514618
override suspend fun getDefaultAttributeOptionCombo(): String {
515619
return withContext(Dispatchers.IO) {
516620
d2.categoryModule().categoryOptionCombos()

app/src/main/java/com/ash/simpledataentry/data/repositoryImpl/DatasetInstancesRepositoryImpl.kt

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,45 @@ class DatasetInstancesRepositoryImpl @Inject constructor(
3434
Log.d(TAG, "Fetching dataset instances for dataset: $datasetId")
3535
return withContext(Dispatchers.IO) {
3636
try {
37-
// Get user's data capture org units
38-
Log.d(TAG, "Fetching user org units")
39-
val userOrgUnits = d2.organisationUnitModule().organisationUnits()
37+
Log.d(TAG, "Fetching scoped org units and dataset attachments for $datasetId")
38+
39+
val scopedOrgUnits = d2.organisationUnitModule().organisationUnits()
4040
.byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE)
4141
.get().await()
4242

43-
Log.d(TAG, "Found ${userOrgUnits.size} org units")
44-
45-
val userOrgUnitUid = userOrgUnits.firstOrNull()?.uid()
46-
if (userOrgUnitUid == null) {
47-
Log.e(TAG, "No organization unit found for user")
43+
if (scopedOrgUnits.isEmpty()) {
44+
Log.e(TAG, "No organization units found in data capture scope")
4845
return@withContext emptyList()
4946
}
5047

51-
Log.d(TAG, "Using org unit: $userOrgUnitUid")
48+
val attachedOrgUnits = d2.organisationUnitModule().organisationUnits()
49+
.byDataSetUids(listOf(datasetId))
50+
.get().await()
5251

53-
// Get dataset instances
52+
val scopedIds = scopedOrgUnits.map { it.uid() }.toSet()
53+
val attachedIds = attachedOrgUnits.map { it.uid() }.toSet()
54+
val relevantOrgUnitIds = scopedIds.intersect(attachedIds).ifEmpty { scopedIds }
5455

55-
val instance = d2.dataSetModule()
56+
Log.d(
57+
TAG,
58+
"Scoped=${scopedIds.size}, Attached=${attachedIds.size}, Relevant=${relevantOrgUnitIds.size}"
59+
)
60+
61+
// Get dataset instances across all relevant org units
62+
val instanceCount = d2.dataSetModule()
5663
.dataSetInstances()
5764
.byDataSetUid().eq(datasetId)
58-
.byOrganisationUnitUid().eq(userOrgUnitUid)
65+
.byOrganisationUnitUid().`in`(relevantOrgUnitIds.toList())
5966
.blockingCount()
6067

61-
62-
63-
6468
val instances = d2.dataSetModule()
6569
.dataSetInstances()
6670
.byDataSetUid().eq(datasetId)
67-
.byOrganisationUnitUid().eq(userOrgUnitUid)
71+
.byOrganisationUnitUid().`in`(relevantOrgUnitIds.toList())
6872
.get().await()
6973

70-
Log.d(TAG, "Found $instance instances for dataset")
71-
72-
Log.d(TAG, "Found ${instances.size} instances for dataset")
74+
Log.d(TAG, "Found $instanceCount instances for dataset")
75+
Log.d(TAG, "Loaded ${instances.size} instances for dataset")
7376

7477

7578
val sdkInstances = instances.map { instance ->
@@ -207,27 +210,42 @@ class DatasetInstancesRepositoryImpl @Inject constructor(
207210
}
208211
}
209212

210-
override suspend fun getDatasetInstanceCount(datasetId: String): Int {
213+
override suspend fun getDatasetInstanceCount(datasetId: String, orgUnitIds: Set<String>?): Int {
211214
return withContext(Dispatchers.IO) {
212215
try {
213216
Log.d(TAG, "Getting instance count for dataset: $datasetId")
214-
215-
// Get user's data capture org units
216-
val userOrgUnits = d2.organisationUnitModule().organisationUnits()
217+
218+
// Get scoped org units
219+
val scopedOrgUnits = d2.organisationUnitModule().organisationUnits()
217220
.byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE)
218221
.get().await()
219222

220-
val userOrgUnitUid = userOrgUnits.firstOrNull()?.uid()
221-
if (userOrgUnitUid == null) {
222-
Log.e(TAG, "No organization unit found for user")
223+
if (scopedOrgUnits.isEmpty()) {
224+
Log.e(TAG, "No organization units found in data capture scope")
225+
return@withContext 0
226+
}
227+
228+
val scopedIds = scopedOrgUnits.map { it.uid() }.toSet()
229+
val relevantOrgUnitIds = if (orgUnitIds != null) {
230+
orgUnitIds.intersect(scopedIds)
231+
} else {
232+
val attachedOrgUnits = d2.organisationUnitModule().organisationUnits()
233+
.byDataSetUids(listOf(datasetId))
234+
.get().await()
235+
val attachedIds = attachedOrgUnits.map { it.uid() }.toSet()
236+
scopedIds.intersect(attachedIds).ifEmpty { scopedIds }
237+
}
238+
239+
if (relevantOrgUnitIds.isEmpty()) {
240+
Log.d(TAG, "No relevant org units for dataset $datasetId under current filter")
223241
return@withContext 0
224242
}
225243

226-
// Count dataset instances
244+
// Count dataset instances across all relevant org units
227245
val count = d2.dataSetModule()
228246
.dataSetInstances()
229247
.byDataSetUid().eq(datasetId)
230-
.byOrganisationUnitUid().eq(userOrgUnitUid)
248+
.byOrganisationUnitUid().`in`(relevantOrgUnitIds.toList())
231249
.blockingCount()
232250

233251
Log.d(TAG, "Found $count instances for dataset $datasetId")
@@ -447,8 +465,24 @@ class DatasetInstancesRepositoryImpl @Inject constructor(
447465
com.ash.simpledataentry.domain.model.ProgramType.TRACKER -> {
448466
withContext(Dispatchers.IO) {
449467
try {
468+
val scopedOrgUnits = d2.organisationUnitModule().organisationUnits()
469+
.byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE)
470+
.get().await()
471+
if (scopedOrgUnits.isEmpty()) {
472+
return@withContext 0
473+
}
474+
475+
val attachedOrgUnits = d2.organisationUnitModule().organisationUnits()
476+
.byProgramUids(listOf(programId))
477+
.get().await()
478+
479+
val scopedIds = scopedOrgUnits.map { it.uid() }.toSet()
480+
val attachedIds = attachedOrgUnits.map { it.uid() }.toSet()
481+
val relevantOrgUnitIds = scopedIds.intersect(attachedIds).ifEmpty { scopedIds }
482+
450483
d2.trackedEntityModule().trackedEntityInstances()
451484
.byProgramUids(listOf(programId))
485+
.byOrganisationUnitUid().`in`(relevantOrgUnitIds.toList())
452486
.blockingCount()
453487
} catch (e: Exception) {
454488
Log.e(TAG, "Failed to get tracker enrollment count for program $programId", e)
@@ -459,8 +493,24 @@ class DatasetInstancesRepositoryImpl @Inject constructor(
459493
com.ash.simpledataentry.domain.model.ProgramType.EVENT -> {
460494
withContext(Dispatchers.IO) {
461495
try {
496+
val scopedOrgUnits = d2.organisationUnitModule().organisationUnits()
497+
.byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE)
498+
.get().await()
499+
if (scopedOrgUnits.isEmpty()) {
500+
return@withContext 0
501+
}
502+
503+
val attachedOrgUnits = d2.organisationUnitModule().organisationUnits()
504+
.byProgramUids(listOf(programId))
505+
.get().await()
506+
507+
val scopedIds = scopedOrgUnits.map { it.uid() }.toSet()
508+
val attachedIds = attachedOrgUnits.map { it.uid() }.toSet()
509+
val relevantOrgUnitIds = scopedIds.intersect(attachedIds).ifEmpty { scopedIds }
510+
462511
d2.eventModule().events()
463512
.byProgramUid().eq(programId)
513+
.byOrganisationUnitUid().`in`(relevantOrgUnitIds.toList())
464514
.blockingCount()
465515
} catch (e: Exception) {
466516
Log.e(TAG, "Failed to get event count for program $programId", e)

app/src/main/java/com/ash/simpledataentry/domain/model/DatasetInstance.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ data class Period(
2424
data class OrganisationUnit(
2525
val id: String,
2626
val name: String,
27-
val path: String? = null
27+
val path: String? = null,
28+
val uidPath: String? = null,
29+
val parentId: String? = null,
30+
val level: Int? = null
2831
)

app/src/main/java/com/ash/simpledataentry/domain/model/FilterState.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ data class FilterState(
1414
val sortBy: SortBy = SortBy.NAME,
1515
val sortOrder: SortOrder = SortOrder.ASCENDING,
1616
val datasetPeriodType: DatasetPeriodType = DatasetPeriodType.ALL,
17-
val organizationUnit: OrganizationUnitFilter = OrganizationUnitFilter.ALL
17+
val organizationUnit: OrganizationUnitFilter = OrganizationUnitFilter.ALL,
18+
val orgUnitIds: Set<String> = emptySet(),
19+
val orgUnitNames: List<String> = emptyList()
1820
)
1921

2022
data class DatasetInstanceFilterState(

app/src/main/java/com/ash/simpledataentry/domain/repository/DataEntryRepository.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ interface DataEntryRepository {
3434
suspend fun getAvailablePeriods(datasetId: String, limit: Int = 5, showAll: Boolean = false): List<Period>
3535
suspend fun getUserOrgUnit(datasetId: String): OrganisationUnit
3636
suspend fun getUserOrgUnits(datasetId: String): List<OrganisationUnit>
37+
suspend fun getScopedOrgUnits(): List<OrganisationUnit>
38+
suspend fun getOrgUnitsAttachedToDataSets(datasetIds: List<String>): Set<String>
39+
suspend fun expandOrgUnitSelection(targetId: String, orgUnitId: String): Set<String>
3740
suspend fun getDefaultAttributeOptionCombo(): String
3841
suspend fun getAttributeOptionCombos(datasetId: String): List<Pair<String, String>>
3942
suspend fun getCategoryComboStructure(categoryComboUid: String): List<Pair<String, List<Pair<String, String>>>>
@@ -59,4 +62,3 @@ interface DataEntryRepository {
5962
// Validation rules for intelligent grouping
6063
suspend fun getValidationRulesForDataset(datasetId: String): List<org.hisp.dhis.android.core.validation.ValidationRule>
6164
}
62-

app/src/main/java/com/ash/simpledataentry/domain/repository/DatasetInstancesRepository.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
88
interface DatasetInstancesRepository {
99
// Existing dataset methods
1010
suspend fun getDatasetInstances(datasetId: String): List<DatasetInstance>
11-
suspend fun getDatasetInstanceCount(datasetId: String): Int
11+
suspend fun getDatasetInstanceCount(datasetId: String, orgUnitIds: Set<String>? = null): Int
1212
suspend fun syncDatasetInstances()
1313
suspend fun completeDatasetInstance(datasetId: String, period: String, orgUnit: String, attributeOptionCombo: String): Result<Unit>
1414
suspend fun markDatasetInstanceIncomplete(datasetId: String, period: String, orgUnit: String, attributeOptionCombo: String): Result<Unit>
@@ -29,4 +29,4 @@ interface DatasetInstancesRepository {
2929
suspend fun createEventInstance(programId: String, programStageId: String, orgUnitId: String): Result<String>
3030
suspend fun completeEvent(eventId: String): Result<Unit>
3131
suspend fun skipEvent(eventId: String): Result<Unit>
32-
}
32+
}

0 commit comments

Comments
 (0)