renalware-core 2.0.0.pre.rc1 → 2.0.0.pre.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/renalware/modules/_dashboard.scss +5 -4
  3. data/app/controllers/renalware/admin/users_controller.rb +6 -2
  4. data/app/controllers/renalware/admissions/consults_controller.rb +1 -1
  5. data/app/controllers/renalware/letters/contacts_controller.rb +4 -1
  6. data/app/controllers/renalware/pathology/current_observation_results_controller.rb +5 -11
  7. data/app/controllers/renalware/research/study_participants_controller.rb +1 -1
  8. data/app/helpers/renalware/dashboards_helper.rb +10 -0
  9. data/app/helpers/renalware/layout_helper.rb +3 -1
  10. data/app/models/concerns/renalware/letters/letter_pathology.rb +20 -0
  11. data/app/models/concerns/renalware/patient_pathology_scopes.rb +15 -7
  12. data/app/models/renalware/admissions/consult.rb +4 -3
  13. data/app/models/renalware/admissions/consult_site.rb +9 -0
  14. data/app/models/renalware/hd/mdm_patients_query.rb +2 -1
  15. data/app/models/renalware/letters/draft_letter.rb +2 -1
  16. data/app/models/renalware/letters/letter.rb +1 -0
  17. data/app/models/renalware/letters/part/recent_pathology_results.rb +2 -16
  18. data/app/models/renalware/letters/revise_letter.rb +2 -0
  19. data/app/models/renalware/pathology/{current_key_observation_set.rb → current_key_observation_set.rb.dead} +0 -0
  20. data/app/models/renalware/pathology/current_observation_set.rb +35 -0
  21. data/app/models/renalware/pathology/message_listener.rb +5 -3
  22. data/app/models/renalware/pathology/message_param_parser.rb +35 -10
  23. data/app/models/renalware/pathology/observation.rb +1 -1
  24. data/app/models/renalware/pathology/observation_request.rb +4 -1
  25. data/app/models/renalware/pathology/observations_jsonb_serializer.rb +73 -0
  26. data/app/models/renalware/pathology/patient.rb +4 -0
  27. data/app/models/renalware/pathology/update_current_observations.rb.dead +25 -0
  28. data/app/models/renalware/patient.rb +1 -1
  29. data/app/models/renalware/patients/mdm_patients_query.rb +4 -8
  30. data/app/models/renalware/pd/mdm_patients_query.rb +4 -8
  31. data/app/models/renalware/renal/low_clearance/mdm_patients_query.rb +5 -4
  32. data/app/models/renalware/transplants/mdm_patients_query.rb +4 -1
  33. data/app/presenters/renalware/admissions/consult_presenter.rb +6 -2
  34. data/app/presenters/renalware/dashboard/dashboard_presenter.rb +0 -4
  35. data/app/presenters/renalware/hd/patient_presenter.rb +6 -0
  36. data/app/presenters/renalware/mdm_patient_presenter.rb +6 -10
  37. data/app/presenters/renalware/pathology/{current_observation_results/html_table_view.rb → current_observation_results.dead/html_table_view.rb.dead} +0 -0
  38. data/app/presenters/renalware/pathology/{current_observation_results/presenter.rb → current_observation_results.dead/presenter.rb.dead} +0 -0
  39. data/app/presenters/renalware/pathology/observation_set_presenter.rb +57 -0
  40. data/app/presenters/renalware/pathology/observations_diff.rb +97 -77
  41. data/app/presenters/renalware/pathology/patient_presenter.rb +0 -4
  42. data/app/presenters/renalware/renal/clinical_summary_presenter.rb +1 -1
  43. data/app/views/renalware/admin/users/_filters.html.slim +19 -0
  44. data/app/views/renalware/admin/users/index.html.slim +9 -12
  45. data/app/views/renalware/admissions/consults/_filters.html.slim +2 -2
  46. data/app/views/renalware/admissions/consults/_form.html.slim +13 -53
  47. data/app/views/renalware/admissions/consults/_table.html.slim +3 -3
  48. data/app/views/renalware/dashboard/dashboards/show.html.slim +1 -1
  49. data/app/views/renalware/hd/mdm_patients/_patient.html.slim +2 -2
  50. data/app/views/renalware/letters/letters/_pathology.html.slim +2 -2
  51. data/app/views/renalware/letters/parts/_recent_pathology_results.html.slim +3 -3
  52. data/app/views/renalware/mdm_patients/_patient.html.slim +7 -7
  53. data/app/views/renalware/pathology/current_observation_results/index.html.slim +12 -1
  54. data/app/views/renalware/pathology/observations/_diff.html.slim +26 -20
  55. data/config/initializers/core_extensions.rb +2 -0
  56. data/config/locales/renalware/admissions/consults.en.yml +9 -0
  57. data/config/locales/renalware/dashboard/dashboard.yml +1 -1
  58. data/config/locales/renalware/patient.yml +0 -1
  59. data/db/migrate/20171204112150_create_consult_sites.rb +14 -0
  60. data/db/migrate/20171211161400_create_pathology_current_table.rb +19 -0
  61. data/db/migrate/20171213111513_create_fn_to_refresh_current_obs.rb +50 -0
  62. data/db/migrate/20171214141335_create_trigger_to_update_current_observation_sets.rb +111 -0
  63. data/db/migrate/20171214190849_enforce_request_id_on_observations.rb +5 -0
  64. data/db/migrate/20171215122454_add_pathology_observation_set_to_letters.rb +10 -0
  65. data/lib/core_extensions/hash.rb +11 -0
  66. data/lib/renalware/version.rb +1 -1
  67. data/spec/factories/admissions/consult_sites.rb +6 -0
  68. data/spec/factories/admissions/consults.rb +1 -1
  69. metadata +21 -5
