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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/malawi_hiv_program_reports/README.md +16 -0
  3. data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
  4. data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
  5. data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
  6. data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
  7. data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
  8. data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
  9. data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
  10. data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
  11. data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
  12. data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
  13. data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
  14. data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
  15. data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
  16. data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
  17. data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
  18. data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
  19. data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
  20. data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
  21. data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
  22. data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
  23. data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
  24. data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
  25. data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
  26. data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
  27. data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
  28. data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
  29. data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
  30. data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
  31. data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
  32. data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
  33. data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
  34. data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
  35. data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
  36. data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
  37. data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
  38. data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
  39. data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
  40. data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
  41. data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
  42. data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
  43. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
  44. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
  45. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
  46. data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
  47. data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
  48. data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
  49. data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
  50. data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
  51. data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
  52. data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
  53. data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
  54. data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
  55. data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
  56. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
  57. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
  58. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
  59. data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
  60. data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
  61. data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
  62. data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
  63. data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
  64. data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
  65. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
  66. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
  67. data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
  68. data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
  69. data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
  70. data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
  71. data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
  72. data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
  73. data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
  74. data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
  75. data/lib/malawi_hiv_program_reports/version.rb +1 -1
  76. metadata +74 -1
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # TB Preventive Therapy indicators for ART cohort
5
+ module MalawiHivProgramReports
6
+ module Cohort
7
+ class Tpt
8
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
9
+ include MalawiHivProgramReports::Adapters::Moh::Custom
10
+
11
+ def initialize(start_date:, end_date:, **kwargs)
12
+ @start_date = start_date
13
+ @end_date = end_date
14
+ @occupation = kwargs[:occupation]
15
+ @location = kwargs[:location]
16
+ @adapter = ActiveRecord::Base.connection.adapter_name.downcase
17
+ return unless @occupation.present?
18
+
19
+ process_occupation(start_date: @start_date, end_date: @end_date,
20
+ occupation: @occupation, location: @location)
21
+ end
22
+
23
+ ##
24
+ # Patients (re-)initiated on 3HP in current reporting period.
25
+ #
26
+ # Candidates for this indicator are patients who either have
27
+ # had their first dispensation in the current reporting period
28
+ # or patients who have restarted 3HP in the current reporting
29
+ # period after breaking from the course for a period of at least
30
+ # 9 months (3 quarters).
31
+ def newly_initiated_on_3hp
32
+ # newly_initiated_on_tpt(start_date, end_date).each_with_object([]) do |patient, patients|
33
+ # patients << patient['patient_id'] unless patient_on_3hp?(patient)
34
+ # end
35
+ processed_tpt_clients.select { |patient| patient_on_3hp?(patient) }
36
+ end
37
+
38
+ ##
39
+ # Patients (re-)initiated on IPT in current reporting period
40
+ #
41
+ # Has a similar definition to 3HP, please refer to 3HP docs
42
+ # above.
43
+ def newly_initiated_on_ipt
44
+ # newly_initiated_on_tpt(start_date, end_date).each_with_object([]) do |patient, patients|
45
+ # patients << patient['patient_id'] if patient_on_3hp?(patient)
46
+ # end
47
+ processed_tpt_clients.reject { |patient| patient_on_3hp?(patient) }
48
+ end
49
+
50
+ private
51
+
52
+ def patient_on_3hp?(patient)
53
+ drug_concepts = patient['drug_concepts'].split(',').collect(&:to_i)
54
+ drug_concepts.intersect?([rifapentine_concept.concept_id, three_hp_concept&.concept_id])
55
+ end
56
+
57
+ def rifapentine_concept
58
+ @rifapentine_concept ||= ::ConceptName.find_by!(name: 'Rifapentine')
59
+ end
60
+
61
+ def three_hp_concept
62
+ @three_hp_concept ||= ::ConceptName.find_by!(name: 'Isoniazid/Rifapentine')
63
+ end
64
+
65
+ def processed_tpt_clients
66
+ @processed_tpt_clients ||= process_tpt_clients
67
+ end
68
+
69
+ def process_tpt_clients
70
+ patients = []
71
+ newly_initiated_on_tpt.each do |patient|
72
+ course = patient['course'].include?('3HP') ? '3HP' : 'IPT'
73
+ next if patient['last_course'].present?
74
+ if patient['tpt_initial_start_date'].present? && patient['tpt_initial_start_date'].to_date < @start_date.to_date
75
+ next
76
+ end
77
+
78
+ if patient['transfer_course'].blank? && patient['last_course'].blank?
79
+ patients << patient
80
+ elsif patient['transfer_course'].present? && patient['last_course'].blank?
81
+ patients << patient if patient['months_since_tpt_transfer'].to_i >= (course == '3HP' ? 1 : 2)
82
+ elsif patient['transfer_course'].blank? && patient['last_course'].present?
83
+ patients << patient if patient['months_since_last_tpt'].to_i >= (course == '3HP' ? 1 : 2)
84
+ elsif patient['last_tpt_end_date'].to_date >= patient['transfer_end_date'].to_date
85
+ patients << patient if patient['months_since_last_tpt'].to_i >= (course == '3HP' ? 1 : 2)
86
+ elsif patient['last_tpt_end_date'].to_date < patient['transfer_end_date'].to_date
87
+ patients << patient if patient['months_since_tpt_transfer'].to_i >= (course == '3HP' ? 1 : 2)
88
+ end
89
+ end
90
+ patients
91
+ end
92
+
93
+ def newly_initiated_on_tpt
94
+ start_date = ActiveRecord::Base.connection.quote(@start_date)
95
+ end_date = ActiveRecord::Base.connection.quote(@end_date)
96
+
97
+ @newly_initiated_on_tpt ||= ActiveRecord::Base.connection.select_all <<~SQL
98
+ SELECT
99
+ cohort_patients.patient_id,
100
+ cohort_patients.earliest_start_date,
101
+ MIN(orders.start_date )as tpt_start_date,
102
+ #{@adapter == 'mysql2' ? "GROUP_CONCAT(DISTINCT orders.concept_id SEPARATOR ',') AS drug_concepts," : "(SELECT STRING_AGG(concept_id::VARCHAR, ',') FROM (SELECT DISTINCT orders.concept_id FROM orders) AS subquery) AS drug_concepts,"}
103
+ CASE
104
+ WHEN count(distinct(orders.concept_id)) > 1 THEN '3HP old'
105
+ WHEN orders.concept_id = #{three_hp_concept.concept_id} THEN '3HP new'
106
+ ELSE '6H'
107
+ END AS course,
108
+ tpt_transfer_in_obs.value_datetime AS tpt_initial_start_date,
109
+ CASE
110
+ WHEN tpt_transfer_in_obs.concept_id IS NULL THEN NULL
111
+ WHEN count(distinct(tpt_transfer_in_obs.concept_id)) > 1 THEN '3HP old'
112
+ WHEN tpt_transfer_in_obs.concept_id = #{three_hp_concept.concept_id} THEN '3HP new'
113
+ ELSE '6H'
114
+ END AS transfer_course,
115
+ tpt_transfer_in_obs.obs_datetime AS transfer_end_date,
116
+ tpt_transfer_in_obs.value_numeric AS transfer_amount,
117
+ #{@adapter == 'mysql2' ? 'TIMESTAMPDIFF(MONTH, tpt_transfer_in_obs.obs_datetime, MIN(orders.start_date)) AS months_since_tpt_transfer,' : '(EXTRACT(YEAR FROM AGE(MIN(orders.start_date), tpt_transfer_in_obs.obs_datetime)) * 12 + EXTRACT(MONTH FROM AGE(MIN(orders.start_date), tpt_transfer_in_obs.obs_datetime))) AS months_since_tpt_transfer,'}
118
+ #{@adapter == 'mysql2' ? 'TIMESTAMPDIFF(MONTH, last_tpt_prescription.auto_expire_date, MIN(orders.start_date)) AS months_since_last_tpt,' : '(EXTRACT(YEAR FROM AGE(MIN(orders.start_date), last_tpt_prescription.auto_expire_date)) * 12 + EXTRACT(MONTH FROM AGE(MIN(orders.start_date), last_tpt_prescription.auto_expire_date))) AS months_since_last_tpt,'}
119
+ last_tpt_prescription.course AS last_course,
120
+ last_tpt_prescription.start_date AS last_tpt_start_date,
121
+ last_tpt_prescription.auto_expire_date AS last_tpt_end_date
122
+ FROM temp_earliest_start_date AS cohort_patients
123
+ INNER JOIN orders
124
+ ON orders.patient_id = cohort_patients.patient_id
125
+ AND orders.order_type_id = (SELECT order_type_id FROM order_type WHERE name = 'Drug order' LIMIT 1)
126
+ AND orders.start_date >= #{start_date}
127
+ AND orders.start_date < #{interval_manager(date: end_date, value: 1, interval: 'DAY', operator: '+')}
128
+ AND orders.voided = 0 #{site_manager(operator: 'AND', column: 'orders.site_id', location: @location)}
129
+ INNER JOIN concept_name AS tpt_drug_concepts
130
+ ON tpt_drug_concepts.concept_id = orders.concept_id
131
+ AND tpt_drug_concepts.name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')
132
+ AND tpt_drug_concepts.voided = 0
133
+ INNER JOIN drug_order AS drug_orders
134
+ ON drug_orders.order_id = orders.order_id
135
+ AND drug_orders.quantity > 0 #{site_manager(operator: 'AND', column: 'drug_orders.site_id', location: @location)}
136
+ INNER JOIN encounter
137
+ /* Ensure we are dealing with ART prescriptions (Treatment encounter) */
138
+ ON encounter.encounter_id = orders.encounter_id
139
+ AND encounter.encounter_type = (SELECT encounter_type_id FROM encounter_type WHERE name = 'Treatment' LIMIT 1)
140
+ AND encounter.program_id = (SELECT program_id FROM program WHERE name = 'HIV PROGRAM' LIMIT 1)
141
+ AND encounter.voided = 0
142
+ LEFT JOIN obs tpt_transfer_in_obs
143
+ ON tpt_transfer_in_obs.person_id = orders.patient_id
144
+ AND tpt_transfer_in_obs.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
145
+ AND tpt_transfer_in_obs.voided = 0 #{site_manager(operator: 'AND', column: 'tpt_transfer_in_obs.site_id', location: @location)}
146
+ 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')))
147
+ -- Get the last TPT prescription
148
+ LEFT JOIN (
149
+ SELECT
150
+ o.patient_id,
151
+ MAX(o.start_date) AS start_date,
152
+ MAX(o.auto_expire_date) AS auto_expire_date,
153
+ CASE
154
+ WHEN count(distinct(o.concept_id)) > 1 THEN '3HP old'
155
+ WHEN o.concept_id = #{three_hp_concept.concept_id} THEN '3HP new'
156
+ ELSE '6H'
157
+ END AS course
158
+ FROM temp_earliest_start_date
159
+ INNER JOIN orders o ON o.patient_id = temp_earliest_start_date.patient_id #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
160
+ INNER JOIN concept_name AS tpt_drug_concepts ON tpt_drug_concepts.concept_id = o.concept_id AND tpt_drug_concepts.name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine') AND tpt_drug_concepts.voided = 0
161
+ INNER JOIN drug_order AS drug_orders ON drug_orders.order_id = o.order_id AND drug_orders.quantity > 0 #{site_manager(operator: 'AND', column: 'drug_orders.site_id', location: @location)}
162
+ INNER JOIN encounter ON encounter.encounter_id = o.encounter_id AND encounter.encounter_type = (SELECT encounter_type_id FROM encounter_type WHERE LOWER(name) = Lower('Treatment') LIMIT 1) AND encounter.program_id = (SELECT program_id FROM program WHERE LOWER(name) = LOWER('HIV Program') LIMIT 1) AND encounter.voided = 0
163
+ WHERE o.voided = 0 #{site_manager(operator: 'AND', column: 'temp_earliest_start_date.site_id', location: @location)}
164
+ AND o.start_date < #{start_date}
165
+ GROUP BY o.patient_id, o.concept_id
166
+ ) AS last_tpt_prescription ON last_tpt_prescription.patient_id = orders.patient_id
167
+ GROUP BY cohort_patients.patient_id #{@adapter == 'mysql2' ? '' : ', tpt_transfer_in_obs.concept_id, tpt_transfer_in_obs.obs_datetime, tpt_transfer_in_obs.value_numeric, last_tpt_prescription.course, last_tpt_prescription.start_date, last_tpt_prescription.auto_expire_date, orders.concept_id, tpt_transfer_in_obs.value_datetime, tpt_transfer_in_obs.concept_id'}
168
+ SQL
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Moh
5
+ # Cohort report builder class.
6
+ #
7
+ # This class only provides one public method (start_build_report) besides
8
+ # the constructor. This method must be called to build report and save
9
+ # it to database.
10
+ # rubocop:disable Metrics/ClassLength
11
+ class Cohort
12
+ include MalawiHivProgramReports::Utils::ConcurrencyUtils
13
+ include MalawiHivProgramReports::Utils::ModelUtils
14
+ include MalawiHivProgramReports::Adapters::Moh::Custom
15
+
16
+ LOCK_FILE = 'art_service/reports/cohort.lock'
17
+
18
+ def initialize(name:, type:, start_date:, end_date:, **kwargs)
19
+ @name = name
20
+ @start_date = start_date
21
+ @end_date = end_date
22
+ @type = type
23
+ @occupation = kwargs[:occupation]
24
+ @location = kwargs[:location]
25
+ @cohort_builder = CohortBuilder.new(outcomes_definition: 'moh', **kwargs)
26
+ @cohort_struct = CohortStruct.new
27
+ @location_name = @location && Location.find(@location).name
28
+ end
29
+
30
+ def build_report
31
+ with_lock(LOCK_FILE, blocking: false) do
32
+ init_drill_down_table
33
+ clear_drill_down
34
+ @cohort_builder.build(@cohort_struct, @start_date, @end_date, @occupation)
35
+ save_report
36
+ end
37
+ rescue ::FailedToAcquireLock => e
38
+ Rails.logger.warn("ART#Cohort report is locked by another process: #{e}")
39
+ end
40
+
41
+ def find_report
42
+ ::Report.where(type: @type, name: "#{@name}#{"-#{@occupation}"}#{"-#{@location_name}"}",
43
+ start_date: @start_date, end_date: @end_date)
44
+ .order(date_created: :desc)
45
+ .first
46
+ end
47
+
48
+ def defaulter_list(pepfar)
49
+ report_type = (pepfar ? 'pepfar' : 'moh')
50
+ defaulter_date_sql = pepfar ? 'current_pepfar_defaulter_date' : 'current_defaulter_date'
51
+ CohortBuilder.new(outcomes_definition: report_type, location: @location)
52
+ .init_temporary_tables(@start_date, @end_date, @occupation)
53
+
54
+ data = ActiveRecord::Base.connection.select_all <<~SQL
55
+ SELECT
56
+ e.patient_id, i.identifier arv_number, e.birthdate,
57
+ e.gender, n.given_name, n.family_name,
58
+ art_reason.name art_reason, a.value cell_number, landmark.value landmark,
59
+ s.state_province district, s.county_district ta,
60
+ s.city_village village, TIMESTAMPDIFF(year, DATE(e.birthdate), DATE('#{@end_date}')) age,
61
+ #{defaulter_date_sql}(e.patient_id, TIMESTAMP('#{@end_date.to_date.strftime('%Y-%m-%d 23:59:59')}')) AS defaulter_date,
62
+ appointment.appointment_date AS appointment_date
63
+ FROM temp_earliest_start_date e
64
+ INNER JOIN temp_patient_outcomes o ON e.patient_id = o.patient_id
65
+ INNER JOIN (
66
+ SELECT e.patient_id, MAX(o.value_datetime) appointment_date
67
+ FROM encounter e
68
+ INNER JOIN obs o ON o.encounter_id = e.encounter_id AND o.voided = 0 AND o.concept_id = 5096 -- appointment date
69
+ WHERE e.encounter_type = 7 -- appointment encounter type
70
+ AND e.program_id = 1 -- HIV PROGRAM
71
+ AND e.patient_id IN (SELECT patient_id FROM temp_patient_outcomes WHERE cum_outcome = 'Defaulted')
72
+ AND e.encounter_datetime < DATE('#{@end_date}') + INTERVAL 1 DAY
73
+ GROUP BY e.patient_id
74
+ ) appointment ON appointment.patient_id = e.patient_id
75
+ LEFT JOIN patient_identifier i ON i.patient_id = e.patient_id
76
+ AND i.voided = 0 AND i.identifier_type = 4
77
+ INNER JOIN person_name n ON n.person_id = e.patient_id AND n.voided = 0
78
+ LEFT JOIN person_attribute a ON a.person_id = e.patient_id
79
+ AND a.voided = 0 AND a.person_attribute_type_id = 12
80
+ LEFT JOIN person_attribute landmark ON landmark.person_id = e.patient_id AND landmark.voided = 0 AND landmark.person_attribute_type_id = 19
81
+ LEFT JOIN person_address s ON s.person_id = e.patient_id AND s.voided = 0
82
+ LEFT JOIN concept_name art_reason ON art_reason.concept_id = e.reason_for_starting_art AND art_reason.voided = 0
83
+ WHERE o.cum_outcome = 'Defaulted' GROUP BY e.patient_id
84
+ ORDER BY e.patient_id, n.date_created DESC;
85
+ SQL
86
+
87
+ patients = []
88
+
89
+ (data || []).each do |person|
90
+ defaulter_date = person['defaulter_date']&.to_date || 'N/A'
91
+
92
+ unless defaulter_date == 'N/A'
93
+ next if defaulter_date < @start_date.to_date
94
+ next if defaulter_date > @end_date.to_date
95
+ end
96
+
97
+ patients << {
98
+ person_id: person['patient_id'],
99
+ given_name: person['given_name'],
100
+ family_name: person['family_name'],
101
+ birthdate: person['birthdate'],
102
+ gender: person['gender'],
103
+ arv_number: person['arv_number'],
104
+ outcome: 'Defaulted',
105
+ defaulter_date:,
106
+ appointment_date: person['appointment_date'].to_date.strftime('%Y-%m-%d'),
107
+ art_reason: person['art_reason'],
108
+ cell_number: person['cell_number'],
109
+ district: person['district'],
110
+ ta: person['ta'],
111
+ village: person['village'],
112
+ current_age: person['age'],
113
+ landmark: person['landmark']
114
+ }
115
+ end
116
+
117
+ patients
118
+ end
119
+
120
+ def cohort_report_drill_down(id)
121
+ id = ActiveRecord::Base.connection.quote(id)
122
+
123
+ patients = ActiveRecord::Base.connection.select_all <<~SQL
124
+ SELECT i.identifier arv_number, p.birthdate,
125
+ p.gender, n.given_name, n.family_name, p.person_id patient_id,
126
+ outcomes.cum_outcome AS outcome
127
+ FROM person p
128
+ INNER JOIN cohort_drill_down c ON c.patient_id = p.person_id
129
+ INNER JOIN temp_patient_outcomes AS outcomes
130
+ ON outcomes.patient_id = c.patient_id
131
+ LEFT JOIN patient_identifier i ON i.patient_id = p.person_id
132
+ AND i.voided = 0 AND i.identifier_type = 4
133
+ LEFT JOIN person_name n ON n.person_id = p.person_id AND n.voided = 0
134
+ WHERE c.reporting_report_design_resource_id = #{id}
135
+ GROUP BY p.person_id ORDER BY p.person_id, p.date_created;
136
+ SQL
137
+
138
+ patients.map do |person|
139
+ {
140
+ person_id: person['patient_id'],
141
+ given_name: person['given_name'],
142
+ family_name: person['family_name'],
143
+ birthdate: person['birthdate'],
144
+ gender: person['gender'],
145
+ arv_number: person['arv_number'],
146
+ outcome: person['outcome']
147
+ }
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ LOGGER = Rails.logger
154
+
155
+ # Writes the report to database
156
+ def save_report
157
+ ::Report.transaction do
158
+ report = ::Report.create(name: "#{@name}#{"-#{@occupation}"}#{"-#{@location_name}"}",
159
+ start_date: @start_date,
160
+ end_date: @end_date,
161
+ type: @type,
162
+ creator: ::User.current.id,
163
+ renderer_type: 'PDF')
164
+ values = save_report_values(report)
165
+
166
+ { report:, values: }
167
+ end
168
+ rescue StandardError => e
169
+ LOGGER.error("Failed to save report: #{e}")
170
+ end
171
+
172
+ # Writes the report values to database
173
+ def save_report_values(report)
174
+ @cohort_struct.values.collect do |value|
175
+ puts "Saving #{value.name} = #{value_contents_to_json(value.contents)}"
176
+ report_value = ::ReportValue.create(report:,
177
+ name: value.name,
178
+ indicator_name: value.indicator_name,
179
+ indicator_short_name: value.indicator_short_name,
180
+ creator: ::User.current.id,
181
+ description: value.description,
182
+ contents: value_contents_to_json(value.contents))
183
+
184
+ raise "Failed to save report value: #{report_value.errors.as_json}" unless report_value.errors.empty?
185
+
186
+ save_patients(report_value, value_contents_to_json(value).contents)
187
+
188
+ report_value
189
+ end
190
+ end
191
+
192
+ def init_drill_down_table
193
+ exe_create_drill_down_table(adapter: ActiveRecord::Base.connection.adapter_name.downcase)
194
+ end
195
+
196
+ def clear_drill_down
197
+ ActiveRecord::Base.connection.execute <<~SQL
198
+ DELETE FROM cohort_drill_down #{site_manager(operator: 'WHERE', column: 'site_id', location: @location)}
199
+ SQL
200
+ rescue StandardError => e
201
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS cohort_drill_down;'
202
+ LOGGER.error("Failed to clear drill down table: #{e}")
203
+ raise e
204
+ end
205
+
206
+ def value_contents_to_json(value_contents)
207
+ if value_contents.respond_to?(:each) && !value_contents.is_a?(String)
208
+ if value_contents.respond_to?(:length)
209
+ value_contents.length
210
+ elsif value_contents.respond_to?(:size)
211
+ value_contents.size
212
+ else
213
+ value_contents
214
+ end
215
+ else
216
+ value_contents
217
+ end
218
+ end
219
+
220
+ PATIENT_ID_KEYS = ['patient_id', :patient_id, 'person_id', :person_id].freeze
221
+
222
+ def save_patients(r, values)
223
+ return if values.blank? || !values.respond_to?(:each)
224
+
225
+ get_patient_id = lambda do |patient, keys = PATIENT_ID_KEYS|
226
+ break nil if keys.empty?
227
+
228
+ patient[keys.first] || get_patient_id[patient, keys[1..keys.size]]
229
+ end
230
+
231
+ patient_ids = values.map do |patient|
232
+ if patient.respond_to?(:key?) && PATIENT_ID_KEYS.any? { |key| patient.key?(key) }
233
+ get_patient_id[patient]
234
+ elsif patient.respond_to?(:each) && patient.respond_to?(:first)
235
+ patient.first
236
+ else
237
+ patient
238
+ end
239
+ end
240
+
241
+ sql_insert_statement = nil
242
+ patient_ids.select do |patient_id|
243
+ if sql_insert_statement.blank?
244
+ sql_insert_statement = "(#{r.id}, #{patient_id}, #{@location})"
245
+ else
246
+ sql_insert_statement += ",(#{r.id}, #{patient_id}, #{@location})"
247
+ end
248
+ end
249
+
250
+ return if sql_insert_statement.blank?
251
+
252
+ ActiveRecord::Base.connection.execute <<~SQL
253
+ INSERT INTO cohort_drill_down (reporting_report_design_resource_id, patient_id, site_id)
254
+ VALUES #{sql_insert_statement};
255
+ SQL
256
+ rescue StandardError => e
257
+ LOGGER.error("Failed to save patients: #{e}")
258
+ raise e
259
+ end
260
+
261
+ def calculate_age(birthdate)
262
+ birthdate = begin
263
+ birthdate.to_date
264
+ rescue StandardError
265
+ nil
266
+ end
267
+ return 'N/A' if birthdate.blank?
268
+
269
+ birthdate = ActiveRecord::Base.connection.select_one <<~SQL
270
+ SELECT #{timestampdiff_manager(date1: DATE(birthdate), date2: DATE(@end_date), interval: 'YEAR')} age;
271
+ SQL
272
+
273
+ birthdate['age']
274
+ end
275
+ end
276
+ # rubocop:enable Metrics/ClassLength
277
+ end
278
+ end