malawi_hiv_program_reports 1.0.1 → 1.0.3
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 +2337 -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 +205 -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
@@ -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
|