@@ -3,7 +3,7 @@ require_dependency "renalware/pathology"
3
3
  module Renalware
4
4
  module Pathology
5
5
  class Observation < ApplicationRecord
6
- belongs_to :request, class_name: "ObservationRequest", touch: true
6
+ belongs_to :request, class_name: "ObservationRequest", touch: true, inverse_of: :observations
7
7
  belongs_to :description, class_name: "ObservationDescription"
8
8
 
9
9
  validates :description, presence: true
@@ -3,7 +3,10 @@ require_dependency "renalware/pathology"
3
3
  module Renalware
4
4
  module Pathology
5
5
  class ObservationRequest < ApplicationRecord
6
- has_many :observations, foreign_key: :request_id
6
+ has_many :observations,
7
+ foreign_key: :request_id,
8
+ inverse_of: :request,
9
+ dependent: :destroy
7
10
  belongs_to :description, class_name: "RequestDescription"
8
11
  belongs_to :patient, class_name: "Patient", touch: true
9
12
 
@@ -0,0 +1,73 @@
1
+ require_dependency "renalware/pathology"
2
+
3
+ module Renalware
4
+ module Pathology
5
+ # A singleton exposing all defined OBX codes as an array of symbols
6
+ class AllObservationCodes
7
+ include Singleton
8
+
9
+ # Example usage:
10
+ # AllObservationCodes.include?(code)
11
+ def self.include?(code)
12
+ instance.all.include?(code)
13
+ end
14
+
15
+ def all
16
+ @all ||= ObservationDescription.order(:code).pluck(:code).map(&:upcase).map(&:to_sym)
17
+ end
18
+ end
19
+
20
+ # We mix this module into any database-returned jsonb hash of observations
21
+ # (e.g. CurrentObservationSet.values and Letter.pathology_snapshot)
22
+ module ObservationSetMethods
23
+ # Support these syntaxes
24
+ # values.hgb # => { result: ... observed_at: ...}
25
+ # values.HGB # => { result: ... observed_at: ...}
26
+ # values.hgb_result # => 3.3
27
+ # values.hgb_observed_at # => "2017-17-01"
28
+ # So the values has methods corresponding to the entire set of possible
29
+ # OBX codes, and also methods to reach in and get their result and observed_at date
30
+ # rubocop:disable Style/MethodMissing
31
+ def method_missing(method_name, *_args, &_block)
32
+ code, suffix = method_parts(method_name)
33
+ return super unless AllObservationCodes.include?(code)
34
+ observation_hash_or_hash_element_for(code, suffix)
35
+ end
36
+ # rubocop:enable Style/MethodMissing
37
+
38
+ # From eg hgb_result, returns
39
+ # [:HGB, "result"]
40
+ def method_parts(method_name)
41
+ matches = method_name.to_s.match(/([^_]*)(\w*)/)
42
+ [matches[1]&.upcase&.to_sym, matches[2]]
43
+ end
44
+
45
+ def observation_hash_or_hash_element_for(code, suffix)
46
+ obs_hash = self[code]
47
+ return nil if obs_hash.nil? # the patient may not have this observation in the set
48
+ return obs_hash[:result] if suffix == "_result"
49
+ return Date.parse(obs_hash[:observed_at]) if suffix == "_observed_at"
50
+ obs_hash
51
+ end
52
+ end
53
+
54
+ class ObservationsJsonbSerializer
55
+ def self.dump(hash)
56
+ JSON.parse hash.to_json
57
+ end
58
+
59
+ def self.load(hash)
60
+ type_check(hash)
61
+ .with_indifferent_access
62
+ .extend(ObservationSetMethods)
63
+ end
64
+
65
+ def self.type_check(hash)
66
+ if hash.nil? then {}
67
+ elsif hash.is_a?(Hash) then hash
68
+ else JSON.parse(hash)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -34,6 +34,10 @@ module Renalware
34
34
  Requests::HighRiskAlgorithm.new(self).patient_is_high_risk?
