malawi_hiv_program_reports 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61e4b91be81b43be794de11234de836a7a8f052b7215270b0b45224ac6fe6574
4
- data.tar.gz: c2032553d4303daa6ee69696470c5ec06bbef933c29b264a588e8161c97be1e6
3
+ metadata.gz: aac7cd1e88a85ad8992b57e4bdd77351d194ca17fe74fcadf1d181c1e2500ae1
4
+ data.tar.gz: 7aa0b9e69decce733bb9c1e345d3e3b1e4a436d97be2db28d533a7d8f844db99
5
5
  SHA512:
6
- metadata.gz: b43032950b1f5c8793c97f0e9339615d303748086a1f1efd6396c1ebd8db8e355ff133ec5027edf3a49f0168ac6980f607d020fd14002bb909be0c9eac9d5cf2
7
- data.tar.gz: afc82ae4cbc88b24ee03ec0810eb5c05223dd56699e41e67c7cce9ab483007fcbdcc961db4923e63b46636f6ddd181c7d94c539c91e050e6a225365bbaa5c210
6
+ metadata.gz: 289ad98c3a4c178e959e80da3265d25c159637896ab363336995d795c10c139bc13fe70f1677993156b5348c381e32dbdf077c8e1ec644b3d6229bbd9f8228a7
7
+ data.tar.gz: 5e71a3c4a72ff1e11cc6e9ae39180270bc4d083cef408d96f42997e71a52019bf826107ceb6dadd39a9eece0f47e8feccaab6fe1aa1c0f6807eac88993293721
@@ -0,0 +1,16 @@
1
+ # About Reports
2
+ This folder contains the reports that are generated for the ART module. There are three main categories of reports in the EMR System namely
3
+ - MOH
4
+ - Clinic
5
+ - PEPFAR
6
+
7
+ ## MOH
8
+ The MOH reports are the reports that are generated for the Ministry of Health. These reports are generated in the format that is required by the Ministry of Health. Some of the reports that fall under other categories may depend on these reports for validations. These reports are requested and vetted by the Ministry. No report should be added to this category without written approval by the Ministry. Below is a list of all reports available in the system
9
+
10
+ ## Clinic
11
+ The clinic reports are the reports that are generated for the clinic. These reports are generated in the format that is required by the clinic. Usually these are requested by the IP's and Clinic Personnel to aid in their daily. Below is a list of all clinic reports available in the system
12
+
13
+ - [Viral Load Result](docs/viral_load_results.md)
14
+
15
+ ### To Do
16
+ ** Some of the reports should be moved to the clinic folder and all references to them updated ** Make sure if we have gems that depends on this they are also update. Highly unlikely though
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Adapters
5
+ module Moh
6
+ # Custom Raw SQL Queries
7
+ module Custom
8
+ # This will hold crucial information for cohort members
9
+ # rubocop:disable Metrics/MethodLength
10
+ def exe_temp_cohort_members_table(adapter:)
11
+ ActiveRecord::Base.connection.execute <<~SQL
12
+ CREATE TABLE temp_cohort_members (
13
+ patient_id INT PRIMARY KEY,
14
+ site_id INT DEFAULT NULL,
15
+ date_enrolled DATE,
16
+ earliest_start_date DATE,
17
+ recorded_start_date DATE DEFAULT NULL,
18
+ birthdate DATE DEFAULT NULL,
19
+ birthdate_estimated BOOLEAN,
20
+ death_date DATE,
21
+ gender VARCHAR(32),
22
+ age_at_initiation INT DEFAULT NULL,
23
+ age_in_days INT DEFAULT NULL,
24
+ reason_for_starting_art INT DEFAULT NULL,
25
+ occupation VARCHAR(255) DEFAULT NULL
26
+ ) #{adapter == 'mysql2' ? 'ENGINE=InnoDB DEFAULT CHARSET=utf8' : ''};
27
+ SQL
28
+ end
29
+
30
+ def exe_create_drill_down_table(adapter:)
31
+ if adapter == 'mysql2'
32
+ ActiveRecord::Base.connection.execute <<~SQL
33
+ CREATE TABLE IF NOT EXISTS cohort_drill_down(
34
+ `reporting_report_design_resource_id` int(11) NOT NULL,
35
+ `patient_id` int(11) NOT NULL,
36
+ `site_id` int(11) DEFAULT 1,
37
+ PRIMARY KEY (`site_id`, `patient_id`, `reporting_report_design_resource_id`),
38
+ KEY `drilldown_report_value` (`reporting_report_design_resource_id`)
39
+ )
40
+ SQL
41
+ else
42
+ ActiveRecord::Base.connection.execute <<~SQL
43
+ CREATE TABLE IF NOT EXISTS cohort_drill_down(
44
+ reporting_report_design_resource_id INT NOT NULL,
45
+ patient_id INT NOT NULL,
46
+ site_id INT DEFAULT 1,
47
+ PRIMARY KEY (site_id, patient_id, reporting_report_design_resource_id)
48
+ );
49
+ SQL
50
+ ActiveRecord::Base.connection.execute <<~SQL
51
+ CREATE INDEX IF NOT EXISTS drilldown_report_value ON cohort_drill_down(reporting_report_design_resource_id);
52
+ SQL
53
+ end
54
+ end
55
+
56
+ def exe_tmp_patient_table(adapter:)
57
+ ActiveRecord::Base.connection.execute <<~SQL
58
+ CREATE TABLE IF NOT EXISTS temp_earliest_start_date (
59
+ patient_id INT PRIMARY KEY,
60
+ site_id INT DEFAULT NULL,
61
+ date_enrolled DATE,
62
+ earliest_start_date DATE,
63
+ recorded_start_date DATE DEFAULT NULL,
64
+ birthdate DATE DEFAULT NULL,
65
+ birthdate_estimated BOOLEAN,
66
+ death_date DATE,
67
+ gender VARCHAR(32),
68
+ age_at_initiation INT DEFAULT NULL,
69
+ age_in_days INT DEFAULT NULL,
70
+ reason_for_starting_art INT DEFAULT NULL
71
+ ) #{adapter == 'mysql2' ? 'ENGINE=InnoDB DEFAULT CHARSET=utf8' : ''};
72
+ SQL
73
+ end
74
+
75
+ def exe_temp_other_patient_types(adapter:)
76
+ ActiveRecord::Base.connection.execute <<~SQL
77
+ CREATE TABLE temp_other_patient_types (
78
+ patient_id INT NOT NULL,
79
+ site_id INT DEFAULT NULL,
80
+ PRIMARY KEY (patient_id)
81
+ ) #{adapter == 'mysql2' ? 'ENGINE=InnoDB DEFAULT CHARSET=utf8' : ''};
82
+ SQL
83
+ end
84
+
85
+ def exe_temp_register_start_date_table(adapter:)
86
+ ActiveRecord::Base.connection.execute <<-SQL
87
+ CREATE TABLE temp_register_start_date (
88
+ patient_id INT NOT NULL,
89
+ site_id INT DEFAULT NULL,
90
+ start_date DATE NOT NULL,
91
+ PRIMARY KEY (patient_id)
92
+ ) #{adapter == 'mysql2' ? 'ENGINE=InnoDB DEFAULT CHARSET=utf8' : ''};
93
+ SQL
94
+ end
95
+
96
+ def exe_temp_order_details_table(adapter:)
97
+ ActiveRecord::Base.connection.execute <<-SQL
98
+ CREATE TABLE temp_order_details (
99
+ patient_id INT NOT NULL,
100
+ site_id INT NOT NULL,
101
+ start_date DATE NOT NULL,
102
+ PRIMARY KEY (patient_id, site_id)
103
+ ) #{adapter == 'mysql2' ? 'ENGINE=InnoDB DEFAULT CHARSET=utf8' : ''};
104
+ SQL
105
+
106
+ # create indexes for the temp_order_details table
107
+ ActiveRecord::Base.connection.execute <<-SQL
108
+ CREATE INDEX tod_patient_id ON temp_order_details(patient_id);
109
+ SQL
110
+
111
+ ActiveRecord::Base.connection.execute <<-SQL
112
+ CREATE INDEX tod_site_id ON temp_order_details(site_id);
113
+ SQL
114
+ end
115
+
116
+ # Generates site filters
117
+ # @operator: string value i.e 'AND', 'WHERE', 'OR'
118
+ # @column: string value
119
+ # @location: integer value
120
+ def site_manager(operator:, column:, location:)
121
+ return '' if location.blank?
122
+
123
+ "#{operator} #{column} = #{location}"
124
+ end
125
+
126
+ def function_manager(function:, location:, args:)
127
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
128
+ args = args.gsub('::int', '').gsub('::date', '') if adapter == 'mysql2'
129
+ args_array = args.split(',')
130
+ args_array = args_array[0...-1] if location.blank?
131
+ "#{function}(#{args_array.join(',')})"
132
+ end
133
+
134
+ # We will be returning an equivalent of interval for both of mysql and postgres
135
+ # @date: date column
136
+ # @value: integer value
137
+ # @interval: string value
138
+ # @operator: string value
139
+ def interval_manager(date:, value:, interval:, operator:)
140
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
141
+ return "DATE('#{date.to_date}') #{operator} INTERVAL #{value} #{interval}" if adapter == 'mysql2'
142
+
143
+ "(DATE('#{date.to_date}') #{operator} INTERVAL '#{value} #{interval}')"
144
+ rescue StandardError
145
+ return "DATE(#{date}) #{operator} INTERVAL #{value} #{interval}" if adapter == 'mysql2'
146
+
147
+ "(DATE(#{date}) #{operator} INTERVAL '#{value} #{interval}')"
148
+ end
149
+
150
+ # This will be used to manage the IN and NOT IN SQL operators
151
+ # @column: string value
152
+ # @values: array or string value
153
+ # @negation: boolean value default to false
154
+ def in_manager(column:, values:, negation: false)
155
+ ActiveRecord::Base.connection.adapter_name.downcase
156
+ values = values.join(',') if values.is_a?(Array)
157
+ return "#{column} NOT IN (#{values})" if negation
158
+
159
+ "#{column} IN (#{values})"
160
+ end
161
+
162
+ def group_by_columns(columns)
163
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
164
+ return ",#{columns}" if adapter != 'mysql2'
165
+
166
+ ''
167
+ end
168
+
169
+ def cast_manager(column:, type:)
170
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
171
+ return column if adapter == 'mysql2'
172
+
173
+ return "CAST(#{column} AS #{type})" if adapter == 'postgresql'
174
+
175
+ column
176
+ end
177
+
178
+ # This will be used to manage the TIMESTAMPDIFF function
179
+ # @date1: string value
180
+ # @date2: string value
181
+ # @interval: string value like 'YEAR', 'MONTH', 'DAY'
182
+ def timestampdiff_manager(date1:, date2:, interval:)
183
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
184
+ return "TIMESTAMPDIFF(#{interval}, #{date1}, #{date2})" if adapter == 'mysql2'
185
+
186
+ "DATE_PART('#{interval}', #{date2}::timestamp - #{date1}::timestamp)" if adapter == 'postgresql'
187
+ end
188
+
189
+ # this is a min filter
190
+ # @occupation: object
191
+ def min_filt(occupation)
192
+ occupation.blank? || occupation.casecmp('all').zero? ? 'WHERE' : 'AND'
193
+ end
194
+
195
+ # rubocop:enable Metrics/MethodLength
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Returns all patients with active filing numbers that are suitable for archival
5
+
6
+ module MalawiHivProgramReports
7
+ class ArchivingCandidates
8
+ def initialize(start_date: nil, **_kwargs)
9
+ @start_date = start_date || Date.today
10
+ end
11
+
12
+ def find_report
13
+ patients = patients_with_adverse_outcomes.to_a
14
+ long_term_defaulters(patients.map { |patient| patient['patient_id'] })
15
+ .each { |defaulter| patients << defaulter }
16
+
17
+ patients
18
+ end
19
+
20
+ private
21
+
22
+ def start_date
23
+ ActiveRecord::Base.connection.quote(@start_date)
24
+ end
25
+
26
+ def patients_with_adverse_outcomes
27
+ ActiveRecord::Base.connection.select_all <<~SQL
28
+ SELECT patient_program.patient_id,
29
+ patient_identifier.identifier AS filing_number,
30
+ concept_name.name AS outcome,
31
+ patient_state.start_date AS outcome_date
32
+ FROM patient_program
33
+ INNER JOIN program
34
+ ON program.program_id = patient_program.program_id
35
+ AND program.name = 'HIV PROGRAM'
36
+ INNER JOIN patient_state
37
+ ON patient_state.patient_program_id = patient_program.patient_program_id
38
+ INNER JOIN program_workflow
39
+ ON program_workflow.program_id = patient_program.program_id
40
+ AND program_workflow.retired = 0
41
+ INNER JOIN program_workflow_state
42
+ ON program_workflow_state.program_workflow_state_id = patient_state.state
43
+ AND program_workflow_state.program_workflow_id = program_workflow.program_workflow_id
44
+ AND program_workflow_state.retired = 0
45
+ INNER JOIN concept_name
46
+ ON concept_name.concept_id = program_workflow_state.concept_id
47
+ AND concept_name.name IN ('Patient died', 'Patient transferred out', 'Treatment stopped')
48
+ AND concept_name.voided = 0
49
+ INNER JOIN (
50
+ SELECT patient_program.patient_program_id,
51
+ MAX(patient_state.start_date) AS outcome_date
52
+ FROM patient_state
53
+ INNER JOIN patient_program
54
+ ON patient_program.patient_program_id = patient_state.patient_program_id
55
+ AND patient_program.voided = 0
56
+ INNER JOIN program
57
+ ON program.program_id = patient_program.program_id
58
+ AND program.name = 'HIV PROGRAM'
59
+ AND program.retired = 0
60
+ WHERE patient_state.voided = 0
61
+ AND patient_state.start_date < DATE(#{start_date}) + INTERVAL 1 DAY
62
+ GROUP BY patient_program.patient_id
63
+ ) AS latest_outcome
64
+ ON latest_outcome.patient_program_id = patient_state.patient_program_id
65
+ AND latest_outcome.outcome_date = patient_state.start_date
66
+ INNER JOIN patient_identifier
67
+ ON patient_identifier.patient_id = patient_program.patient_id
68
+ AND patient_identifier.voided = 0
69
+ INNER JOIN patient_identifier_type
70
+ ON patient_identifier_type.patient_identifier_type_id = patient_identifier.identifier_type
71
+ AND patient_identifier_type.name = 'Filing number'
72
+ AND patient_identifier_type.retired = 0
73
+ WHERE patient_program.voided = 0
74
+ GROUP BY patient_program.patient_id
75
+ SQL
76
+ end
77
+
78
+ ##
79
+ # Returns all patients that haven't been seen in the last 6 months
80
+ # and are defaulters
81
+ def long_term_defaulters(patients_to_exclude = [])
82
+ ActiveRecord::Base.connection.select_all <<~SQL
83
+ SELECT orders.patient_id,
84
+ patient_identifier.identifier AS filing_number,
85
+ 'Defaulted' AS outcome,
86
+ current_defaulter_date(orders.patient_id, #{start_date}) AS outcome_date
87
+ FROM orders
88
+ INNER JOIN order_type
89
+ ON order_type.order_type_id = orders.order_type_id
90
+ AND order_type.name = 'Drug order'
91
+ AND order_type.retired = 0
92
+ INNER JOIN drug_order
93
+ ON drug_order.order_id = orders.order_id
94
+ AND drug_order.quantity > 0
95
+ INNER JOIN arv_drug
96
+ ON arv_drug.drug_id = drug_order.drug_inventory_id
97
+ INNER JOIN (
98
+ SELECT orders.patient_id,
99
+ MAX(auto_expire_date) AS drug_run_out_date
100
+ FROM orders
101
+ INNER JOIN order_type
102
+ ON order_type.order_type_id = orders.order_type_id
103
+ AND order_type.name = 'Drug order'
104
+ AND order_type.retired = 0
105
+ INNER JOIN drug_order
106
+ ON drug_order.order_id = orders.order_id
107
+ AND drug_order.quantity > 0
108
+ INNER JOIN arv_drug
109
+ ON arv_drug.drug_id = drug_order.drug_inventory_id
110
+ WHERE orders.voided = 0
111
+ #{"AND orders.patient_id NOT IN (#{patients_to_exclude.join(',')})" unless patients_to_exclude.empty?}
112
+ GROUP BY orders.patient_id
113
+ LIMIT 100
114
+ ) AS last_patient_drug_order
115
+ ON last_patient_drug_order.patient_id = orders.patient_id
116
+ AND last_patient_drug_order.drug_run_out_date < DATE(#{start_date}) - INTERVAL 6 MONTH
117
+ INNER JOIN patient_identifier
118
+ ON patient_identifier.patient_id = orders.patient_id
119
+ AND patient_identifier.voided = 0
120
+ INNER JOIN patient_identifier_type
121
+ ON patient_identifier_type.patient_identifier_type_id = patient_identifier.identifier_type
122
+ AND patient_identifier_type.name = 'Filing number'
123
+ AND patient_identifier_type.retired = 0
124
+ WHERE orders.voided = 0
125
+ GROUP BY orders.patient_id
126
+ HAVING outcome_date IS NOT NULL
127
+ SQL
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ class ArvRefillPeriods
5
+
6
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
7
+ include MalawiHivProgramReports::Adapters::Moh::Custom
8
+ include MalawiHivProgramReports::Utils::ModelUtils
9
+ include Utils
10
+
11
+ def initialize(start_date:, end_date:, min_age:, max_age:, org:, initialize_tables:, **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
+ @min_age = min_age
15
+ @max_age = max_age
16
+ @org = org
17
+ @initialize_tables = (initialize_tables == 'true')
18
+ @occupation = kwargs[:occupation]
19
+ @location = kwargs[:location]
20
+ end
21
+
22
+ def find_report
23
+ arv_refill_periods
24
+ end
25
+
26
+ def arv_refill_periods
27
+ break_down
28
+ end
29
+
30
+ def tx_mmd_client_level_data(patient_ids)
31
+ client_level_data(patient_ids)
32
+ end
33
+
34
+ private
35
+
36
+ def break_down
37
+ program_id = ::Program.find_by(name: 'HIV PROGRAM').id
38
+ arv_concept_set = ::ConceptName.find_by(name: 'ARVS').concept_id
39
+ encounter_type = ::EncounterType.find_by(name: 'DISPENSING').id
40
+
41
+ if @initialize_tables
42
+ report_type = (/pepfar/i.match?(@org) ? 'pepfar' : 'moh')
43
+ MalawiHivProgramReports::Moh::CohortBuilder.new(outcomes_definition: report_type, location: @location).init_temporary_tables(@start_date,
44
+ @end_date, @occupation)
45
+ end
46
+
47
+ patients = ActiveRecord::Base.connection.select_all <<~SQL
48
+ SELECT
49
+ p.patient_id, p.date_enrolled, p.birthdate, p.gender,
50
+ outcome.cum_outcome AS outcome
51
+ FROM temp_earliest_start_date p
52
+ LEFT JOIN temp_patient_outcomes outcome USING(patient_id)
53
+ WHERE DATE(date_enrolled) <= DATE('#{@end_date}')
54
+ AND TIMESTAMPDIFF(year, p.birthdate, DATE('#{@end_date}')) BETWEEN #{@min_age} AND #{@max_age}
55
+ AND cum_outcome = 'On antiretrovirals';
56
+ SQL
57
+
58
+ return {} if patients.blank?
59
+
60
+ data = []
61
+ patients.each do |p|
62
+ data << [p['patient_id'].to_i, p['gender'], p['birthdate']]
63
+ end
64
+
65
+ results = {}
66
+ (data || []).each do |patient_id, sex, birthdate|
67
+ gender = (sex.blank? ? 'Unknown' : sex)
68
+ if gender != 'Unknown'
69
+ gender = (/F/i.match?(gender) ? 'Female' : 'Male')
70
+ end
71
+
72
+ # birthdate = birthdate
73
+ results[gender] = {} if results[gender].blank?
74
+
75
+ dispensing_info = get_dispensing_info(patient_id,
76
+ encounter_type, arv_concept_set, program_id)
77
+
78
+ results[gender][patient_id] = {
79
+ prescribed_days: dispensing_info,
80
+ birthdate:, gender:
81
+ }
82
+ end
83
+
84
+ results
85
+ end
86
+
87
+ def get_dispensing_info(patient_id, encounter_type,
88
+ arv_concept_set, program_id)
89
+
90
+ data = ActiveRecord::Base.connection.select_all <<~SQL
91
+ SELECT
92
+ o.patient_id, p.gender, p.birthdate, o.start_date,
93
+ o.auto_expire_date, d.name, quantity, d.drug_id,
94
+ TIMESTAMPDIFF(day, DATE(o.start_date), DATE(o.auto_expire_date)) prescribed_days
95
+ FROM orders o
96
+ INNER JOIN drug_order od ON od.order_id = o.order_id
97
+ INNER JOIN drug d ON d.drug_id = od.drug_inventory_id
98
+ INNER JOIN concept_set s ON s.concept_id = d.concept_id
99
+ INNER JOIN person p ON p.person_id = o.patient_id
100
+ INNER JOIN encounter e ON e.patient_id = p.person_id
101
+ WHERE s.concept_set = #{arv_concept_set} AND o.voided = 0
102
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
103
+ AND DATE(o.start_date) = (
104
+ SELECT DATE(MAX(t.start_date)) FROM orders t
105
+ INNER JOIN drug_order t2 ON t2.order_id = t.order_id
106
+ INNER JOIN drug t3 ON t3.drug_id = t2.drug_inventory_id
107
+ INNER JOIN concept_set t4 ON t4.concept_id = t3.concept_id
108
+ WHERE t.patient_id = #{patient_id}
109
+ AND t.voided = 0 AND t.start_date <= '#{@end_date}'
110
+ #{site_manager(operator: 'AND', column: 't.site_id', location: @location)}
111
+ AND t4.concept_set = #{arv_concept_set} AND t2.quantity > 0
112
+ ) AND e.program_id = #{program_id} AND o.patient_id = #{patient_id}
113
+ AND od.quantity > 0 AND e.encounter_type = #{encounter_type}
114
+ GROUP BY o.order_id;
115
+ SQL
116
+
117
+ return if data.blank?
118
+
119
+ regimen_info = ActiveRecord::Base.connection.select_one <<~SQL
120
+ SELECT #{function_manager(function: 'patient_current_regimen', location:@location , args: "#{patient_id}, DATE('#{@end_date}'), #{@location}")} regimen;
121
+ SQL
122
+
123
+ regimen = regimen_info['regimen']
124
+ prescribed_days = nil
125
+
126
+ unless /N/i.match?(regimen)
127
+ weight_sql = get_weight(patient_id)
128
+ regimen_index = regimen.to_i
129
+ moh_regimen_ingredients = ActiveRecord::Base.connection.select_all <<~SQL
130
+ SELECT
131
+ regimen_index, min_weight, max_weight,
132
+ drug_inventory_id, am, pm
133
+ FROM moh_regimens r
134
+ INNER JOIN moh_regimen_ingredient i ON r.regimen_id = i.regimen_id
135
+ AND r.regimen_index = #{regimen_index}
136
+ INNER JOIN moh_regimen_doses d ON i.dose_id = d.dose_id #{weight_sql}
137
+ GROUP BY min_weight, max_weight, drug_inventory_id;
138
+ SQL
139
+
140
+ doses = {}
141
+ (moh_regimen_ingredients || []).each do |i|
142
+ drug_id = i['drug_inventory_id'].to_i
143
+ am = i['am'].to_f
144
+ pm = i['pm'].to_f
145
+ doses[drug_id] = (am.to_f + pm.to_f).to_f
146
+ end
147
+
148
+ unless doses.blank?
149
+ data.each do |info|
150
+ drug_id = info['drug_id'].to_i
151
+ quantity = info['quantity'].to_f
152
+ dose_per_day = doses[drug_id]
153
+ next if dose_per_day.blank?
154
+
155
+ if prescribed_days.blank?
156
+ prescribed_days = (quantity / dose_per_day).to_i
157
+ else
158
+ days = (quantity / dose_per_day).to_i
159
+ prescribed_days = days if days > prescribed_days
160
+ end
161
+ end
162
+ end
163
+
164
+ return prescribed_days unless prescribed_days.blank?
165
+ end
166
+
167
+ data.each do |info|
168
+ days = (info['prescribed_days'].to_i + 1)
169
+ if prescribed_days.blank?
170
+ prescribed_days = days
171
+ elsif days > prescribed_days
172
+ prescribed_days = days
173
+ end
174
+ end
175
+
176
+ prescribed_days
177
+ end
178
+
179
+ def get_weight(patient_id)
180
+ concept_id = ::ConceptName.find_by_name('Weight (Kg)').concept_id
181
+ weight_details = ::Observation.where("person_id = ? AND concept_id = ?
182
+ AND obs_datetime <= ? AND ( CAST(value_numeric as DECIMAL(4,1)) > 0 OR
183
+ CAST(value_text as DECIMAL(4,1)) > 0)", patient_id,
184
+ concept_id, @end_date).order('obs_datetime DESC, date_created DESC')
185
+
186
+ return nil if weight_details.blank?
187
+
188
+ weight_details = weight_details.first
189
+ weight = (weight_details.value_numeric.to_f.positive? ? weight_details.value_numeric.to_f : weight_details.value_text.to_f)
190
+ " WHERE #{weight} >= min_weight AND #{weight} <= max_weight "
191
+ end
192
+
193
+ def client_level_data(patient_ids)
194
+ program_id = ::Program.find_by(name: 'HIV PROGRAM').id
195
+ arv_concept_set = ::ConceptName.find_by(name: 'ARVS').concept_id
196
+ encounter_type = ::EncounterType.find_by(name: 'DISPENSING').id
197
+ identifier_type = ::PatientIdentifierType.find_by_name('ARV number').id
198
+ info = []
199
+
200
+ patient_ids.each do |patient_id|
201
+ info << client_data(patient_id, encounter_type,
202
+ program_id, arv_concept_set, identifier_type)
203
+ end
204
+
205
+ info
206
+ end
207
+
208
+ def client_data(patient_id, _encounter_type, program_id, arv_concept_set, identifier_type)
209
+ info = {}
210
+ info[patient_id] = {}
211
+
212
+ data = ActiveRecord::Base.connection.select_all <<~SQL
213
+ SELECT
214
+ o.patient_id, p.gender, p.birthdate, o.start_date,
215
+ o.auto_expire_date, d.name, quantity, d.drug_id, identifier arv_number,
216
+ TIMESTAMPDIFF(day, DATE(o.start_date), DATE(o.auto_expire_date)) prescribed_days
217
+ FROM orders o
218
+ INNER JOIN drug_order od ON od.order_id = o.order_id
219
+ INNER JOIN drug d ON d.drug_id = od.drug_inventory_id
220
+ INNER JOIN concept_set s ON s.concept_id = d.concept_id
221
+ INNER JOIN person p ON p.person_id = o.patient_id
222
+ INNER JOIN encounter e ON e.patient_id = p.person_id
223
+ AND e.program_id = #{program_id}
224
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
225
+ LEFT JOIN patient_identifier i ON i.patient_id = o.patient_id
226
+ AND i.identifier_type = #{identifier_type}
227
+ AND LENGTH(identifier) > 0 AND i.voided = 0
228
+ WHERE s.concept_set = #{arv_concept_set} AND o.voided = 0
229
+ AND DATE(o.start_date) = (
230
+ SELECT DATE(MAX(t.start_date)) FROM orders t
231
+ INNER JOIN drug_order t2 ON t2.order_id = t.order_id
232
+ INNER JOIN drug t3 ON t3.drug_id = t2.drug_inventory_id
233
+ INNER JOIN concept_set t4 ON t4.concept_id = t3.concept_id
234
+ WHERE t.patient_id = #{patient_id}
235
+ AND t.voided = 0 AND t.start_date <= '#{@end_date}'
236
+ #{site_manager(operator: 'AND', column: 't.site_id', location: @location)}
237
+ AND t4.concept_set = #{arv_concept_set} AND t2.quantity > 0
238
+ )AND e.program_id = #{program_id} AND o.patient_id = #{patient_id}
239
+ AND od.quantity > 0 GROUP BY o.order_id;
240
+ SQL
241
+
242
+ regimen_info = ActiveRecord::Base.connection.select_one <<~SQL
243
+ SELECT #{function_manager(function: 'patient_current_regimen', location: @location, args: "#{patient_id}, DATE('#{@end_date}'), #{@location}")} regimen;
244
+ SQL
245
+
246
+ regimen = regimen_info['regimen']
247
+ regimen = (/N/i.match?(regimen) ? 'Unknown' : regimen)
248
+
249
+ unless /Unknown/i.match?(regimen)
250
+ weight_sql = get_weight(patient_id)
251
+ regimen_index = regimen.to_i
252
+ moh_regimen_ingredients = ActiveRecord::Base.connection.select_all <<~SQL
253
+ SELECT
254
+ regimen_index, min_weight, max_weight,
255
+ drug_inventory_id, am, pm
256
+ FROM moh_regimens r
257
+ INNER JOIN moh_regimen_ingredient i ON r.regimen_id = i.regimen_id
258
+ AND r.regimen_index = #{regimen_index}
259
+ INNER JOIN moh_regimen_doses d ON i.dose_id = d.dose_id #{weight_sql}
260
+ #{site_manager(operator: 'AND', column: 'r.site_id', location: @location)}
261
+ GROUP BY min_weight, max_weight, drug_inventory_id;
262
+ SQL
263
+
264
+ doses = {}
265
+ (moh_regimen_ingredients || []).each do |i|
266
+ drug_id = i['drug_inventory_id'].to_i
267
+ am = i['am'].to_f
268
+ pm = i['pm'].to_f
269
+ doses[drug_id] = (am.to_f + pm.to_f).to_f
270
+ end
271
+ end
272
+
273
+ info[patient_id][regimen] = {}
274
+ data.each do |i|
275
+ drug_id = i['drug_id'].to_i
276
+ quantity = i['quantity'].to_f
277
+ drug_name = i['name']
278
+ start_date = i['start_date'].to_date
279
+ auto_expire_date = i['auto_expire_date']
280
+ dose_per_day = begin
281
+ doses[drug_id]
282
+ rescue StandardError
283
+ 'N/A'
284
+ end
285
+ quantity = quantity.to_f
286
+ arv_number = i['arv_number']
287
+ birthdate = i['birthdate']
288
+
289
+ info[patient_id][regimen][drug_id] = {
290
+ drug_name:,
291
+ start_date: start_date.to_date.strftime('%d/%b/%Y'),
292
+ auto_expire_date: begin
293
+ auto_expire_date.to_date.strftime('%d/%b/%Y')
294
+ rescue StandardError
295
+ nil
296
+ end,
297
+ dose_per_day:,
298
+ quantity:,
299
+ arv_number: (arv_number.blank? ? 'N/A' : arv_number),
300
+ birthdate: begin
301
+ birthdate.to_date.strftime('%d/%b/%Y')
302
+ rescue StandardError
303
+ 'N/A'
304
+ end
305
+ }
306
+ end
307
+
308
+ info
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,5 @@
1
+ # Clinic Reports
2
+ This folder contains the reports that are specific to the ART clinic. Below is the list of the available reports.
3
+
4
+ ## Reports
5
+ - [Hypertension Report](docs/hypertension_report.md)