Skip to content

Conversation

@Gyan-Gupta-Rtsl
Copy link
Contributor

@Gyan-Gupta-Rtsl Gyan-Gupta-Rtsl commented Jan 15, 2026

Story card: sc-17708

Because

The DM dashboard "DM patients with controlled BP" card was using dummy data and needed backend support to display real metrics.

This addresses

  1. Adds backend methods in RegionSummarySchema to calculate DM patients with controlled BP (<140/90 and <130/80)
  2. Wires DmBpControlledComponent to use real data from RepositoryPresenter
  3. Adds specs for the new metrics

Test instructions

Suit test cases.

@Gyan-Gupta-Rtsl Gyan-Gupta-Rtsl changed the base branch from master to dm-dashboard-new-controlled-bp-statin-cards January 15, 2026 03:25
@Gyan-Gupta-Rtsl Gyan-Gupta-Rtsl changed the title Sc 17708 Draft: Sc 17708 Jan 15, 2026
@Gyan-Gupta-Rtsl Gyan-Gupta-Rtsl changed the title Draft: Sc 17708 Sc 17708 Jan 16, 2026
@Gyan-Gupta-Rtsl Gyan-Gupta-Rtsl requested a review from a team January 19, 2026 07:22
}
end

def dm_controlled_bp_query(entry, systolic_threshold:, diastolic_threshold:, with_ltfu: false)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this whole calculation be persisted under reporting_facility_states, as this is going to be our standard dashboard design going forward?
The numbers can then be used for metabase reports as well.

version: 12,
revert_to_version: 11,
materialized: true
end
Copy link
Contributor

@aagrawalrtsl aagrawalrtsl Jan 20, 2026

Choose a reason for hiding this comment

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

Changing the matview definition like this is efficient but it creates them with data. Currently in BD this takes 1.14 hours to refresh this data.
India it takes 3.17 hours
Semaphore will timeout by then.
We need to drop the mat view and recreate it without data. Then do a refresh of this later after deployment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care') AS diabetes_patients_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'lost_to_follow_up') AS diabetes_patients_lost_to_follow_up,

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_140_90_under_care,
Copy link
Contributor

Choose a reason for hiding this comment

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

can we rename dm_bp_140_90_under_care to dm_bp_below_140_90_under_care ? This makes it more clear on what data the field is holding.

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'lost_to_follow_up') AS diabetes_patients_lost_to_follow_up,

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_140_90_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state IN ('under_care', 'lost_to_follow_up') AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_140_90_ltfu_under_care,
Copy link
Contributor

Choose a reason for hiding this comment

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

might not be required.


COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_140_90_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state IN ('under_care', 'lost_to_follow_up') AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_140_90_ltfu_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 130 AND diastolic < 80 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_130_80_under_care,
Copy link
Contributor

Choose a reason for hiding this comment

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

dm_bp_130_80_under_care -> dm_bp_below_130_80_under_care

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_140_90_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state IN ('under_care', 'lost_to_follow_up') AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_140_90_ltfu_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 130 AND diastolic < 80 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_130_80_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state IN ('under_care', 'lost_to_follow_up') AND months_since_visit < 3 AND systolic < 130 AND diastolic < 80 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_130_80_ltfu_under_care
Copy link
Contributor

Choose a reason for hiding this comment

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

might not be required

region_period_cached_query(__method__, with_ltfu: with_ltfu) do |entry|
slug, period = entry.slug, entry.period
denominator = with_ltfu ? adjusted_diabetes_patients_with_ltfu[slug][period] : adjusted_diabetes_patients_without_ltfu[slug][period]
numerator = dm_patients_with_controlled_bp_140_90(with_ltfu: with_ltfu)[slug][period]
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to calculate with_ltfu here?
In all the other charts, "with_ltfu" is used to include the lost to follow up patients in the denominator only.
Adding this in the numerator seems counter-intuitive. Patients who are ltfu - will never qualify BP controlled indicator because they should have atleast one visit in the past 3 months - which implies they are not ltfu.
We only calculate the rates based on a different ltfu denominator, because then the percentages would change.

Copy link
Contributor

@jamiecarter7 jamiecarter7 Jan 20, 2026

Choose a reason for hiding this comment

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