35
35
  end
36
36
 
37
+ def fetch_current_observation_set
38
+ current_observation_set || build_current_observation_set
39
+ end
40
+
37
41
  private
38
42
 
39
43
  def date_for_algorithms
@@ -0,0 +1,25 @@
1
+ require_dependency "renalware/pathology"
2
+
3
+ module Renalware
4
+ module Pathology
5
+ class UpdateCurrentObservations
6
+ def call(params)
7
+ patient = find_patient(params[:patient_id])
8
+ set = patient.fetch_current_observation_set
9
+
10
+ params[:observation_request][:observations_attributes].each do |new_obs|
11
+ description = ObservationDescription.select(:code).find(new_obs[:description_id])
12
+ set.values[description.code] = new_obs.slice(:result, :observed_at)
13
+ end
14
+
15
+ set.save!
16
+ end
17
+
18
+ private
19
+
20
+ def find_patient(id)
21
+ Pathology::Patient.find_by(id: id)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -32,7 +32,7 @@ module Renalware
32
32
 
33
33
  serialize :sex, Gender
34
34
 
35
- has_one :current_key_observation_set, class_name: "Pathology::CurrentKeyObservationSet"
35
+ has_one :current_observation_set, class_name: "Pathology::CurrentObservationSet"
36
36
  has_one :current_address, as: :addressable, class_name: "Address"
37
37
  has_one :summary, class_name: "Patients::Summary"
38
38
  belongs_to :ethnicity, class_name: "Patients::Ethnicity"
@@ -2,6 +2,7 @@ module Renalware
2
2
  module Patients
3
3
  class MDMPatientsQuery
4
4
  include ModalityScopes
5
+ include PatientPathologyScopes
5
6
  attr_reader :modality_names, :q, :relation
6
7
 
7
8
  # modality_names: eg "HD" or "PD"
@@ -19,19 +20,14 @@ module Renalware
19
20
  @search ||= begin
20
21
  relation
21
22
  .extending(ModalityScopes)
22
- .extending(Scopes)
23
- .with_current_key_pathology
23
+ .extending(PatientPathologyScopes)
24
+ .with_current_pathology
24
25
  .with_current_modality_matching(modality_names)
25
26
  .search(q)
26
- # .order("pathology_current_key_observations.hgb_result asc")
27
27
  end
28
28
  end
29
29
 
30
- module Scopes
31
- def with_current_key_pathology
32
- includes(:current_key_observation_set) # .joins(:current_key_observation)
33
- end
34
- end
30
+ # `
35
31
  end
36
32
  end
37
33
  end
@@ -2,6 +2,7 @@ module Renalware
2
2
  module PD
3
3
  class MDMPatientsQuery
4
4
  include ModalityScopes
5
+ include PatientPathologyScopes
5
6
  MODALITY_NAMES = "PD".freeze
6
7
  attr_reader :q, :relation
7
8
 
@@ -18,18 +19,13 @@ module Renalware
18
19
  @search ||= begin
19
20
  relation
20
21
  .extending(ModalityScopes)
21
- .extending(Scopes)
22
+ .extending(PatientPathologyScopes)
22
23
  .with_current_modality_matching(MODALITY_NAMES)
23
- .with_current_key_pathology
24
+ .with_current_pathology
25
+ .left_joins(:current_observation_set)
24
26
  .search(q)
25
27
  end
26
28
  end
27
-
28
- module Scopes
29
- def with_current_key_pathology
30
- includes(:current_key_observation_set) # . joins(:current_key_observation)
31
- end
32
- end
33
29
  end
34
30
  end
35
31
  end
@@ -25,7 +25,8 @@ module Renalware
25
25
  .extending(PatientPathologyScopes)
