Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
- Bump Jackson Core to v2.21.1
- Bump Compose BOM to v2026.03.00

### Changes

- Sort Overdue list based on return score when feature `sort_overdue_based_on_return_score` is enabled

## 2026.03.02

### Internal
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/simple/clinic/feature/Feature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ enum class Feature(
LabBasedStatinNudge(false, "lab_based_statin_nudge"),
Screening(false, "screening_feature_v0"),
ShowDiagnosisButton(false, "show_diagnosis_button"),
SortOverdueBasedOnReturnScore(false, "sort_overdue_based_on_return_score"),
ShowReturnScoreDebugValues(false, "show_return_score_debug_values"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package org.simple.clinic.home.overdue

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.UUID

@Parcelize
data class OverdueAppointmentSections(
val pendingAppointments: List<OverdueAppointment>,
val pendingDebugInfo: Map<UUID, Pair<Float, OverdueBucket>>,
val agreedToVisitAppointments: List<OverdueAppointment>,
val remindToCallLaterAppointments: List<OverdueAppointment>,
val removedFromOverdueAppointments: List<OverdueAppointment>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.simple.clinic.home.overdue

import org.simple.clinic.feature.Feature
import org.simple.clinic.feature.Features
import org.simple.clinic.returnscore.LikelyToReturnIfCalledScoreType
import org.simple.clinic.returnscore.ReturnScore
import java.util.UUID
import javax.inject.Inject
import kotlin.math.max
import kotlin.random.Random

class OverdueAppointmentSorter @Inject constructor(
private val returnScoreDao: ReturnScore.RoomDao,
private val features: Features,
private val random: Random = Random.Default
) {

fun sort(overdueAppointments: List<OverdueAppointment>): List<SortedOverdueAppointment> {

if (!features.isEnabled(Feature.SortOverdueBasedOnReturnScore)) {
return overdueAppointments.map {
SortedOverdueAppointment(
appointment = it,
score = 0f,
bucket = OverdueBucket.REMAINING
)
}
}

val scores = returnScoreDao.getAllImmediate()
.filter { it.scoreType == LikelyToReturnIfCalledScoreType }

val scoreMap: Map<UUID, Float> = scores.associate {
it.patientUuid to it.scoreValue
}

val withScores = overdueAppointments.map { appointment ->
val score = scoreMap[appointment.appointment.patientUuid] ?: 0f
appointment to score
}

val sorted = withScores.sortedByDescending { it.second }

val total = sorted.size
if (total == 0) return emptyList()

val top20End = max((total * 0.2).toInt(), 1)
val next30End = max((total * 0.5).toInt(), top20End)

val top20 = sorted.take(top20End)
val next30 = sorted.subList(top20End, next30End)
val rest = sorted.drop(next30End)

val topPickCount = max((top20.size * 0.5).toInt(), 1)
val nextPickCount = max((next30.size * 0.5).toInt(), 1)

val topPicked = top20.shuffled(random).take(topPickCount)
val nextPicked = next30.shuffled(random).take(nextPickCount)

val selectedAppointments = (topPicked + nextPicked)
.map { it.first }
.toSet()

val topRemaining = top20.filterNot { it.first in selectedAppointments }
val nextRemaining = next30.filterNot { it.first in selectedAppointments }

fun mapToSorted(
list: List<Pair<OverdueAppointment, Float>>,
bucket: OverdueBucket
) = list.map { (appointment, score) ->
SortedOverdueAppointment(
appointment = appointment,
score = score,
bucket = bucket
)
}

return buildList {
addAll(mapToSorted(topPicked, OverdueBucket.TOP_20))
addAll(mapToSorted(nextPicked, OverdueBucket.NEXT_30))
addAll(mapToSorted(topRemaining, OverdueBucket.TOP_20))
addAll(mapToSorted(nextRemaining, OverdueBucket.NEXT_30))
addAll(mapToSorted(rest, OverdueBucket.REMAINING))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many returns (count = 3): sort [qlty:return-statements]

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.simple.clinic.home.overdue

enum class OverdueBucket {
TOP_20,
NEXT_30,
REMAINING
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class OverdueEffectHandler @AssistedInject constructor(
private val overdueDownloadScheduler: OverdueDownloadScheduler,
private val userClock: UserClock,
private val overdueAppointmentSelector: OverdueAppointmentSelector,
private val overdueAppointmentSorter: OverdueAppointmentSorter,
@Assisted private val viewEffectsConsumer: Consumer<OverdueViewEffect>
) {

Expand Down Expand Up @@ -82,8 +83,17 @@ class OverdueEffectHandler @AssistedInject constructor(
overdueAppointments = overdueAppointments
)
val overdueSections = overdueAppointmentsWithInYear.groupBy { it.callResult?.outcome }

val pendingAppointments = overdueSections[null].orEmpty()

val sortedPendingAppointments = overdueAppointmentSorter.sort(pendingAppointments)
val debugMap = sortedPendingAppointments.associate {
it.appointment.appointment.patientUuid to (it.score to it.bucket)
}

val overdueAppointmentSections = OverdueAppointmentSections(
pendingAppointments = overdueSections[null].orEmpty(),
pendingAppointments = sortedPendingAppointments.map { it.appointment },
pendingDebugInfo = debugMap,
agreedToVisitAppointments = overdueSections[Outcome.AgreedToVisit].orEmpty(),
remindToCallLaterAppointments = overdueSections[Outcome.RemindToCallLater].orEmpty(),
removedFromOverdueAppointments = overdueSections[Outcome.RemovedFromOverdueList].orEmpty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.simple.clinic.databinding.ScreenOverdueBinding
import org.simple.clinic.di.injector
import org.simple.clinic.feature.Feature.OverdueInstantSearch
import org.simple.clinic.feature.Feature.PatientReassignment
import org.simple.clinic.feature.Feature.ShowReturnScoreDebugValues
import org.simple.clinic.feature.Features
import org.simple.clinic.home.HomeScreen
import org.simple.clinic.home.overdue.compose.OverdueScreenView
Expand Down Expand Up @@ -249,6 +250,7 @@ class OverdueScreen : BaseScreen<
isOverdueSelectAndDownloadEnabled = country.isoCountryCode == Country.INDIA,
selectedOverdueAppointments = selectedOverdueAppointments,
isPatientReassignmentFeatureEnabled = features.isEnabled(PatientReassignment),
showDebugValues = features.isEnabled(ShowReturnScoreDebugValues),
locale = locale,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.simple.clinic.home.overdue

data class SortedOverdueAppointment(
val appointment: OverdueAppointment,
val score: Float,
val bucket: OverdueBucket
)
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ fun OverdueAppointmentSections(
isOverdueSelectAndDownloadEnabled = model.isOverdueSelectAndDownloadEnabled,
isAppointmentSelected = model.isAppointmentSelected,
isEligibleForReassignment = model.isEligibleForReassignment,
showDebugValues = model.showDebugValues,
returnScore = model.returnScore,
bucket = model.bucket,
onCallClicked = onCallClicked,
onRowClicked = onRowClicked,
onCheckboxClicked = onCheckboxClicked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.simple.clinic.R
import org.simple.clinic.common.ui.theme.SimpleTheme
import org.simple.clinic.home.overdue.OverdueBucket
import org.simple.clinic.patient.Gender
import org.simple.clinic.patient.displayIconRes
import java.util.UUID
Expand All @@ -44,6 +45,9 @@ fun OverduePatientListItem(
isOverdueSelectAndDownloadEnabled: Boolean,
isAppointmentSelected: Boolean,
isEligibleForReassignment: Boolean,
showDebugValues: Boolean,
returnScore: Float?,
bucket: OverdueBucket?,
onCallClicked: (UUID) -> Unit,
onRowClicked: (UUID) -> Unit,
onCheckboxClicked: (UUID) -> Unit
Expand Down Expand Up @@ -97,6 +101,10 @@ fun OverduePatientListItem(
style = SimpleTheme.typography.material.body2,
color = SimpleTheme.colors.material.error,
)

if (showDebugValues) {
DebugScoreView(returnScore = returnScore, bucket = bucket)
}
}

OverduePatientListItemRightButton(
Expand Down Expand Up @@ -210,6 +218,28 @@ fun OverduePatientListItemRightButton(
}
}

@Composable
private fun DebugScoreView(
returnScore: Float?,
bucket: OverdueBucket?
) {
if (returnScore != null && bucket != null) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex binary expression [qlty:boolean-logic]


val bucketText = when (bucket) {
OverdueBucket.TOP_20 -> "Top 20%"
OverdueBucket.NEXT_30 -> "Next 30%"
OverdueBucket.REMAINING -> "Remaining"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deeply nested control flow (level = 2) [qlty:nested-control-flow]

}

Text(
modifier = Modifier.padding(top = 4.dp),
text = "Score: ${"%.1f".format(returnScore)} | $bucketText",
style = SimpleTheme.typography.material.caption,
color = Color.Gray
)
}
}

@Preview
@Composable
private fun OverduePatientListItemPreview() {
Expand All @@ -226,6 +256,9 @@ private fun OverduePatientListItemPreview() {
isOverdueSelectAndDownloadEnabled = false,
isAppointmentSelected = false,
isEligibleForReassignment = true,
showDebugValues = true,
returnScore = 9.2f,
bucket = OverdueBucket.TOP_20,
onCallClicked = {},
onRowClicked = {},
onCheckboxClicked = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.simple.clinic.home.overdue.compose

import androidx.annotation.StringRes
import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle
import org.simple.clinic.home.overdue.OverdueBucket
import org.simple.clinic.home.overdue.PendingListState
import org.simple.clinic.patient.Gender
import java.util.Locale
Expand All @@ -20,6 +21,9 @@ sealed class OverdueUiModel {
val isOverdueSelectAndDownloadEnabled: Boolean,
val isAppointmentSelected: Boolean,
val isEligibleForReassignment: Boolean,
val showDebugValues: Boolean,
val returnScore: Float? = null,
val bucket: OverdueBucket? = null
) : OverdueUiModel()

data class Header(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.PENDING_TO_
import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.REMIND_TO_CALL
import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle.REMOVED_FROM_OVERDUE
import org.simple.clinic.home.overdue.OverdueAppointmentSections
import org.simple.clinic.home.overdue.OverdueBucket
import org.simple.clinic.home.overdue.OverdueListSectionStates
import org.simple.clinic.home.overdue.PendingListState.SEE_ALL
import org.simple.clinic.home.overdue.PendingListState.SEE_LESS
Expand All @@ -31,6 +32,7 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled: Boolean,
selectedOverdueAppointments: Set<UUID>,
isPatientReassignmentFeatureEnabled: Boolean,
showDebugValues: Boolean,
locale: Locale,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 10): from [qlty:function-parameters]

): List<OverdueUiModel> {
val searchOverduePatientsButtonListItem = searchOverduePatientItem(
Expand All @@ -45,6 +47,7 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled,
selectedOverdueAppointments,
isPatientReassignmentFeatureEnabled,
showDebugValues,
locale,
)

Expand Down Expand Up @@ -236,6 +239,7 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled: Boolean,
selectedOverdueAppointments: Set<UUID>,
isPatientReassignmentFeatureEnabled: Boolean,
showDebugValues: Boolean,
locale: Locale,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 9): pendingToCallItem [qlty:function-parameters]

): List<OverdueUiModel> {
val pendingAppointments = overdueAppointmentSections.pendingAppointments
Expand All @@ -256,6 +260,7 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled,
selectedOverdueAppointments,
isPatientReassignmentFeatureEnabled,
showDebugValues
)

val showPendingListFooter = pendingAppointments.size > pendingListDefaultStateSize && overdueListSectionStates.isPendingHeaderExpanded
Expand All @@ -278,6 +283,7 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled: Boolean,
selectedOverdueAppointments: Set<UUID>,
isPatientReassignmentFeatureEnabled: Boolean,
showDebugValues: Boolean,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 8): generatePendingAppointmentsContent [qlty:function-parameters]

): List<OverdueUiModel> {
val pendingAppointmentsList = when (overdueListSectionStates.pendingListState) {
SEE_LESS -> overdueAppointmentSections.pendingAppointments.take(pendingListDefaultStateSize)
Expand All @@ -291,6 +297,8 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled,
selectedOverdueAppointments,
isPatientReassignmentFeatureEnabled,
showDebugValues,
overdueAppointmentSections.pendingDebugInfo
)

return if (pendingAppointmentsList.isEmpty() && overdueListSectionStates.isPendingHeaderExpanded) {
Expand All @@ -307,6 +315,8 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled: Boolean,
selectedOverdueAppointments: Set<UUID>,
isPatientReassignmentFeatureEnabled: Boolean,
showDebugValues: Boolean = false,
debugMap: Map<UUID, Pair<Float, OverdueBucket>> = emptyMap()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 10): expandedOverdueAppointmentList [qlty:function-parameters]

): List<OverdueUiModel> {
return if (isListExpanded) {
overdueAppointment.map {
Expand All @@ -317,6 +327,8 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled,
isAppointmentSelected,
isPatientReassignmentFeatureEnabled,
showDebugValues,
debugMap
)
}
} else {
Expand All @@ -330,7 +342,12 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled: Boolean,
isAppointmentSelected: Boolean,
isPatientReassignmentFeatureEnabled: Boolean,
showDebugValues: Boolean,
debugMap: Map<UUID, Pair<Float, OverdueBucket>>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 7): from [qlty:function-parameters]

): OverdueUiModel {
val patientUuid = overdueAppointment.appointment.patientUuid
val debugInfo = debugMap[patientUuid]

return OverdueUiModel.Patient(
appointmentUuid = overdueAppointment.appointment.uuid,
patientUuid = overdueAppointment.appointment.patientUuid,
Expand All @@ -343,6 +360,9 @@ class OverdueUiModelMapper {
isOverdueSelectAndDownloadEnabled = isOverdueSelectAndDownloadEnabled,
isAppointmentSelected = isAppointmentSelected,
isEligibleForReassignment = (overdueAppointment.eligibleForReassignment == Answer.Yes) && isPatientReassignmentFeatureEnabled,
showDebugValues = showDebugValues,
returnScore = debugInfo?.first,
bucket = debugInfo?.second,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import org.simple.clinic.overdue.TimeToAppointment.Days
import org.simple.clinic.overdue.TimeToAppointment.Months
import org.simple.clinic.overdue.TimeToAppointment.Weeks
import org.simple.clinic.remoteconfig.ConfigReader
import org.simple.clinic.returnscore.ReturnScore
import org.simple.clinic.util.preference.StringPreferenceConverter
import org.simple.clinic.util.preference.getOptional
import retrofit2.Retrofit
import java.time.Period
import java.util.Optional
import javax.inject.Named
import kotlin.random.Random

@Module
class AppointmentModule {
Expand Down Expand Up @@ -133,4 +135,9 @@ class AppointmentModule {

@Provides
fun providePendingAppointmentsConfig(configReader: ConfigReader) = PendingAppointmentsConfig.read(configReader)

@Provides
fun provideRandom(): Random {
return Random.Default
}
}
Loading
Loading