We should include the LTFU toggle option here as it's a denominator change.
With LTFU shows all patients assigned, without showing patients under care, which is more accurate.
This pattern follows the design for the outcomes section of the dashboard (Controlled, uncontrolled, missed), where only the denominator values change aswell.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, only the denominator should have the ltfu change, the numerator remains same in both case (with or without ltfu).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I will remove this from the numerator.

region_period_cached_query(__method__, with_ltfu: with_ltfu) do |entry|
slug, period = entry.slug, entry.period
denominator = with_ltfu ? adjusted_diabetes_patients_with_ltfu[slug][period] : adjusted_diabetes_patients_without_ltfu[slug][period]
numerator = dm_patients_with_controlled_bp_130_80(with_ltfu: with_ltfu)[slug][period]
Copy link
Contributor

Choose a reason for hiding this comment

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

same as above

else
values_at("adjusted_dm_bp_140_90_under_care")
end
end
Copy link
Contributor

@aagrawalrtsl aagrawalrtsl Jan 20, 2026

Choose a reason for hiding this comment

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

just a suggestion to use tertiary operator, reads better
field = with_ltfu ? "adjusted_dm_bp_140_90_ltfu_under_care" : "adjusted_dm_bp_140_90_under_care" values_at(field)

adjusted_fasting_bs_over_300_under_care
adjusted_hba1c_bs_over_300_under_care
adjusted_dm_bp_below_130_80_under_care
adjusted_dm_bp_below_130_80_ltfu_under_care
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably don't need this. A patient can either be under_care or ltfu.
We anyways don't need this value because this value will be used as a numerator for this chart, which doesn't include ltfu patients.

adjusted_dm_bp_below_130_80_ltfu_under_care
adjusted_dm_bp_below_140_90_under_care
adjusted_dm_bp_below_140_90_ltfu_under_care
diabetes_total_appts_scheduled
Copy link
Contributor

Choose a reason for hiding this comment

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

same as above

diabetes_missed_visits_rates
visited_without_bs_taken_rates
dm_controlled_bp_140_90_rates
dm_controlled_bp_130_80_rates
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably need dm_controlled_bp_140_90_with_ltfu_rates and dm_controlled_bp_130_80_with_ltfu_rates here

db/structure.sql Outdated
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE (reporting_patient_states.htn_care_state = 'lost_to_follow_up'::text)) AS diabetes_patients_lost_to_follow_up
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE (reporting_patient_states.htn_care_state = 'lost_to_follow_up'::text)) AS diabetes_patients_lost_to_follow_up,
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE ((reporting_patient_states.htn_care_state = 'under_care'::text) AND (reporting_patient_states.months_since_visit < (3)::double precision) AND (reporting_patient_states.systolic < (140)::double precision) AND (reporting_patient_states.diastolic < (90)::double precision) AND (reporting_patient_states.systolic IS NOT NULL) AND (reporting_patient_states.diastolic IS NOT NULL))) AS dm_bp_below_140_90_under_care,
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE ((reporting_patient_states.htn_care_state IN ('under_care'::text, 'lost_to_follow_up'::text)) AND (reporting_patient_states.months_since_visit < (3)::double precision) AND (reporting_patient_states.systolic < (140)::double precision) AND (reporting_patient_states.diastolic < (90)::double precision) AND (reporting_patient_states.systolic IS NOT NULL) AND (reporting_patient_states.diastolic IS NOT NULL))) AS dm_bp_below_140_90_ltfu_under_care,
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need this value.

