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,292 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MalawiHivProgramReports
|
4
|
+
module Clinic
|
5
|
+
class ViralLoad
|
6
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
7
|
+
include MalawiHivProgramReports::Utils::ModelUtils
|
8
|
+
|
9
|
+
def initialize(start_date:, end_date:, **kwargs)
|
10
|
+
@start_date = start_date.to_date.strftime('%Y-%m-%d 00:00:00')
|
11
|
+
@end_date = end_date.to_date.strftime('%Y-%m-%d 23:59:59')
|
12
|
+
@program = ::Program.find_by_name 'HIV PROGRAM'
|
13
|
+
@possible_milestones = possible_milestones
|
14
|
+
@use_filing_number = ::GlobalProperty.find_by(property: 'use.filing.numbers')
|
15
|
+
&.property_value
|
16
|
+
&.casecmp?('true')
|
17
|
+
@occupation = kwargs[:occupation]
|
18
|
+
end
|
19
|
+
|
20
|
+
def clients_due
|
21
|
+
clients = potential_get_clients
|
22
|
+
return [] if clients.blank?
|
23
|
+
|
24
|
+
clients_due_list = []
|
25
|
+
|
26
|
+
clients.each do |person|
|
27
|
+
vl_details = get_vl_due_details(person) # person[:patient_id], person[:appointment_date], person[:start_date])
|
28
|
+
next if vl_details.blank?
|
29
|
+
|
30
|
+
clients_due_list << vl_details
|
31
|
+
end
|
32
|
+
|
33
|
+
clients_due_list
|
34
|
+
end
|
35
|
+
|
36
|
+
def vl_results
|
37
|
+
read_results
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def start_date
|
43
|
+
ActiveRecord::Base.connection.quote(@start_date)
|
44
|
+
end
|
45
|
+
|
46
|
+
def end_date
|
47
|
+
ActiveRecord::Base.connection.quote(@end_date)
|
48
|
+
end
|
49
|
+
|
50
|
+
def patient_identifier_type_id
|
51
|
+
identifier_type_name = @use_filing_number ? 'Filing Number' : 'ARV Number'
|
52
|
+
identifier_type = ::PatientIdentifierType.find_by_name!(identifier_type_name)
|
53
|
+
|
54
|
+
ActiveRecord::Base.connection.quote(identifier_type.id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def program_id
|
58
|
+
ActiveRecord::Base.connection.quote(@program.program_id)
|
59
|
+
end
|
60
|
+
|
61
|
+
def closing_states
|
62
|
+
# state_concepts = ::ConceptName.where(name: ['Patient died', 'Patient transferred out', 'Treatment stopped'])
|
63
|
+
# .select(:concept_id)
|
64
|
+
# states = ::ProgramWorkflowState.where(concept_id: state_concepts)
|
65
|
+
# .joins(:program_workflow)
|
66
|
+
# .merge(::ProgramWorkflow.where(program: @program))
|
67
|
+
|
68
|
+
# ::PatientState.joins(:program_workflow_state)
|
69
|
+
# .merge(states)
|
70
|
+
# .select(:state)
|
71
|
+
# .distinct(:state)
|
72
|
+
# .to_sql
|
73
|
+
::ProgramWorkflowState.joins(:program_workflow)
|
74
|
+
.where(initial: 0, terminal: 1,
|
75
|
+
program_workflow: { program_id: @program.id })
|
76
|
+
.select(:program_workflow_state_id).to_sql
|
77
|
+
end
|
78
|
+
|
79
|
+
def potential_get_clients
|
80
|
+
observations = ActiveRecord::Base.connection.select_all <<~SQL
|
81
|
+
SELECT obs.person_id,
|
82
|
+
obs.value_datetime,
|
83
|
+
date_antiretrovirals_started(obs.person_id, NULL) AS start_date,
|
84
|
+
patient_identifier.identifier,
|
85
|
+
person_name.given_name,
|
86
|
+
person_name.family_name,
|
87
|
+
person.birthdate,
|
88
|
+
person.gender
|
89
|
+
FROM obs
|
90
|
+
INNER JOIN encounter
|
91
|
+
ON encounter.encounter_id = obs.encounter_id
|
92
|
+
AND encounter.program_id = #{program_id}
|
93
|
+
AND encounter_type = (
|
94
|
+
SELECT encounter_type_id
|
95
|
+
FROM encounter_type
|
96
|
+
WHERE encounter_type.name = 'Appointment'
|
97
|
+
AND encounter_type.retired = 0
|
98
|
+
LIMIT 1
|
99
|
+
)
|
100
|
+
AND encounter.voided = 0
|
101
|
+
LEFT JOIN person
|
102
|
+
ON person.person_id = obs.person_id
|
103
|
+
AND person.voided = 0
|
104
|
+
LEFT JOIN person_name
|
105
|
+
ON person_name.person_id = obs.person_id
|
106
|
+
AND person_name.voided = 0
|
107
|
+
LEFT JOIN patient_identifier
|
108
|
+
ON patient_identifier.patient_id = obs.person_id
|
109
|
+
AND patient_identifier.identifier_type = #{patient_identifier_type_id}
|
110
|
+
AND patient_identifier.voided = 0
|
111
|
+
INNER JOIN patient_program
|
112
|
+
ON patient_program.program_id = encounter.program_id
|
113
|
+
AND patient_program.patient_id = encounter.patient_id
|
114
|
+
AND patient_program.voided = 0
|
115
|
+
INNER JOIN patient_state
|
116
|
+
ON patient_state.patient_program_id = patient_program.patient_program_id
|
117
|
+
AND patient_state.voided = 0
|
118
|
+
AND patient_state.state NOT IN (#{closing_states})
|
119
|
+
/* Limit states above to most recent states for each patient */
|
120
|
+
INNER JOIN (
|
121
|
+
SELECT patient_state.patient_program_id,
|
122
|
+
MAX(patient_state.start_date) AS start_date
|
123
|
+
FROM patient_state
|
124
|
+
INNER JOIN patient_program
|
125
|
+
ON patient_program.program_id = #{program_id}
|
126
|
+
AND patient_program.voided = 0
|
127
|
+
AND patient_program.patient_program_id = patient_state.patient_program_id
|
128
|
+
WHERE patient_state.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
|
129
|
+
AND patient_state.voided = 0
|
130
|
+
GROUP BY patient_state.patient_program_id
|
131
|
+
) AS patient_recent_state_dates
|
132
|
+
ON patient_recent_state_dates.patient_program_id = patient_state.patient_program_id
|
133
|
+
AND patient_recent_state_dates.start_date = patient_state.start_date
|
134
|
+
LEFT JOIN (#{current_occupation_query}) a ON a.person_id = obs.person_id
|
135
|
+
WHERE obs.concept_id = (
|
136
|
+
SELECT concept_id
|
137
|
+
FROM concept_name
|
138
|
+
WHERE concept_name.name = 'Appointment date'
|
139
|
+
AND concept_name.voided = 0
|
140
|
+
LIMIT 1
|
141
|
+
) #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
|
142
|
+
AND obs.value_datetime >= DATE(#{start_date})
|
143
|
+
AND obs.value_datetime < DATE(#{end_date}) + INTERVAL 1 DAY
|
144
|
+
AND obs.voided = 0
|
145
|
+
GROUP BY obs.person_id
|
146
|
+
ORDER BY obs.value_datetime
|
147
|
+
SQL
|
148
|
+
|
149
|
+
observations.map do |ob|
|
150
|
+
{
|
151
|
+
patient_id: ob['person_id'].to_i,
|
152
|
+
appointment_date: ob['value_datetime'],
|
153
|
+
start_date: ob['start_date'],
|
154
|
+
given_name: ob['given_name'],
|
155
|
+
family_name: ob['family_name'],
|
156
|
+
birthdate: ob['birthdate'],
|
157
|
+
gender: ob['gender'],
|
158
|
+
arv_number: ob['identifier']
|
159
|
+
}
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# patient_id, appointment_date, patient_start_date)
|
164
|
+
def get_vl_due_details(person)
|
165
|
+
patient_start_date = begin
|
166
|
+
person[:start_date].to_date
|
167
|
+
rescue StandardError
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
return if patient_start_date.blank?
|
171
|
+
|
172
|
+
# start_date = patient_start_date
|
173
|
+
appointment_date = person[:appointment_date].to_date
|
174
|
+
# months_on_art = date_diff(patient_start_date.to_date, @end_date.to_date)
|
175
|
+
vl_info = get_vl_due_info(person[:patient_id], appointment_date)
|
176
|
+
months_on_art = vl_info[:period_on_art]
|
177
|
+
|
178
|
+
# if @possible_milestones.include?(months_on_art)
|
179
|
+
return unless vl_info[:eligibile] || (vl_info[:due_date] <= end_date.to_date + 28.day)
|
180
|
+
|
181
|
+
last_result = last_vl_result(person[:patient_id])
|
182
|
+
{
|
183
|
+
patient_id: person[:patient_id],
|
184
|
+
mile_stone: vl_info[:due_date], # (patient_start_date.to_date + months_on_art.month).to_date,
|
185
|
+
start_date: patient_start_date,
|
186
|
+
months_on_art:,
|
187
|
+
appointment_date:,
|
188
|
+
given_name: person[:given_name],
|
189
|
+
family_name: person[:family_name],
|
190
|
+
gender: person[:gender],
|
191
|
+
birthdate: person[:birthdate],
|
192
|
+
arv_number: use_filing_number(person[:patient_id], person[:arv_number]),
|
193
|
+
last_result_order_date: begin
|
194
|
+
last_result.order_date.to_date
|
195
|
+
rescue StandardError
|
196
|
+
'N/A'
|
197
|
+
end,
|
198
|
+
last_result: last_result.result_value,
|
199
|
+
last_result_date: last_result.result_date
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def date_diff(date1, date2)
|
204
|
+
diff_cal = ActiveRecord::Base.connection.select_one <<~SQL
|
205
|
+
SELECT TIMESTAMPDIFF(MONTH, DATE('#{date1.to_date}'), DATE('#{date2.to_date}')) AS months;
|
206
|
+
SQL
|
207
|
+
|
208
|
+
diff_cal['months'].to_i
|
209
|
+
end
|
210
|
+
|
211
|
+
def possible_milestones
|
212
|
+
milestones = [6]
|
213
|
+
start_month = 6
|
214
|
+
|
215
|
+
1.upto(1000).each do |_y|
|
216
|
+
milestones << (start_month += 12)
|
217
|
+
end
|
218
|
+
|
219
|
+
milestones
|
220
|
+
end
|
221
|
+
|
222
|
+
def read_results
|
223
|
+
all_results = LaboratoryService::Reports::Clinic::ProcessedResults.new(start_date: @start_date,
|
224
|
+
end_date: @end_date,
|
225
|
+
occupation: @occupation).read
|
226
|
+
processed_vl_results = []
|
227
|
+
|
228
|
+
all_results.each do |result|
|
229
|
+
measures = result[:measures]
|
230
|
+
measures.each do |measure|
|
231
|
+
next unless measure[:name].include?('viral load')
|
232
|
+
|
233
|
+
processed_vl_results << {
|
234
|
+
accession_number: result[:accession_number],
|
235
|
+
result_date: result[:result_date],
|
236
|
+
patient_id: result[:patient_id],
|
237
|
+
order_date: result[:order_date],
|
238
|
+
specimen: result[:test],
|
239
|
+
gender: result[:gender],
|
240
|
+
arv_number: result[:arv_number],
|
241
|
+
birthdate: result[:birthdate],
|
242
|
+
age_group: result[:age_group],
|
243
|
+
result: measure[:value],
|
244
|
+
result_modifier: measure[:modifier]
|
245
|
+
}
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
processed_vl_results
|
250
|
+
end
|
251
|
+
|
252
|
+
def last_vl_result(patient_id)
|
253
|
+
viral_load_concept = ::ConceptName.where(name: 'HIV Viral Load').select(:concept_id)
|
254
|
+
result_sql = <<~SQL
|
255
|
+
INNER JOIN obs AS parent
|
256
|
+
ON parent.obs_id = obs.obs_group_id
|
257
|
+
AND parent.concept_id IN (SELECT concept_id FROM concept_name WHERE name = 'Lab test result' AND voided = 0)
|
258
|
+
AND parent.voided = 0
|
259
|
+
AND parent.person_id = #{patient_id}
|
260
|
+
SQL
|
261
|
+
|
262
|
+
measure = ::Observation.joins(result_sql)
|
263
|
+
.where(concept: viral_load_concept)
|
264
|
+
.where('(obs.value_numeric IS NOT NULL OR obs.value_text IS NOT NULL)
|
265
|
+
AND obs.obs_datetime < DATE(?) + INTERVAL 1 DAY',
|
266
|
+
@end_date)
|
267
|
+
.order(obs_datetime: :desc)
|
268
|
+
.first
|
269
|
+
|
270
|
+
return OpenStruct.new(order_date: 'N/A', result_date: 'N/A', result_value: 'N/A') unless measure
|
271
|
+
|
272
|
+
OpenStruct.new(order_date: measure&.order&.start_date&.to_date,
|
273
|
+
result_date: measure&.obs_datetime&.to_date,
|
274
|
+
result_value: "#{measure&.value_modifier || '='}#{measure&.value_numeric || measure&.value_text}")
|
275
|
+
end
|
276
|
+
|
277
|
+
def use_filing_number(patient_id, arv_number)
|
278
|
+
return arv_number unless @use_filing_number
|
279
|
+
|
280
|
+
identifier_types = ::PatientIdentifierType.where("name LIKE '%Filing number%'").map(&:patient_identifier_type_id)
|
281
|
+
filing_numbers = PatientIdentifier.where('patient_id = ? AND identifier_type IN(?)',
|
282
|
+
patient_id, identifier_types)
|
283
|
+
filing_numbers.blank? ? '' : filing_numbers.last.identifier
|
284
|
+
end
|
285
|
+
|
286
|
+
def get_vl_due_info(patient_id, appointment_date)
|
287
|
+
vl_info = ArtService::VlReminder.new(patient_id:, date: appointment_date)
|
288
|
+
vl_info.vl_reminder_info
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MalawiHivProgramReports
|
4
|
+
module Clinic
|
5
|
+
class ViralLoadDisaggregated
|
6
|
+
attr_reader :start_date, :end_date, :from, :to
|
7
|
+
|
8
|
+
def initialize(start_date:, end_date:, from: nil, to: nil, **_kwargs)
|
9
|
+
@start_date = start_date.to_date
|
10
|
+
@end_date = end_date.to_date
|
11
|
+
@from = from&.to_f
|
12
|
+
@to = to&.to_f
|
13
|
+
end
|
14
|
+
|
15
|
+
def find_report
|
16
|
+
patients.each_with_object({}) do |patient, report|
|
17
|
+
age_group = find_age_group(patient.birthdate)
|
18
|
+
regimen = patient.regimen&.upcase
|
19
|
+
report[age_group] ||= {}
|
20
|
+
|
21
|
+
if report[age_group].include?(regimen)
|
22
|
+
report[age_group][regimen] += 1
|
23
|
+
else
|
24
|
+
report[age_group][regimen] = 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
AGE_GROUPS = [
|
32
|
+
[1, 4],
|
33
|
+
[5, 9],
|
34
|
+
[10, 14],
|
35
|
+
[15, 19],
|
36
|
+
[20, 24],
|
37
|
+
[25, 29],
|
38
|
+
[30, 34],
|
39
|
+
[35, 39],
|
40
|
+
[40, 44],
|
41
|
+
[45, 49],
|
42
|
+
[50, 54],
|
43
|
+
[55, 59],
|
44
|
+
[60, 64],
|
45
|
+
[65, 69],
|
46
|
+
[70, 74],
|
47
|
+
[75, 79],
|
48
|
+
[80, 84],
|
49
|
+
[85, 89],
|
50
|
+
[90, Float::INFINITY]
|
51
|
+
].freeze
|
52
|
+
|
53
|
+
# Returns all patients with a viral load in the selected range
|
54
|
+
# and the regimens they are on the given point in time.
|
55
|
+
def patients
|
56
|
+
::Observation.select('patient_id, birthdate, patient_current_regimen(patient_id, obs_datetime) as regimen')
|
57
|
+
.joins(:encounter)
|
58
|
+
.joins('INNER JOIN person USING (person_id)')
|
59
|
+
.where(concept_id: ::ConceptName.find_by_name('Viral load').concept_id,
|
60
|
+
obs_datetime: (start_date..end_date),
|
61
|
+
value_numeric: viral_load_range)
|
62
|
+
.merge(::Encounter.where(encounter_type: ::EncounterType.find_by_name('LAB ORDERS').encounter_type_id))
|
63
|
+
.group(:person_id)
|
64
|
+
.order(obs_datetime: :desc)
|
65
|
+
end
|
66
|
+
|
67
|
+
DAYS_IN_YEAR = 365
|
68
|
+
|
69
|
+
def find_age_group(birthdate)
|
70
|
+
return 'Unknown' unless birthdate
|
71
|
+
|
72
|
+
age = (end_date.to_date - birthdate.to_date).to_i / DAYS_IN_YEAR
|
73
|
+
return '<1 year' if age < 1
|
74
|
+
|
75
|
+
start_age, end_age = AGE_GROUPS.find { |start_age, end_age| (start_age..end_age).cover?(age) }
|
76
|
+
|
77
|
+
if end_age == Float::INFINITY
|
78
|
+
"#{start_age} years +"
|
79
|
+
else
|
80
|
+
"#{start_age} - #{end_age} years"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def viral_load_range
|
85
|
+
if from && to
|
86
|
+
from..to
|
87
|
+
elsif from
|
88
|
+
from..Float::INFINITY
|
89
|
+
elsif to
|
90
|
+
0..to
|
91
|
+
else
|
92
|
+
0...1
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Pulls patient's whose most recent viral load result in selected reporting period
|
5
|
+
# is in specified viral load classification.
|
6
|
+
#
|
7
|
+
# Classification are passed as parameter range and are limited to the following
|
8
|
+
# values:
|
9
|
+
# - suppressed
|
10
|
+
# - low-level-viraemia
|
11
|
+
# - viraemia-1000
|
12
|
+
#
|
13
|
+
# The classifications above follow the ART guidelines 2018 Addendum.
|
14
|
+
module MalawiHivProgramReports
|
15
|
+
module Clinic
|
16
|
+
class ViralLoadResults
|
17
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
18
|
+
|
19
|
+
def initialize(start_date:, end_date: nil, range: nil, **kwargs)
|
20
|
+
@start_date = start_date
|
21
|
+
@end_date = end_date
|
22
|
+
@range = range || 'viraemia-1000+'
|
23
|
+
@occupation = kwargs[:occupation]
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_report
|
27
|
+
start_date = ActiveRecord::Base.connection.quote(@start_date)
|
28
|
+
end_date = ActiveRecord::Base.connection.quote(@end_date)
|
29
|
+
|
30
|
+
ActiveRecord::Base.connection.select_all <<~SQL
|
31
|
+
SELECT orders.patient_id,
|
32
|
+
patient_identifier.identifier AS arv_number,
|
33
|
+
person.birthdate AS birthdate,
|
34
|
+
disaggregated_age_group(person.birthdate, #{end_date}) AS age_group,
|
35
|
+
person.gender AS gender,
|
36
|
+
orders.start_date AS order_date,
|
37
|
+
specimen_type.name AS specimen,
|
38
|
+
COALESCE(orders.discontinued_date, orders.start_date) AS specimen_drawn_date,
|
39
|
+
test_results_obs.obs_datetime AS result_date,
|
40
|
+
COALESCE(test_result_measure_obs.value_modifier, '=') AS result_modifier,
|
41
|
+
COALESCE(test_result_measure_obs.value_numeric, test_result_measure_obs.value_text) AS result,
|
42
|
+
patient_current_regimen(orders.patient_id, orders.start_date) AS current_regimen
|
43
|
+
FROM orders
|
44
|
+
INNER JOIN concept_name AS specimen_type
|
45
|
+
ON specimen_type.concept_id = orders.concept_id
|
46
|
+
AND specimen_type.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', 'Plasma')
|
47
|
+
AND specimen_type.voided = 0
|
48
|
+
LEFT JOIN patient_identifier
|
49
|
+
ON patient_identifier.patient_id = orders.patient_id
|
50
|
+
AND patient_identifier.voided = 0
|
51
|
+
AND patient_identifier.identifier_type IN (
|
52
|
+
SELECT patient_identifier_type_id FROM patient_identifier_type WHERE name = 'ARV Number' AND retired = 0
|
53
|
+
)
|
54
|
+
INNER JOIN person
|
55
|
+
ON person.person_id = orders.patient_id
|
56
|
+
AND person.voided = 0
|
57
|
+
LEFT JOIN (#{current_occupation_query}) AS a ON a.person_id = orders.patient_id
|
58
|
+
/* For each lab order find an HIV Viral Load test */
|
59
|
+
INNER JOIN obs AS test_obs
|
60
|
+
ON test_obs.order_id = orders.order_id
|
61
|
+
AND test_obs.concept_id IN (
|
62
|
+
SELECT concept_id FROM concept_name INNER JOIN concept USING (concept_id)
|
63
|
+
WHERE concept_name.name = 'Test type' AND concept.retired = 0 AND concept_name.voided = 0
|
64
|
+
)
|
65
|
+
AND test_obs.value_coded IN (
|
66
|
+
SELECT concept_id FROM concept_name INNER JOIN concept USING (concept_id)
|
67
|
+
WHERE concept_name.name = 'Viral load' AND concept.retired = 0 AND concept_name.voided = 0
|
68
|
+
)
|
69
|
+
AND test_obs.voided = 0
|
70
|
+
/* Select each test's results */
|
71
|
+
INNER JOIN obs AS test_results_obs
|
72
|
+
ON test_results_obs.obs_group_id = test_obs.obs_id
|
73
|
+
AND test_results_obs.concept_id IN (
|
74
|
+
SELECT concept_id FROM concept_name INNER JOIN concept USING (concept_id)
|
75
|
+
WHERE concept_name.name = 'Lab test result' AND concept.retired = 0 AND concept_name.voided = 0
|
76
|
+
)
|
77
|
+
AND test_results_obs.voided = 0
|
78
|
+
AND test_results_obs.obs_datetime >= DATE(#{start_date})
|
79
|
+
AND test_results_obs.obs_datetime < DATE(#{end_date}) + INTERVAL 1 DAY
|
80
|
+
/* Limit the test result's to each patient's most recent result. */
|
81
|
+
INNER JOIN (
|
82
|
+
SELECT MAX(obs_datetime) AS obs_datetime,
|
83
|
+
person_id
|
84
|
+
FROM obs
|
85
|
+
INNER JOIN orders
|
86
|
+
ON orders.order_id = obs.order_id
|
87
|
+
AND orders.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Lab' AND retired = 0)
|
88
|
+
AND orders.concept_id IN (
|
89
|
+
SELECT concept_id FROM concept_name INNER JOIN concept USING (concept_id)
|
90
|
+
WHERE concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', 'Plasma')
|
91
|
+
AND concept.retired = 0 AND concept_name.voided = 0
|
92
|
+
)
|
93
|
+
AND orders.voided = 0
|
94
|
+
WHERE obs.concept_id IN (
|
95
|
+
SELECT concept_id FROM concept_name INNER JOIN concept USING (concept_id)
|
96
|
+
WHERE concept_name.name = 'Lab test result' AND concept.retired = 0 AND concept_name.voided = 0
|
97
|
+
)
|
98
|
+
AND obs.voided = 0
|
99
|
+
AND obs.obs_datetime >= DATE(#{start_date})
|
100
|
+
AND obs.obs_datetime < DATE(#{end_date}) + INTERVAL 1 DAY
|
101
|
+
GROUP BY person_id
|
102
|
+
) AS max_test_results
|
103
|
+
ON max_test_results.obs_datetime = test_results_obs.obs_datetime
|
104
|
+
AND max_test_results.person_id = test_results_obs.person_id
|
105
|
+
/* Find a viral load measure that can be classified as High on the test results */
|
106
|
+
INNER JOIN obs AS test_result_measure_obs
|
107
|
+
ON test_result_measure_obs.obs_group_id = test_results_obs.obs_id
|
108
|
+
AND test_result_measure_obs.concept_id IN (
|
109
|
+
SELECT concept_id FROM concept_name INNER JOIN concept USING (concept_id)
|
110
|
+
WHERE concept_name.name = 'Viral load' AND concept.retired = 0 AND concept_name.voided = 0
|
111
|
+
)
|
112
|
+
AND (test_result_measure_obs.value_numeric IS NOT NULL
|
113
|
+
OR test_result_measure_obs.value_text IS NOT NULL)
|
114
|
+
AND test_result_measure_obs.voided = 0
|
115
|
+
AND (#{query_range})
|
116
|
+
WHERE orders.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Lab' AND retired = 0)
|
117
|
+
AND orders.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
|
118
|
+
GROUP BY orders.patient_id
|
119
|
+
SQL
|
120
|
+
end
|
121
|
+
|
122
|
+
def specimen_types
|
123
|
+
::Concept.joins(:concept_names)
|
124
|
+
.merge(::ConceptName.where(name: ['Blood']))
|
125
|
+
.select(:concept_id)
|
126
|
+
.to_sql
|
127
|
+
end
|
128
|
+
|
129
|
+
def dbs_query_range
|
130
|
+
case @range.downcase
|
131
|
+
when 'suppressed' then <<~SQL
|
132
|
+
|
133
|
+
SQL
|
134
|
+
when 'low-level-viremia' then <<~SQL
|
135
|
+
|
136
|
+
SQL
|
137
|
+
when 'viraemia-1000+' then <<~SQL
|
138
|
+
(test_result_measure_obs.value_numeric >= 1000)
|
139
|
+
SQL
|
140
|
+
else
|
141
|
+
raise ::InvalidParameterError, "Invalid viral load range: #{@range}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def query_range
|
146
|
+
case @range.downcase
|
147
|
+
when 'suppressed' then <<~SQL
|
148
|
+
(/* Plasma/Blood */
|
149
|
+
(specimen_type.name IN ('Blood', 'Plasma')
|
150
|
+
AND ((test_result_measure_obs.value_modifier IN ('<', '=') AND test_result_measure_obs.value_text = 'LDL')
|
151
|
+
OR (test_result_measure_obs.value_modifier = '<' AND test_result_measure_obs.value_numeric IN (20, 40, 150)))
|
152
|
+
OR (test_result_measure_obs.value_numeric >= 20 AND test_result_measure_obs.value_numeric < 200))
|
153
|
+
/* DBS */
|
154
|
+
OR (specimen_type.name IN ('DBS (Free drop to DBS card)', 'DBS (Using capillary tube)')
|
155
|
+
AND (test_result_measure_obs.value_modifier IN ('<', '=') AND test_result_measure_obs.value_text = 'LDL')))
|
156
|
+
SQL
|
157
|
+
when 'low-level-viraemia' then <<~SQL
|
158
|
+
(/* Plasma/Blood */
|
159
|
+
(specimen_type.name IN ('Blood', 'Plasma')
|
160
|
+
AND (test_result_measure_obs.value_numeric >= 200 AND test_result_measure_obs.value_numeric < 1000))
|
161
|
+
/* DBS */
|
162
|
+
OR (specimen_type.name IN ('DBS (Free drop to DBS card)', 'DBS (Using capillary tube)')
|
163
|
+
AND (test_result_measure_obs.value_modifier = '<' AND test_result_measure_obs.value_numeric IN (400, 550, 839))
|
164
|
+
OR (test_result_measure_obs.value_numeric >= 400 AND test_result_measure_obs.value_numeric < 1000)))
|
165
|
+
SQL
|
166
|
+
when 'viraemia-1000' then <<~SQL
|
167
|
+
(test_result_measure_obs.value_numeric >= 1000)
|
168
|
+
SQL
|
169
|
+
else
|
170
|
+
raise ::InvalidParameterError, "Invalid viral load range: #{@range}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MalawiHivProgramReports
|
4
|
+
module Clinic
|
5
|
+
class VisitsReport
|
6
|
+
include MalawiHivProgramReports::Utils::ModelUtils
|
7
|
+
|
8
|
+
def initialize(name:, type:, start_date:, end_date:)
|
9
|
+
@name = name
|
10
|
+
@type = type
|
11
|
+
@start_date = start_date.to_date
|
12
|
+
@end_date = end_date.to_date
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_report
|
16
|
+
visits = (@start_date..@end_date).each_with_object({}) do |date, visits|
|
17
|
+
visits[date] ||= { incomplete: 0, complete: 0 }
|
18
|
+
|
19
|
+
find_visiting_patients(date).each do |patient|
|
20
|
+
if workflow_engine(patient, date).next_encounter
|
21
|
+
visits[date][:incomplete] += 1
|
22
|
+
else
|
23
|
+
visits[date][:complete] += 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
save_report visits
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_report
|
32
|
+
(@start_date..@end_date).each_with_object({}) do |date, parsed_report|
|
33
|
+
report = fetch_report date
|
34
|
+
|
35
|
+
parsed_values = report.values.each_with_object({}) do |report_value, parsed_values|
|
36
|
+
parsed_values[report_value.indicator_name] = report_value.contents.to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
break nil if parsed_values.empty? # Force regeneration of report
|
40
|
+
|
41
|
+
parsed_report[date] = parsed_values
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Returns a list of patients who visited the ART clinic on given day.
|
48
|
+
def find_visiting_patients(date)
|
49
|
+
day_start, day_end = ::TimeUtils.day_bounds(date)
|
50
|
+
query = <<~SQL
|
51
|
+
SELECT patient.* FROM patient INNER JOIN encounter USING (patient_id)
|
52
|
+
WHERE encounter.encounter_datetime BETWEEN ? AND ?
|
53
|
+
AND encounter.encounter_type NOT IN (
|
54
|
+
SELECT encounter_type_id FROM encounter_type WHERE name IN ('LAB', 'LAB ORDER', 'LAB ORDERS', 'LAB RESULTS')
|
55
|
+
)
|
56
|
+
AND encounter.program_id = #{hiv_program.program_id}
|
57
|
+
AND encounter.voided = 0
|
58
|
+
AND patient.voided = 0
|
59
|
+
GROUP BY patient.patient_id
|
60
|
+
SQL
|
61
|
+
|
62
|
+
::Patient.find_by_sql([query, day_start, day_end])
|
63
|
+
end
|
64
|
+
|
65
|
+
def workflow_engine(patient, date)
|
66
|
+
::ArtService::WorkflowEngine.new patient:,
|
67
|
+
program: hiv_program,
|
68
|
+
date:
|
69
|
+
end
|
70
|
+
|
71
|
+
def hiv_program
|
72
|
+
@hiv_program ||= program('HIV PROGRAM')
|
73
|
+
end
|
74
|
+
|
75
|
+
def save_report(visits)
|
76
|
+
visits.each do |date, values|
|
77
|
+
report = fetch_report date
|
78
|
+
|
79
|
+
values.each do |indicator, value|
|
80
|
+
::ReportValue.create name: "#{date} - #{indicator}",
|
81
|
+
indicator_name: indicator,
|
82
|
+
contents: value,
|
83
|
+
content_type: 'integer',
|
84
|
+
report:,
|
85
|
+
creator: ::User.first.user_id
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def fetch_report(date)
|
91
|
+
report = ::Report.find_by type: report_type('Visits'),
|
92
|
+
name: 'Daily visits',
|
93
|
+
start_date: date,
|
94
|
+
end_date: date
|
95
|
+
report || create_report(date)
|
96
|
+
end
|
97
|
+
|
98
|
+
def create_report(date)
|
99
|
+
::Report.create type: fetch_report_type,
|
100
|
+
name: 'Daily visits',
|
101
|
+
start_date: date,
|
102
|
+
end_date: date,
|
103
|
+
renderer_type: 'Plain text',
|
104
|
+
creator: ::User.first.user_id
|
105
|
+
end
|
106
|
+
|
107
|
+
def fetch_report_type
|
108
|
+
type = report_type('Visits')
|
109
|
+
type || ::ReportType.create(name: 'Visits', creator: ::User.current.id)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|