26
26
  .extending(ModalityScopes)
27
27
  .extending(NamedFilterScopes)
28
- .with_current_key_pathology
28
+ .with_current_pathology
29
+ .left_joins(:current_observation_set)
29
30
  .with_current_modality_of_class(LowClearance::ModalityDescription)
30
31
  .public_send(named_filter.to_s)
31
32
  .search(query)
@@ -46,15 +47,15 @@ module Renalware
46
47
  end
47
48
 
48
49
  def urea
49
- where("pathology_current_key_observation_sets.ure_result::float >= 30")
50
+ where("cast(values->'URE'->>'result' as float) >= 30.0")
50
51
  end
51
52
 
52
53
  def hgb_low
53
- where("pathology_current_key_observation_sets.hgb_result::float < 100")
54
+ where("cast(values->'HGB'->>'result' as float) < 100.0")
54
55
  end
55
56
 
56
57
  def hgb_high
57
- where("pathology_current_key_observation_sets.hgb_result::float > 130")
58
+ where("cast(values->'HGB'->>'result' as float) > 130.0")
58
59
  end
59
60
  end
60
61
  end
@@ -6,6 +6,7 @@ module Renalware
6
6
  # different filter groups
7
7
  class MDMPatientsQuery
8
8
  include ModalityScopes
9
+ include PatientPathologyScopes
9
10
  MODALITY_NAMES = ["Transplant"].freeze
10
11
  attr_reader :q, :relation, :named_filter
11
12
 
@@ -23,10 +24,12 @@ module Renalware
23
24
  def search
24
25
  @search ||= begin
25
26
  relation
26
- .includes(:current_key_observation_set)
27
27
  .extending(ModalityScopes)
28
+ .extending(PatientPathologyScopes)
28
29
  .extending(NamedFilterScopes)
29
30
  .with_current_modality_matching(MODALITY_NAMES)
31
+ .with_current_pathology
32
+ .left_joins(:current_observation_set)
30
33
  .public_send(named_filter.to_s)
31
34
  .search(q)
32
35
  end
@@ -16,8 +16,12 @@ module Renalware
16
16
  @patient ||= Renalware::PatientPresenter.new(__getobj__.patient)
17
17
  end
18
18
 
19
- def unit_and_ward
20
- [hospital_unit&.unit_code, hospital_ward&.name].compact.join("/")
19
+ def location
20
+ [
21
+ consult_site&.name,
22
+ hospital_ward&.name,
23
+ other_site_or_ward
24
+ ].compact.join(", ")
21
25
  end
22
26
 
23
27
  def patient_name
@@ -10,10 +10,6 @@ module Renalware
10
10
 
11
11
  attr_reader :user
12
12
 
13
- def title
14
- I18n.t("renalware.dashboard.dashboards.title", name: @user.username&.capitalize)
15
- end
16
-
17
13
  def bookmarks
18
14
  @bookmarks ||= begin
19
15
  Patients.cast_user(user)
@@ -22,6 +22,12 @@ module Renalware
22
22
  hd_sessions.eager_load(:hospital_unit).where(type: "Renalware::HD::Session::Closed")
23
23
  end
24
24
 
25
+ def current_observation_set
26
+ @current_observation_set ||= begin
27
+ Renalware::Pathology::ObservationSetPresenter.new(__getobj__.current_observation_set)
28
+ end
29
+ end
30
+
25
31
  private
26
32
 
27
33
  def hd_profile
@@ -1,18 +1,14 @@
1
1
  module Renalware
2
2
  # Presenter formatting a single patient for use behind any MDM Patients listing.
3
3
  class MDMPatientPresenter < PatientPresenter
4
- delegate :hgb_result,
5
- :hgb_observed_at,
6
- :ure_result,
7
- :ure_observed_at,
8
- :cre_result,
9
- :cre_observed_at,
10
- :mdrd_result,
11
- :mdrd_observed_at,
12
- to: :current_key_observation_set, allow_nil: true
13
-
14
4
  def esrf_date
15
5
  Renalware::Renal.cast_patient(__getobj__).profile&.esrf_on
16
6
  end
7
+
8
+ def current_observation_set
9
+ @current_observation_set ||= begin
10
+ Renalware::Pathology::ObservationSetPresenter.new(__getobj__.current_observation_set)
11
+ end
12
+ end
17
13
  end
18
14
  end
