From 58b63c3354c6706d4a9ed7243cd63fa0c718ff66 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Wed, 8 Jul 2026 14:54:26 +0530 Subject: [PATCH] Add CVD Risk View to patient summary screen (#5848) --- CHANGELOG.md | 3 + .../java/org/simple/clinic/cvdrisk/CVDRisk.kt | 8 + .../org/simple/clinic/cvdrisk/CVDRiskLevel.kt | 30 +++- .../clinic/cvdrisk/CVDRiskRepository.kt | 4 + .../java/org/simple/clinic/feature/Feature.kt | 1 + .../clinic/medicalhistory/ui/BMIContainer.kt | 3 +- .../clinic/summary/PatientSummaryEffect.kt | 2 + .../summary/PatientSummaryEffectHandler.kt | 26 ++++ .../clinic/summary/PatientSummaryEvent.kt | 6 + .../clinic/summary/PatientSummaryModel.kt | 20 ++- .../clinic/summary/PatientSummaryScreen.kt | 53 +++++-- .../clinic/summary/PatientSummaryScreenUi.kt | 9 +- .../clinic/summary/PatientSummaryUpdate.kt | 30 ++++ .../summary/PatientSummaryViewRenderer.kt | 22 ++- .../{StatinNudgeView.kt => StatinNudge.kt} | 0 .../simple/clinic/summary/ui/CVDRiskInfo.kt | 25 +++ .../simple/clinic/summary/ui/CVDRiskView.kt | 144 ++++++++++++++++++ .../res/layout/screen_patient_summary.xml | 7 +- app/src/main/res/values/strings.xml | 11 ++ .../summary/PatientSummaryScreenLogicTest.kt | 1 + .../summary/PatientSummaryUpdateTest.kt | 23 +++ .../summary/PatientSummaryViewRendererTest.kt | 30 ++-- 22 files changed, 402 insertions(+), 56 deletions(-) rename app/src/main/java/org/simple/clinic/summary/compose/{StatinNudgeView.kt => StatinNudge.kt} (100%) create mode 100644 app/src/main/java/org/simple/clinic/summary/ui/CVDRiskInfo.kt create mode 100644 app/src/main/java/org/simple/clinic/summary/ui/CVDRiskView.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c84332424..226faebd684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Next Release +### Changes +- Add CVD risk view to patient summary screen behind feature flag `show_cvd_risk` + ## 2026.06.25 ### Changes diff --git a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRisk.kt b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRisk.kt index 41471425095..54f9e68d605 100644 --- a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRisk.kt +++ b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRisk.kt @@ -82,6 +82,14 @@ data class CVDRisk( @Query("SELECT uuid FROM CVDRisk WHERE syncStatus = :syncStatus") fun recordIdsWithSyncStatus(syncStatus: SyncStatus): List + @Query(""" + SELECT * FROM CVDRisk + WHERE patientUuid = :patientUuid AND deletedAt IS NULL + ORDER BY updatedAt DESC + LIMIT 1 + """) + fun cvdRisk(patientUuid: UUID): Flowable + @Query(""" SELECT * FROM CVDRisk WHERE patientUuid = :patientUuid AND deletedAt IS NULL diff --git a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt index 09a4809969d..2cfdf4c205c 100644 --- a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt +++ b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt @@ -3,11 +3,31 @@ package org.simple.clinic.cvdrisk import androidx.compose.ui.graphics.Color import org.simple.clinic.R -enum class CVDRiskLevel(val displayStringResId: Int, val color: Color) { - LOW_HIGH(R.string.statin_alert_low_high_risk_patient_x, Color(0xFFFF7A00)), - MEDIUM_HIGH(R.string.statin_alert_medium_high_risk_patient_x, Color(0xFFFF7A00)), - HIGH(R.string.statin_alert_high_risk_patient_x, Color(0xFFFF3355)), - VERY_HIGH(R.string.statin_alert_very_high_risk_range, Color(0xFFFF3355)); +enum class CVDRiskLevel( + val displayStringResId: Int, + val shortLabelResId: Int, + val color: Color +) { + LOW_HIGH( + R.string.statin_alert_low_high_risk_patient_x, + R.string.cvd_risk_low_high, + Color(0xFFFF7A00) + ), + MEDIUM_HIGH( + R.string.statin_alert_medium_high_risk_patient_x, + R.string.cvd_risk_medium_high, + Color(0xFFFF7A00) + ), + HIGH( + R.string.statin_alert_high_risk_patient_x, + R.string.cvd_risk_high, + Color(0xFFFF3355) + ), + VERY_HIGH( + R.string.statin_alert_very_high_risk_range, + R.string.cvd_risk_very_high, + Color(0xFFFF3355) + ); companion object { fun compute(cvdRiskRange: CVDRiskRange): CVDRiskLevel { diff --git a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskRepository.kt b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskRepository.kt index 2e455b824be..0c51234340f 100644 --- a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskRepository.kt +++ b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskRepository.kt @@ -83,6 +83,10 @@ class CVDRiskRepository @Inject constructor( return dao.cvdRiskImmediate(patientUuid) } + fun getCVDRisk(patientUuid: UUID): Observable { + return dao.cvdRisk(patientUuid).toObservable() + } + fun recordsWithSyncStatus(syncStatus: SyncStatus): List { return dao.recordsWithSyncStatus(syncStatus) } diff --git a/app/src/main/java/org/simple/clinic/feature/Feature.kt b/app/src/main/java/org/simple/clinic/feature/Feature.kt index 7b9bf9733a9..3390599eeb9 100644 --- a/app/src/main/java/org/simple/clinic/feature/Feature.kt +++ b/app/src/main/java/org/simple/clinic/feature/Feature.kt @@ -31,4 +31,5 @@ enum class Feature( SortOverdueBasedOnReturnScore(false, "sort_overdue_based_on_return_score"), ShowReturnScoreDebugValues(false, "show_return_score_debug_values"), ShowBMIContainer(false, "show_bmi_container"), + ShowCVDRisk(false, "show_cvd_risk"), } diff --git a/app/src/main/java/org/simple/clinic/medicalhistory/ui/BMIContainer.kt b/app/src/main/java/org/simple/clinic/medicalhistory/ui/BMIContainer.kt index b7406cb97e1..c0da8835101 100644 --- a/app/src/main/java/org/simple/clinic/medicalhistory/ui/BMIContainer.kt +++ b/app/src/main/java/org/simple/clinic/medicalhistory/ui/BMIContainer.kt @@ -3,6 +3,7 @@ package org.simple.clinic.medicalhistory.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.material.Card @@ -31,7 +32,7 @@ fun BMIContainer( onAddOrClick: () -> Unit, ) { val isBmiRecorded = bmiReading?.calculateBMI() != null - Card(modifier = modifier) { + Card(modifier = modifier.fillMaxWidth()) { Column( modifier = Modifier.padding( start = dimensionResource(R.dimen.spacing_16), diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt index ceb9f67fdb5..fe10fd42e15 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt @@ -87,6 +87,8 @@ data class UpdateCVDRisk( data class LoadStatinInfo(val patientUuid: UUID) : PatientSummaryEffect() +data class LoadCVDRiskInfo(val patientUuid: UUID) : PatientSummaryEffect() + data class UpdateTobaccoUse( val patientId: UUID, val isSmoker: MedicalHistoryAnswer, diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt index 0af0badff87..89273771060 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt @@ -116,6 +116,7 @@ class PatientSummaryEffectHandler @AssistedInject constructor( .addTransformer(UpdateCVDRisk::class.java, updateCVDRisk()) .addTransformer(SaveCVDRisk::class.java, saveCVDRisk()) .addTransformer(LoadStatinInfo::class.java, loadStatinInfo()) + .addTransformer(LoadCVDRiskInfo::class.java, loadCVDRiskInfo()) .addConsumer(UpdateTobaccoUse::class.java, { updateTobaccoUse(it.patientId, it.isSmoker, it.isUsingSmokelessTobacco) }, schedulersProvider.io()) .addTransformer(CreateNewBMIEntry::class.java, createNewBMIEntry()) .build() @@ -177,6 +178,31 @@ class PatientSummaryEffectHandler @AssistedInject constructor( } } + private fun loadCVDRiskInfo(): ObservableTransformer { + return ObservableTransformer { effects -> + effects + .observeOn(schedulersProvider.io()) + .switchMap { effect -> + val patientUuid = effect.patientUuid + Observable.combineLatest( + medicalHistoryRepository.historyForPatientOrDefault( + defaultHistoryUuid = uuidGenerator.v4(), + patientUuid = patientUuid + ), + prescriptionRepository.newestPrescriptionsForPatient(patientUuid), + cvdRiskRepository.getCVDRisk(patientUuid) + ) { medicalHistory, prescriptions, cvdRisk -> + + CVDRiskInfoLoaded( + medicalHistory = medicalHistory, + prescriptions = prescriptions, + cvdRiskRange = cvdRisk.riskScore, + ) + } + } + } + } + private fun calculateNonLabBasedCVDRisk(): ObservableTransformer { return ObservableTransformer { effects -> effects diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt index 8b1d291984b..5cd055afb3f 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt @@ -157,6 +157,12 @@ data class StatinPrescriptionCheckInfoLoaded( val wasCVDCalculatedWithin90Days: Boolean, ) : PatientSummaryEvent() +data class CVDRiskInfoLoaded( + val medicalHistory: MedicalHistory, + val prescriptions: List, + val cvdRiskRange: CVDRiskRange?, +) : PatientSummaryEvent() + data class CVDRiskCalculated( val oldRisk: CVDRisk?, val newRiskRange: CVDRiskRange? 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 84ab41de3a7..d8a42568daa 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryModel.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryModel.kt @@ -8,6 +8,7 @@ import org.simple.clinic.overdue.Appointment import org.simple.clinic.patient.PatientStatus import org.simple.clinic.patientattribute.BMIReading import org.simple.clinic.summary.teleconsultation.sync.MedicalOfficer +import org.simple.clinic.summary.ui.CVDRiskInfo import org.simple.clinic.user.User import org.simple.clinic.util.ParcelableOptional import org.simple.clinic.util.parcelable @@ -31,8 +32,9 @@ data class PatientSummaryModel( val hasShownDiagnosisWarningDialog: Boolean, val statinInfo: StatinInfo?, val hasShownTobaccoUseDialog: Boolean, - val showBMIContainer: Boolean, - val bmiReading: BMIReading? + val showBMIView: Boolean, + val bmiReading: BMIReading?, + val cVDRiskInfo: CVDRiskInfo?, ) : Parcelable, PatientSummaryChildModel { companion object { @@ -53,8 +55,9 @@ data class PatientSummaryModel( hasShownDiagnosisWarningDialog = false, statinInfo = null, hasShownTobaccoUseDialog = false, - showBMIContainer = false, - bmiReading = null + showBMIView = false, + bmiReading = null, + cVDRiskInfo = null ) } } @@ -92,6 +95,9 @@ data class PatientSummaryModel( val hasStatinInfoLoaded: Boolean get() = statinInfo != null + val hasCVDRiskInfoLoaded: Boolean + get() = cVDRiskInfo != null + override fun readyToRender(): Boolean { return hasLoadedPatientSummaryProfile && hasLoadedCurrentFacility && hasPatientRegistrationData != null } @@ -145,11 +151,15 @@ data class PatientSummaryModel( } fun bmiVisibilityUpdated(isEnabled: Boolean): PatientSummaryModel { - return copy(showBMIContainer = isEnabled) + return copy(showBMIView = isEnabled) } fun bmiReadingsLoaded(bmiReading: BMIReading?): PatientSummaryModel { return copy(bmiReading = bmiReading) } + fun cvdRiskInfoLoaded(cVDRiskInfo: CVDRiskInfo): PatientSummaryModel { + return copy(cVDRiskInfo = cVDRiskInfo) + } + } 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 75b816d357e..135f6926bb9 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt @@ -10,6 +10,8 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -70,6 +72,8 @@ import org.simple.clinic.summary.clinicaldecisionsupport.ui.ClinicalDecisionHigh import org.simple.clinic.summary.compose.StatinNudge import org.simple.clinic.summary.linkId.LinkIdWithPatientSheet.LinkIdWithPatientSheetKey import org.simple.clinic.summary.teleconsultation.contactdoctor.ContactDoctorSheet +import org.simple.clinic.summary.ui.CVDRiskInfo +import org.simple.clinic.summary.ui.CVDRiskView import org.simple.clinic.summary.ui.PatientSummaryToolbar import org.simple.clinic.summary.updatephone.UpdatePhoneNumberDialog import org.simple.clinic.teleconsultlog.teleconsultrecord.screen.TeleconsultRecordScreenKey @@ -143,8 +147,8 @@ class PatientSummaryScreen : private val facilityNameAndDateTextView get() = binding.facilityNameAndDateTextView - private val bmiContainerComposeView - get() = binding.bmiContainerComposeView + private val bottomComposeView + get() = binding.bottomComposeView private val labelRegistered get() = binding.labelRegistered @@ -202,6 +206,9 @@ class PatientSummaryScreen : private var animateClinicalDecisionSupportAlert by mutableStateOf(false) private var bmiReading by mutableStateOf(null) + private var showBMIView by mutableStateOf(false) + + private var cvdRiskInfo by mutableStateOf(CVDRiskInfo.default()) override fun defaultModel(): PatientSummaryModel { return PatientSummaryModel.from(screenKey.intention, screenKey.patientUuid) @@ -250,6 +257,7 @@ class PatientSummaryScreen : isPatientStatinNudgeV1Enabled = features.isEnabled(Feature.PatientStatinNudge), isNonLabBasedStatinNudgeEnabled = features.isEnabled(Feature.NonLabBasedStatinNudge), isLabBasedStatinNudgeEnabled = features.isEnabled(Feature.LabBasedStatinNudge), + isCVDRiskViewEnabled = features.isEnabled(Feature.ShowCVDRisk), ) } @@ -341,14 +349,27 @@ class PatientSummaryScreen : } } - bmiContainerComposeView.setContent { + bottomComposeView.setContent { SimpleTheme { - BMIContainer( - bmiReading = bmiReading, - onAddOrClick = { - additionalEvents.notify(AddBMIClicked) - } - ) + Column( + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_8)) + ) { + if (showBMIView) { + BMIContainer( + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_8)), + bmiReading = bmiReading, + onAddOrClick = { + additionalEvents.notify(AddBMIClicked) + } + ) + } + if (cvdRiskInfo.canShowCVDRisk) { + CVDRiskView( + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing_8)), + cvdRiskInfo = cvdRiskInfo + ) + } + } } } } @@ -842,17 +863,21 @@ class PatientSummaryScreen : animateClinicalDecisionSupportAlert = false } - override fun updateStatinAlert(statinInfo: StatinInfo) { + override fun updateStatinNudge(statinInfo: StatinInfo) { this.statinInfo = statinInfo } - override fun showBMIContainer(bmiReading: BMIReading?) { - bmiContainerComposeView.visibility = VISIBLE + override fun showBMIView(bmiReading: BMIReading?) { + showBMIView = true this.bmiReading = bmiReading } - override fun hideBMIContainer() { - bmiContainerComposeView.visibility = GONE + override fun hideBMIView() { + showBMIView = false + } + + override fun updateCVDRiskView(cvdRiskInfo: CVDRiskInfo) { + this.cvdRiskInfo = cvdRiskInfo } override fun showReassignPatientWarningSheet( 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 5c5236e8fd4..ec66d6e3441 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreenUi.kt @@ -2,6 +2,7 @@ package org.simple.clinic.summary import org.simple.clinic.cvdrisk.StatinInfo import org.simple.clinic.patientattribute.BMIReading +import org.simple.clinic.summary.ui.CVDRiskInfo interface PatientSummaryScreenUi { fun renderPatientSummaryToolbar(patientSummaryProfile: PatientSummaryProfile) @@ -21,7 +22,9 @@ interface PatientSummaryScreenUi { fun showClinicalDecisionSupportAlert() fun hideClinicalDecisionSupportAlert() fun hideClinicalDecisionSupportAlertWithoutAnimation() - fun updateStatinAlert(statinInfo: StatinInfo) - fun showBMIContainer(bmiReading: BMIReading?) - fun hideBMIContainer() + fun updateStatinNudge(statinInfo: StatinInfo) + fun showBMIView(bmiReading: BMIReading?) + fun hideBMIView() + + fun updateCVDRiskView(cvdRiskInfo: CVDRiskInfo) } 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 a68421f2c8b..38e8965a08a 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt @@ -24,6 +24,7 @@ import org.simple.clinic.summary.OpenIntention.LinkIdWithPatient import org.simple.clinic.summary.OpenIntention.ViewExistingPatient import org.simple.clinic.summary.OpenIntention.ViewExistingPatientWithTeleconsultLog import org.simple.clinic.summary.OpenIntention.ViewNewPatient +import org.simple.clinic.summary.ui.CVDRiskInfo import java.util.UUID import org.simple.clinic.medicalhistory.Answer as MedicalHistoryAnswer @@ -32,6 +33,7 @@ class PatientSummaryUpdate( private val isPatientStatinNudgeV1Enabled: Boolean, private val isNonLabBasedStatinNudgeEnabled: Boolean, private val isLabBasedStatinNudgeEnabled: Boolean, + private val isCVDRiskViewEnabled: Boolean, private val minAgeForStatin: Int = 40, private val maxAgeForCVDRisk: Int = 74, ) : Update { @@ -122,6 +124,7 @@ class PatientSummaryUpdate( } is BMIFeatureLoaded -> next(model.bmiVisibilityUpdated(event.isEnabled)) + is CVDRiskInfoLoaded -> cvdRiskInfoLoaded(event, model) } } @@ -548,6 +551,10 @@ class PatientSummaryUpdate( } } + if (isCVDRiskViewEnabled) { + effects.add(LoadCVDRiskInfo(patientUuid = model.patientUuid)) + } + if (model.openIntention is LinkIdWithPatient && !event.patientSummaryProfile.hasIdentifier(model.openIntention.identifier)) { effects.add(ShowLinkIdWithPatientView(model.patientUuid, model.openIntention.identifier)) @@ -750,4 +757,27 @@ class PatientSummaryUpdate( is ViewExistingPatientWithTeleconsultLog -> GoToHomeScreen } } + + private fun cvdRiskInfoLoaded( + event: CVDRiskInfoLoaded, + model: PatientSummaryModel + ): Next { + val hasHadStroke = event.medicalHistory.hasHadStroke == Yes + val hasHadHeartAttack = event.medicalHistory.hasHadHeartAttack == Yes + val hasDiabetes = event.medicalHistory.diagnosedWithDiabetes == Yes + + val hasCVD = hasHadStroke || hasHadHeartAttack + val areStatinsPrescribedAlready = event.prescriptions.any { it.name.contains("statin", ignoreCase = true) } + val canShowCVDRisk = areStatinsPrescribedAlready && ( + event.cvdRiskRange?.canPrescribeStatin == true || hasCVD || hasDiabetes + ) + + val cvdRiskInfo = CVDRiskInfo( + canShowCVDRisk = canShowCVDRisk, + cvdRisk = event.cvdRiskRange, + hasCVD = hasCVD, + hasDiabetes = hasDiabetes, + ) + return next(model.cvdRiskInfoLoaded(cvdRiskInfo)) + } } 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 7a355e25336..317b6558862 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt @@ -26,7 +26,7 @@ class PatientSummaryViewRenderer( setupUiForAssignedFacility(model) renderPatientDiedStatus(model) renderPatientSummaryToolbar(model.patientSummaryProfile) - renderBMIContainer(model) + renderBMIView(model) } if (model.hasLoadedCurrentFacility) { @@ -43,7 +43,8 @@ class PatientSummaryViewRenderer( ui.hideClinicalDecisionSupportAlertWithoutAnimation() } - renderStatinAlert(model) + renderStatinNudge(model) + renderCVDRiskView(model) } } @@ -151,20 +152,25 @@ class PatientSummaryViewRenderer( } } - private fun renderStatinAlert(model: PatientSummaryModel) { + private fun renderStatinNudge(model: PatientSummaryModel) { if (model.hasStatinInfoLoaded.not()) return - ui.updateStatinAlert(model.statinInfo!!) + ui.updateStatinNudge(model.statinInfo!!) if (model.statinInfo.canShowStatinNudge) { ui.hideClinicalDecisionSupportAlertWithoutAnimation() } } - private fun renderBMIContainer(model: PatientSummaryModel) { - if (model.showBMIContainer) { - ui.showBMIContainer(model.bmiReading) + private fun renderBMIView(model: PatientSummaryModel) { + if (model.showBMIView) { + ui.showBMIView(model.bmiReading) } else { - ui.hideBMIContainer() + ui.hideBMIView() } } + + private fun renderCVDRiskView(model: PatientSummaryModel) { + if (model.hasCVDRiskInfoLoaded.not()) return + ui.updateCVDRiskView(model.cVDRiskInfo!!) + } } diff --git a/app/src/main/java/org/simple/clinic/summary/compose/StatinNudgeView.kt b/app/src/main/java/org/simple/clinic/summary/compose/StatinNudge.kt similarity index 100% rename from app/src/main/java/org/simple/clinic/summary/compose/StatinNudgeView.kt rename to app/src/main/java/org/simple/clinic/summary/compose/StatinNudge.kt diff --git a/app/src/main/java/org/simple/clinic/summary/ui/CVDRiskInfo.kt b/app/src/main/java/org/simple/clinic/summary/ui/CVDRiskInfo.kt new file mode 100644 index 00000000000..7dd587fe2fb --- /dev/null +++ b/app/src/main/java/org/simple/clinic/summary/ui/CVDRiskInfo.kt @@ -0,0 +1,25 @@ +package org.simple.clinic.summary.ui + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.simple.clinic.cvdrisk.CVDRiskRange + +@Parcelize +data class CVDRiskInfo( + val canShowCVDRisk: Boolean, + val cvdRisk: CVDRiskRange?, + val hasCVD: Boolean, + val hasDiabetes: Boolean, +) : Parcelable { + companion object { + fun default(): CVDRiskInfo { + return CVDRiskInfo( + canShowCVDRisk = false, + cvdRisk = null, + hasCVD = false, + hasDiabetes = false + ) + } + } +} + diff --git a/app/src/main/java/org/simple/clinic/summary/ui/CVDRiskView.kt b/app/src/main/java/org/simple/clinic/summary/ui/CVDRiskView.kt new file mode 100644 index 00000000000..386a406e85b --- /dev/null +++ b/app/src/main/java/org/simple/clinic/summary/ui/CVDRiskView.kt @@ -0,0 +1,144 @@ +package org.simple.clinic.summary.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import org.simple.clinic.R +import org.simple.clinic.common.ui.theme.SimpleTheme +import org.simple.clinic.cvdrisk.CVDRiskRange + + +@Composable +fun CVDRiskView( + modifier: Modifier = Modifier, + cvdRiskInfo: CVDRiskInfo +) { + Card(modifier = modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(dimensionResource(R.dimen.spacing_16)), + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing_8)) + ) { + Text( + text = stringResource(R.string.medicalhistorysummaryview_cvd_risk), + style = SimpleTheme.typography.subtitle1Medium, + color = MaterialTheme.colors.onSurface, + ) + val riskRange = cvdRiskInfo.cvdRisk + val secondaryStyle = SpanStyle(color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)) + val riskText = when { + cvdRiskInfo.hasCVD -> + buildAnnotatedString { + withStyle(secondaryStyle) { + append(stringResource(R.string.cvd_risk_patient_has_cvd)) + } + } + + cvdRiskInfo.hasDiabetes -> + buildAnnotatedString { + withStyle(secondaryStyle) { + append(stringResource(R.string.cvd_risk_patient_has_diabetes)) + } + } + + riskRange != null && riskRange.min == riskRange.max -> + buildAnnotatedString { + withStyle(SpanStyle(color = riskRange.level.color)) { + append(stringResource(riskRange.level.shortLabelResId)) + } + + withStyle(secondaryStyle) { + append(" ${riskRange.min}%") + } + } + + else -> + buildAnnotatedString { + withStyle(secondaryStyle) { + append(stringResource(R.string.cvd_risk_add_smoking_and_cholesterol_info)) + } + } + } + Text( + modifier = Modifier + .offset(y = (-dimensionResource(R.dimen.spacing_4))) + .padding(end = dimensionResource(R.dimen.spacing_16)), + text = riskText, + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.onSurface, + ) + } + } +} + +@Preview +@Composable +private fun CVDRiskViewPreview() { + SimpleTheme { + CVDRiskView( + cvdRiskInfo = CVDRiskInfo( + canShowCVDRisk = true, + cvdRisk = CVDRiskRange(min = 10, max = 10), + hasCVD = false, + hasDiabetes = false + ) + ) + } +} + +@Preview +@Composable +private fun CVDRiskDiabetesViewPreview() { + SimpleTheme { + CVDRiskView( + cvdRiskInfo = CVDRiskInfo( + canShowCVDRisk = true, + cvdRisk = CVDRiskRange(min = 10, max = 10), + hasCVD = false, + hasDiabetes = true + ) + ) + } +} + +@Preview +@Composable +private fun CVDRiskHeartDiseaseViewPreview() { + SimpleTheme { + CVDRiskView( + cvdRiskInfo = CVDRiskInfo( + canShowCVDRisk = true, + cvdRisk = CVDRiskRange(min = 10, max = 10), + hasCVD = true, + hasDiabetes = true + ) + ) + } +} + +@Preview +@Composable +private fun CVDRiskAddDataViewPreview() { + SimpleTheme { + CVDRiskView( + cvdRiskInfo = CVDRiskInfo( + canShowCVDRisk = true, + cvdRisk = CVDRiskRange(min = 4, max = 14), + hasCVD = false, + hasDiabetes = false + ) + ) + } +} diff --git a/app/src/main/res/layout/screen_patient_summary.xml b/app/src/main/res/layout/screen_patient_summary.xml index f92b78ed903..8895dfdc019 100644 --- a/app/src/main/res/layout/screen_patient_summary.xml +++ b/app/src/main/res/layout/screen_patient_summary.xml @@ -97,12 +97,9 @@ android:layout_height="wrap_content" /> + android:layout_height="wrap_content"/> Patient has diabetes? Tobacco use BMI + Cardiovascular risk No medicines added @@ -1135,4 +1136,14 @@ The Simple app contains private health information of patients (“Data”).\n\n Cholesterol (mg/dL) Cholesterol cannot be more than 1000 Cholesterol cannot be less than 25 + + + Add smoking status and BMI for risk score. + Patient has diabetes + History of heart disease + Low–High + Medium–High + High + Very High + diff --git a/app/src/test/java/org/simple/clinic/summary/PatientSummaryScreenLogicTest.kt b/app/src/test/java/org/simple/clinic/summary/PatientSummaryScreenLogicTest.kt index e86240e5e73..79f92d58c46 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryScreenLogicTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryScreenLogicTest.kt @@ -250,6 +250,7 @@ class PatientSummaryScreenLogicTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false ), effectHandler = effectHandler.build(), modelUpdateListener = viewRenderer::render 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 6e7437bceb0..3ab38b6c55e 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt @@ -90,6 +90,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) @Test @@ -116,6 +117,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) updateSpec @@ -134,6 +136,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) updateSpec @@ -152,6 +155,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) updateSpec @@ -170,6 +174,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) updateSpec @@ -1859,6 +1864,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false, )) val model = defaultModel .currentFacilityLoaded(facility) @@ -1889,6 +1895,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) val model = defaultModel .currentFacilityLoaded(facility) @@ -1919,6 +1926,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) val model = defaultModel .currentFacilityLoaded(facility) @@ -2316,6 +2324,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) updateSpec .given(defaultModel) @@ -2349,6 +2358,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = false, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) updateSpec .given(defaultModel) @@ -2382,6 +2392,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) updateSpec .given(defaultModel) @@ -2415,6 +2426,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) @@ -2451,6 +2463,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) @@ -2570,6 +2583,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = false, + isCVDRiskViewEnabled = false, )) updateSpec @@ -2610,6 +2624,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = true, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false, )) updateSpec @@ -2800,6 +2815,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) @@ -2835,6 +2851,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) @@ -2870,6 +2887,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) updateSpec .given(defaultModel) @@ -2903,6 +2921,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) @@ -2939,6 +2958,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) @@ -2975,6 +2995,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) @@ -3010,6 +3031,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) updateSpec @@ -3028,6 +3050,7 @@ class PatientSummaryUpdateTest { isPatientStatinNudgeV1Enabled = true, isNonLabBasedStatinNudgeEnabled = false, isLabBasedStatinNudgeEnabled = true, + isCVDRiskViewEnabled = false )) val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) 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 17eb20a5bd0..dd7c4941bb2 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt @@ -137,7 +137,7 @@ class PatientSummaryViewRendererTest { verify(ui).hidePatientDiedStatus() verify(ui).hideNextAppointmentCard() verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -236,7 +236,7 @@ class PatientSummaryViewRendererTest { verify(ui).hidePatientDiedStatus() verify(ui).hideNextAppointmentCard() verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -277,7 +277,7 @@ class PatientSummaryViewRendererTest { verify(ui).hidePatientDiedStatus() verify(ui).hideNextAppointmentCard() verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -340,7 +340,7 @@ class PatientSummaryViewRendererTest { verify(ui).hidePatientDiedStatus() verify(ui).hideNextAppointmentCard() verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -379,7 +379,7 @@ class PatientSummaryViewRendererTest { verify(ui).showPatientDiedStatus() verify(ui).hideNextAppointmentCard() verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -473,7 +473,7 @@ class PatientSummaryViewRendererTest { verify(ui).hideTeleconsultButton() verify(ui).showClinicalDecisionSupportAlert() verify(ui).showNextAppointmentCard() - verify(ui).showBMIContainer(bmiReading) + verify(ui).showBMIView(bmiReading) verifyNoMoreInteractions(ui) } @@ -528,7 +528,7 @@ class PatientSummaryViewRendererTest { verify(ui).hideTeleconsultButton() verify(ui).hideClinicalDecisionSupportAlert() verify(ui).showNextAppointmentCard() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -583,7 +583,7 @@ class PatientSummaryViewRendererTest { verify(ui).hideTeleconsultButton() verify(ui).hideClinicalDecisionSupportAlert() verify(ui).showNextAppointmentCard() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -653,7 +653,7 @@ class PatientSummaryViewRendererTest { verify(ui).hideDiabetesView() verify(ui).hideTeleconsultButton() verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -714,7 +714,7 @@ class PatientSummaryViewRendererTest { verify(ui).hideTeleconsultButton() verify(ui).showClinicalDecisionSupportAlert() verify(ui).showNextAppointmentCard() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @@ -775,12 +775,12 @@ class PatientSummaryViewRendererTest { verify(ui).hideTeleconsultButton() verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() verify(ui).showNextAppointmentCard() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } @Test - fun `when statin info is loaded then update the statin alert`() { + fun `when statin info is loaded then update the statin nudge`() { //given val statinInfo = StatinInfo(canShowStatinNudge = true) val model = defaultModel @@ -795,7 +795,7 @@ class PatientSummaryViewRendererTest { verify(ui).hideTeleconsultButton() verify(ui).hideNextAppointmentCard() verify(ui, times(2)).hideClinicalDecisionSupportAlertWithoutAnimation() - verify(ui).updateStatinAlert(statinInfo) + verify(ui).updateStatinNudge(statinInfo) verifyNoMoreInteractions(ui) } @@ -855,10 +855,10 @@ class PatientSummaryViewRendererTest { verify(ui).showPatientDiedStatus() verify(ui).hideDiabetesView() verify(ui).hideTeleconsultButton() - verify(ui).updateStatinAlert(StatinInfo(canShowStatinNudge = true)) + verify(ui).updateStatinNudge(StatinInfo(canShowStatinNudge = true)) verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() verify(ui).showNextAppointmentCard() - verify(ui).hideBMIContainer() + verify(ui).hideBMIView() verifyNoMoreInteractions(ui) } }