Skip to content

Commit ca20fbd

Browse files
authored
Sort Overdue list based on return_score (#5764)
1 parent e6ab840 commit ca20fbd

19 files changed

Lines changed: 524 additions & 22 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
- Bump Jackson Core to v2.21.1
1818
- Bump Compose BOM to v2026.03.00
1919

20+
### Changes
21+
22+
- Sort Overdue list based on return score when feature `sort_overdue_based_on_return_score` is enabled
23+
2024
## 2026.03.02
2125

2226
### Internal

app/src/main/java/org/simple/clinic/feature/Feature.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ enum class Feature(
2828
LabBasedStatinNudge(false, "lab_based_statin_nudge"),
2929
Screening(false, "screening_feature_v0"),
3030
ShowDiagnosisButton(false, "show_diagnosis_button"),
31+
SortOverdueBasedOnReturnScore(false, "sort_overdue_based_on_return_score"),
32+
ShowReturnScoreDebugValues(false, "show_return_score_debug_values"),
3133
}

app/src/main/java/org/simple/clinic/home/overdue/OverdueAppointmentSections.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package org.simple.clinic.home.overdue
22

33
import android.os.Parcelable
44
import kotlinx.parcelize.Parcelize
5+
import java.util.UUID
56

67
@Parcelize
78
data class OverdueAppointmentSections(
89
val pendingAppointments: List<OverdueAppointment>,
10+
val pendingDebugInfo: Map<UUID, Pair<Float, OverdueBucket>>,
911
val agreedToVisitAppointments: List<OverdueAppointment>,
1012
val remindToCallLaterAppointments: List<OverdueAppointment>,
1113
val removedFromOverdueAppointments: List<OverdueAppointment>,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.simple.clinic.home.overdue
2+
3+
import org.simple.clinic.feature.Feature
4+
import org.simple.clinic.feature.Features
5+
import org.simple.clinic.returnscore.LikelyToReturnIfCalledScoreType
6+
import org.simple.clinic.returnscore.ReturnScore
7+
import java.util.UUID
8+
import javax.inject.Inject
9+
import kotlin.math.max
10+
import kotlin.random.Random
11+
12+
class OverdueAppointmentSorter @Inject constructor(
13+
private val returnScoreDao: ReturnScore.RoomDao,
14+
private val features: Features,
15+
private val random: Random = Random.Default
16+
) {
17+
18+
fun sort(overdueAppointments: List<OverdueAppointment>): List<SortedOverdueAppointment> {
19+
20+
if (!features.isEnabled(Feature.SortOverdueBasedOnReturnScore)) {
21+
return overdueAppointments.map {
22+
SortedOverdueAppointment(
23+
appointment = it,
24+
score = 0f,
25+
bucket = OverdueBucket.REMAINING
26+
)
27+
}
28+
}
29+
30+
val scores = returnScoreDao.getAllImmediate()
31+
.filter { it.scoreType == LikelyToReturnIfCalledScoreType }
32+
33+
val scoreMap: Map<UUID, Float> = scores.associate {
34+
it.patientUuid to it.scoreValue
35+
}
36+
37+
val withScores = overdueAppointments.map { appointment ->
38+
val score = scoreMap[appointment.appointment.patientUuid] ?: 0f
39+
appointment to score
40+
}
41+
42+
val sorted = withScores.sortedByDescending { it.second }
43+
44+
val total = sorted.size
45+
if (total == 0) return emptyList()
46+
47+
val top20End = max((total * 0.2).toInt(), 1)
48+
val next30End = max((total * 0.5).toInt(), top20End)
49+
50+
val top20 = sorted.take(top20End)
51+
val next30 = sorted.subList(top20End, next30End)
52+
val rest = sorted.drop(next30End)
53+
54+
val topPickCount = max((top20.size * 0.5).toInt(), 1)
55+
val nextPickCount = max((next30.size * 0.5).toInt(), 1)
56+
57+
val topPicked = top20.shuffled(random).take(topPickCount)
58+
val nextPicked = next30.shuffled(random).take(nextPickCount)
59+
60+
val selectedAppointments = (topPicked + nextPicked)
61+
.map { it.first }
62+
.toSet()
63+
64+
val topRemaining = top20.filterNot { it.first in selectedAppointments }
65+
val nextRemaining = next30.filterNot { it.first in selectedAppointments }
66+
67+
fun mapToSorted(
68+
list: List<Pair<OverdueAppointment, Float>>,
69+
bucket: OverdueBucket
70+
) = list.map { (appointment, score) ->
71+
SortedOverdueAppointment(
72+
appointment = appointment,
73+
score = score,
74+
bucket = bucket
75+
)
76+
}
77+
78+
return buildList {
79+
addAll(mapToSorted(topPicked, OverdueBucket.TOP_20))
80+
addAll(mapToSorted(nextPicked, OverdueBucket.NEXT_30))
81+
addAll(mapToSorted(topRemaining, OverdueBucket.TOP_20))
82+
addAll(mapToSorted(nextRemaining, OverdueBucket.NEXT_30))
83+
addAll(mapToSorted(rest, OverdueBucket.REMAINING))
84+
}
85+
}
86+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.simple.clinic.home.overdue
2+
3+
enum class OverdueBucket {
4+
TOP_20,
5+
NEXT_30,
6+
REMAINING
7+
}

app/src/main/java/org/simple/clinic/home/overdue/OverdueEffectHandler.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class OverdueEffectHandler @AssistedInject constructor(
2727
private val overdueDownloadScheduler: OverdueDownloadScheduler,
2828
private val userClock: UserClock,
2929
private val overdueAppointmentSelector: OverdueAppointmentSelector,
30+
private val overdueAppointmentSorter: OverdueAppointmentSorter,
3031
@Assisted private val viewEffectsConsumer: Consumer<OverdueViewEffect>
3132
) {
3233

@@ -82,8 +83,17 @@ class OverdueEffectHandler @AssistedInject constructor(
8283
overdueAppointments = overdueAppointments
8384
)
8485
val overdueSections = overdueAppointmentsWithInYear.groupBy { it.callResult?.outcome }
86+
87+
val pendingAppointments = overdueSections[null].orEmpty()
88+
89+
val sortedPendingAppointments = overdueAppointmentSorter.sort(pendingAppointments)
90+
val debugMap = sortedPendingAppointments.associate {
91+
it.appointment.appointment.patientUuid to (it.score to it.bucket)
92+
}
93+
8594
val overdueAppointmentSections = OverdueAppointmentSections(
86-
pendingAppointments = overdueSections[null].orEmpty(),
95+
pendingAppointments = sortedPendingAppointments.map { it.appointment },
96+
pendingDebugInfo = debugMap,
8797
agreedToVisitAppointments = overdueSections[Outcome.AgreedToVisit].orEmpty(),
8898
remindToCallLaterAppointments = overdueSections[Outcome.RemindToCallLater].orEmpty(),
8999
removedFromOverdueAppointments = overdueSections[Outcome.RemovedFromOverdueList].orEmpty(),

app/src/main/java/org/simple/clinic/home/overdue/OverdueScreen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.simple.clinic.databinding.ScreenOverdueBinding
3131
import org.simple.clinic.di.injector
3232
import org.simple.clinic.feature.Feature.OverdueInstantSearch
3333
import org.simple.clinic.feature.Feature.PatientReassignment
34+
import org.simple.clinic.feature.Feature.ShowReturnScoreDebugValues
3435
import org.simple.clinic.feature.Features
3536
import org.simple.clinic.home.HomeScreen
3637
import org.simple.clinic.home.overdue.compose.OverdueScreenView
@@ -249,6 +250,7 @@ class OverdueScreen : BaseScreen<
249250
isOverdueSelectAndDownloadEnabled = country.isoCountryCode == Country.INDIA,
250251
selectedOverdueAppointments = selectedOverdueAppointments,
251252
isPatientReassignmentFeatureEnabled = features.isEnabled(PatientReassignment),
253+
showDebugValues = features.isEnabled(ShowReturnScoreDebugValues),
252254
locale = locale,
253255
)
254256

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.simple.clinic.home.overdue
2+
3+
data class SortedOverdueAppointment(
4+
val appointment: OverdueAppointment,
5+
val score: Float,
6+
val bucket: OverdueBucket
7+
)

app/src/main/java/org/simple/clinic/home/overdue/compose/OverdueAppointmentSections.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ fun OverdueAppointmentSections(
6060
isOverdueSelectAndDownloadEnabled = model.isOverdueSelectAndDownloadEnabled,
6161
isAppointmentSelected = model.isAppointmentSelected,
6262
isEligibleForReassignment = model.isEligibleForReassignment,
63+
showDebugValues = model.showDebugValues,
64+
returnScore = model.returnScore,
65+
bucket = model.bucket,
6366
onCallClicked = onCallClicked,
6467
onRowClicked = onRowClicked,
6568
onCheckboxClicked = onCheckboxClicked

app/src/main/java/org/simple/clinic/home/overdue/compose/OverduePatientListItem.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview
2626
import androidx.compose.ui.unit.dp
2727
import org.simple.clinic.R
2828
import org.simple.clinic.common.ui.theme.SimpleTheme
29+
import org.simple.clinic.home.overdue.OverdueBucket
2930
import org.simple.clinic.patient.Gender
3031
import org.simple.clinic.patient.displayIconRes
3132
import java.util.UUID
@@ -44,6 +45,9 @@ fun OverduePatientListItem(
4445
isOverdueSelectAndDownloadEnabled: Boolean,
4546
isAppointmentSelected: Boolean,
4647
isEligibleForReassignment: Boolean,
48+
showDebugValues: Boolean,
49+
returnScore: Float?,
50+
bucket: OverdueBucket?,
4751
onCallClicked: (UUID) -> Unit,
4852
onRowClicked: (UUID) -> Unit,
4953
onCheckboxClicked: (UUID) -> Unit
@@ -97,6 +101,10 @@ fun OverduePatientListItem(
97101
style = SimpleTheme.typography.material.body2,
98102
color = SimpleTheme.colors.material.error,
99103
)
104+
105+
if (showDebugValues) {
106+
DebugScoreView(returnScore = returnScore, bucket = bucket)
107+
}
100108
}
101109

102110
OverduePatientListItemRightButton(
@@ -210,6 +218,28 @@ fun OverduePatientListItemRightButton(
210218
}
211219
}
212220

221+
@Composable
222+
private fun DebugScoreView(
223+
returnScore: Float?,
224+
bucket: OverdueBucket?
225+
) {
226+
if (returnScore != null && bucket != null) {
227+
228+
val bucketText = when (bucket) {
229+
OverdueBucket.TOP_20 -> "Top 20%"
230+
OverdueBucket.NEXT_30 -> "Next 30%"
231+
OverdueBucket.REMAINING -> "Remaining"
232+
}
233+
234+
Text(
235+
modifier = Modifier.padding(top = 4.dp),
236+
text = "Score: ${"%.1f".format(returnScore)} | $bucketText",
237+
style = SimpleTheme.typography.material.caption,
238+
color = Color.Gray
239+
)
240+
}
241+
}
242+
213243
@Preview
214244
@Composable
215245
private fun OverduePatientListItemPreview() {
@@ -226,6 +256,9 @@ private fun OverduePatientListItemPreview() {
226256
isOverdueSelectAndDownloadEnabled = false,
227257
isAppointmentSelected = false,
228258
isEligibleForReassignment = true,
259+
showDebugValues = true,
260+
returnScore = 9.2f,
261+
bucket = OverdueBucket.TOP_20,
229262
onCallClicked = {},
230263
onRowClicked = {},
231264
onCheckboxClicked = {}

0 commit comments

Comments
 (0)