@@ -0,0 +1,57 @@
1
+ require_dependency "renalware/pathology"
2
+ require_dependency "attr_extras"
3
+
4
+ module Renalware
5
+ module Pathology
6
+ class ObservationSetPresenter < DumbDelegator
7
+ class Observation
8
+ attr_reader_initialize [:code, :description, :result, :observed_at]
9
+ end
10
+
11
+ def method_missing(method_name, **args, &_block)
12
+ return if __getobj__.nil?
13
+ vals = __getobj__.values
14
+ vals.public_send(method_name)
15
+ end
16
+
17
+ def respond_to_missing?(method_name, _include_private = false)
18
+ (values.present? && values.respond_to?(method_name)) || super
19
+ end
20
+
21
+ def each_observation
22
+ return unless block_given?
23
+ __getobj__.values.sort.sort.each do |code, observation_hash|
24
+ observation = build_observation(
25
+ code: code,
26
+ observation_hash: observation_hash,
27
+ with_description: true
28
+ )
29
+ yield observation
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def build_observation(code:, observation_hash:, with_description: false)
36
+ Observation.new(
37
+ code: code,
38
+ result: observation_hash["result"],
39
+ observed_at: ::Time.zone.parse(observation_hash["observed_at"]),
40
+ description: with_description ? description_for(code) : nil
41
+ )
42
+ end
43
+
44
+ def description_for(code)
45
+ observation_description_map.fetch(code, "#{code} (no description found!)")
46
+ end
47
+
48
+ def observation_description_map
49
+ @observation_description_map ||= begin
50
+ ::Renalware::Pathology::ObservationDescription
51
+ .pluck(:code, :name)
52
+ .each_with_object({}) { |desc, hash| hash[desc.first] = desc.last }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,37 +1,123 @@
1
1
  require "attr_extras"
2
2
  require_dependency "renalware/pathology"
3
3
 
4
- # Uses Rails 5 `render anywhere` feature.
4
+ # Compares two hashes of pathology observations (OBXs).
5
+ # Used for example in a Letter, where a snapshot of pathology is stored on the letter,
6
+ # but when the letter we compare the snapshot to the latest hash in the jsonb column
7
+ # CurrentObserverationSet#values to see if there are newer results that the user might want
8
+ # to bring into the letter.
9
+ #
10
+ # NB: uses Rails 5 `render anywhere` feature.
5
11
  module Renalware
6
12
  module Pathology
7
13
  class ObservationsDiff
8
- def initialize(patient:, between_dates:, descriptions:)
9
- @patient = patient
10
- @descriptions = descriptions
11
- @between_dates = Array(between_dates)
12
- if between_dates.length != 2
13
- raise ArgumentError, "between_dates argument must be an array of 2 dates"
14
- end
14
+ pattr_initialize [:patient!, :observation_set_a!, :observation_set_b!, :descriptions!]
15
+
16
+ def changes?
17
+ @changes
15
18
  end
16
19
 
17
20
  def to_html
18
21
  render
19
22
  end
20
23
 
24
+ class Observation
25
+ delegate :any?, :to_h, to: :hash
26
+
27
+ def initialize(hash)
28
+ @hash = hash
29
+ end
30
+
31
+ def result
32
+ hash.fetch(:result, 0)
33
+ end
34
+
35
+ def observed_at
36
+ Time.zone.parse(hash.fetch(:observed_at, "1970-01-01"))
37
+ end
38
+
39
+ def supercedes?(other)
40
+ (observed_at > other.observed_at) ||
41
+ ((observed_at == other.observed_at) && result > other.result)
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :hash
47
+ end
48
+
49
+ # Given two hashes like this
50
+ # {
51
+ # HGB: { result: 2.1, observed_at: "2017-12-12 00:01:01"},
52
+ # CRE: { result: 9, observed_at: "2017-12-12 00:01:01"}
53
+ # }
54
+ # {
55
+ # HGB: { result: 1.0, observed_at: "2018-12-12 00:01:01"}, # changed
56
+ # CRE: { result: 1.1, observed_at: "2017-12-11 00:01:01"}, # no change!
57
+ # PTH: { result: 1.1, observed_at: "2017-12-11 00:01:01"} # new!
58
+ # }
59
+ # Return a hash that looks like this
60
+ # {
61
+ # HGB: [
62
+ # { result: 2.1, observed_at: "2017-12-12 00:01:01"}, # original
63
+ # { result: 1.0, observed_at: "2018-12-12 00:01:01"}, # changed
64
+ # -1.1 # digg
65
+ # ],
66
+ # CRE: [
67
+ # { result: 9, observed_at: "2017-12-12 00:01:01"},
68
+ # nil, # no new value
69
+ # nil # no change
70
+ # ],
71
+ # PTH: [
72
+ # nil, # no original
73
+ # { result: 1.1, observed_at: "2017-12-11 00:01:01"} # new!
74
+ # 1.1
75
+ # ]
76
+ # }
77
+ # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
21
78
  def to_h