db/structure.sql Outdated
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE ((reporting_patient_states.htn_care_state = 'under_care'::text) AND (reporting_patient_states.months_since_visit < (3)::double precision) AND (reporting_patient_states.systolic < (140)::double precision) AND (reporting_patient_states.diastolic < (90)::double precision) AND (reporting_patient_states.systolic IS NOT NULL) AND (reporting_patient_states.diastolic IS NOT NULL))) AS dm_bp_below_140_90_under_care,
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE ((reporting_patient_states.htn_care_state IN ('under_care'::text, 'lost_to_follow_up'::text)) AND (reporting_patient_states.months_since_visit < (3)::double precision) AND (reporting_patient_states.systolic < (140)::double precision) AND (reporting_patient_states.diastolic < (90)::double precision) AND (reporting_patient_states.systolic IS NOT NULL) AND (reporting_patient_states.diastolic IS NOT NULL))) AS dm_bp_below_140_90_ltfu_under_care,
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE ((reporting_patient_states.htn_care_state = 'under_care'::text) AND (reporting_patient_states.months_since_visit < (3)::double precision) AND (reporting_patient_states.systolic < (130)::double precision) AND (reporting_patient_states.diastolic < (80)::double precision) AND (reporting_patient_states.systolic IS NOT NULL) AND (reporting_patient_states.diastolic IS NOT NULL))) AS dm_bp_below_130_80_under_care,
count(DISTINCT reporting_patient_states.patient_id) FILTER (WHERE ((reporting_patient_states.htn_care_state IN ('under_care'::text, 'lost_to_follow_up'::text)) AND (reporting_patient_states.months_since_visit < (3)::double precision) AND (reporting_patient_states.systolic < (130)::double precision) AND (reporting_patient_states.diastolic < (80)::double precision) AND (reporting_patient_states.systolic IS NOT NULL) AND (reporting_patient_states.diastolic IS NOT NULL))) AS dm_bp_below_130_80_ltfu_under_care
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need this value.

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'lost_to_follow_up') AS diabetes_patients_lost_to_follow_up,

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_below_140_90_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state IN ('under_care', 'lost_to_follow_up') AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_below_140_90_ltfu_under_care,
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need this value.

COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_below_140_90_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state IN ('under_care', 'lost_to_follow_up') AND months_since_visit < 3 AND systolic < 140 AND diastolic < 90 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_below_140_90_ltfu_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state = 'under_care' AND months_since_visit < 3 AND systolic < 130 AND diastolic < 80 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_below_130_80_under_care,
COUNT(distinct(patient_id)) FILTER (WHERE htn_care_state IN ('under_care', 'lost_to_follow_up') AND months_since_visit < 3 AND systolic < 130 AND diastolic < 80 AND systolic IS NOT NULL AND diastolic IS NOT NULL) AS dm_bp_below_130_80_ltfu_under_care
Copy link
Contributor

Choose a reason for hiding this comment

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

not needed

adjusted_diabetes_outcomes.diabetes_patients_lost_to_follow_up AS adjusted_diabetes_patients_lost_to_follow_up,

adjusted_diabetes_outcomes.dm_bp_below_140_90_under_care AS adjusted_dm_bp_below_140_90_under_care,
adjusted_diabetes_outcomes.dm_bp_below_140_90_ltfu_under_care AS adjusted_dm_bp_below_140_90_ltfu_under_care,
Copy link
Contributor

Choose a reason for hiding this comment

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

not needed

adjusted_diabetes_outcomes.dm_bp_below_140_90_under_care AS adjusted_dm_bp_below_140_90_under_care,
adjusted_diabetes_outcomes.dm_bp_below_140_90_ltfu_under_care AS adjusted_dm_bp_below_140_90_ltfu_under_care,
adjusted_diabetes_outcomes.dm_bp_below_130_80_under_care AS adjusted_dm_bp_below_130_80_under_care,
adjusted_diabetes_outcomes.dm_bp_below_130_80_ltfu_under_care AS adjusted_dm_bp_below_130_80_ltfu_under_care,
Copy link
Contributor

Choose a reason for hiding this comment

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

not needed

expect(schema.dm_controlled_bp_130_80_rates[facility_1.region.slug]["Mar 2020".to_period]).to eq(25)
expect(schema.dm_controlled_bp_130_80_rates(with_ltfu: true)[facility_1.region.slug]["Mar 2020".to_period]).to eq(25)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

would be nice to have specs around 140_90_with_ltfu_rates and 130_80_with_ltfu_rates

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay


def dm_controlled_bp_130_80_with_ltfu_rates
dm_controlled_bp_130_80_rates(with_ltfu: true)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need these methods. My understanding is that we can get ltfu rates by just calling dm_controlled_bp_130_80_rates(with_ltfu: true)
I could not find where dm_controlled_bp_140_90_with_ltfu_rates and dm_controlled_bp_130_80_with_ltfu_rates are called

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing this out. These are no longer needed, I missed removing them earlier. I will clean this up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants