Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- Handle Screening feature visibility based on feature flag `Screening`
- Add `hypertensionDiagnosedAt` and `diabetesDiagnosedAt` in `MedicalHistory` table
- Hide schedule appointment sheet for suspected patients
- Add `Suspected` flag to recent patient list item

## 2025.09.09

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,8 @@ class PatientRepositoryAndroidTest {
patientRecordedAt = this.recordedAt,
updatedAt = recordedAt,
eligibleForReassignment = eligibleForReassignment,
diagnosedWithDiabetes = Yes,
diagnosedWithHypertension = Yes
)

private fun verifyRecentPatientOrder(
Expand Down Expand Up @@ -977,6 +979,8 @@ class PatientRepositoryAndroidTest {
patientRecordedAt = this.recordedAt,
updatedAt = updatedAt,
eligibleForReassignment = eligibleForReassignment,
diagnosedWithDiabetes = Yes,
diagnosedWithHypertension = Yes
)
}
}
Expand Down Expand Up @@ -1119,6 +1123,8 @@ class PatientRepositoryAndroidTest {
patientRecordedAt = this.recordedAt,
updatedAt = createdAt,
eligibleForReassignment = eligibleForReassignment,
diagnosedWithDiabetes = Yes,
diagnosedWithHypertension = Yes
)
}
}
Expand Down
73 changes: 45 additions & 28 deletions app/src/main/java/org/simple/clinic/patient/RecentPatient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.simple.clinic.overdue.Appointment.Status
import org.simple.clinic.util.Unicode
import java.time.Instant
import java.util.UUID
import org.simple.clinic.medicalhistory.Answer as MedicalHistoryAnswer

