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,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
|