diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c9e09ea56..4d67e852560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Bump AndroidX ViewModel to v2.8.5 - Bump AndroidX Lifecycle to v2.8.5 - Check statin nudge status in patient summary screen +- Show statin nudge alert when statins can be prescribed ## 2024-08-05-9175 diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryModel.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryModel.kt index 2ce5132e504..db370fd5359 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryModel.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryModel.kt @@ -27,7 +27,7 @@ data class PatientSummaryModel( val hasPrescribedDrugsChangedToday: Boolean?, val scheduledAppointment: ParcelableOptional?, val hasShownDiagnosisWarningDialog: Boolean, - val canPrescribeStatin: Boolean, + val statin: StatinModel?, ) : Parcelable, PatientSummaryChildModel { companion object { @@ -46,7 +46,7 @@ data class PatientSummaryModel( hasPrescribedDrugsChangedToday = null, scheduledAppointment = null, hasShownDiagnosisWarningDialog = false, - canPrescribeStatin = false, + statin = null, ) } } @@ -81,6 +81,9 @@ data class PatientSummaryModel( val hasScheduledAppointment: Boolean get() = scheduledAppointment != null && scheduledAppointment.isPresent() + val hasStatinInfoLoaded: Boolean + get() = statin != null + override fun readyToRender(): Boolean { return hasLoadedPatientSummaryProfile && hasLoadedCurrentFacility && hasPatientRegistrationData != null } @@ -125,7 +128,7 @@ data class PatientSummaryModel( return copy(scheduledAppointment = appointment.toOptional().parcelable()) } - fun updateStatinPrescriptionStatus(canPrescribeStatin: Boolean): PatientSummaryModel { - return copy(canPrescribeStatin = canPrescribeStatin) + fun updateStatinInfo(statin: StatinModel): PatientSummaryModel { + return copy(statin = statin) } } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt index c326d9de796..1a484e0115c 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt @@ -168,6 +168,12 @@ class PatientSummaryScreen : private val clinicalDecisionSupportAlertView get() = binding.clinicalDecisionSupportBpHighAlert.rootView + private val statinAlertView + get() = binding.statinAlert.rootView + + private val statinAlertDescription + get() = binding.statinAlert.statinAlertSubtitle + @Inject lateinit var router: Router @@ -792,6 +798,42 @@ class PatientSummaryScreen : clinicalDecisionSupportAlertView.visibility = GONE } + override fun showStatinAlert(statin: StatinModel) { + statinAlertDescription.text = buildString { + append("${getString(R.string.statin_alert_patient)} ") + + if (statin.hasDiabetes) { + append(String.format(getString(R.string.statin_alert_has_diabetes), statin.age.toString())) + + if (statin.hasHadHeartAttack.xor(statin.hasHadStroke)) { + append(" ${getString(R.string.statin_alert_and_seperator)} ") + } + } + + when { + statin.hasHadHeartAttack && statin.hasHadStroke -> append(getCVDString(statin.hasDiabetes)) + statin.hasHadHeartAttack -> append(getString(R.string.statin_alert_heart_attack)) + statin.hasHadStroke -> append(getString(R.string.statin_alert_stroke)) + } + + append(".") + } + + statinAlertView.visibility = VISIBLE + } + + private fun getCVDString(hasDiabetes: Boolean): String { + return if (hasDiabetes) { + getString(R.string.statin_alert_cvd_with_diabetes) + } else { + getString(R.string.statin_alert_cvd) + } + } + + override fun hideStatinAlert() { + statinAlertView.visibility = GONE + } + override fun showReassignPatientWarningSheet( patientUuid: UUID, currentFacility: Facility, diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt index 4d7cdd9b5ea..9578ee33df7 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt @@ -18,4 +18,6 @@ interface PatientSummaryScreenUi { fun showClinicalDecisionSupportAlert() fun hideClinicalDecisionSupportAlert() fun hideClinicalDecisionSupportAlertWithoutAnimation() + fun showStatinAlert(statin: StatinModel) + fun hideStatinAlert() } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt index 4047ecf9902..ca89540df07 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt @@ -102,12 +102,12 @@ class PatientSummaryUpdate( model: PatientSummaryModel ): Next { val minAgeForStatin = 40 - val hasCVD = with(event.medicalHistory) { - hasHadStroke == Yes || hasHadHeartAttack == Yes - } - val isPatientEligibleForStatin = (event.age >= minAgeForStatin && - event.medicalHistory.diagnosedWithDiabetes == Yes) || - hasCVD + val hasHadStroke = event.medicalHistory.hasHadStroke == Yes + val hasHadHeartAttack = event.medicalHistory.hasHadHeartAttack == Yes + val hasDiabetes = event.medicalHistory.diagnosedWithDiabetes == Yes + + val hasCVD = hasHadStroke || hasHadHeartAttack + val isPatientEligibleForStatin = (event.age >= minAgeForStatin && hasDiabetes) || hasCVD val hasStatinsPrescribedAlready = event.prescriptions.any { it.name.contains("statin", ignoreCase = true) } val canPrescribeStatin = event.isPatientDead.not() && event.assignedFacility?.facilityType.equals("UHC", ignoreCase = true) && @@ -115,8 +115,14 @@ class PatientSummaryUpdate( hasStatinsPrescribedAlready.not() && isPatientEligibleForStatin - val updatedModel = model.updateStatinPrescriptionStatus( - canPrescribeStatin = canPrescribeStatin + val updatedModel = model.updateStatinInfo( + StatinModel( + canPrescribeStatin = canPrescribeStatin, + age = event.age, + hasDiabetes = hasDiabetes, + hasHadStroke = hasHadStroke, + hasHadHeartAttack = hasHadHeartAttack, + ) ) return next(updatedModel) } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt index 39648f75b38..16f9fa763a7 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt @@ -19,7 +19,7 @@ class PatientSummaryViewRenderer( private val today = LocalDate.now(userClock) override fun render(model: PatientSummaryModel) { - modelUpdateCallback?.invoke(model) + modelUpdateCallback.invoke(model) with(ui) { if (model.hasLoadedPatientSummaryProfile) { @@ -42,10 +42,15 @@ class PatientSummaryViewRenderer( } else { ui.hideClinicalDecisionSupportAlertWithoutAnimation() } + + renderStatinAlert(model) } } private fun renderClinicalDecisionBasedOnAppointment(model: PatientSummaryModel) { + if (model.statin?.canPrescribeStatin == true) + return + if (model.hasScheduledAppointment) { renderClinicalDecisionBasedOnAppointmentOverdue(model) } else { @@ -145,4 +150,15 @@ class PatientSummaryViewRenderer( ui.hideDiabetesView() } } + + private fun renderStatinAlert(model: PatientSummaryModel) { + if (model.hasStatinInfoLoaded.not()) return + + if (model.statin!!.canPrescribeStatin) { + ui.showStatinAlert(model.statin) + ui.hideClinicalDecisionSupportAlertWithoutAnimation() + } else { + ui.hideStatinAlert() + } + } } diff --git a/app/src/main/java/org/simple/clinic/summary/StatinModel.kt b/app/src/main/java/org/simple/clinic/summary/StatinModel.kt new file mode 100644 index 00000000000..4ef2d4dc3de --- /dev/null +++ b/app/src/main/java/org/simple/clinic/summary/StatinModel.kt @@ -0,0 +1,13 @@ +package org.simple.clinic.summary + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class StatinModel( + val canPrescribeStatin: Boolean, + val age: Int, + val hasDiabetes: Boolean, + val hasHadStroke: Boolean, + val hasHadHeartAttack: Boolean, +) : Parcelable diff --git a/app/src/main/res/drawable/background_statin_alert.xml b/app/src/main/res/drawable/background_statin_alert.xml new file mode 100644 index 00000000000..c2b791c084d --- /dev/null +++ b/app/src/main/res/drawable/background_statin_alert.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_statin_alert.xml b/app/src/main/res/drawable/ic_statin_alert.xml new file mode 100644 index 00000000000..0efb0bbb515 --- /dev/null +++ b/app/src/main/res/drawable/ic_statin_alert.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/screen_patient_summary.xml b/app/src/main/res/layout/screen_patient_summary.xml index 0425466143a..1dce37059ac 100644 --- a/app/src/main/res/layout/screen_patient_summary.xml +++ b/app/src/main/res/layout/screen_patient_summary.xml @@ -158,6 +158,10 @@ android:id="@+id/clinicalDecisionSupportBpHighAlert" layout="@layout/view_clinical_decision_support_bp_high" /> + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b76a6fcc22f..be3d4919242 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1070,7 +1070,7 @@ The Simple app contains private health information of patients (“Data”).\n\n Eligible for refills at CC. Transfer patient? Assign to preferred community clinic for future visits. Change - + Add Diabetes diagnosis? This patient is on diabetes medication. @@ -1082,4 +1082,15 @@ The Simple app contains private health information of patients (“Data”).\n\n This patient is on hypertension medication. Yes, has hypertension Not now + + + High risk. Add statin medicine + Patient + is %s with diabetes + and + , has history of heart attack and stroke + has history of heart attack and stroke + has history of heart attack + has history of stroke + diff --git a/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt b/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt index ba4194af1a5..abbb42150ca 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt @@ -2108,7 +2108,15 @@ class PatientSummaryUpdateTest { ), )) .then(assertThatNext( - hasModel(defaultModel.updateStatinPrescriptionStatus(canPrescribeStatin = true)), + hasModel(defaultModel.updateStatinInfo( + StatinModel( + canPrescribeStatin = true, + age = 50, + hasDiabetes = true, + hasHadStroke = false, + hasHadHeartAttack = false, + ) + )), hasNoEffects() )) } diff --git a/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt b/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt index b7e1a08d1b0..f135712a438 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt @@ -1,9 +1,10 @@ package org.simple.clinic.summary +import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions -import org.junit.Test import org.simple.clinic.facility.FacilityConfig import org.simple.clinic.patient.PatientStatus import org.simple.clinic.patient.businessid.Identifier @@ -790,4 +791,130 @@ class PatientSummaryViewRendererTest { verify(ui).hideNextAppointmentCard() verifyNoMoreInteractions(ui) } + + @Test + fun `when statin info is loaded and can prescribe statin then show the statin alert`() { + //given + val statinModel = StatinModel( + canPrescribeStatin = true, + age = 40, + hasDiabetes = true, + hasHadStroke = false, + hasHadHeartAttack = false, + ) + + val model = defaultModel + .currentFacilityLoaded(facilityWithDiabetesManagementDisabled) + .updateStatinInfo(statinModel) + + // when + uiRenderer.render(model) + + // then + verify(ui).hideDiabetesView() + verify(ui).hideTeleconsultButton() + verify(ui).hideNextAppointmentCard() + verify(ui, times(2)).hideClinicalDecisionSupportAlertWithoutAnimation() + verify(ui).showStatinAlert(statinModel) + verifyNoMoreInteractions(ui) + } + + @Test + fun `when statin info is loaded and can not prescribe statin then hide the statin alert`() { + //given + val statinModel = StatinModel( + canPrescribeStatin = false, + age = 40, + hasDiabetes = false, + hasHadStroke = false, + hasHadHeartAttack = false, + ) + + val model = defaultModel + .currentFacilityLoaded(facilityWithDiabetesManagementDisabled) + .updateStatinInfo(statinModel) + + // when + uiRenderer.render(model) + + // then + verify(ui).hideDiabetesView() + verify(ui).hideTeleconsultButton() + verify(ui).hideNextAppointmentCard() + verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() + verify(ui).hideStatinAlert() + verifyNoMoreInteractions(ui) + } + + @Test + fun `when both cdss alert and statin alert can be shown, then show statin alert and hide cdss alert`() { + // given + val patientUuid = UUID.fromString("6274ca08-2432-43fe-ae04-35f623e5325c") + val patient = TestData.patient( + uuid = patientUuid, + status = PatientStatus.Dead, + createdAt = Instant.parse("2017-12-30T00:00:00Z"), + updatedAt = Instant.parse("2017-12-30T00:00:00Z"), + recordedAt = Instant.parse("2017-12-30T00:00:00Z") + ) + val patientAddress = TestData.patientAddress(patient.addressUuid) + val phoneNumber = TestData.patientPhoneNumber(patientUuid = patientUuid) + val bpPassport = TestData.businessId(patientUuid = patientUuid, identifier = Identifier("526 780", Identifier.IdentifierType.BpPassport)) + val bangladeshNationalId = TestData.businessId(patientUuid = patientUuid, identifier = Identifier("123456789012", Identifier.IdentifierType.BangladeshNationalId)) + val facility = TestData.facility(uuid = UUID.fromString("744ac1b1-8352-4793-876c-538fc1129239")) + + val patientSummaryProfile = PatientSummaryProfile( + patient = patient, + address = patientAddress, + phoneNumber = phoneNumber, + bpPassport = bpPassport, + alternativeId = bangladeshNationalId, + facility = facility + ) + + val appointment = TestData.appointment( + uuid = UUID.fromString("fd7d65be-05e4-4ab4-869f-ba9d96f7c556"), + scheduledDate = LocalDate.parse("2018-01-01") + ) + + val statinModel = StatinModel( + canPrescribeStatin = true, + age = 40, + hasDiabetes = true, + hasHadStroke = false, + hasHadHeartAttack = false, + ) + + val updatedModel = defaultModel + .patientRegistrationDataLoaded(hasPatientRegistrationData = true) + .currentFacilityLoaded(facility = facility) + .patientSummaryProfileLoaded(patientSummaryProfile = patientSummaryProfile) + .clinicalDecisionSupportInfoLoaded(isNewestBpEntryHigh = true, hasPrescribedDrugsChangedToday = false) + .scheduledAppointmentLoaded(appointment) + .updateStatinInfo(statinModel) + + + val uiRenderer = PatientSummaryViewRenderer( + ui = ui, + isNextAppointmentFeatureEnabled = false, + modelUpdateCallback = { /* no-op */ }, + userClock = TestUserClock(LocalDate.parse("2018-01-01")), + cdssOverdueLimit = 2 + ) + + // when + uiRenderer.render(updatedModel) + + // then + verify(ui).populatePatientProfile(patientSummaryProfile) + verify(ui).showEditButton() + verify(ui).hideAssignedFacilityView() + verify(ui).showPatientDiedStatus() + verify(ui).hideDiabetesView() + verify(ui).hideTeleconsultButton() + verify(ui).showStatinAlert(statinModel) + verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() + verify(ui).hideNextAppointmentCard() + verifyNoMoreInteractions(ui) + } } diff --git a/common-ui/src/main/res/values/colors.xml b/common-ui/src/main/res/values/colors.xml index 4fd19eb0718..8fd6eb49cc0 100644 --- a/common-ui/src/main/res/values/colors.xml +++ b/common-ui/src/main/res/values/colors.xml @@ -23,9 +23,13 @@ #80FFD6DD #B48E00 + #66E0B000 #FFC800 #FFF8E0 + #874D00 + #CC874D00 + #FFFFFF #B8FFFFFF