malawi_hiv_program_reports 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MalawiHivProgramReports
|
|
4
|
+
module Pepfar
|
|
5
|
+
##
|
|
6
|
+
# Patients who started TPT just before the start of the current
|
|
7
|
+
# and have finished within the current reporting period.
|
|
8
|
+
class TbPrev3
|
|
9
|
+
|
|
10
|
+
attr_reader :start_date, :end_date, :check_date, :cut_off_point, :occupation, :location
|
|
11
|
+
|
|
12
|
+
include MalawiHivProgramReports::Adapters::Moh::Custom
|
|
13
|
+
include Utils
|
|
14
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
|
15
|
+
|
|
16
|
+
def initialize(start_date:, end_date:, **kwargs)
|
|
17
|
+
@start_date = ActiveRecord::Base.connection.quote(start_date)
|
|
18
|
+
@check_date = start_date.to_date - 6.months
|
|
19
|
+
@cut_off_point = start_date.to_date
|
|
20
|
+
@end_date = ActiveRecord::Base.connection.quote(end_date)
|
|
21
|
+
@occupation = kwargs[:occupation]
|
|
22
|
+
@location = kwargs[:location]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def find_report
|
|
26
|
+
report = init_report
|
|
27
|
+
patients = group_patients_by_tpt_course(patients_on_tpt)
|
|
28
|
+
|
|
29
|
+
load_patients_into_report(report, patients.six_h, '6H') do |patient|
|
|
30
|
+
# 6H has a constant dosage of 1 pill per day
|
|
31
|
+
patient_completed_tpt?(patient, '6H')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
load_patients_into_report(report, patients.three_hp, '3HP') do |patient|
|
|
35
|
+
# 3HP daily dosages vary by patient weight can't use easily use pills
|
|
36
|
+
# to determine course completion
|
|
37
|
+
patient_completed_tpt?(patient, '3HP')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
report
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetch_individual_report(patient_id)
|
|
44
|
+
individual_tpt_report(patient_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def init_report
|
|
50
|
+
pepfar_age_groups.each_with_object({}) do |age_group, report|
|
|
51
|
+
report[age_group] = %w[M F Unknown].each_with_object({}) do |gender, gender_sub_report|
|
|
52
|
+
gender_sub_report[gender] = %w[6H 3HP].each_with_object({}) do |tpt, tpt_sub_report|
|
|
53
|
+
tpt_sub_report[tpt] = {
|
|
54
|
+
started_new_on_art: [],
|
|
55
|
+
started_previously_on_art: [],
|
|
56
|
+
completed_new_on_art: [],
|
|
57
|
+
completed_previously_on_art: []
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def load_patients_into_report(report, patients, tpt, &patient_has_completed_tpt)
|
|
65
|
+
patients.each do |patient|
|
|
66
|
+
next if patient['transfer_in'] == 1 && !patient_has_completed_tpt[patient]
|
|
67
|
+
|
|
68
|
+
age_group = patient['age_group']
|
|
69
|
+
gender = patient['gender']&.first&.upcase || 'Unknown'
|
|
70
|
+
tpt_states = find_patient_tpt_state(patient, &patient_has_completed_tpt)
|
|
71
|
+
|
|
72
|
+
tpt_states.each do |tpt_state|
|
|
73
|
+
report[age_group][gender][tpt][tpt_state] << patient
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def find_patient_tpt_state(patient, &patient_has_completed_tpt)
|
|
79
|
+
if patient_has_completed_tpt[patient]
|
|
80
|
+
return %i[completed_new_on_art] if patient_new_on_art?(patient) && patient['transfer_in'] == 1
|
|
81
|
+
|
|
82
|
+
return %i[started_new_on_art completed_new_on_art] if patient_new_on_art?(patient)
|
|
83
|
+
|
|
84
|
+
return %i[completed_previously_on_art] if patient['transfer_in'] == 1
|
|
85
|
+
|
|
86
|
+
return %i[started_previously_on_art completed_previously_on_art]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return %i[started_new_on_art] if patient_new_on_art?(patient)
|
|
90
|
+
|
|
91
|
+
%i[started_previously_on_art]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def patient_new_on_art?(patient)
|
|
95
|
+
tpt_initiation_date = patient['tpt_initiation_date'].to_date
|
|
96
|
+
art_start_date = patient['art_start_date'].to_date
|
|
97
|
+
|
|
98
|
+
(tpt_initiation_date >= art_start_date) && (tpt_initiation_date < art_start_date + 180.days)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def patients_on_tpt
|
|
102
|
+
clients = fetch_patients_on_tpt.to_a
|
|
103
|
+
results = []
|
|
104
|
+
clients.each do |client|
|
|
105
|
+
next if client['tpt_initiation_date'].to_date > cut_off_point
|
|
106
|
+
|
|
107
|
+
result = individual_tpt_report(client['patient_id'])
|
|
108
|
+
next if result.blank?
|
|
109
|
+
next if result['tpt_initiation_date'].to_date < check_date
|
|
110
|
+
|
|
111
|
+
client['tpt_initiation_date'] = result['tpt_initiation_date']
|
|
112
|
+
client['total_pills_taken'] = result['total_pills_taken']
|
|
113
|
+
client['months_on_tpt'] = result['months_on_tpt']
|
|
114
|
+
client['total_days_on_medication'] = result['total_days_on_medication']
|
|
115
|
+
client['drug_concepts'] = result['drug_concepts']
|
|
116
|
+
client['transfer_in'] = result['transfer_in']
|
|
117
|
+
results << client
|
|
118
|
+
end
|
|
119
|
+
results
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def fetch_patients_on_tpt
|
|
123
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
124
|
+
SELECT person.person_id AS patient_id,
|
|
125
|
+
patient_identifier.identifier AS arv_number,
|
|
126
|
+
DATE(MIN(orders.start_date)) AS tpt_initiation_date,
|
|
127
|
+
#{function_manager(function: 'date_antiretrovirals_started', location: @location, args: "person.person_id::int, MIN(denominator_patient.start_date)::date, #{@location}::int")} AS art_start_date,
|
|
128
|
+
#{function_manager(function: 'patient_outcome', location: @location, args: "person.person_id::int, #{@end_date}::date, #{@location}::int")} AS outcome,
|
|
129
|
+
person.gender,
|
|
130
|
+
person.birthdate,
|
|
131
|
+
#{function_manager(function: 'disaggregated_age_group', location: @location, args: "person.birthdate::date, #{@end_date}::date")} AS age_group
|
|
132
|
+
FROM person
|
|
133
|
+
LEFT JOIN patient_identifier
|
|
134
|
+
ON patient_identifier.patient_id = person.person_id
|
|
135
|
+
AND patient_identifier.voided = 0
|
|
136
|
+
AND #{in_manager(column: 'patient_identifier.identifier_type', values: "(SELECT patient_identifier_type_id FROM patient_identifier_type WHERE name = 'ARV Number')")}
|
|
137
|
+
#{site_manager(operator: 'AND', column: 'patient_identifier.site_id', location: @location)}
|
|
138
|
+
LEFT JOIN (#{current_occupation_query}) AS current_occupation ON current_occupation.person_id = person.person_id
|
|
139
|
+
#{site_manager(operator: 'AND', column: 'current_occupation.site_id', location: @location)}
|
|
140
|
+
INNER JOIN(
|
|
141
|
+
SELECT denominator_encounter.patient_id AS patient_id, patient_state.start_date AS start_date
|
|
142
|
+
FROM person
|
|
143
|
+
INNER JOIN patient_program
|
|
144
|
+
ON patient_program.patient_id = person.person_id
|
|
145
|
+
AND #{in_manager(column: 'patient_program.program_id', values: "(SELECT program_id FROM program WHERE name = 'HIV PROGRAM')")}
|
|
146
|
+
AND patient_program.voided = 0
|
|
147
|
+
INNER JOIN patient_state
|
|
148
|
+
ON patient_state.patient_program_id = patient_program.patient_program_id
|
|
149
|
+
AND patient_state.state = 7 /* State: 7 == On antiretrovirals */
|
|
150
|
+
AND patient_state.start_date < DATE(#{start_date})
|
|
151
|
+
AND patient_state.voided = 0
|
|
152
|
+
INNER JOIN encounter AS denominator_encounter
|
|
153
|
+
ON denominator_encounter.patient_id = patient_program.patient_id
|
|
154
|
+
AND #{in_manager(column: 'denominator_encounter.program_id', values: "(SELECT program_id FROM program WHERE name = 'HIV PROGRAM')")}
|
|
155
|
+
AND #{in_manager(column: 'denominator_encounter.encounter_type', values: "(SELECT encounter_type_id FROM encounter_type WHERE name = 'Treatment')")}
|
|
156
|
+
AND denominator_encounter.encounter_datetime >= #{interval_manager(date: start_date, value: 6, interval: 'MONTH', operator: '-')}
|
|
157
|
+
AND denominator_encounter.encounter_datetime <= DATE(#{start_date})
|
|
158
|
+
AND denominator_encounter.voided = 0
|
|
159
|
+
#{site_manager(operator: 'AND', column: 'patient_program.site_id', location: @location)}
|
|
160
|
+
GROUP BY denominator_encounter.patient_id, patient_state.start_date
|
|
161
|
+
) AS denominator_patient ON denominator_patient.patient_id = person.person_id
|
|
162
|
+
INNER JOIN encounter AS prescription_encounter
|
|
163
|
+
ON prescription_encounter.patient_id = denominator_patient.patient_id
|
|
164
|
+
AND prescription_encounter.program_id = 1
|
|
165
|
+
AND #{in_manager(column: 'prescription_encounter.encounter_type', values: "(SELECT encounter_type_id FROM encounter_type WHERE name = 'Treatment')")}
|
|
166
|
+
AND prescription_encounter.encounter_datetime >= #{interval_manager(date: start_date, value: 6, interval: 'MONTH', operator: '-')}
|
|
167
|
+
AND prescription_encounter.encounter_datetime <= DATE(#{end_date})
|
|
168
|
+
AND prescription_encounter.voided = 0
|
|
169
|
+
INNER JOIN orders
|
|
170
|
+
ON orders.encounter_id = prescription_encounter.encounter_id
|
|
171
|
+
AND #{in_manager(column: 'orders.order_type_id', values: "(SELECT order_type_id FROM order_type WHERE name = 'Drug order')")}
|
|
172
|
+
AND orders.start_date >= #{interval_manager(date: start_date, value: 6, interval: 'MONTH', operator: '-')}
|
|
173
|
+
AND orders.start_date <= CAST(#{end_date} as DATE)
|
|
174
|
+
AND orders.voided = 0
|
|
175
|
+
INNER JOIN concept_name
|
|
176
|
+
ON concept_name.concept_id = orders.concept_id
|
|
177
|
+
AND #{in_manager(column: 'concept_name.name', values: "'Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine'" )}
|
|
178
|
+
INNER JOIN drug_order
|
|
179
|
+
ON drug_order.order_id = orders.order_id
|
|
180
|
+
AND drug_order.quantity > 0
|
|
181
|
+
WHERE person.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'current_occupation', include_clause: false)}
|
|
182
|
+
#{site_manager(operator: 'AND', column: 'person.site_id', location: @location)}
|
|
183
|
+
AND person.person_id NOT IN (
|
|
184
|
+
/* External consultations */
|
|
185
|
+
SELECT DISTINCT registration_encounter.patient_id
|
|
186
|
+
FROM patient_program pp
|
|
187
|
+
INNER JOIN program p ON p.program_id = pp.program_id AND p.name = 'HIV PROGRAM' AND p.retired = 0
|
|
188
|
+
INNER JOIN encounter AS registration_encounter
|
|
189
|
+
ON registration_encounter.patient_id = pp.patient_id
|
|
190
|
+
AND registration_encounter.program_id = pp.program_id
|
|
191
|
+
AND registration_encounter.encounter_datetime < #{interval_manager(date: end_date, value: 1, interval: 'MONTH', operator: '+')}
|
|
192
|
+
AND registration_encounter.voided = 0
|
|
193
|
+
INNER JOIN (
|
|
194
|
+
SELECT MAX(encounter.encounter_datetime) AS encounter_datetime, encounter.patient_id
|
|
195
|
+
FROM encounter
|
|
196
|
+
INNER JOIN encounter_type
|
|
197
|
+
ON encounter_type.encounter_type_id = encounter.encounter_type
|
|
198
|
+
AND encounter_type.name = 'Registration'
|
|
199
|
+
INNER JOIN program
|
|
200
|
+
ON program.program_id = encounter.program_id
|
|
201
|
+
AND program.name = 'HIV PROGRAM'
|
|
202
|
+
WHERE encounter.encounter_datetime < CAST(#{end_date} AS DATE) AND encounter.voided = 0
|
|
203
|
+
GROUP BY encounter.patient_id
|
|
204
|
+
) AS max_registration_encounter
|
|
205
|
+
ON max_registration_encounter.patient_id = registration_encounter.patient_id
|
|
206
|
+
AND max_registration_encounter.encounter_datetime = registration_encounter.encounter_datetime
|
|
207
|
+
INNER JOIN obs AS patient_type_obs
|
|
208
|
+
ON patient_type_obs.encounter_id = registration_encounter.encounter_id
|
|
209
|
+
AND #{in_manager(column: 'patient_type_obs.concept_id', values: "(SELECT concept_id FROM concept_name WHERE name = 'Type of patient' AND voided = 0)")}
|
|
210
|
+
AND #{in_manager(column: 'patient_type_obs.value_coded', values: "(SELECT concept_id FROM concept_name WHERE name IN ('Drug refill', 'External consultation') AND voided = 0)")}
|
|
211
|
+
AND patient_type_obs.voided = 0
|
|
212
|
+
WHERE pp.voided = 0
|
|
213
|
+
)
|
|
214
|
+
GROUP BY person.person_id #{group_by_columns('patient_identifier.identifier, person.gender, person.birthdate, denominator_patient.start_date')}
|
|
215
|
+
SQL
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def individual_tpt_report(patient_id)
|
|
219
|
+
result = process_current_tpt_course_date(patient_id)
|
|
220
|
+
c_start_date = ActiveRecord::Base.connection.quote(result[:start_date])
|
|
221
|
+
c_end_date = ActiveRecord::Base.connection.quote(client_tpt_end_date(patient_id, c_start_date))
|
|
222
|
+
ActiveRecord::Base.connection.select_one <<-SQL
|
|
223
|
+
SELECT
|
|
224
|
+
CASE
|
|
225
|
+
WHEN tpt_transfer_in_obs.value_datetime IS NULL THEN DATE(MIN(o.start_date))
|
|
226
|
+
WHEN tpt_transfer_in_obs.value_datetime > MIN(o.start_date) THEN DATE(MIN(o.start_date))
|
|
227
|
+
ELSE DATE(tpt_transfer_in_obs.value_datetime)
|
|
228
|
+
END AS tpt_initiation_date,
|
|
229
|
+
COUNT(DISTINCT(DATE(o.start_date))) AS months_on_tpt,
|
|
230
|
+
SUM(dor.quantity) + SUM(CASE WHEN tpt_transfer_in_obs.value_numeric IS NOT NULL THEN tpt_transfer_in_obs.value_numeric ELSE 0 END) AS total_pills_taken,
|
|
231
|
+
SUM(DATEDIFF(o.auto_expire_date, o.start_date)) + SUM(CASE WHEN tpt_transfer_in_obs.value_datetime IS NOT NULL THEN DATEDIFF(tpt_transfer_in_obs.obs_datetime, tpt_transfer_in_obs.value_datetime) ElSE 0 END) AS total_days_on_medication,
|
|
232
|
+
GROUP_CONCAT(DISTINCT o.concept_id SEPARATOR ',') AS drug_concepts,
|
|
233
|
+
CASE
|
|
234
|
+
WHEN tpt_transfer_in_obs.value_numeric IS NOT NULL THEN 1
|
|
235
|
+
ELSE 0
|
|
236
|
+
END AS transfer_in,
|
|
237
|
+
MAX(o.start_date) AS last_dispensed_date,
|
|
238
|
+
MAX(o.auto_expire_date) AS auto_expire_date
|
|
239
|
+
FROM orders o
|
|
240
|
+
INNER JOIN concept_name cn
|
|
241
|
+
ON cn.concept_id = o.concept_id
|
|
242
|
+
AND cn.name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')
|
|
243
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
244
|
+
LEFT JOIN obs tpt_transfer_in_obs
|
|
245
|
+
ON tpt_transfer_in_obs.person_id = o.patient_id
|
|
246
|
+
AND tpt_transfer_in_obs.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
|
|
247
|
+
AND tpt_transfer_in_obs.voided = 0
|
|
248
|
+
#{site_manager(operator: 'AND', column: 'tpt_transfer_in_obs.site_id', location: @location)}
|
|
249
|
+
AND tpt_transfer_in_obs.value_drug IN (SELECT drug_id FROM drug WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')))
|
|
250
|
+
INNER JOIN drug_order dor
|
|
251
|
+
ON dor.order_id = o.order_id
|
|
252
|
+
AND dor.quantity > 0
|
|
253
|
+
WHERE DATE(o.start_date) BETWEEN DATE(#{c_start_date}) AND DATE(#{c_end_date})
|
|
254
|
+
AND o.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Drug order')
|
|
255
|
+
AND o.voided = 0
|
|
256
|
+
AND o.patient_id = #{patient_id}
|
|
257
|
+
GROUP BY o.patient_id
|
|
258
|
+
SQL
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def process_current_tpt_course_date(patient_id)
|
|
262
|
+
result = client_tpt_dates(patient_id)
|
|
263
|
+
return { start_date: '1900-01-01', end_date: } if result.blank?
|
|
264
|
+
|
|
265
|
+
sorted_result = result.sort { |a, b| a['start_date'].to_date <=> b['start_date'].to_date }.reverse
|
|
266
|
+
return_date = { start_date: sorted_result.last['start_date'], end_date: }
|
|
267
|
+
|
|
268
|
+
course_interruption = result.first['course'] == '3HP' ? 1 : 2
|
|
269
|
+
# loop through the result array and find the first gap in the dates that equals the course interruption
|
|
270
|
+
sorted_result.each_with_index do |row, index|
|
|
271
|
+
next if index.zero?
|
|
272
|
+
|
|
273
|
+
diff = ActiveRecord::Base.connection.select_one("SELECT TIMESTAMPDIFF(MONTH,DATE('#{row['end_date']}'), DATE('#{sorted_result[index - 1]['start_date']}')) as months")['months']
|
|
274
|
+
|
|
275
|
+
if diff.to_i >= course_interruption
|
|
276
|
+
return_date = { start_date: sorted_result[index - 1]['start_date'], end_date: }
|
|
277
|
+
break
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
return_date
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def client_tpt_dates(patient_id)
|
|
284
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
|
285
|
+
(
|
|
286
|
+
SELECT
|
|
287
|
+
DATE(o.value_datetime) AS start_date,
|
|
288
|
+
DATE(o.obs_datetime) AS end_date,
|
|
289
|
+
CASE
|
|
290
|
+
WHEN count(distinct(o.value_drug)) > 1 THEN '3HP'
|
|
291
|
+
WHEN o.value_drug = #{isoniazid_rifapentine_drug.drug_id} THEN '3HP'
|
|
292
|
+
ELSE '6H'
|
|
293
|
+
END AS course
|
|
294
|
+
FROM obs o
|
|
295
|
+
WHERE o.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
|
|
296
|
+
AND o.voided = 0
|
|
297
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
298
|
+
AND o.value_drug IN (SELECT drug_id FROM drug WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')))
|
|
299
|
+
AND o.person_id = #{patient_id}
|
|
300
|
+
AND o.value_numeric IS NOT NULL
|
|
301
|
+
AND DATE(o.obs_datetime) <= DATE(#{start_date})
|
|
302
|
+
GROUP BY DATE(o.obs_datetime)
|
|
303
|
+
ORDER BY DATE(o.obs_datetime) DESC
|
|
304
|
+
)
|
|
305
|
+
UNION
|
|
306
|
+
(
|
|
307
|
+
SELECT
|
|
308
|
+
DATE(o.start_date) AS start_date,
|
|
309
|
+
DATE(o.auto_expire_date) AS end_date,
|
|
310
|
+
CASE
|
|
311
|
+
WHEN count(distinct(o.concept_id)) > 1 THEN '3HP'
|
|
312
|
+
WHEN o.concept_id = #{isoniazid_rifapentine_concept.concept_id} THEN '3HP'
|
|
313
|
+
ELSE '6H'
|
|
314
|
+
END AS course
|
|
315
|
+
FROM orders o
|
|
316
|
+
INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.program_id = 1 /* HIV PROGRAM */
|
|
317
|
+
INNER JOIN drug_order dor ON dor.order_id = o.order_id AND dor.quantity > 0
|
|
318
|
+
WHERE o.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Drug order')
|
|
319
|
+
AND o.voided = 0
|
|
320
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
321
|
+
AND o.concept_id IN (#{::ConceptName.where(name: ['Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine']).select(:concept_id).to_sql})
|
|
322
|
+
AND o.patient_id = #{patient_id}
|
|
323
|
+
AND o.auto_expire_date IS NOT NULL
|
|
324
|
+
AND DATE(o.start_date) <= DATE(#{start_date})
|
|
325
|
+
GROUP BY DATE(o.start_date)
|
|
326
|
+
ORDER BY DATE(o.start_date) DESC
|
|
327
|
+
)
|
|
328
|
+
SQL
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def client_tpt_end_date(patient_id, start_date)
|
|
332
|
+
# Get patient tpt dispensations dates after the start date and before the end date
|
|
333
|
+
result = ActiveRecord::Base.connection.select_all <<~SQL
|
|
334
|
+
(
|
|
335
|
+
SELECT
|
|
336
|
+
DATE(o.value_datetime) AS start_date,
|
|
337
|
+
DATE(MAX(o.obs_datetime)) AS end_date,
|
|
338
|
+
CASE
|
|
339
|
+
WHEN count(distinct(o.value_drug)) > 1 THEN '3HP'
|
|
340
|
+
WHEN o.value_drug = #{isoniazid_rifapentine_drug.drug_id} THEN '3HP'
|
|
341
|
+
ELSE '6H'
|
|
342
|
+
END AS course
|
|
343
|
+
FROM obs o
|
|
344
|
+
WHERE o.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
|
|
345
|
+
AND o.voided = 0
|
|
346
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
347
|
+
AND o.value_drug IN (SELECT drug_id FROM drug WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')))
|
|
348
|
+
AND o.person_id = #{patient_id}
|
|
349
|
+
AND o.value_numeric IS NOT NULL
|
|
350
|
+
AND DATE(o.obs_datetime) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
|
|
351
|
+
GROUP BY DATE(o.obs_datetime)
|
|
352
|
+
ORDER BY DATE(o.obs_datetime) DESC
|
|
353
|
+
)
|
|
354
|
+
UNION
|
|
355
|
+
(
|
|
356
|
+
SELECT
|
|
357
|
+
DATE(o.start_date) AS start_date,
|
|
358
|
+
DATE(MAX(o.auto_expire_date)) AS end_date,
|
|
359
|
+
CASE
|
|
360
|
+
WHEN count(distinct(o.concept_id)) > 1 THEN '3HP'
|
|
361
|
+
WHEN o.concept_id = #{isoniazid_rifapentine_concept.concept_id} THEN '3HP'
|
|
362
|
+
ELSE '6H'
|
|
363
|
+
END AS course
|
|
364
|
+
FROM orders o
|
|
365
|
+
INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.program_id = 1 /* HIV PROGRAM */
|
|
366
|
+
INNER JOIN drug_order dor ON dor.order_id = o.order_id AND dor.quantity > 0
|
|
367
|
+
WHERE o.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Drug order')
|
|
368
|
+
AND o.voided = 0
|
|
369
|
+
#{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
|
|
370
|
+
AND o.concept_id IN (#{::ConceptName.where(name: ['Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine']).select(:concept_id).to_sql})
|
|
371
|
+
AND o.patient_id = #{patient_id}
|
|
372
|
+
AND o.auto_expire_date IS NOT NULL
|
|
373
|
+
AND DATE(o.start_date) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
|
|
374
|
+
GROUP BY DATE(o.start_date)
|
|
375
|
+
ORDER BY DATE(o.start_date) DESC
|
|
376
|
+
)
|
|
377
|
+
SQL
|
|
378
|
+
|
|
379
|
+
return end_date if result.blank?
|
|
380
|
+
|
|
381
|
+
sorted_result = result.sort { |a, b| a['start_date'].to_date <=> b['start_date'].to_date }
|
|
382
|
+
return_date = sorted_result.last['end_date']
|
|
383
|
+
course_interruption = result.first['course'] == '3HP' ? 1 : 2
|
|
384
|
+
# use a for loop to check if there is a course interruption
|
|
385
|
+
sorted_result.each_with_index do |row, i|
|
|
386
|
+
next if i.zero?
|
|
387
|
+
|
|
388
|
+
if row['course'] != sorted_result[i - 1]['course']
|
|
389
|
+
return_date = sorted_result[i - 1]['end_date']
|
|
390
|
+
break
|
|
391
|
+
end
|
|
392
|
+
diff = ActiveRecord::Base.connection.select_one("SELECT TIMESTAMPDIFF(MONTH,DATE('#{sorted_result[i - 1]['end_date']}'),DATE('#{row['start_date']}')) as months")['months']
|
|
393
|
+
if diff.to_i >= course_interruption
|
|
394
|
+
return_date = sorted_result[i - 1]['end_date']
|
|
395
|
+
break
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
return_date
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
##
|
|
402
|
+
# Groups patients into their TPT categories (ie 6H and 3HP) based on their drugs
|
|
403
|
+
#
|
|
404
|
+
# Returns an object with a three_hp and six_h methods, each of which
|
|
405
|
+
# is an array of patients for that category.
|
|
406
|
+
def group_patients_by_tpt_course(patients)
|
|
407
|
+
patients.each_with_object(OpenStruct.new(six_h: [], three_hp: [])) do |patient, categories|
|
|
408
|
+
if patient_on_3hp?(patient)
|
|
409
|
+
categories.three_hp << patient
|
|
410
|
+
else
|
|
411
|
+
categories.six_h << patient
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def isoniazid_rifapentine_drug
|
|
417
|
+
@isoniazid_rifapentine_drug ||= ::Drug.find_by!(concept_id: isoniazid_rifapentine_concept.concept_id)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MalawiHivProgramReports
|
|
4
|
+
module Pepfar
|
|
5
|
+
# This class is used to generate the TPT Status report for an ART patient
|
|
6
|
+
class TptStatus
|
|
7
|
+
attr_reader :start_date, :end_date, :patient_id
|
|
8
|
+
|
|
9
|
+
include Utils
|
|
10
|
+
|
|
11
|
+
def initialize(start_date:, end_date:, **kwargs)
|
|
12
|
+
@start_date = start_date
|
|
13
|
+
@end_date = end_date
|
|
14
|
+
@patient_id = kwargs[:patient_id]
|
|
15
|
+
@tpt_status = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def find_report
|
|
19
|
+
patient_tpt_status
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
Rails.logger.error("Error generating TPT Status report for patient #{patient_id}: #{e.message}")
|
|
22
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
23
|
+
raise e
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def patient_tpt_status
|
|
29
|
+
return tb_treatment_status if patient_on_tb_treatment?(patient_id)
|
|
30
|
+
return completed_tpt_status if patient_history_on_completed_tpt
|
|
31
|
+
|
|
32
|
+
patient = TbPrev3.new(start_date:, end_date:).fetch_individual_report(patient_id)
|
|
33
|
+
return default_status if patient.blank?
|
|
34
|
+
|
|
35
|
+
tpt_status_based_on_patient(patient)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tb_treatment_status
|
|
39
|
+
{
|
|
40
|
+
tpt: nil,
|
|
41
|
+
completed: false,
|
|
42
|
+
tb_treatment: true,
|
|
43
|
+
tpt_init_date: nil,
|
|
44
|
+
tpt_complete_date: nil,
|
|
45
|
+
eligible: {
|
|
46
|
+
'3HP': false,
|
|
47
|
+
'6H': false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def completed_tpt_status
|
|
53
|
+
tpt = patient_history_on_completed_tpt.include?('IPT') ? '6H' : '3HP'
|
|
54
|
+
{ tpt:, completed: true, tb_treatment: false, tpt_init_date: nil, tpt_complete_date: nil, tpt_end_date: nil,
|
|
55
|
+
eligible: {
|
|
56
|
+
'3HP': false,
|
|
57
|
+
'6H': false
|
|
58
|
+
} }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def default_status
|
|
62
|
+
patient = ::Patient.find(patient_id)
|
|
63
|
+
art_start_date = patient.art_start_date
|
|
64
|
+
{ tpt: nil, completed: false, tb_treatment: false, tpt_init_date: nil, tpt_complete_date: nil, tpt_end_date: nil,
|
|
65
|
+
eligible: {
|
|
66
|
+
'3HP': art_start_date ? difference_in_months(end_date.to_date, art_start_date.to_date) < 3 : true,
|
|
67
|
+
'6H': art_start_date ? difference_in_months(end_date.to_date, art_start_date.to_date) < 3 : true
|
|
68
|
+
} }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tpt_status_based_on_patient(patient)
|
|
72
|
+
tpt = determine_tpt(patient)
|
|
73
|
+
completed = patient_has_totally_completed_tpt?(patient, tpt)
|
|
74
|
+
tpt_init_date = patient['tpt_initiation_date']
|
|
75
|
+
tpt_current_expiry_date = patient['auto_expire_date']&.to_date
|
|
76
|
+
diff_in_months = difference_in_months(end_date.to_date, tpt_current_expiry_date)
|
|
77
|
+
art_start_date = ::Patient.find(patient_id).art_start_date
|
|
78
|
+
tpt_complete_date = completed ? patient['auto_expire_date']&.to_date : nil
|
|
79
|
+
tpt_end_date = calculate_tpt_end_date(tpt, tpt_init_date)
|
|
80
|
+
tpt_name = determine_tpt_name(tpt, patient)
|
|
81
|
+
arv_drug_runout_date = patient_arv_drug_runout_date
|
|
82
|
+
|
|
83
|
+
@tpt_status.merge!({ tpt: tpt_name, completed:, tb_treatment: false,
|
|
84
|
+
tpt_init_date:, tpt_complete_date:,
|
|
85
|
+
tpt_end_date:, art_start_date:,
|
|
86
|
+
art_drug_auto_expire_date: arv_drug_runout_date })
|
|
87
|
+
determine_eligibility(tpt, diff_in_months, art_start_date, arv_drug_runout_date)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def determine_tpt(patient)
|
|
91
|
+
patient_on_3hp?(patient) ? '3HP' : '6H'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def determine_tpt_name(tpt, patient)
|
|
95
|
+
if tpt == '6H'
|
|
96
|
+
'IPT'
|
|
97
|
+
else
|
|
98
|
+
(patient['drug_concepts'].split(',').length > 1 ? '3HP (RFP + INH)' : 'INH 300 / RFP 300 (3HP)')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def calculate_tpt_end_date(tpt, tpt_init_date)
|
|
103
|
+
tpt == '6H' ? tpt_init_date + 6.months : tpt_init_date + 3.months
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def determine_eligibility(tpt, diff_in_months, art_start_date, arv_drug_runout_date)
|
|
107
|
+
three_hp_eligible = false
|
|
108
|
+
six_h_eligible = false
|
|
109
|
+
tpt_init_date = @tpt_status[:tpt_init_date]
|
|
110
|
+
tpt_end_date = @tpt_status[:tpt_end_date]
|
|
111
|
+
case tpt
|
|
112
|
+
when '3HP'
|
|
113
|
+
# 3HP is taken 1 dose per week
|
|
114
|
+
# if client misses dose for less than a month, they are eligible
|
|
115
|
+
# if client misses more than a month:
|
|
116
|
+
# check if they have been on ART for less than 3 months, they are eligible
|
|
117
|
+
# if they have been on ART continuosly for more than 3 months, they are not eligible
|
|
118
|
+
three_hp_eligible = true if diff_in_months <= 1
|
|
119
|
+
if diff_in_months > 1 && (arv_drug_runout_date && difference_in_months(arv_drug_runout_date.to_date,
|
|
120
|
+
art_start_date.to_date) < 3)
|
|
121
|
+
# Patient defaulted for ART and TPT and was on ART for less than 3 months: patient TPT status is reset
|
|
122
|
+
three_hp_eligible = true
|
|
123
|
+
six_h_eligible = true
|
|
124
|
+
tpt_end_date = nil
|
|
125
|
+
tpt_init_date = nil
|
|
126
|
+
@tpt_status[:tpt] = nil
|
|
127
|
+
end
|
|
128
|
+
when '6H'
|
|
129
|
+
# 6H is taken 1 dose per day
|
|
130
|
+
# if client misses dose for less than 2 months, they are eligible
|
|
131
|
+
# if client misses more than a month:
|
|
132
|
+
# check if they have been on ART for less than 3 months, they are eligible
|
|
133
|
+
# if they have been on ART continuosly for more than 3 months, they are not eligible
|
|
134
|
+
six_h_eligible = true if diff_in_months <= 2
|
|
135
|
+
if diff_in_months > 2 && (arv_drug_runout_date && difference_in_months(arv_drug_runout_date.to_date,
|
|
136
|
+
art_start_date.to_date) < 3)
|
|
137
|
+
# Patient defaulted for ART and TPT and was on ART for less than 3 months: patient TPT status is reset
|
|
138
|
+
three_hp_eligible = true
|
|
139
|
+
six_h_eligible = true
|
|
140
|
+
tpt_end_date = nil
|
|
141
|
+
tpt_init_date = nil
|
|
142
|
+
@tpt_status[:tpt] = nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
@tpt_status.merge!({
|
|
146
|
+
tpt_init_date:,
|
|
147
|
+
tpt_end_date:,
|
|
148
|
+
eligible: {
|
|
149
|
+
'3HP': three_hp_eligible,
|
|
150
|
+
'6H': six_h_eligible
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def patient_has_totally_completed_tpt?(patient, tpt)
|
|
156
|
+
if tpt == '3HP'
|
|
157
|
+
init_date = patient['tpt_initiation_date'].to_date
|
|
158
|
+
end_date = patient['auto_expire_date'].to_date
|
|
159
|
+
days_on_medication = (end_date - init_date).to_i
|
|
160
|
+
days_on_medication >= 80
|
|
161
|
+
else
|
|
162
|
+
patient['total_days_on_medication'].to_i >= 176
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def patient_arv_drug_runout_date
|
|
167
|
+
::Patient.find(patient_id).last_arv_drug_expire_date
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def patient_history_on_completed_tpt
|
|
171
|
+
@patient_history_on_completed_tpt ||= ::Observation.where(person_id: patient_id,
|
|
172
|
+
concept_id: ::ConceptName.find_by_name('Previous TB treatment history').concept_id)
|
|
173
|
+
.where("value_text LIKE '%complete%' AND obs_datetime < DATE('#{end_date}') + INTERVAL 1 DAY")&.first&.value_text
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def difference_in_months(date1, date2)
|
|
177
|
+
((date1.year * 12) + date1.month) - ((date2.year * 12) + date2.month)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|