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,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Clinic
5
+ # Generates a hypertension report for a clinic
6
+ # rubocop:disable Metrics/ClassLength
7
+ class HypertensionReport
8
+ AGE_GROUPS = [
9
+ '20-24 years',
10
+ '25-29 years', '30-34 years',
11
+ '35-39 years', '40-44 years',
12
+ '45-49 years', '50-54 years',
13
+ '55-59 years', '60-64 years',
14
+ '65-69 years', '70-74 years',
15
+ '75-79 years', '80-84 years',
16
+ '85-89 years',
17
+ '90 plus years'
18
+ ].freeze
19
+
20
+ GENDER = %w[M F].freeze
21
+
22
+ DRUG_MAPPING = {
23
+ 'Hydrochlorothiazide (25mg tablet)' => :hydrochlorothiazide_25mg,
24
+ 'HCZ (25mg tablet)' => :hydrochlorothiazide_25mg,
25
+ 'Hctz (25mg)' => :hydrochlorothiazide_25mg,
26
+ 'Amlodipine (5mg tablet)' => :amlodipine_5mg,
27
+ 'Amlodipine 5mg' => :amlodipine_5mg,
28
+ 'Amlodipine (5mg)' => :amlodipine_5mg,
29
+ 'Amlodipine (10mg tablet)' => :amlodipine_10mg,
30
+ 'Enalapril (5mg tablet)' => :enalapril_5mg,
31
+ 'Enalapril (5mg)' => :enalapril_5mg,
32
+ 'Enalapril (10mg tablet)' => :enalapril_10mg,
33
+ 'Enalapril (10mg)' => :enalapril_10mg,
34
+ 'Atenolol (50mg tablet)' => :atenolol_50mg,
35
+ 'Atenolol (50mg)' => :atenolol_50mg,
36
+ 'Atenolol (100mg tablet)' => :atenolol_100mg
37
+ }.freeze
38
+
39
+ def initialize(start_date:, end_date:, **_kwargs)
40
+ @start_date = ActiveRecord::Base.connection.quote(start_date)
41
+ @end_date = ActiveRecord::Base.connection.quote(end_date)
42
+ end
43
+
44
+ def find_report
45
+ @report = init_report
46
+ process_data
47
+ @report
48
+ rescue StandardError => e
49
+ puts e.message
50
+ puts e.backtrace.join("\n")
51
+ raise e
52
+ end
53
+
54
+ private
55
+
56
+ def init_report
57
+ AGE_GROUPS.each_with_object({}) do |age_group, report|
58
+ report[age_group] = GENDER.each_with_object({}) do |gender, gender_sub_report|
59
+ gender_sub_report[gender] = initialize_gender_metrics
60
+ end
61
+ end
62
+ end
63
+
64
+ # rubocop:disable Metrics/MethodLength
65
+ def initialize_gender_metrics
66
+ {
67
+ screened: [],
68
+ normal_reading: [],
69
+ mild_reading: [],
70
+ moderate_reading: [],
71
+ severe_reading: [],
72
+ hydrochlorothiazide_25mg: [],
73
+ amlodipine_5mg: [],
74
+ amlodipine_10mg: [],
75
+ enalapril_5mg: [],
76
+ enalapril_10mg: [],
77
+ atenolol_50mg: [],
78
+ atenolol_100mg: [],
79
+ total_regimen: []
80
+ }
81
+ end
82
+ # rubocop:enable Metrics/MethodLength
83
+
84
+ # rubocop:disable Metrics/AbcSize
85
+ # rubocop:disable Metrics/MethodLength
86
+ def process_data
87
+ (data || []).each do |row|
88
+ age_group = row['age_group']
89
+ gender = row['gender']
90
+ next unless AGE_GROUPS.include?(age_group)
91
+ next unless GENDER.include?(gender)
92
+
93
+ patient = client_info(row)
94
+ cluster = @report[age_group][gender]
95
+ cluster[:screened] << patient
96
+ process_bp_classification(cluster, patient, row['systolic_classification'], row['diastolic_classification'])
97
+ process_drug_data(cluster, patient, row['drug_names'].split(',')) if row['drug_names'].present?
98
+ cluster[:total_regimen] << patient if row['drug_names'].present?
99
+ end
100
+ end
101
+ # rubocop:enable Metrics/AbcSize
102
+ # rubocop:enable Metrics/MethodLength
103
+
104
+ def process_bp_classification(cluster, patient_id, sys_class, dia_class)
105
+ classification = SEVERITY_ORDER[sys_class.to_sym] > SEVERITY_ORDER[dia_class.to_sym] ? sys_class : dia_class
106
+ case SEVERITY_CLASSIFICATION[classification.to_sym]
107
+ when 'NORMAL'
108
+ cluster[:normal_reading] << patient_id
109
+ when 'MILD'
110
+ cluster[:mild_reading] << patient_id
111
+ when 'MODERATE'
112
+ cluster[:moderate_reading] << patient_id
113
+ when 'SEVERE'
114
+ cluster[:severe_reading] << patient_id
115
+ end
116
+ end
117
+
118
+ def process_drug_data(cluster, patient_id, drugs)
119
+ return if drugs.blank?
120
+
121
+ drugs.each do |drug|
122
+ cluster_key = DRUG_MAPPING[drug]
123
+ cluster[cluster_key] << patient_id if cluster_key
124
+ end
125
+ end
126
+
127
+ SEVERITY_ORDER = {
128
+ severe_reading: 4,
129
+ moderate_reading: 3,
130
+ mild_reading: 2,
131
+ normal_reading: 1
132
+ }.freeze
133
+
134
+ SEVERITY_CLASSIFICATION = {
135
+ severe_reading: 'SEVERE',
136
+ moderate_reading: 'MODERATE',
137
+ mild_reading: 'MILD',
138
+ normal_reading: 'NORMAL'
139
+ }.freeze
140
+
141
+ def data
142
+ ActiveRecord::Base.connection.select_all <<~SQL
143
+ SELECT
144
+ tpo.patient_id,
145
+ sys.value_numeric systolic,
146
+ dia.value_numeric diastolic,
147
+ CASE
148
+ WHEN sys.value_numeric <= 139 THEN 'normal_reading'
149
+ WHEN sys.value_numeric > 139 AND sys.value_numeric <= 159 THEN 'mild_reading'
150
+ WHEN sys.value_numeric > 159 AND sys.value_numeric <= 179 THEN 'moderate_reading'
151
+ WHEN sys.value_numeric > 179 THEN 'severe_reading'
152
+ END AS systolic_classification,
153
+ CASE
154
+ WHEN dia.value_numeric <= 89 THEN 'normal_reading'
155
+ WHEN dia.value_numeric > 89 AND dia.value_numeric <= 99 THEN 'mild_reading'
156
+ WHEN dia.value_numeric > 99 AND dia.value_numeric <= 109 THEN 'moderate_reading'
157
+ WHEN dia.value_numeric > 109 THEN 'severe_reading'
158
+ END AS diastolic_classification,
159
+ UPPER(LEFT(p.gender, 1)) gender,
160
+ disaggregated_age_group(p.birthdate, #{@end_date}) age_group,
161
+ i.identifier arv_number,
162
+ latest_drug_order.start_date,
163
+ GROUP_CONCAT(DISTINCT d.name) drug_names
164
+ FROM (
165
+ SELECT MAX(o.obs_datetime) obs_date, o.person_id patient_id
166
+ FROM obs o
167
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.program_id = 1 -- HIV PROGRAM
168
+ WHERE o.concept_id = 5085 -- Systolic blood pressure
169
+ AND o.voided = 0 AND o.obs_datetime >= #{@start_date} AND o.obs_datetime < #{@end_date} + INTERVAL 1 DAY
170
+ GROUP BY o.person_id
171
+ ) AS tpo
172
+ INNER JOIN obs sys ON sys.concept_id = 5085 AND sys.person_id = tpo.patient_id AND sys.obs_datetime = tpo.obs_date AND sys.voided = 0
173
+ INNER JOIN obs dia ON dia.concept_id = 5086 AND dia.person_id = tpo.patient_id AND dia.obs_datetime = tpo.obs_date AND dia.voided = 0
174
+ INNER JOIN person p ON p.person_id = tpo.patient_id AND p.voided = 0
175
+ INNER JOIN encounter e ON e.encounter_id = dia.encounter_id AND e.program_id = 1 AND e.voided = 0
176
+ LEFT JOIN patient_identifier i ON i.patient_id = e.patient_id AND i.identifier_type = 4 AND i.voided = 0
177
+ LEFT JOIN (
178
+ SELECT o.patient_id, MAX(o.start_date) start_date
179
+ FROM orders o
180
+ INNER JOIN drug_order dor ON dor.order_id = o.order_id AND dor.quantity > 0
181
+ WHERE o.voided = 0
182
+ AND o.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE '%Hydrochlorothiazide%' OR name LIKE '%Amlodipine%' OR name LIKE '%Enalapril%' OR name LIKE '%Atenolol%')
183
+ AND o.start_date >= #{@start_date} AND o.start_date < #{@end_date} + INTERVAL 1 DAY
184
+ GROUP BY o.patient_id
185
+ ) AS latest_drug_order ON latest_drug_order.patient_id = tpo.patient_id AND DATE(latest_drug_order.start_date) >= DATE(tpo.obs_date)
186
+ LEFT JOIN orders ord ON ord.start_date = latest_drug_order.start_date AND ord.patient_id = latest_drug_order.patient_id AND ord.voided = 0 AND ord.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE '%Hydrochlorothiazide%' OR name LIKE '%Amlodipine%' OR name LIKE '%Enalapril%' OR name LIKE '%Atenolol%')
187
+ LEFT JOIN drug_order dor ON dor.order_id = ord.order_id AND dor.quantity > 0
188
+ LEFT JOIN drug d ON d.drug_id = dor.drug_inventory_id AND d.retired = 0
189
+ WHERE tpo.patient_id NOT IN (#{external_clients})
190
+ GROUP BY tpo.patient_id
191
+ ORDER BY tpo.patient_id ASC
192
+ SQL
193
+ end
194
+
195
+ def external_clients
196
+ <<~SQL
197
+ SELECT obs.person_id FROM obs,
198
+ (SELECT person_id, Max(obs_datetime) AS obs_datetime, concept_id FROM obs
199
+ WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name = 'Type of patient' AND voided = 0)
200
+ AND DATE(obs_datetime) <= #{@end_date}
201
+ AND voided = 0
202
+ GROUP BY person_id) latest_record
203
+ WHERE obs.person_id = latest_record.person_id
204
+ AND obs.concept_id = latest_record.concept_id
205
+ AND obs.obs_datetime = latest_record.obs_datetime
206
+ AND obs.value_coded IN (SELECT concept_id FROM concept_name WHERE name = 'Drug refill' || name = 'External consultation')
207
+ AND obs.voided = 0
208
+ SQL
209
+ end
210
+
211
+ def client_info(data)
212
+ {
213
+ patient_id: data['patient_id'],
214
+ arv_number: data['arv_number'],
215
+ gender: data['gender'],
216
+ diastolic: data['diastolic'],
217
+ systolic: data['systolic']
218
+ }
219
+ end
220
+ end
221
+ # rubocop:enable Metrics/ClassLength
222
+ end
223
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Cohort report builder class.
4
+ #
5
+ # This class only provides one public method (start_build_report) besides
6
+ # the constructor. This method must be called to build report and save
7
+ # it to database.
8
+ module MalawiHivProgramReports
9
+ module Clinic
10
+ class IptCoverage
11
+ def initialize(start_date:, end_date:)
12
+ @start_date = start_date.to_date.strftime('%Y-%m-%d 00:00:00')
13
+ @end_date = end_date.to_date.strftime('%Y-%m-%d 23:59:59')
14
+ end
15
+
16
+ def data
17
+ patient_ids = []
18
+ patients = {}
19
+
20
+ (on_art_in_reporting_period || []).each do |data|
21
+ patient_ids << data['patient_id'].to_i
22
+ end
23
+
24
+ return patients if patient_ids.blank?
25
+
26
+ data = ipt_dispensations(patient_ids)
27
+
28
+ (data || []).each do |record|
29
+ patient_id = record['patient_id'].to_i
30
+ age_group = record['age_group']
31
+ gender = record['gender']
32
+
33
+ gender = if gender.blank?
34
+ 'Unknown'
35
+ else
36
+ (/F/i.match?(gender) ? 'Female' : 'Male')
37
+ end
38
+
39
+ patients[age_group] = {} if patients[age_group].blank?
40
+ patients[age_group][gender] = {} if patients[age_group][gender].blank?
41
+ patients[age_group][gender][patient_id] = 0 if patients[age_group][gender][patient_id].blank?
42
+
43
+ prescription_info = ActiveRecord::Base.connection.select_one <<-SQL
44
+ SELECT TIMESTAMPDIFF(day, DATE('#{record['start_date']}'), DATE('#{record['auto_expire_date']}')) days;
45
+ SQL
46
+
47
+ next if prescription_info['days'].blank?
48
+
49
+ patients[age_group][gender][patient_id] += prescription_info['days'].to_i
50
+ end
51
+
52
+ age_groups = {}
53
+
54
+ (patients || {}).each do |keys, values|
55
+ pats = values
56
+ (pats || {}).each do |sex, ids|
57
+ (ids || {}).each do |patient_id, count|
58
+ next unless count >= 168
59
+
60
+ age_groups[keys] = {} if age_groups[keys].blank?
61
+ age_groups[keys][sex] = [] if age_groups[keys][sex].blank?
62
+ age_groups[keys][sex] << patient_id
63
+ end
64
+ end
65
+ end
66
+
67
+ age_groups
68
+ end
69
+
70
+ private
71
+
72
+ def on_art_in_reporting_period
73
+ ActiveRecord::Base.connection.select_all <<-SQL
74
+ select
75
+ `p`.`patient_id` AS `patient_id`, pe.birthdate, pe.gender,
76
+ cast(patient_date_enrolled(`p`.`patient_id`) as date) AS `date_enrolled`
77
+ from
78
+ ((`patient_program` `p`
79
+ left join `person` `pe` ON ((`pe`.`person_id` = `p`.`patient_id`))
80
+ left join `patient_state` `s` ON ((`p`.`patient_program_id` = `s`.`patient_program_id`)))
81
+ left join `person` ON ((`person`.`person_id` = `p`.`patient_id`)))
82
+ where
83
+ ((`p`.`voided` = 0)
84
+ and (`s`.`voided` = 0)
85
+ and (`p`.`program_id` = 1)
86
+ and (`s`.`state` = 7))
87
+ /*and (DATE(`s`.`start_date`) BETWEEN '#{@start_date}' AND '#{@end_date}')*/
88
+ and pepfar_patient_outcome(p.patient_id, DATE('#{@end_date}')) = 'On antiretrovirals'
89
+ group by `p`.`patient_id`
90
+ HAVING date_enrolled IS NOT NULL;
91
+ SQL
92
+ end
93
+
94
+ def ipt_dispensations(patient_ids)
95
+ date_six_months_ago = (@start_date.to_date - 6.months).strftime('%Y-%m-%d 00:00:00')
96
+
97
+ ActiveRecord::Base.connection.select_all <<-SQL
98
+ SELECT
99
+ o.patient_id, birthdate, gender, o.start_date, o.auto_expire_date,
100
+ disaggregated_age_group(p.birthdate, DATE('#{@end_date}')) age_group
101
+ FROM person p
102
+ INNER JOIN orders o ON o.patient_id = p.person_id
103
+ INNER JOIN drug_order t ON o.order_id = t.order_id
104
+ INNER JOIN drug d ON d.drug_id = t.drug_inventory_id
105
+ WHERE o.voided = 0 AND (o.start_date BETWEEN '#{date_six_months_ago}' AND '#{@end_date}')
106
+ AND d.concept_id = 656 AND o.patient_id IN(#{patient_ids.join(',')})
107
+ AND t.quantity > 0 ORDER BY o.patient_id;
108
+ SQL
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Clinic
5
+ class IptReport
6
+ def initialize(start_date:, end_date:)
7
+ @start_date = start_date
8
+ @end_date = end_date
9
+ end
10
+
11
+ def ipt_coverage
12
+ coverage
13
+ end
14
+
15
+ private
16
+
17
+ def coverage
18
+ program_id = ::Program.find_by_name('HIV PROGRAM').id
19
+ data = ActiveRecord::Base.connection.select_all <<~SQL
20
+ SELECT patient_id FROM encounter
21
+ WHERE encounter_type = #{::EncounterType.find_by_name('HIV Reception').id}
22
+ AND encounter_datetime BETWEEN '#{@start_date.to_date.strftime('%Y-%m-%d 00:00:00')}'
23
+ AND '#{@end_date.to_date.strftime('%Y-%m-%d 23:59:59')}' AND voided = 0
24
+ AND program_id = #{program_id}
25
+ GROUP BY DATE(encounter_datetime), patient_id;
26
+ SQL
27
+
28
+ patient_ids = data.map { |p| p['patient_id'].to_i }
29
+ return { patients: 0, prescribed: 0, dispensed: 0 } if patient_ids.blank?
30
+
31
+ ipt_drugs = ::Drug.where('name LIKE (?)', '%Isoniazid%').map(&:drug_id)
32
+
33
+ data = ActiveRecord::Base.connection.select_one <<~SQL
34
+ SELECT count(o.patient_id) AS prescribed FROM orders o
35
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id
36
+ INNER JOIN drug_order i ON i.order_id = o.order_id
37
+ WHERE o.voided = 0 AND e.patient_id IN(#{patient_ids.join(',')})
38
+ AND start_date BETWEEN "#{@start_date.to_date.strftime('%Y-%m-%d 00:00:00')}"
39
+ AND "#{@end_date.to_date.strftime('%Y-%m-%d 23:59:59')}"#{' '}
40
+ AND drug_inventory_id IN(#{ipt_drugs.join(',')})#{' '}
41
+ AND e.program_id = #{program_id};
42
+ SQL
43
+
44
+ #
45
+ # data = ::Order.where("start_date BETWEEN ? AND ? AND drug_inventory_id IN(?)
46
+ # AND program_id = ? AND orders.patient_id IN(?)",
47
+ # @start_date.to_date.strftime('%Y-%m-%d 00:00:00'),
48
+ # @end_date.to_date.strftime('%Y-%m-%d 23:59:59'), ipt_drugs, program_id, patient_ids).\
49
+ # joins("INNER JOIN encounter e ON e.encounter_id = orders.encounter_id
50
+ # INNER JOIN drug_order i ON i.order_id = orders.order_id").count(:patient_id)
51
+
52
+ data2 = ActiveRecord::Base.connection.select_one <<~SQL
53
+ SELECT count(o.patient_id) AS dispensed FROM orders o
54
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id
55
+ INNER JOIN drug_order i ON i.order_id = o.order_id
56
+ WHERE o.voided = 0 AND e.patient_id IN(#{patient_ids.join(',')})
57
+ AND start_date BETWEEN "#{@start_date.to_date.strftime('%Y-%m-%d 00:00:00')}"
58
+ AND "#{@end_date.to_date.strftime('%Y-%m-%d 23:59:59')}"
59
+ AND drug_inventory_id IN(#{ipt_drugs.join(',')})
60
+ AND quantity > 0 AND e.program_id = #{program_id};
61
+ SQL
62
+
63
+ prescribed = data['prescribed']
64
+ dispensed = data2['dispensed']
65
+ { patients: patient_ids.length, prescribed:, dispensed: }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class generates the LIMS results report (results delivered electronically)
4
+ module MalawiHivProgramReports
5
+ module Clinic
6
+ class LimsResults
7
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
8
+ attr_reader :start_date, :end_date
9
+
10
+ def initialize(start_date:, end_date:, **kwargs)
11
+ @start_date = start_date
12
+ @end_date = end_date
13
+ @occupation = kwargs[:occupation]
14
+ end
15
+
16
+ def find_report
17
+ data
18
+ end
19
+
20
+ private
21
+
22
+ def data
23
+ ActiveRecord::Base.connection.select_all <<~SQL
24
+ SELECT o.start_date AS date_ordered, pn.given_name, pn.family_name, pi.identifier AS arv_number, e.patient_id,
25
+ las.date_received, o.accession_number, CONCAT(COALESCE(res.value_modifier, '='), COALESCE(res.value_text, res.value_numeric)) AS result,
26
+ cn.name AS test_name, las.acknowledgement_type AS result_delivery_mode, statuses.value_text AS order_status, reason_test.name AS test_reason
27
+ FROM orders o
28
+ INNER JOIN lab_lims_order_mappings llom ON llom.order_id = o.order_id
29
+ LEFT JOIN lims_acknowledgement_statuses las ON las.order_id = o.order_id
30
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND (e.program_id = 1 OR e.program_id = 23) -- HIV PROGRAM AND Laboratory program
31
+ INNER JOIN users u ON u.user_id = o.orderer
32
+ INNER JOIN person_name pn ON pn.person_id = u.person_id
33
+ INNER JOIN obs test ON test.person_id = e.patient_id AND test.voided = 0 AND test.order_id = o.order_id AND test.concept_id = 9737 -- 'Test Type'
34
+ INNER JOIN concept_name cn ON cn.concept_id = test.value_coded AND cn.voided = 0 AND cn.locale_preferred = 1
35
+ LEFT JOIN patient_identifier pi ON pi.patient_id = e.patient_id AND pi.voided = 0 AND pi.identifier_type = #{identifier_type}
36
+ LEFT JOIN obs ON obs.person_id = e.patient_id AND obs.voided = 0 AND obs.order_id = o.order_id
37
+ AND obs.concept_id = 7363 -- 'Lab test result'
38
+ LEFT JOIN obs statuses ON statuses.order_id = o.order_id AND statuses.voided = 0 AND statuses.concept_id = 10682 -- 'lab order status'
39
+ LEFT JOIN obs res ON res.obs_group_id = obs.obs_id AND res.voided = 0 AND res.order_id = o.order_id
40
+ LEFT JOIN obs reason ON reason.order_id = o.order_id AND reason.voided = 0 AND reason.concept_id = 2429 -- 'Reason for test'
41
+ LEFT JOIN concept_name reason_test ON reason_test.concept_id = reason.value_coded AND reason_test.voided = 0
42
+ LEFT JOIN (#{current_occupation_query}) AS a ON a.person_id = e.patient_id
43
+ WHERE DATE(o.start_date) BETWEEN '#{start_date}' AND '#{end_date}' AND o.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
44
+ GROUP BY o.order_id
45
+ SQL
46
+ end
47
+
48
+ def identifier_type
49
+ filling_number = ::GlobalPropertyService.use_filing_numbers?
50
+
51
+ filling_number ? 17 : 4
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Outcome List Report
4
+ module MalawiHivProgramReports
5
+ module Clinic
6
+ class OutcomeList
7
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
8
+
9
+ REPORTS = Set.new(%i[transfer_out died stopped]).freeze
10
+
11
+ def initialize(start_date:, end_date:, outcome:, **kwargs)
12
+ @start_date = start_date.to_date.strftime('%Y-%m-%d 00:00:00')
13
+ @end_date = end_date.to_date.strftime('%Y-%m-%d 23:59:59')
14
+ @occupation = kwargs[:occupation]
15
+ @report = load_report outcome.downcase.split.join('_')
16
+ end
17
+
18
+ def get_list
19
+ @report
20
+ end
21
+
22
+ private
23
+
24
+ def start_date
25
+ ActiveRecord::Base.connection.quote(@start_date)
26
+ end
27
+
28
+ def end_date
29
+ ActiveRecord::Base.connection.quote(@end_date)
30
+ end
31
+
32
+ def load_report(name)
33
+ name = name.to_sym
34
+
35
+ raise "Invalid report: #{name}" unless REPORTS.include?(name)
36
+
37
+ method(name).call
38
+ end
39
+
40
+ def transfer_out
41
+ outcome_query 'Patient transferred out'
42
+ end
43
+
44
+ def died
45
+ outcome_query 'Patient died'
46
+ end
47
+
48
+ def stopped
49
+ outcome_query 'Treatment stopped'
50
+ end
51
+
52
+ def outcome_query(outcome_state)
53
+ report_type = 'moh'
54
+ MalawiHivProgramReports::Moh::CohortBuilder.new(outcomes_definition: report_type).init_temporary_tables(
55
+ @start_date.to_date, @end_date.to_date, @occupation
56
+ )
57
+
58
+ transfer_out_to_location_sql = ''
59
+ transfer_out_to_location_name_sql = ''
60
+ if /Transfer/i.match?(outcome_state)
61
+ concept_id = ::ConceptName.find_by_name('Transfer out to location').concept_id
62
+ transfer_out_to_location_name_sql = ' ,to_location.value_text transferred_out_to'
63
+ transfer_out_to_location_sql = ' LEFT JOIN obs to_location ON to_location.person_id = e.patient_id'
64
+ transfer_out_to_location_sql += " AND to_location.concept_id = #{concept_id} AND to_location.voided = 0"
65
+ end
66
+
67
+ data = ActiveRecord::Base.connection.select_all <<~SQL
68
+ SELECT
69
+ e.patient_id, i.identifier, e.birthdate,
70
+ e.gender, n.given_name, n.family_name,
71
+ art_reason.name art_reason, a.value cell_number,
72
+ s3.state_province district, s3.county_district ta,
73
+ s3.city_village village, TIMESTAMPDIFF(year, DATE(e.birthdate), DATE('#{@end_date}')) age,
74
+ pp.date_enrolled, pp.date_completed,
75
+ s2.start_date outcome_date, s2.end_date, s2.state
76
+ #{transfer_out_to_location_name_sql}
77
+ FROM temp_earliest_start_date e
78
+ INNER JOIN temp_patient_outcomes o ON e.patient_id = o.patient_id
79
+ LEFT JOIN patient_identifier i ON i.patient_id = e.patient_id
80
+ AND i.voided = 0 AND i.identifier_type = 4
81
+ INNER JOIN person_name n ON n.person_id = e.patient_id AND n.voided = 0
82
+ LEFT JOIN person_attribute a ON a.person_id = e.patient_id
83
+ AND a.voided = 0 AND a.person_attribute_type_id = 12
84
+ LEFT JOIN person_address s3 ON s3.person_id = e.patient_id
85
+ LEFT JOIN concept_name art_reason ON art_reason.concept_id = e.reason_for_starting_art
86
+ #{transfer_out_to_location_sql}
87
+ INNER JOIN patient_program pp ON pp.patient_id = e.patient_id AND pp.program_id = 1
88
+ INNER JOIN patient_state s2 ON s2.patient_program_id = pp.patient_program_id
89
+ INNER JOIN program_workflow_state ws ON ws.program_workflow_state_id = s2.state
90
+ INNER JOIN program_workflow w ON w.program_workflow_id = ws.program_workflow_id
91
+ INNER JOIN concept_name n2 ON n2.concept_id = ws.concept_id
92
+ WHERE o.cum_outcome = '#{outcome_state}'
93
+ AND pp.voided = 0
94
+ AND s2.voided = 0
95
+ AND s2.start_date
96
+ BETWEEN '#{@start_date}' AND '#{@end_date}' AND s2.state NOT IN(7,1, 12)
97
+ GROUP BY e.patient_id ORDER BY e.patient_id, n.date_created DESC;
98
+ SQL
99
+
100
+ patients = []
101
+
102
+ (data || []).each do |person|
103
+ patients << {
104
+ patient_id: person['patient_id'],
105
+ given_name: person['given_name'],
106
+ family_name: person['family_name'],
107
+ birthdate: person['birthdate'],
108
+ gender: person['gender'],
109
+ arv_number: person['arv_number'],
110
+ outcome: 'Defaulted',
111
+ art_reason: person['art_reason'],
112
+ cell_number: person['cell_number'],
113
+ district: person['district'],
114
+ ta: person['ta'],
115
+ village: person['village'],
116
+ current_age: person['age'],
117
+ identifier: person['identifier'],
118
+ transferred_out_to: person['transferred_out_to'] || 'N/A',
119
+ outcome_date: person['outcome_date']&.to_date || 'N/A'
120
+ }
121
+ end
122
+
123
+ patients
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Returns all patients alive and on treatment within a given
4
+ # time period (Tx Curr).
5
+ module MalawiHivProgramReports
6
+ module Clinic
7
+ class PatientsAliveAndOnTreatment
8
+ attr_reader :start_date, :end_date
9
+
10
+ include MalawiHivProgramReports::Utils::ConcurrencyUtils
11
+
12
+ def initialize(start_date:, end_date:, outcomes_definition: 'moh', rebuild_outcomes: true, **kwargs)
13
+ @start_date = start_date
14
+ @end_date = end_date
15
+ @rebuild_outcomes = rebuild_outcomes
16
+ @outcomes_definition = outcomes_definition
17
+ @occupation = kwargs[:occupation]
18
+ @location = kwargs[:location]
19
+ end
20
+
21
+ ##
22
+ # Repopulates temp_patient_outcomes and temp_earliest_start_date tables.
23
+ #
24
+ # The data in temp_patient_outcomes can be used to quickly find patients
25
+ # with various outcomes.
26
+ def refresh_outcomes_table
27
+ logger.debug('Initialising cohort temporary tables...')
28
+ MalawiHivProgramReports::Moh::CohortBuilder.new(outcomes_definition: @outcomes_definition, location:@location)
29
+ .init_temporary_tables(@start_date, @end_date, @occupation)
30
+ end
31
+
32
+ def find_report
33
+ query.map(&:patient_id)
34
+ end
35
+
36
+ def query
37
+ with_lock(Cohort::LOCK_FILE) do
38
+ refresh_outcomes_table if @rebuild_outcomes || !outcomes_table_exists?
39
+
40
+ ::Patient.find_by_sql <<~SQL
41
+ SELECT patient_id FROM temp_patient_outcomes WHERE cum_outcome LIKE 'On antiretrovirals'
42
+ SQL
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def outcomes_table_exists?
49
+ ActiveRecord::Base.connection.table_exists?(:temp_patient_outcomes)
50
+ end
51
+
52
+ def logger
53
+ Rails.logger
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Clinic
5
+ class PatientsDueForViralLoad
6
+ attr_accessor :start_date, :end_date
7
+
8
+ def initialize(start_date:, end_date:, **_kwargs)
9
+ @start_date = start_date
10
+ @end_date = end_date
11
+ end
12
+
13
+ def find_report
14
+ patients_alive_and_on_treatment.select do |patient_id|
15
+ patient_viral_load_due?(patient_id)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def patient_viral_load_due?(patient_id)
22
+ reminder = viral_load_reminder(patient_id)
23
+ reminder[:eligibile] # Yes its spelt eligibile not eligible...
24
+ end
25
+
26
+ def patients_alive_and_on_treatment
27
+ PatientsAliveAndOnTreatment
28
+ .new(start_date:, end_date:)
29
+ .find_report
30
+ end
31
+
32
+ def viral_load_reminder(patient_id)
33
+ ArtService::VlReminder
34
+ .new(patient_id:, date: end_date)
35
+ .vl_reminder_info
36
+ end
37
+ end
38
+ end
39
+ end