22
- @to_h ||= build_diff_hash
79
+ return {} if observation_set_a.blank? && observation_set_a.blank?
80
+
81
+ filter_observations
82
+
83
+ description_codes.each_with_object({}) do |code, hash|
84
+ obs_a = Observation.new(observation_set_a.fetch(code, {}))
85
+ obs_b = Observation.new(observation_set_b.fetch(code, {}))
86
+
87
+ arr = Array.new(3)
88
+ arr[0] = obs_a if obs_a.any?
89
+
90
+ if obs_b.supercedes?(obs_a)
91
+ arr[1] = obs_b
92
+ arr[2] = obs_b.result.to_f - obs_a.result.to_f
93
+ end
94
+
95
+ hash[code] = arr
96
+ end
23
97
  end
98
+ # rubocop:enable Metrics/AbcSize,Metrics/MethodLength
24
99
 
25
100
  private
26
101
 
27
- attr_reader :patient, :between_dates, :descriptions
102
+ def filter_observations
103
+ filter_observations_by_descriptions(observation_set_a)
104
+ filter_observations_by_descriptions(observation_set_b)
105
+ end
106
+
107
+ def filter_observations_by_descriptions(observations)
108
+ observations.select!{ |code, _obs| description_codes.include?(code.to_sym) }
109
+ end
110
+
111
+ def description_codes
112
+ @description_codes ||= descriptions.map(&:code).map(&:to_sym)
113
+ end
28
114
 
29
115
  def render
30
116
  renderer.render(
31
117
  partial: "renalware/pathology/observations/diff",
32
118
  locals: {
33
119
  patient: patient,
34
- diffs: to_h
120
+ diff: self
35
121
  }
36
122
  )
37
123
  end
@@ -39,72 +125,6 @@ module Renalware
39
125
  def renderer
40
126
  ApplicationController.renderer
41
127
  end
42
-
43
- def observations1
44
- @observations1 ||= begin
45
- observations_in_daterange(Time.zone.at(1)..between_dates.first)
46
- end
47
- end
48
-
49
- def observations2
50
- @observations2 ||= begin
51
- obs1_ids = observations1.map(&:id)
52
- observations_in_daterange(between_dates.first..between_dates.last)
53
- .reject{ |obs| obs1_ids.include?(obs.id) }
54
- end
55
- end
56
-
57
- def observations_in_daterange(range)
58
- Pathology::CurrentObservationsForDescriptionsQuery.new(
59
- patient: patient,
60
- descriptions: descriptions
61
- ).call.where(observed_at: range).reject{ |obs| obs.id.nil? }.to_a
62
- end
63
-
64
- class ObsWithDiff
65
- include Virtus.model
66
- attribute :observed_at
67
- attribute :result, Float
68
- attribute :result_diff
69
-
70
- def result_diff(more_recent)
71
- more_recent && result && (result.to_f - more_recent.result&.to_f)
72
- end
73
- end
74
-
75
- # Build
76
- # [
77
- # { "HGB" => [
78
- # { observed_at:, value: },
79
- # { observed_at:, value:, diff: }
80
- # ]
81
- # }
82
- # ]
83
- # Note this could be done with a SQL window function
84
- # rubocop:disable Metrics/MethodLength
85
- # rubocop:disable Metrics/AbcSize
86
- def build_diff_hash
87
- result = {}
88
-
89
- observations1.each do |older_obs|
90
- result[older_obs.description.code] = [
91
- ObsWithDiff.new(older_obs.attributes.symbolize_keys),
92
- nil
93
- ]
94
- end
95
-
96
- observations2.each do |newer_obs|
97
- arr = result[newer_obs.description.code] || Array(2)
98
- older_obs = arr[0]
99
- args = newer_obs.attributes.symbolize_keys
100
- .merge(result_diff: older_obs&.result_diff(newer_obs))
101
- arr[1] = ObsWithDiff.new(args)
102
- end
103
-
104
- result
105
- end
106
- # rubocop:enable Metrics/MethodLength Metrics/AbcSize
107
- # rubocop:enable Metrics/AbcSize
108
128
  end
109
129
  end
110
130
  end