Skip to content

Commit e3d4e48

Browse files
committed
Show changes to child records (manual or uploaded) in the activity log
This displays the collected info on changes to patient records in the child activity log
1 parent 3d2d098 commit e3d4e48

6 files changed

Lines changed: 283 additions & 22 deletions

app/components/app_activity_log_component.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ class AppActivityLogComponent < ViewComponent::Base
3434
<%= event[:invalidated] ? tag.s(body) : body %>
3535
</p></blockquote>
3636
<% end %>
37+
38+
<% if (changes = event[:changes]).present? %>
39+
<%= render AppDetailsComponent.new(open: true) do |details| %>
40+
<% details.with_summary { pluralize(changes.size, "field") + " updated" } %>
41+
<%= changes_summary_list(changes) %>
42+
<% end %>
43+
<% end %>
3744
<% end %>
3845
<% end %>
3946
</div>
@@ -57,6 +64,7 @@ def all_events
5764
gillick_assessment_events,
5865
note_events,
5966
notify_events,
67+
patient_change_events,
6068
patient_merge_events,
6169
patient_specific_direction_events,
6270
pre_screening_events,
@@ -270,6 +278,26 @@ def notify_events
270278
end
271279
end
272280

281+
CHANGE_SOURCE_TITLES = {
282+
"manual_edit" => "Record updated manually",
283+
"cohort_import" =>
284+
"Record updated after new details were imported in a cohort upload",
285+
"class_import" =>
286+
"Record updated after new details were imported in a class upload"
287+
}.freeze
288+
289+
def patient_change_events
290+
patient_change_log_entries.map do |entry|
291+
title = CHANGE_SOURCE_TITLES[entry.source]
292+
{
293+
title:,
294+
at: entry.created_at,
295+
by: entry.user,
296+
changes: entry.recorded_changes
297+
}
298+
end
299+
end
300+
273301
def patient_merge_events
274302
patient_merge_log_entries.map do |patient_merge_log_entry|
275303
{
@@ -435,6 +463,8 @@ def attendance_events
435463

436464
private
437465

466+
delegate :format_nhs_number, :govuk_summary_list, to: :helpers
467+
438468
def include_programme_specific_events?
439469
@programme_type.present? || @session.present?
440470
end
@@ -446,6 +476,13 @@ def archive_reasons
446476
@patient.archive_reasons.where(team: @team).includes(:created_by)
447477
end
448478

479+
def patient_change_log_entries
480+
return [] if include_programme_specific_events?
481+
482+
@patient_change_log_entries ||=
483+
@patient.patient_change_log_entries.includes(:user)
484+
end
485+
449486
def patient_merge_log_entries
450487
return [] if include_programme_specific_events?
451488

@@ -615,6 +652,75 @@ def vaccination_records
615652
end
616653
end
617654

655+
def changes_summary_list(changes)
656+
rows =
657+
PatientChangeLogEntry::TRACKED_ATTRIBUTES.filter_map do |attr|
658+
next unless changes.key?(attr)
659+
660+
old_val, new_val = changes[attr]
661+
{
662+
key: {
663+
text: PatientChangeLogEntry.label_for(attr)
664+
},
665+
value: {
666+
text: change_value_html(attr, old_val, new_val)
667+
}
668+
}
669+
end
670+
govuk_summary_list(rows:)
671+
end
672+
673+
def change_value_html(attr, old_val, new_val)
674+
arrow =
675+
tag.svg(
676+
safe_join(
677+
[
678+
tag.title("changed to"),
679+
tag.path(
680+
d:
681+
"m14.7 6.3 5 5c.2.2.3.4.3.7 0 .3-.1.5-.3.7l-5 5" \
682+
"a1 1 0 0 1-1.4-1.4l3.3-3.3H5a1 1 0 0 1 0-2" \
683+
"h11.6l-3.3-3.3a1 1 0 1 1 1.4-1.4Z"
684+
)
685+
]
686+
),
687+
class: "nhsuk-icon nhsuk-icon--arrow-right",
688+
style: "vertical-align: middle",
689+
xmlns: "http://www.w3.org/2000/svg",
690+
viewBox: "0 0 24 24",
691+
width: "16",
692+
height: "16",
693+
focusable: "false",
694+
role: "img",
695+
"aria-label": "changed to"
696+
)
697+
698+
safe_join(
699+
[
700+
format_change_value(attr, old_val),
701+
" ",
702+
arrow,
703+
" ",
704+
tag.mark(format_change_value(attr, new_val), class: "app-highlight")
705+
]
706+
)
707+
end
708+
709+
def format_change_value(attr, value)
710+
return "Not provided" if value.nil? || value.to_s.empty?
711+
712+
case attr
713+
when "date_of_birth"
714+
Date.parse(value.to_s).to_fs(:long)
715+
when "gender_code"
716+
Patient.human_enum_name(:gender_code, value)
717+
when "nhs_number"
718+
format_nhs_number(value.to_s)
719+
else
720+
value.to_s
721+
end
722+
end
723+
618724
def expired_items_for(academic_year:, programmes:)
619725
{
620726
consents:,

app/models/patient_change_log_entry.rb

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,26 @@
2323
# fk_rails_... (user_id => users.id)
2424
#
2525
class PatientChangeLogEntry < ApplicationRecord
26-
TRACKED_ATTRIBUTES = %w[
27-
given_name
28-
family_name
29-
preferred_given_name
30-
preferred_family_name
31-
date_of_birth
32-
gender_code
33-
nhs_number
34-
address_line_1
35-
address_line_2
36-
address_town
37-
address_postcode
38-
].freeze
26+
ATTRIBUTE_LABELS = {
27+
"nhs_number" => "NHS number",
28+
"given_name" => "First name",
29+
"family_name" => "Last name",
30+
"preferred_given_name" => "Preferred first name",
31+
"preferred_family_name" => "Preferred last name",
32+
"date_of_birth" => "Date of birth",
33+
"gender_code" => "Gender",
34+
"address_line_1" => "Address line 1",
35+
"address_line_2" => "Address line 2",
36+
"address_town" => "Town",
37+
"address_postcode" => "Postcode"
38+
}.freeze
39+
private_constant :ATTRIBUTE_LABELS
40+
41+
TRACKED_ATTRIBUTES = ATTRIBUTE_LABELS.keys.freeze
42+
43+
def self.label_for(attribute)
44+
ATTRIBUTE_LABELS[attribute]
45+
end
3946

4047
belongs_to :patient
4148
belongs_to :user, optional: true
@@ -56,7 +63,13 @@ def self.log_import_changes!(patients:, import:)
5663
patients.each do |patient|
5764
next if patient.id.blank?
5865

59-
recorded_changes = patient.changes.slice(*TRACKED_ATTRIBUTES)
66+
recorded_changes =
67+
patient
68+
.changes
69+
.slice(*TRACKED_ATTRIBUTES)
70+
.reject do |_attr, (old_val, new_val)|
71+
old_val.presence == new_val.presence
72+
end
6073
next if recorded_changes.empty?
6174

6275
create!(patient:, user:, source:, recorded_changes:)

spec/components/app_activity_log_component_spec.rb

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,117 @@
788788
end
789789
end
790790

791+
describe "patient change log events" do
792+
let(:component) { described_class.new(patient:, team:) }
793+
794+
context "with a manual edit" do
795+
before do
796+
create(
797+
:patient_change_log_entry,
798+
patient:,
799+
user:,
800+
source: :manual_edit,
801+
recorded_changes: {
802+
"nhs_number" => %w[9990000018 9758623168]
803+
},
804+
created_at: Time.zone.local(2025, 6, 1, 12)
805+
)
806+
end
807+
808+
include_examples "card",
809+
title: "Record updated manually",
810+
date: "1 June 2025 at 12:00pm",
811+
by: "JOY, Nurse"
812+
813+
it "renders a details expandable with the number of changed fields" do
814+
expect(rendered).to have_css(
815+
"details.nhsuk-details[open]",
816+
text: "1 field updated"
817+
)
818+
end
819+
820+
it "renders the field label and formatted old and new values" do
821+
expect(rendered).to have_css(
822+
".nhsuk-summary-list__key",
823+
text: "NHS number"
824+
)
825+
expect(rendered).to have_css(
826+
".nhsuk-summary-list__value",
827+
text: "999 000 0018"
828+
)
829+
expect(rendered).to have_css(
830+
".nhsuk-summary-list__value mark.app-highlight",
831+
text: "975 862 3168"
832+
)
833+
end
834+
end
835+
836+
context "with a cohort import" do
837+
before do
838+
create(
839+
:patient_change_log_entry,
840+
patient:,
841+
user:,
842+
source: :cohort_import,
843+
recorded_changes: {
844+
"given_name" => %w[Sarah Sara]
845+
},
846+
created_at: Time.zone.local(2025, 6, 1, 12)
847+
)
848+
end
849+
850+
include_examples "card",
851+
title:
852+
"Record updated after new details were imported in a cohort upload",
853+
date: "1 June 2025 at 12:00pm",
854+
by: "JOY, Nurse"
855+
end
856+
857+
context "with a class import" do
858+
before do
859+
create(
860+
:patient_change_log_entry,
861+
patient:,
862+
user:,
863+
source: :class_import,
864+
recorded_changes: {
865+
"given_name" => %w[Sarah Sara]
866+
},
867+
created_at: Time.zone.local(2025, 6, 1, 12)
868+
)
869+
end
870+
871+
include_examples "card",
872+
title:
873+
"Record updated after new details were imported in a class upload",
874+
date: "1 June 2025 at 12:00pm",
875+
by: "JOY, Nurse"
876+
end
877+
878+
context "with multiple changed fields" do
879+
before do
880+
create(
881+
:patient_change_log_entry,
882+
patient:,
883+
user:,
884+
source: :manual_edit,
885+
recorded_changes: {
886+
"given_name" => %w[Sarah Sara],
887+
"family_name" => %w[Doe Dow]
888+
},
889+
created_at: Time.zone.local(2025, 6, 1, 12)
890+
)
891+
end
892+
893+
it "renders the count of changed fields in the disclosure" do
894+
expect(rendered).to have_css(
895+
"details.nhsuk-details[open]",
896+
text: "2 fields updated"
897+
)
898+
end
899+
end
900+
end
901+
791902
describe "patient merge events" do
792903
let(:component) { described_class.new(patient:, team:) }
793904

spec/features/import_child_records_with_duplicates_spec.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
then_i_should_see_a_success_message
3030
and_i_should_return_to_the_import_page
3131
and_the_first_duplicate_record_should_be_persisted
32+
and_a_change_log_entry_is_created_for_the_cohort_import_change
3233

3334
when_i_visit_the_import_issues_page
3435
and_i_review_the_second_duplicate_record
@@ -79,6 +80,7 @@
7980
then_i_should_see_a_success_message
8081
and_i_should_return_to_the_import_page
8182
and_the_first_duplicate_record_should_be_persisted
83+
and_a_change_log_entry_is_created_for_the_cohort_import_change
8284

8385
when_i_visit_the_import_issues_page
8486
and_i_review_the_second_duplicate_record
@@ -383,6 +385,18 @@ def and_the_first_duplicate_record_should_be_persisted
383385
expect(@first_patient.pending_changes).to eq({})
384386
end
385387

388+
def and_a_change_log_entry_is_created_for_the_cohort_import_change
389+
visit patient_path(@first_patient)
390+
within(".nhsuk-card", text: "Activity log") do
391+
expect(page).to have_content(
392+
"Record updated after new details were imported in a cohort upload"
393+
)
394+
expect(page).to have_css(".nhsuk-summary-list__key", text: "Postcode")
395+
expect(page).to have_content("SW11 1AA")
396+
expect(page).to have_css("mark.app-highlight", text: "SW1A 1AA")
397+
end
398+
end
399+
386400
def and_the_second_record_should_not_be_updated
387401
@second_patient.reload
388402
expect(@second_patient.given_name).to eq("James")
@@ -448,8 +462,10 @@ def when_i_go_to_the_fourth_uploaded_record
448462
end
449463

450464
def then_i_should_see_the_address_is_updated
451-
expect(page).to have_content("10 Downing Street")
452-
expect(page).not_to have_content("11 Downing Street")
465+
within(".nhsuk-card", text: "Child record") do
466+
expect(page).to have_content("10 Downing Street")
467+
expect(page).not_to have_content("11 Downing Street")
468+
end
453469
end
454470

455471
def and_the_required_feature_flags_are_enabled

0 commit comments

Comments
 (0)