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.
- checksums.yaml +4 -4
- data/app/services/malawi_hiv_program_reports/README.md +16 -0
- data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
- data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
- data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
- data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
- data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
- data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
- data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
- data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
- data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
- data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
- data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
- data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
- data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
- data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
- data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
- data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
- data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
- data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
- data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
- data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
- data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
- data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
- data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
- data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
- data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
- data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
- data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
- data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
- data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
- data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
- data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
- data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
- data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
- data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
- data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
- data/lib/malawi_hiv_program_reports/version.rb +1 -1
- metadata +74 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aac7cd1e88a85ad8992b57e4bdd77351d194ca17fe74fcadf1d181c1e2500ae1
|
4
|
+
data.tar.gz: 7aa0b9e69decce733bb9c1e345d3e3b1e4a436d97be2db28d533a7d8f844db99
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|