malawi_hiv_program_reports 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/malawi_hiv_program_reports/README.md +16 -0
  3. data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
  4. data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
  5. data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
  6. data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
  7. data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
  8. data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
  9. data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
  10. data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
  11. data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
  12. data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
  13. data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
  14. data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
  15. data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
  16. data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
  17. data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
  18. data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
  19. data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
  20. data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
  21. data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
  22. data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
  23. data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
  24. data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
  25. data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
  26. data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
  27. data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
  28. data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
  29. data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
  30. data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
  31. data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
  32. data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
  33. data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
  34. data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
  35. data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
  36. data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
  37. data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
  38. data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
  39. data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
  40. data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
  41. data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
  42. data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
  43. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
  44. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
  45. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
  46. data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
  47. data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
  48. data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
  49. data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
  50. data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
  51. data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
  52. data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
  53. data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
  54. data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
  55. data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
  56. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
  57. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
  58. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
  59. data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
  60. data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
  61. data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
  62. data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
  63. data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
  64. data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
  65. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
  66. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
  67. data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
  68. data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
  69. data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
  70. data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
  71. data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
  72. data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
  73. data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
  74. data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
  75. data/lib/malawi_hiv_program_reports/version.rb +1 -1
  76. metadata +74 -1
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module MalawiHivProgramReports
6
+ module Moh
7
+ # This class is used to add additional cohort disaggregated data
8
+ # rubocop:disable Metrics/ClassLength
9
+ class CohortStruct
10
+ FIELD_DESCRIPTIONS = {
11
+ # Table contains various fields of the Cohort report and their descriptions.
12
+ # A fields description is simply "Indicator: Human Readable Name",
13
+ # or "Human Readable Name". The former is for fields with an indicator
14
+ # label and the latter is for fields without.
15
+ # Examples:
16
+ # ...,
17
+ # six_a: "KS: Kaposis Sarcoma",
18
+ # regimen_2p: "Regimen 2P",
19
+ # ...
20
+ total_other_patients: 'All others (not circled)',
21
+ patients_with_7_plus_doses_missed_at_their_last_visit: 'Adherence: 4+ Doses',
22
+ unknown_age: 'Unknown age',
23
+ cum_unknown_age: 'Unknown Age (Cumulative)',
24
+ quarterly_unknown_age: 'Unknown Age (Quarterly)',
25
+ died_total: 'Died: Died Total',
26
+ died_within_the_1st_month_of_art_initiation: 'M1: Died within the 1st month after ART initiation',
27
+ died_within_the_2nd_month_of_art_initiation: 'M2: Died within the 2nd month of art initiation',
28
+ died_within_the_3rd_month_of_art_initiation: 'M3: Died within the 3rd month after ART initiation',
29
+ died_after_the_3rd_month_of_art_initiation: 'M4+: Died within the 3rd month after ART initiation',
30
+ no_tb: 'Nev/>2 Years: Never TB or TB over 2 years ago',
31
+ cum_no_tb: 'Nev/>2 Years: Never TB or TB over 2 years ago (Cumulative)',
32
+ quarterly_no_tb: 'Nev/>2 Years: Never TB or TB over 2 years ago (Quarterly)',
33
+ who_stage_two: 'CD4: CD4 below threshold',
34
+ cum_who_stage_two: 'CD4: CD4 below threshold (Cumulative)',
35
+ quarterly_who_stage_two: 'CD4: CD4 below threshold (Quarterly)',
36
+ asymptomatic: 'Asy: Asymptomatic / mild',
37
+ cum_asymptomatic: 'Asy: Asymptomatic / mild (Cumulative)',
38
+ quarterly_asymptomatic: 'Asy: Asymptomatic / mild (Quarterly)',
39
+ pregnant_women: 'Preg: Pregnant women',
40
+ cum_pregnant_women: 'Preg: Pregnant Women (Cumulative)',
41
+ quarterly_pregnant_women: 'Preg: Pregnant Women (Quarterly)',
42
+ defaulted: 'Def: Defaulted (more than 2 months overdue after expected to have run out of ARVs',
43
+ tb_within_the_last_two_years: 'Last 2 Years: TB within the last 2 years',
44
+ cum_tb_within_the_last_two_years: 'Last 2 Years: TB within the last 2 years (Cumulative)',
45
+ quarterly_tb_within_the_last_two_years: 'Last 2 Years: TB within the last 2 years (Quarterly)',
46
+ total_patients_without_side_effects: 'Side Effects (as of the last visit before end of quarter)',
47
+ current_episode_of_tb: 'Curr: Current episode of TB',
48
+ cum_current_episode_of_tb: 'Curr: Current episode of TB (Cumulative)',
49
+ quarterly_current_episode_of_tb: 'Curr: Current episode of TB (Quarterly)',
50
+ re_initiated_on_art: 'Re: Patients re-initiated on ART',
51
+ cum_re_initiated_on_art: 'Re: Patients re-initiated on ART (Cumulative)',
52
+ quarterly_re_initiated_on_art: 'Re: Patients re-initiated on ART (Quarterly)',
53
+ zero_p: 'Regimen 0P',
54
+ zero_a: 'Regimen 0 A',
55
+ two_p: 'Regimen 2P',
56
+ two_a: 'Regimen 2 A',
57
+ four_pp: ' Regimen 4PP',
58
+ four_pa: ' Regimen 4PA',
59
+ four_a: 'Regimen 4 A',
60
+ five_a: 'Regimen 5 A',
61
+ six_a: 'Regimen 6 A',
62
+ seven_a: 'Regimen 7 A',
63
+ eight_a: 'Regimen 8 A',
64
+ nine_pp: 'Regimen 9PP',
65
+ nine_pa: 'Regimen 9PA',
66
+ nine_a: 'Regimen 9 A',
67
+ ten_a: 'Regimen 10 A',
68
+ eleven_pp: 'Regimen 11PP',
69
+ eleven_pa: 'Regimen 11PA',
70
+ eleven_a: 'Regimen 11 A',
71
+ twelve_pp: 'Regimen 12PP',
72
+ twelve_pa: 'Regimen 12PA',
73
+ twelve_a: 'Regimen 12 A',
74
+ thirteen_a: 'Regimen 13 A',
75
+ fourteen_pp: 'Regimen 14PP',
76
+ fourteen_pa: 'Regimen 14PA',
77
+ fourteen_a: 'Regimen 14 A',
78
+ fifteen_pp: 'Regimen 15PP',
79
+ fifteen_pa: 'Regimen 15PA',
80
+ fifteen_a: 'Regimen 15 A',
81
+ sixteen_p: 'Regimen 16P',
82
+ sixteen_a: 'Regimen 16 A',
83
+ seventeen_pa: 'Regimen 17PA',
84
+ seventeen_pp: 'Regimen 17PP',
85
+ seventeen_a: 'Regimen 17 A',
86
+ total_patients_with_side_effects: 'Any side effects',
87
+ total_patients_on_family_planning: 'PIFP: Apprx. % of women who received Depo at ART in the last quarter',
88
+ total_pregnant_women: 'Pregnant/BreastFeeding as of the last visit before end of quarter',
89
+ transfered_out: 'TO: Transferred Out',
90
+ children_12_59_months: 'U5: Children 12 - 59 months',
91
+ cum_children_12_59_months: 'U5: Children 12 - 59 months (Cumulative)',
92
+ quarterly_children_12_59_months: 'U5: Children 12 - 59 months (Quarterly)',
93
+ tb_not_suspected: 'Current TB status any form of TB',
94
+ tb_confirmed_on_tb_treatment: 'TB confirmed, on TB Treatment',
95
+ tb_confirmed_currently_not_yet_on_tb_treatment: 'TB confirmed, not on TB Treatment',
96
+ breastfeeding_mothers: 'BF: Breastfeeding mothers',
97
+ cum_breastfeeding_mothers: 'BF: Breastfeeding mothers (Cumulative)',
98
+ quarterly_breastfeeding_mothers: 'BF: Breastfeeding mothers (Quarterly)',
99
+ patients_with_0_6_doses_missed_at_their_last_visit: 'Adnerence: as of the last visit before end of quarter',
100
+ total_patients_on_arvs_and_ipt: 'IPT: Apprx. % of patients retained in <b>ART</b> who are currently on IPT',
101
+ total_breastfeeding_women: 'Total Breastfeeding Women',
102
+ total_alive_and_on_art: ' Total alive and on ART',
103
+ kaposis_sarcoma: "KS: Kaposi's Sarcoma",
104
+ cum_kaposis_sarcoma: "KS: Kaposi's Sarcoma (Cumulative)",
105
+ quarterly_kaposis_sarcoma: "KS: Kaposi's Sarcoma (Quarterly)",
106
+ unknown_outcome: 'Patient status is unknown',
107
+ unknown_regimen: "Specify above regimens counted as 'Other' Other (paed. / adult)",
108
+ total_patients_with_screened_bp: 'BP screen: Apprx. % of adult ART patients with BP recorded at least once this year',
109
+ cum_total_registered: 'Total Registered (Cummulative)',
110
+ quarterly_total_registered: 'Total Registered (Quarterly)',
111
+ transfer_in: 'TI: Patients transferred in on ART',
112
+ cum_transfer_in: 'TI: Patients transferred in on ART (Cumulative)',
113
+ quarterly_transfer_in: 'TI: Patients transferred in on ART (Quarterly)',
114
+ confirmed_hiv_infection_in_infants_pcr: 'PCR: Infants < 12 months PCR+',
115
+ cum_confirmed_hiv_infection_in_infants_pcr: 'PCR: Infants < 12 months PCR+ (Cumulative)',
116
+ quarterly_confirmed_hiv_infection_in_infants_pcr: 'PCR: Infants < 12 months PCR+ (Quarterly)',
117
+ who_stage_four: '4: WHO stage 4',
118
+ cum_who_stage_four: '4: WHO stage 4 (Cumulative)',
119
+ quarterly_who_stage_four: '4: WHO stage 4 (Quarterly)',
120
+ non_pregnant_females: 'FNP: Non-pregnant females all ages',
121
+ cum_non_pregnant_females: 'FNP: Non-pregnant females all ages (Cumulative)',
122
+ quarterly_non_pregnant_females: 'FNP: Non-pregnant females all ages (Quarterly)',
123
+ unknown_tb_status: 'Unknown (not circled)',
124
+ total_patients_on_arvs_and_cpt: 'CPT: Apprx. % of patients retained in <b>ART</b> who are currently on CPT',
125
+ tb_suspected: 'TB Suspected',
126
+ unknown_side_effects: 'Unkown (not circled)',
127
+ total_registered: 'Total Registered',
128
+ pregnant_females_all_ages: 'Female pregnant_females all ages',
129
+ cum_pregnant_females_all_ages: 'Female pregnant patients all ages (Cumulative)',
130
+ quarterly_pregnant_females_all_ages: 'Cumulative female pregnant patients (Quarterly)',
131
+ patients_with_unknown_adhrence: 'Unknown (not circled)',
132
+ who_stage_three: '3: WHO stage 3',
133
+ cum_who_stage_three: '3: WHO stage 3 (Cumulative)',
134
+ quarterly_who_stage_three: '3: WHO stage 3 (Quarterly)',
135
+ unknown_other_reason_outside_guidelines: 'Unk: Unknown / reason outside guidelines',
136
+ cum_unknown_other_reason_outside_guidelines: 'Unk: Unknown / reason outside guidelines (Cumulative)',
137
+ quarterly_unknown_other_reason_outside_guidelines: 'Unk: Unknown / reason outside guidelines (Quarterly)',
138
+ initiated_on_art_first_time: 'FT: Patients initiated on ART first time',
139
+ cum_initiated_on_art_first_time: 'FT: Patients initiated on ART first time (Cummulative)',
140
+ quarterly_initiated_on_art_first_time: 'FT: Patients initiated on ART first time (Quarterly)',
141
+ presumed_severe_hiv_disease_in_infants: 'PSHD: Pres. Sev. HIV disease age < 12 months',
142
+ cum_presumed_severe_hiv_disease_in_infants: 'PSHD: Pres. Sev. HIV disease age < 12 months (Cumulative)',
143
+ quarterly_presumed_severe_hiv_disease_in_infants: 'PSHD: Pres. Sev. HIV disease age < 12 months (Quarterly)',
144
+ all_males: 'M: Males all ages',
145
+ cum_all_males: 'M: Males all ages (Cumulative)',
146
+ quarterly_all_males: 'M: Males all ages (Quarterly)',
147
+ stopped_art: 'Stop: Stopped taking ARVs (clinician or patient own decision, last known alive)',
148
+ children_below_24_months_at_art_initiation: 'A: Children below 24 months at ART initiation',
149
+ cum_children_below_24_months_at_art_initiation: 'A: Children below 24 months at ART initiation (Cumulative)',
150
+ quarterly_children_below_24_months_at_art_initiation: 'A: Children below 24 m at ART initiation (Quarterly)',
151
+ children_24_months_14_years_at_art_initiation: 'B: Children 24 months - 14 years at ART initiation',
152
+ cum_children_24_months_14_years_at_art_initiation: 'B: Children 24 months - 14 years at ART initiation (Cumulative)',
153
+ quarterly_children_24_months_14_years_at_art_initiation: 'B: Children 24 months - 14 years at ART initiation (Quarterly)',
154
+ adults_at_art_initiation: 'C: Adults 15 years or older at ART initiation',
155
+ cum_adults_at_art_initiation: 'C: Adults 15 years or older at ART initiation (Cumulative)',
156
+ quarterly_adults_at_art_initiation: 'C: Adults 15 years or older at ART initiation (Quarterly)',
157
+ males_initiated_on_art_first_time: 'Newly initiated male patients',
158
+ cum_males_initiated_on_art_first_time: 'Newly initiated male patients (Cummulative)',
159
+ initial_pregnant_females_all_ages: 'Newly initiated pregnant females',
160
+ cum_initial_pregnant_females_all_ages: 'Newly initiated pregnant females (Cummulative)',
161
+ initial_non_pregnant_females_all_ages: 'Newly initiated non-pregnant females',
162
+ cum_initial_non_pregnant_females_all_ages: 'Newly initiated non-pregnant females (Cummulative)',
163
+ unknown_gender: 'All clients registered but has not gender specified',
164
+ cum_unknown_gender: 'All clients registered but has not gender specified (Cummulative)',
165
+ newly_initiated_on_3hp: 'All patients who started 3HP in current reporting period',
166
+ newly_initiated_on_ipt: 'All patients who started IPT in current reporting period'
167
+ }.freeze
168
+
169
+ def initialize
170
+ @values = ActiveSupport::HashWithIndifferentAccess.new
171
+ end
172
+
173
+ def method_missing(name, *args, &)
174
+ name_prefix, name_suffix = split_missing_method_name(name)
175
+
176
+ return super(name, *args, &) unless FIELD_DESCRIPTIONS.include?(name_prefix)
177
+
178
+ field = value(name_prefix)
179
+ field.contents = args[0] if name_suffix == '='
180
+ field.contents
181
+
182
+ field.contents
183
+ end
184
+
185
+ def respond_to_missing?(name)
186
+ field_name, = split_missing_method_name(name)
187
+ FIELD_DESCRIPTIONS.include?(field_name)
188
+ end
189
+
190
+ def values
191
+ @values.values
192
+ end
193
+
194
+ private
195
+
196
+ # Returns a ReportValue object for the given name
197
+ def value(name)
198
+ description = FIELD_DESCRIPTIONS[name]
199
+ iname_parts = description.split(':', 2)
200
+ iname_parts.insert(0, nil) unless iname_parts.size == 2
201
+ short_iname, long_iname = iname_parts
202
+
203
+ @values[name] ||= OpenStruct.new(
204
+ name:,
205
+ indicator_name: long_iname.strip,
206
+ indicator_short_name: short_iname ? short_iname.strip : short_iname,
207
+ description:,
208
+ contents: nil
209
+ )
210
+ end
211
+
212
+ def split_missing_method_name(name)
213
+ match = name.to_s.match(/^([_A-Z0-9]+)(=)?$/i)
214
+ [match[1].to_sym, match[2]]
215
+ end
216
+ end
217
+ # rubocop:enable Metrics/ClassLength
218
+ end
219
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Moh
5
+ # This class is used to add additional cohort disaggregated data
6
+ # rubocop:disable Metrics/ClassLength
7
+ class CohortSurvivalAnalysis
8
+ def initialize(name:, type:, start_date:, end_date:, regenerate:, occupation:)
9
+ @name = name
10
+ @type = type
11
+ @start_date = start_date
12
+ @end_date = end_date
13
+ @regenerate = regenerate
14
+ @occupation = occupation
15
+ end
16
+
17
+ def survival_analysis(quarter, age_group)
18
+ art_service = CohortDisaggregated.new(name: 'survival_analysis',
19
+ type: 'survival_analysis', start_date: Date.today,
20
+ end_date: Date.today, rebuild: @regenerate)
21
+
22
+ start_date, end_date = art_service.generate_start_date_and_end_date(quarter)
23
+ CohortBuilder.new.init_temporary_tables(start_date, @end_date, @occupation) if @regenerate
24
+ art_service = CohortDisaggregated.new(name: 'survival_analysis',
25
+ type: 'survival_analysis', start_date: @start_date.to_date,
26
+ end_date: @end_date.to_date, rebuild: @regenerate)
27
+
28
+ qtr = quarter.split[0]
29
+ results = {}
30
+ years = 1
31
+
32
+ years_to_backto = ActiveRecord::Base.connection.select_one <<~SQL
33
+ SELECT
34
+ TIMESTAMPDIFF(YEAR, DATE(MIN(encounter_datetime)), DATE('#{end_date}')) years
35
+ FROM encounter e WHERE program_id = 1 AND voided = 0;
36
+ SQL
37
+
38
+ clinic_start_years = begin
39
+ years_to_backto['years'].to_i
40
+ rescue StandardError
41
+ nil
42
+ end
43
+ clinic_start_years = 10 if clinic_start_years.blank?
44
+
45
+ while years < clinic_start_years
46
+ yr = (quarter.split[1].to_i - years)
47
+ set_qtr = "#{qtr} #{yr}"
48
+ qstart_date, qend_date = art_service.generate_start_date_and_end_date(set_qtr)
49
+ results[set_qtr] = {}
50
+
51
+ case age_group
52
+ when 'General'
53
+ additional_sql = ' GROUP BY e.patient_id'
54
+ when 'Children'
55
+ additional_sql = ' GROUP BY e.patient_id'
56
+ additional_sql += ' HAVING patient_age < 15'
57
+ when 'Women'
58
+ option_Bplus_women_ids = pregnant_and_breastfeeding_women(qstart_date, qend_date)
59
+
60
+ option_Bplus_women_ids = [0] if option_Bplus_women_ids.blank?
61
+
62
+ additional_sql = " AND e.patient_id IN (#{option_Bplus_women_ids.join(', ')})"
63
+ additional_sql += ' GROUP BY e.patient_id'
64
+ additional_sql += ' HAVING patient_age >= 15'
65
+ additional_sql += ' AND gender = "F"'
66
+ end
67
+
68
+ begin
69
+ data = ActiveRecord::Base.connection.select_all <<~SQL
70
+ SELECT
71
+ cum_outcome, timestampdiff(month, DATE('#{qend_date}'), DATE('#{end_date}')) qinterval,
72
+ timestampdiff(year, DATE(e.birthdate), DATE('#{end_date}')) AS patient_age,
73
+ e.gender
74
+ FROM temp_earliest_start_date e
75
+ INNER JOIN temp_patient_outcomes o ON o.patient_id = e.patient_id
76
+ WHERE date_enrolled BETWEEN '#{qstart_date.strftime('%Y-%m-%d')}'
77
+ AND '#{qend_date.strftime('%Y-%m-%d')}'
78
+ #{additional_sql};
79
+ SQL
80
+ rescue StandardError
81
+ return results
82
+ end
83
+
84
+ (data || []).each do |r|
85
+ outcome = r['cum_outcome']
86
+ outcome = (outcome.blank? ? 'Unknown' : outcome)
87
+
88
+ if results[set_qtr][outcome].blank?
89
+ results[set_qtr][outcome] = {}
90
+ results[set_qtr][outcome][r['qinterval']] = 0
91
+ end
92
+ results[set_qtr][outcome][r['qinterval']] += 1
93
+ end
94
+
95
+ years += 1
96
+ end
97
+
98
+ append_last_six_months(quarter, results, end_date) if age_group == 'Women'
99
+
100
+ results
101
+ end
102
+
103
+ def pregnant_and_breastfeeding_women(start_date, end_date)
104
+ patient_ids = []
105
+
106
+ patients = ActiveRecord::Base.connection.select_all <<~SQL
107
+ SELECT
108
+ e.*, patient_reason_for_starting_art_text(e.patient_id) reason
109
+ FROM temp_earliest_start_date e
110
+ WHERE date_enrolled BETWEEN '#{start_date.to_date}' AND '#{end_date.to_date}'
111
+ AND gender IN('F','Female') GROUP BY e.patient_id
112
+ HAVING reason LIKE '%pregnant%' OR reason LIKE '%breast%';
113
+ SQL
114
+
115
+ (patients || []).each do |aRow|
116
+ patient_ids << aRow['patient_id'].to_i
117
+ end
118
+
119
+ concept_ids = []
120
+ ::ConceptName.where(name: 'Breastfeeding').select do |c|
121
+ concept_ids << c.concept_id
122
+ end
123
+ concept_ids << ::ConceptName.find_by_name('Patient pregnant').concept_id
124
+ concept_ids << ::ConceptName.find_by_name('Is patient pregnant?').concept_id
125
+ concept_ids << ::ConceptName.find_by_name('Is patient breast feeding?').concept_id
126
+ yes_concept_id = ::ConceptName.find_by_name('Yes').concept_id
127
+
128
+ patients = ActiveRecord::Base.connection.select_all <<~SQL
129
+ SELECT
130
+ e.*, patient_reason_for_starting_art_text(e.patient_id) reason
131
+ FROM temp_earliest_start_date e
132
+ INNER JOIN obs ON obs.person_id = e.patient_id
133
+ WHERE date_enrolled BETWEEN '#{start_date.to_date}' AND '#{end_date.to_date}'
134
+ AND gender IN('F','Female') AND obs.voided = 0
135
+ AND DATE(obs_datetime) = DATE(earliest_start_date)
136
+ AND obs.concept_id IN(#{concept_ids.join(',')})
137
+ AND value_coded = #{yes_concept_id} GROUP BY e.patient_id
138
+ HAVING reason LIKE '%Lymphocyte count below threshold with who stage%'
139
+ ORDER BY obs_datetime DESC;
140
+ SQL
141
+
142
+ pregnant_and_breastfeeding_clients = []
143
+
144
+ (patients || []).each do |aRow|
145
+ pregnant_and_breastfeeding_clients << aRow['patient_id'].to_i
146
+ end
147
+
148
+ (patient_ids + pregnant_and_breastfeeding_clients).uniq
149
+ end
150
+
151
+ def append_last_six_months(quarter, results, end_date)
152
+ art_service = CohortDisaggregated.new(name: 'survival_analysis',
153
+ type: 'survival_analysis', start_date: @start_date.to_date,
154
+ end_date: @end_date.to_date, rebuild: @regenerate)
155
+
156
+ qstart_date, qend_date = art_service.generate_start_date_and_end_date(quarter)
157
+ qstart_date -= 6.month
158
+ qend_date -= 6.month
159
+ # set_qtr = "Q#{quarter.split(' ')[0][1..1].to_i - 2} #{qend_date.year}"
160
+ set_qtr = (quarter[1..1].to_i - 2)
161
+
162
+ set_qtr = (set_qtr.zero? ? 4 : set_qtr)
163
+ set_qtr = (set_qtr == -1 ? 3 : set_qtr)
164
+ set_qtr = (set_qtr == -2 ? 2 : set_qtr)
165
+ set_qtr = "Q#{set_qtr} #{qend_date.year}"
166
+
167
+ option_Bplus_women_ids = pregnant_and_breastfeeding_women(qstart_date, qend_date)
168
+ option_Bplus_women_ids = [0] if option_Bplus_women_ids.blank?
169
+
170
+ data = ActiveRecord::Base.connection.select_all <<~SQL
171
+ SELECT
172
+ cum_outcome, timestampdiff(month, DATE('#{qend_date}'), DATE('#{end_date}')) qinterval,
173
+ timestampdiff(year, DATE(e.birthdate), DATE('#{end_date}')) AS patient_age,
174
+ e.gender
175
+ FROM temp_earliest_start_date e
176
+ INNER JOIN temp_patient_outcomes o ON o.patient_id = e.patient_id
177
+ WHERE date_enrolled BETWEEN '#{qstart_date.strftime('%Y-%m-%d')}'
178
+ AND '#{qend_date.strftime('%Y-%m-%d')}' AND gender = 'F' AND e.patient_id IN (#{option_Bplus_women_ids.join(', ')});
179
+ SQL
180
+
181
+ (data || []).each do |r|
182
+ outcome = r['cum_outcome']
183
+ outcome = (outcome.blank? ? 'Unknown' : outcome)
184
+
185
+ if results[set_qtr].blank?
186
+ results[set_qtr] = {}
187
+ results[set_qtr][outcome] = {}
188
+ results[set_qtr][outcome][r['qinterval']] = 0
189
+ elsif results[set_qtr][outcome].blank?
190
+ results[set_qtr][outcome] = {}
191
+ results[set_qtr][outcome][r['qinterval']] = 0
192
+ end
193
+
194
+ puts "######### #{r['qinterval']}"
195
+ results[set_qtr][outcome][r['qinterval']] += 1
196
+ end
197
+
198
+ results
199
+ end
200
+ end
201
+ # rubocop:enable Metrics/ClassLength
202
+ end
203
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Moh
5
+ # This class generates the MOH TPT report
6
+ # rubocop:disable Metrics/ClassLength
7
+ class MohTpt
8
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
9
+ attr_reader :start_date, :end_date, :start_of_month, :end_of_month, :raw_end_date, :raw_start_date
10
+
11
+ def initialize(start_date:, end_date:, **kwargs)
12
+ @raw_end_date = end_date.to_date
13
+ @raw_start_date = start_date.to_date
14
+ @start_date = start_date - 9.months
15
+ @end_date = @start_date + 3.months
16
+ @start_of_month = @start_date.beginning_of_month
17
+ @end_of_month = @end_date.end_of_month
18
+ @occupation = kwargs[:occupation]
19
+ end
20
+
21
+ def data
22
+ report = init_report
23
+ drop_tables
24
+ initiated_on_art
25
+ initiated_on_tpt
26
+ create_indexes
27
+ process_initiated_on_art report, fetch_initiated_on_art
28
+ process_initiated_tpt report, fetch_initiated_on_tpt
29
+ response = []
30
+ report.each do |key, value|
31
+ response << { age_group: key, gender: 'F', **value['F'] }
32
+ response << { age_group: key, gender: 'M', **value['M'] }
33
+ end
34
+ response
35
+ end
36
+
37
+ private
38
+
39
+ GENDERS = %w[F M].freeze
40
+ AGE_GROUPS = ['<1 year', '1-4 years', '5-9 years', '10-14 years', '15-19 years',
41
+ '20-24 years', '25-29 years', '30-34 years', '35-39 years', '40-44 years',
42
+ '45-49 years', '50-54 years', '55-59 years', '60-64 years', '65-69 years',
43
+ '70-74 years', '75-79 years', '80-84 years', '85-89 years',
44
+ '90 plus years'].freeze
45
+
46
+ def init_report
47
+ AGE_GROUPS.each_with_object({}) do |age_group, report|
48
+ report[age_group] = GENDERS.each_with_object({}) do |gender, tpt_report|
49
+ tpt_report[gender] = {
50
+ initiated_art: [], started_tpt: [], not_completed_tpt: [],
51
+ completed_tpt: [], died: [], pregnant: [],
52
+ defaulted: [], stopped: [],
53
+ transfer_out: [], confirmed_tb: [],
54
+ breast_feeding: [], skin_rash: [],
55
+ nausea: [], peripheral_neuropathy: [],
56
+ dizziness: [], yellow_eyes: []
57
+ }
58
+ end
59
+ end
60
+ end
61
+
62
+ def process_initiated_on_art(report, patients)
63
+ patients.each do |patient|
64
+ report[patient['age_group']][patient['gender']][:initiated_art] << patient['patient_id']
65
+ end
66
+ end
67
+
68
+ def process_initiated_tpt(report, patients)
69
+ TptOutcome.new(start_date:, end_date: raw_end_date)
70
+ .moh_report(report, patients, start_date, end_date)
71
+ end
72
+
73
+ def fetch_initiated_on_art
74
+ ActiveRecord::Base.connection.select_all <<~SQL
75
+ SELECT patient_id, age_group, gender FROM temp_initiated_on_art
76
+ WHERE art_start_date >= DATE('#{start_of_month}')
77
+ SQL
78
+ end
79
+
80
+ def fetch_initiated_on_tpt
81
+ result = ActiveRecord::Base.connection.select_all <<~SQL
82
+ SELECT * FROM temp_initiated_on_tpt
83
+ WHERE start_date >= DATE('#{start_of_month}')
84
+ SQL
85
+ # convert to array of hashes
86
+ result.to_a
87
+ end
88
+
89
+ def initiated_on_art
90
+ ActiveRecord::Base.connection.execute <<~SQL
91
+ CREATE TABLE temp_initiated_on_art
92
+ SELECT pp.patient_id, coalesce(o.value_datetime, min(art_order.start_date)) art_start_date, p.gender, disaggregated_age_group(p.birthdate, DATE('#{raw_end_date}')) age_group
93
+ FROM patient_program pp
94
+ INNER JOIN person p ON p.person_id = pp.patient_id AND p.voided = 0
95
+ INNER JOIN patient_state ps ON ps.patient_program_id = pp.patient_program_id AND ps.voided = 0 AND ps.state = 7 -- ON ART
96
+ INNER JOIN orders art_order ON art_order.patient_id = pp.patient_id
97
+ AND art_order.start_date >= DATE('#{start_of_month}')
98
+ AND art_order.start_date < DATE('#{end_of_month}') + INTERVAL 1 DAY
99
+ AND art_order.voided = 0
100
+ AND art_order.order_type_id = 1 -- Drug order
101
+ AND art_order.concept_id IN (#{arv_concepts})
102
+ INNER JOIN drug_order do ON do.order_id = art_order.order_id AND do.quantity > 0
103
+ LEFT JOIN encounter e ON e.patient_id = pp.patient_id
104
+ AND e.encounter_type = 9 -- HIV CLINIC REGISTRATION
105
+ AND e.voided = 0
106
+ AND e.encounter_datetime < DATE('#{end_of_month}') + INTERVAL 1 DAY
107
+ AND e.program_id = 1 -- HIV PROGRAM
108
+ LEFT JOIN obs o ON o.person_id = pp.patient_id
109
+ AND o.concept_id = 2516 -- ART start date
110
+ AND o.encounter_id = e.encounter_id
111
+ AND o.voided = 0
112
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = pp.patient_id
113
+ WHERE pp.patient_id NOT IN (
114
+ SELECT o.patient_id
115
+ FROM orders o
116
+ INNER JOIN drug_order do ON do.order_id = o.order_id AND do.quantity > 0
117
+ WHERE o.order_type_id = 1 -- Drug order
118
+ AND o.voided = 0
119
+ AND o.concept_id IN (#{arv_concepts})
120
+ AND o.start_date < DATE('#{start_of_month}')
121
+ GROUP BY o.patient_id
122
+ )
123
+ AND pp.program_id = 1 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
124
+ GROUP BY pp.patient_id
125
+ SQL
126
+ end
127
+
128
+ def initiated_on_tpt
129
+ ActiveRecord::Base.connection.execute <<~SQL
130
+ CREATE TABLE temp_initiated_on_tpt
131
+ SELECT
132
+ pp.patient_id,
133
+ coalesce(tpt_transfer_in_obs.value_datetime, min(tpt_order.start_date)) start_date,
134
+ p.gender, disaggregated_age_group(p.birthdate, DATE('#{raw_end_date}')) age_group,
135
+ patient_outcome(p.person_id, DATE('#{@raw_end_date}')) AS outcome,
136
+ DATE(COALESCE(art_start_date_obs.value_datetime, MIN(art_order.start_date))) AS earliest_start_date,
137
+ GROUP_CONCAT(DISTINCT tpt_order.concept_id SEPARATOR ',') AS drug_concepts,
138
+ CASE
139
+ WHEN count(DISTINCT tpt_order.concept_id) > 1 THEN '3HP'
140
+ WHEN tpt_order.concept_id = 10565 THEN '3HP'
141
+ ELSE '6H'
142
+ END AS tpt_type
143
+ FROM patient_program pp
144
+ INNER JOIN person p ON p.person_id = pp.patient_id AND p.voided = 0
145
+ INNER JOIN patient_state ps ON ps.patient_program_id = pp.patient_program_id AND ps.voided = 0 AND ps.state = 7 -- ON ART
146
+ INNER JOIN orders tpt_order ON tpt_order.patient_id = pp.patient_id
147
+ AND tpt_order.start_date >= DATE('#{start_of_month}')
148
+ AND tpt_order.start_date < DATE('#{end_of_month}') + INTERVAL 1 DAY
149
+ AND tpt_order.voided = 0
150
+ AND tpt_order.order_type_id = 1 -- Drug order
151
+ AND tpt_order.concept_id IN (#{tpt_concepts})
152
+ INNER JOIN drug_order do ON do.order_id = tpt_order.order_id AND do.quantity > 0
153
+ LEFT JOIN encounter AS clinic_registration_encounter
154
+ ON clinic_registration_encounter.encounter_type = (
155
+ SELECT encounter_type_id FROM encounter_type WHERE name = 'HIV CLINIC REGISTRATION' LIMIT 1
156
+ )
157
+ AND clinic_registration_encounter.patient_id = pp.patient_id
158
+ AND clinic_registration_encounter.program_id = pp.program_id
159
+ AND clinic_registration_encounter.encounter_datetime < DATE('#{@end_date}') + INTERVAL 1 DAY
160
+ AND clinic_registration_encounter.voided = 0
161
+ INNER JOIN orders AS art_order
162
+ ON art_order.patient_id = pp.patient_id
163
+ /* AND art_order.encounter_id = prescription_encounter.encounter_id */
164
+ AND art_order.concept_id IN (SELECT concept_id FROM concept_set WHERE concept_set = 1085)
165
+ AND art_order.start_date < DATE('#{@end_of_month}') + INTERVAL 1 DAY
166
+ AND art_order.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Drug order')
167
+ AND art_order.start_date >= DATE('1901-01-01')
168
+ AND art_order.voided = 0
169
+ LEFT JOIN obs AS art_start_date_obs
170
+ ON art_start_date_obs.concept_id = 2516
171
+ AND art_start_date_obs.person_id = pp.patient_id
172
+ AND art_start_date_obs.voided = 0
173
+ AND art_start_date_obs.obs_datetime < (DATE('#{@end_of_month}') + INTERVAL 1 DAY)
174
+ AND art_start_date_obs.encounter_id = clinic_registration_encounter.encounter_id
175
+ LEFT JOIN obs tpt_transfer_in_obs ON tpt_transfer_in_obs.person_id = pp.patient_id
176
+ AND tpt_transfer_in_obs.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
177
+ AND tpt_transfer_in_obs.voided = 0
178
+ AND tpt_transfer_in_obs.value_drug IN (#{tpt_concepts})
179
+ AND tpt_transfer_in_obs.obs_datetime < DATE('#{end_of_month}') + INTERVAL 1 DAY
180
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = pp.patient_id
181
+ WHERE pp.patient_id NOT IN (
182
+ SELECT o.patient_id
183
+ FROM orders o
184
+ INNER JOIN drug_order do ON do.order_id = o.order_id AND do.quantity > 0
185
+ WHERE o.order_type_id = 1 -- Drug order
186
+ AND o.voided = 0
187
+ AND o.concept_id IN (#{tpt_concepts})
188
+ AND o.start_date < DATE('#{start_of_month}')
189
+ GROUP BY o.patient_id
190
+ )
191
+ AND pp.program_id = 1 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
192
+ GROUP BY pp.patient_id
193
+ SQL
194
+ end
195
+
196
+ def drop_tables
197
+ execute_query 'DROP TABLE IF EXISTS temp_initiated_on_art'
198
+ execute_query 'DROP TABLE IF EXISTS temp_initiated_on_tpt'
199
+ end
200
+
201
+ def create_indexes
202
+ execute_query 'CREATE INDEX idx_temp_initiated_on_art ON temp_initiated_on_art(patient_id)'
203
+ execute_query 'CREATE INDEX idx_temp_initiated_on_tpt ON temp_initiated_on_tpt(patient_id)'
204
+ end
205
+
206
+ def execute_query(query)
207
+ ActiveRecord::Base.connection.execute(query)
208
+ end
209
+
210
+ def arv_concepts
211
+ @arv_concepts ||= ::ConceptSet.where(concept_set: ::ConceptName.where(name: 'Antiretroviral drugs')
212
+ .select(:concept_id))
213
+ .collect(&:concept_id).join(',')
214
+ end
215
+
216
+ def tpt_concepts
217
+ @tpt_concepts ||= ::ConceptName.where(name: ['Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine'])
218
+ .collect(&:concept_id).join(',')
219
+ end
220
+ end
221
+ # rubocop:enable Metrics/ClassLength
222
+ end
223
+ end