@Parcelize
data class RecentPatient(
Expand All @@ -30,6 +31,10 @@ data class RecentPatient(
val updatedAt: Instant,

val eligibleForReassignment: Answer,

val diagnosedWithHypertension: MedicalHistoryAnswer?,

val diagnosedWithDiabetes: MedicalHistoryAnswer?
) : Parcelable {

override fun toString(): String {
Expand All @@ -43,51 +48,65 @@ data class RecentPatient(

const val RECENT_PATIENT_QUERY = """
SELECT P.uuid, P.fullName, P.gender, P.dateOfBirth, P.age_value,
P.age_updatedAt, P.recordedAt patientRecordedAt, P.eligibleForReassignment,
MAX(
IFNULL(BP.latestRecordedAt, '0'),
IFNULL(PD.latestUpdatedAt, '0'),
IFNULL(AP.latestCreatedAt, '0'),
IFNULL(BloodSugar.latestRecordedAt, '0')
) updatedAt
P.age_updatedAt, P.recordedAt patientRecordedAt, P.eligibleForReassignment,
(
SELECT diagnosedWithHypertension
FROM MedicalHistory
WHERE patientUuid = P.uuid
ORDER BY updatedAt DESC
LIMIT 1
) diagnosedWithHypertension,
(
SELECT hasDiabetes
FROM MedicalHistory
WHERE patientUuid = P.uuid
ORDER BY updatedAt DESC
LIMIT 1
) diagnosedWithDiabetes,
MAX(
IFNULL(BP.latestRecordedAt, '0'),
IFNULL(PD.latestUpdatedAt, '0'),
IFNULL(AP.latestCreatedAt, '0'),
IFNULL(BloodSugar.latestRecordedAt, '0')
) updatedAt
FROM Patient P
LEFT JOIN (
SELECT MAX(recordedAt) latestRecordedAt, patientUuid, facilityUuid
FROM BloodPressureMeasurement
WHERE facilityUuid = :facilityUuid
FROM BloodPressureMeasurement
WHERE facilityUuid = :facilityUuid
AND deletedAt IS NULL
GROUP BY patientUuid
GROUP BY patientUuid
) BP ON P.uuid = BP.patientUuid
LEFT JOIN (
SELECT MAX(updatedAt) latestUpdatedAt, patientUuid, facilityUuid
FROM PrescribedDrug
WHERE facilityUuid = :facilityUuid
FROM PrescribedDrug
WHERE facilityUuid = :facilityUuid
AND deletedAt IS NULL
GROUP BY patientUuid
GROUP BY patientUuid
) PD ON P.uuid = PD.patientUuid
LEFT JOIN (
SELECT MAX(createdAt) latestCreatedAt, uuid, patientUuid, facilityUuid, creationFacilityUuid
FROM Appointment
WHERE creationFacilityUuid = :facilityUuid
FROM Appointment
WHERE creationFacilityUuid = :facilityUuid
AND deletedAt IS NULL
AND status = :appointmentStatus
AND appointmentType = :appointmentType
GROUP BY patientUuid
GROUP BY patientUuid
) AP ON P.uuid = AP.patientUuid
LEFT JOIN (
SELECT MAX(recordedAt) latestRecordedAt, patientUuid, facilityUuid
FROM BloodSugarMeasurements
WHERE facilityUuid = :facilityUuid
FROM BloodSugarMeasurements
WHERE facilityUuid = :facilityUuid
AND deletedAt IS NULL
GROUP BY patientUuid
GROUP BY patientUuid
) BloodSugar ON P.uuid = BloodSugar.patientUuid
WHERE (
(
BP.facilityUuid = :facilityUuid OR
PD.facilityUuid = :facilityUuid OR
AP.creationFacilityUuid = :facilityUuid OR
BloodSugar.facilityUuid = :facilityUuid
)
)
AND P.deletedAt IS NULL
AND P.status = :patientStatus
)
Expand All @@ -96,14 +115,12 @@ data class RecentPatient(
}

/**
Goal: Fetch a list of patients with 10 most recent changes.
There are tables like BloodPressureMeasurement (BP), PrescribedDrug (PD), Appointment (AP), etc. Let’s call each table T1, T2, T3, etc.

Algo:
1. Get a list of all patients
2. For each patient, from each table T, get the latest change for them. Columns: T1.latestUpdatedAt, T2.latestUpdatedAt, etc.
3. Pick latestUpdatedAt for each patient
4. Order by updatedAt from final list and cap it to 10 entries.
* Goal: Fetch a list of patients with 10 most recent changes.
* Algo:
* 1. Get a list of all patients
* 2. For each patient, from each table T, get the latest change for them.
* 3. Pick latestUpdatedAt for each patient
* 4. Order by updatedAt from final list and cap it to the limit.
*/
@Query("$RECENT_PATIENT_QUERY LIMIT :limit")
fun recentPatientsWithLimit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,17 @@ import androidx.paging.PagingData
import androidx.paging.map
import androidx.recyclerview.widget.DiffUtil
import io.reactivex.subjects.Subject
import org.simple.clinic.R
import org.simple.clinic.databinding.RecentPatientItemViewBinding
import org.simple.clinic.patient.Gender
import org.simple.clinic.patient.RecentPatient
import org.simple.clinic.patient.displayIconRes
import org.simple.clinic.util.UserClock
import org.simple.clinic.util.toLocalDateAtZone
import org.simple.clinic.widgets.PagingItemAdapter
import org.simple.clinic.widgets.UiEvent
import org.simple.clinic.widgets.recyclerview.BindingViewHolder
import org.simple.clinic.widgets.visibleOrGone
import java.time.Instant
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.UUID

data class RecentPatientItem(
val uuid: UUID,
val name: String,
val age: Int,
val gender: Gender,
val updatedAt: Instant,
val dateFormatter: DateTimeFormatter,
val clock: UserClock,
val isNewRegistration: Boolean
private val model: RecentPatientUiModel
) : PagingItemAdapter.Item<UiEvent> {

companion object {
Expand All @@ -38,7 +24,6 @@ data class RecentPatientItem(
dateFormatter: DateTimeFormatter
): PagingData<RecentPatientItem> {
val today = LocalDate.now(userClock)

return recentPatients.map { recentPatientItem(it, today, userClock, dateFormatter) }
}

Expand All @@ -48,36 +33,26 @@ data class RecentPatientItem(
userClock: UserClock,
dateFormatter: DateTimeFormatter
): RecentPatientItem {
val patientRegisteredOnDate = recentPatient.patientRecordedAt.toLocalDateAtZone(userClock.zone)
val isNewRegistration = today == patientRegisteredOnDate

return RecentPatientItem(
uuid = recentPatient.uuid,
name = recentPatient.fullName,
age = recentPatient.ageDetails.estimateAge(userClock),
gender = recentPatient.gender,
updatedAt = recentPatient.updatedAt,
val model = RecentPatientUiModel.from(
recentPatient = recentPatient,
today = today,
userClock = userClock,
dateFormatter = dateFormatter,
clock = userClock,
isNewRegistration = isNewRegistration
isEligibleForReassignment = false
)

return RecentPatientItem(model)
}
}

override fun layoutResId(): Int = R.layout.recent_patient_item_view
val uuid: UUID get() = model.uuid

override fun render(holder: BindingViewHolder, subject: Subject<UiEvent>) {
val context = holder.itemView.context
val binding = holder.binding as RecentPatientItemViewBinding
override fun layoutResId(): Int = org.simple.clinic.R.layout.recent_patient_item_view

holder.itemView.setOnClickListener {
override fun render(holder: BindingViewHolder, subject: Subject<UiEvent>) {
RecentPatientViewBinder.bind(holder, model) {
subject.onNext(RecentPatientItemClicked(patientUuid = uuid))
}

binding.newRegistrationTextView.visibleOrGone(isNewRegistration)
binding.patientNameTextView.text = context.resources.getString(R.string.patients_recentpatients_nameage, name, age.toString())
binding.genderImageView.setImageResource(gender.displayIconRes)
binding.lastSeenTextView.text = dateFormatter.format(updatedAt.toLocalDateAtZone(clock.zone))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.simple.clinic.recentpatient

import org.simple.clinic.R
import org.simple.clinic.databinding.RecentPatientItemViewBinding
import org.simple.clinic.medicalhistory.Answer.Suspected
import org.simple.clinic.patient.Gender
import org.simple.clinic.patient.RecentPatient
import org.simple.clinic.patient.displayIconRes
import org.simple.clinic.util.UserClock
import org.simple.clinic.util.toLocalDateAtZone
import org.simple.clinic.widgets.recyclerview.BindingViewHolder
import org.simple.clinic.widgets.visibleOrGone
import java.time.Instant
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.UUID

/**
* Shared UI model used by both paged and non-paged adapters.
*/
data class RecentPatientUiModel(
val uuid: UUID,
val name: String,
val age: Int,
val gender: Gender,
val updatedAt: Instant,
val dateFormatter: DateTimeFormatter,
val clock: UserClock,
val isNewRegistration: Boolean,
val isEligibleForReassignment: Boolean = false,
val isSuspectedForHypertension: Boolean = false,
val isSuspectedForDiabetes: Boolean = false
) {
companion object {
fun from(
recentPatient: RecentPatient,
today: LocalDate,
userClock: UserClock,
dateFormatter: DateTimeFormatter,
isEligibleForReassignment: Boolean = false
): RecentPatientUiModel {
val patientRegisteredOnDate = recentPatient.patientRecordedAt.toLocalDateAtZone(userClock.zone)
val isNewRegistration = today == patientRegisteredOnDate

return RecentPatientUiModel(
uuid = recentPatient.uuid,
name = recentPatient.fullName,
age = recentPatient.ageDetails.estimateAge(userClock),
gender = recentPatient.gender,
updatedAt = recentPatient.updatedAt,
dateFormatter = dateFormatter,
clock = userClock,
isNewRegistration = isNewRegistration,
isEligibleForReassignment = isEligibleForReassignment,
isSuspectedForHypertension = recentPatient.diagnosedWithHypertension == Suspected,
isSuspectedForDiabetes = recentPatient.diagnosedWithDiabetes == Suspected
)
}
}
}

object RecentPatientViewBinder {

fun bind(
holder: BindingViewHolder,
model: RecentPatientUiModel,
onClick: (UUID) -> Unit
) {
val context = holder.itemView.context
val binding = holder.binding as RecentPatientItemViewBinding

holder.itemView.setOnClickListener {
onClick(model.uuid)
}

val statusText: String? = when {
model.isSuspectedForHypertension && model.isSuspectedForDiabetes ->
context.getString(R.string.recent_patients_itemview_suspected_for_hypertension_and_diabetes)

model.isSuspectedForHypertension ->
context.getString(R.string.recent_patients_itemview_suspected_for_hypertension)

model.isSuspectedForDiabetes ->
context.getString(R.string.recent_patients_itemview_suspected_for_diabetes)

model.isNewRegistration ->
context.getString(R.string.recent_patients_itemview_new_registration)

else -> null
}

binding.patientStatusTextView.visibleOrGone(statusText != null)
binding.patientStatusTextView.text = statusText
binding.facilityReassignmentView.root.visibleOrGone(model.isEligibleForReassignment)

binding.patientNameTextView.text =
context.resources.getString(R.string.patients_recentpatients_nameage, model.name, model.age.toString())

binding.genderImageView.setImageResource(model.gender.displayIconRes)
binding.lastSeenTextView.text = model.dateFormatter.format(model.updatedAt.toLocalDateAtZone(model.clock.zone))
}
}
Loading