From 7db6382d5d16b90ae6e8d362c48031dcad5370b3 Mon Sep 17 00:00:00 2001 From: Steve Hook Date: Thu, 26 Feb 2026 13:47:04 +0000 Subject: [PATCH 1/6] Add programme_type filter to `AppPatientSessionTableComponent` Also adds a filtered session table to the per-programme tabs on the child record page. Jira-issue: MAV-3826 --- .../app_patient_session_table_component.rb | 27 +++++++++--- app/views/patients/programmes/show.html.erb | 9 ++++ app/views/patients/show.html.erb | 12 +++--- ...pp_patient_session_table_component_spec.rb | 41 ++++++++++++++++++- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/app/components/app_patient_session_table_component.rb b/app/components/app_patient_session_table_component.rb index ce447bfd2a..719c8ffa9a 100644 --- a/app/components/app_patient_session_table_component.rb +++ b/app/components/app_patient_session_table_component.rb @@ -8,7 +8,11 @@ class AppPatientSessionTableComponent < ViewComponent::Base <% head.with_row do |row| %> <% row.with_cell(text: "Location") %> <% row.with_cell(text: "Session dates") %> - <% row.with_cell(text: "Programme") %> + <% if @programme_type.nil? %> + <% row.with_cell(text: "Programme") %> + <% else %> + <% row.with_cell(text: "Session outcome") %> + <% end %> <% end %> <% end %> @@ -32,8 +36,13 @@ class AppPatientSessionTableComponent < ViewComponent::Base <% end %> <% row.with_cell do %> - Programme - <%= render AppProgrammeTagsComponent.new([programme]) %> + <% if @programme_type.nil? %> + Programme + <%= render AppProgrammeTagsComponent.new([programme]) %> + <% else %> + Session outcome + <%= '?' %> + <% end %> <% end %> <% end %> <% end %> @@ -45,14 +54,15 @@ class AppPatientSessionTableComponent < ViewComponent::Base <% end %> ERB - def initialize(patient, current_team:) + def initialize(patient, current_team:, programme_type: nil) @patient = patient @current_team = current_team + @programme_type = programme_type end private - attr_reader :patient, :current_team + attr_reader :patient, :current_team, :programme_type delegate :govuk_table, to: :helpers @@ -61,6 +71,13 @@ def sessions patient .sessions .for_team(current_team) + .then { |sessions| + if @programme_type + sessions.has_any_programme_types_of(programme_type) + else + sessions + end + } .includes(:location, :session_programme_year_groups) end end diff --git a/app/views/patients/programmes/show.html.erb b/app/views/patients/programmes/show.html.erb index 09b259fb0a..a3e0d43404 100644 --- a/app/views/patients/programmes/show.html.erb +++ b/app/views/patients/programmes/show.html.erb @@ -15,3 +15,12 @@ @patient, academic_year: AcademicYear.current, show_caption: false, programme: @programme, ) %> <% end %> + +<%= render AppCardComponent.new(section: true) do |card| %> + <% card.with_heading { "Sessions" } %> + <%= render AppPatientSessionTableComponent.new(@patient, current_team:, programme_type: @programme.type) %> + + <% unless @in_generic_clinic %> + <%= govuk_button_to "Invite to community clinic", invite_to_clinic_patient_path(@patient), secondary: true %> + <% end %> +<% end %> diff --git a/app/views/patients/show.html.erb b/app/views/patients/show.html.erb index e7a6dfa0e9..86f2fba5b0 100644 --- a/app/views/patients/show.html.erb +++ b/app/views/patients/show.html.erb @@ -49,12 +49,14 @@ ) %> <% end %> - <%= render AppCardComponent.new(section: true) do |card| %> - <% card.with_heading { "Sessions" } %> - <%= render AppPatientSessionTableComponent.new(@patient, current_team:) %> + <% unless Flipper.enabled?(:child_record_redesign) %> + <%= render AppCardComponent.new(section: true) do |card| %> + <% card.with_heading { "Sessions" } %> + <%= render AppPatientSessionTableComponent.new(@patient, current_team:) %> - <% unless @in_generic_clinic %> - <%= govuk_button_to "Invite to community clinic", invite_to_clinic_patient_path(@patient), secondary: true %> + <% unless @in_generic_clinic %> + <%= govuk_button_to "Invite to community clinic", invite_to_clinic_patient_path(@patient), secondary: true %> + <% end %> <% end %> <% end %> diff --git a/spec/components/app_patient_session_table_component_spec.rb b/spec/components/app_patient_session_table_component_spec.rb index ec2d4d558e..fa2d712e9e 100644 --- a/spec/components/app_patient_session_table_component_spec.rb +++ b/spec/components/app_patient_session_table_component_spec.rb @@ -14,7 +14,7 @@ end context "with a session" do - let(:programmes) { [Programme.hpv] } + let(:programmes) { [Programme.hpv, Programme.mmr] } let(:location) do create(:school, name: "Waterloo Road", programmes:, academic_year: 2024) @@ -42,5 +42,44 @@ it { should have_link("Waterloo Road") } it { should have_content("1 January 2025") } it { should have_content("HPV") } + + context "with multiple sessions" do + let(:other_location) do + create( + :school, + name: "Paddington Road", + programmes: other_programmes, + academic_year: 2024 + ) + end + let(:other_session) do + create( + :session, + team:, + location: other_location, + programmes: other_programmes, + date: Date.new(2025, 2, 1) + ) + end + let(:other_programmes) { [Programme.hpv, Programme.menacwy] } + + before { create(:patient_location, patient:, session: other_session) } + + it { should have_link("Waterloo Road") } + it { should have_link("Paddington Road") } + + context "with a programme type filter that matches only one session" do + let(:component) do + described_class.new( + patient, + current_team: team, + programme_type: :menacwy + ) + end + + it { should_not have_link("Waterloo Road") } + it { should have_link("Paddington Road") } + end + end end end From 0fd38438abb586726b0f7fe22de1f53f9952d551 Mon Sep 17 00:00:00 2001 From: Steve Hook Date: Fri, 27 Feb 2026 09:30:53 +0000 Subject: [PATCH 2/6] Refactor to extract `AppPatientProgrammeSessionTableComponent` The behaviour and appearance of the session table on the programme tabs is sufficiently different from `AppPatientProgrammeSessionTableComponent` appears on the _Child record_ tab to make a separate component worthwhile. Jira-issue: MAV-3836 --- ...tient_programme_session_table_component.rb | 73 ++++++++++++++ .../app_patient_session_table_component.rb | 25 +---- app/views/patients/programmes/show.html.erb | 2 +- ..._programme_session_table_component_spec.rb | 96 +++++++++++++++++++ ...pp_patient_session_table_component_spec.rb | 39 -------- 5 files changed, 174 insertions(+), 61 deletions(-) create mode 100644 app/components/app_patient_programme_session_table_component.rb create mode 100644 spec/components/app_patient_programme_session_table_component_spec.rb diff --git a/app/components/app_patient_programme_session_table_component.rb b/app/components/app_patient_programme_session_table_component.rb new file mode 100644 index 0000000000..96cc7eede1 --- /dev/null +++ b/app/components/app_patient_programme_session_table_component.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class AppPatientProgrammeSessionTableComponent < ViewComponent::Base + erb_template <<-ERB + <% if sessions.any? %> + <%= govuk_table(html_attributes: { class: "nhsuk-table-responsive" }) do |table| %> + <% table.with_head do |head| %> + <% head.with_row do |row| %> + <% row.with_cell(text: "Location") %> + <% row.with_cell(text: "Session dates") %> + <% row.with_cell(text: "Session outcome") %> + <% end %> + <% end %> + + <% table.with_body do |body| %> + <% sessions.each do |session| %> + <% body.with_row do |row| %> + <% row.with_cell do %> + Location + <%= link_to session.location.name, + session_patient_programme_path(session, patient, programme_type) %> + <% end %> + + <% row.with_cell do %> + Session dates + + <% end %> + + <% row.with_cell do %> + Session outcome + <%= session_outcome_tag(session, programme_type) %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% else %> +

No sessions

+ <% end %> + ERB + + def initialize(patient, current_team:, programme_type:) + @patient = patient + @current_team = current_team + @programme_type = programme_type + end + + private + + attr_reader :patient, :current_team, :programme_type + + delegate :govuk_table, to: :helpers + + def sessions + @sessions ||= + patient + .sessions + .for_team(current_team) + .has_any_programme_types_of(programme_type) + .includes(:location, :session_programme_year_groups) + end + + def session_outcome_tag(session, programme_type) + vaccination_record = session.vaccination_records.where(programme_type:).last + return "No outcome" unless vaccination_record + + helpers.vaccination_record_status_tag(vaccination_record) + end +end diff --git a/app/components/app_patient_session_table_component.rb b/app/components/app_patient_session_table_component.rb index 719c8ffa9a..de17a5d27f 100644 --- a/app/components/app_patient_session_table_component.rb +++ b/app/components/app_patient_session_table_component.rb @@ -8,11 +8,7 @@ class AppPatientSessionTableComponent < ViewComponent::Base <% head.with_row do |row| %> <% row.with_cell(text: "Location") %> <% row.with_cell(text: "Session dates") %> - <% if @programme_type.nil? %> <% row.with_cell(text: "Programme") %> - <% else %> - <% row.with_cell(text: "Session outcome") %> - <% end %> <% end %> <% end %> @@ -36,13 +32,8 @@ class AppPatientSessionTableComponent < ViewComponent::Base <% end %> <% row.with_cell do %> - <% if @programme_type.nil? %> - Programme - <%= render AppProgrammeTagsComponent.new([programme]) %> - <% else %> - Session outcome - <%= '?' %> - <% end %> + Programme + <%= render AppProgrammeTagsComponent.new([programme]) %> <% end %> <% end %> <% end %> @@ -54,15 +45,14 @@ class AppPatientSessionTableComponent < ViewComponent::Base <% end %> ERB - def initialize(patient, current_team:, programme_type: nil) + def initialize(patient, current_team:) @patient = patient @current_team = current_team - @programme_type = programme_type end private - attr_reader :patient, :current_team, :programme_type + attr_reader :patient, :current_team delegate :govuk_table, to: :helpers @@ -71,13 +61,6 @@ def sessions patient .sessions .for_team(current_team) - .then { |sessions| - if @programme_type - sessions.has_any_programme_types_of(programme_type) - else - sessions - end - } .includes(:location, :session_programme_year_groups) end end diff --git a/app/views/patients/programmes/show.html.erb b/app/views/patients/programmes/show.html.erb index a3e0d43404..cf8bc5e6fe 100644 --- a/app/views/patients/programmes/show.html.erb +++ b/app/views/patients/programmes/show.html.erb @@ -18,7 +18,7 @@ <%= render AppCardComponent.new(section: true) do |card| %> <% card.with_heading { "Sessions" } %> - <%= render AppPatientSessionTableComponent.new(@patient, current_team:, programme_type: @programme.type) %> + <%= render AppPatientProgrammeSessionTableComponent.new(@patient, current_team:, programme_type: @programme.type) %> <% unless @in_generic_clinic %> <%= govuk_button_to "Invite to community clinic", invite_to_clinic_patient_path(@patient), secondary: true %> diff --git a/spec/components/app_patient_programme_session_table_component_spec.rb b/spec/components/app_patient_programme_session_table_component_spec.rb new file mode 100644 index 0000000000..8a859a57ae --- /dev/null +++ b/spec/components/app_patient_programme_session_table_component_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +describe AppPatientProgrammeSessionTableComponent do + subject { render_inline(component) } + + let(:component) do + described_class.new(patient, current_team: team, programme_type:) + end + let(:programme_type) { :hpv } + let(:team) { create(:team) } + + context "without a session" do + let(:patient) { create(:patient) } + + it { should have_content("No sessions") } + end + + context "with one session" do + let(:programmes) { [Programme.hpv, Programme.mmr] } + + let(:location) do + create(:school, name: "Waterloo Road", programmes:, academic_year: 2024) + end + let(:session) do + create( + :session, + team:, + location:, + programmes:, + date: Date.new(2025, 1, 1) + ) + end + + # Can't use year_group here because we need an absolute date, not one + # relative to the current academic year. + let(:patient) { create(:patient, date_of_birth: Date.new(2011, 9, 1)) } + + before { create_list(:patient_location, 1, patient:, session:) } + + it { should have_link("Waterloo Road") } + it { should have_content("1 January 2025") } + + context "with multiple sessions" do + let(:other_location) do + create( + :school, + name: "Paddington Road", + programmes: other_programmes, + academic_year: 2024 + ) + end + let(:other_session) do + create( + :session, + team:, + location: other_location, + programmes: other_programmes, + date: Date.new(2025, 2, 1) + ) + end + let(:other_programmes) { [Programme.hpv, Programme.menacwy] } + + before do + create(:patient_location, patient:, session: other_session) + create( + :vaccination_record, + patient:, + session:, + outcome: :administered, + programme_type: + ) + end + + it { should have_link("Waterloo Road") } + it { should have_content("1 January 2025") } + it { should have_content("Vaccinated") } + + it { should have_link("Paddington Road") } + it { should have_content("1 February 2025") } + it { should have_content("No outcome") } + + context "with a programme type filter that matches only one session" do + let(:component) do + described_class.new( + patient, + current_team: team, + programme_type: :menacwy + ) + end + + it { should_not have_link("Waterloo Road") } + it { should have_link("Paddington Road") } + end + end + end +end diff --git a/spec/components/app_patient_session_table_component_spec.rb b/spec/components/app_patient_session_table_component_spec.rb index fa2d712e9e..dbb957ca49 100644 --- a/spec/components/app_patient_session_table_component_spec.rb +++ b/spec/components/app_patient_session_table_component_spec.rb @@ -42,44 +42,5 @@ it { should have_link("Waterloo Road") } it { should have_content("1 January 2025") } it { should have_content("HPV") } - - context "with multiple sessions" do - let(:other_location) do - create( - :school, - name: "Paddington Road", - programmes: other_programmes, - academic_year: 2024 - ) - end - let(:other_session) do - create( - :session, - team:, - location: other_location, - programmes: other_programmes, - date: Date.new(2025, 2, 1) - ) - end - let(:other_programmes) { [Programme.hpv, Programme.menacwy] } - - before { create(:patient_location, patient:, session: other_session) } - - it { should have_link("Waterloo Road") } - it { should have_link("Paddington Road") } - - context "with a programme type filter that matches only one session" do - let(:component) do - described_class.new( - patient, - current_team: team, - programme_type: :menacwy - ) - end - - it { should_not have_link("Waterloo Road") } - it { should have_link("Paddington Road") } - end - end end end From 4decc53794b172d30bca5fad0b96248d9c9d8255 Mon Sep 17 00:00:00 2001 From: Steve Hook Date: Fri, 27 Feb 2026 10:57:35 +0000 Subject: [PATCH 3/6] Update feature specs for per-programme sessions Jira-issue: MAV-3836 --- spec/features/viewing_child_records_spec.rb | 31 ++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/spec/features/viewing_child_records_spec.rb b/spec/features/viewing_child_records_spec.rb index f2b4922b6a..291a47f018 100644 --- a/spec/features/viewing_child_records_spec.rb +++ b/spec/features/viewing_child_records_spec.rb @@ -19,11 +19,13 @@ and_i_can_only_see_tabs_for_relevant_programmes when_i_click_on_the_flu_tab - then_i_see_the_childs_flu_information + then_i_see_the_childs_flu_vaccinations + and_i_see_the_childs_flu_sessions and_the_flu_tab_is_selected when_i_click_on_the_hpv_tab - then_i_see_the_childs_hpv_information + then_i_see_the_childs_hpv_vaccinations + and_i_see_the_childs_hpv_sessions and_the_hpv_tab_is_selected end @@ -88,6 +90,7 @@ def and_patients_exist def and_the_patient_is_vaccinated create( :vaccination_record, + outcome: :administered, patient: @patient, programme: @hpv, session: @session @@ -134,10 +137,9 @@ def when_i_click_on_the_flu_tab click_on "Flu" end - def then_i_see_the_childs_flu_information + def then_i_see_the_childs_flu_vaccinations expect(page).to have_current_path(patient_programme_path(@patient, "flu")) expect(page).to have_css("h3.nhsuk-card__heading", text: "Vaccinations") - expect(page).to have_css(".nhsuk-body", text: "No vaccinations") end def and_the_flu_tab_is_selected @@ -148,14 +150,29 @@ def when_i_click_on_the_hpv_tab click_on "HPV" end - def then_i_see_the_childs_hpv_information + def then_i_see_the_childs_hpv_vaccinations expect(page).to have_current_path(patient_programme_path(@patient, "hpv")) expect(page).to have_css("h3.nhsuk-card__heading", text: "Vaccinations") - expect(page).to have_css(".nhsuk-table__cell", text: "Recorded in Mavis") - expect(page).to have_css(".nhsuk-table__cell", text: "Vaccinated") end def and_the_hpv_tab_is_selected expect(page).to have_css(".app-secondary-navigation__current", text: "HPV") end + + def and_i_see_the_childs_flu_sessions + within(".nhsuk-card", text: "Sessions") do + expect(page).to have_content("No sessions") + end + end + + def and_i_see_the_childs_hpv_sessions + within(".nhsuk-card", text: "Sessions") do + expect(page).to have_link( + @session.location.name, + href: session_patient_programme_path(@session, @patient, "hpv") + ) + expect(page).to have_content(@session.dates.first.to_fs(:long)) + expect(page).to have_content("Vaccinated") + end + end end From 58576ebf118a04f5ec4a6471b22c09a7c733b1c5 Mon Sep 17 00:00:00 2001 From: Steve Hook Date: Fri, 27 Feb 2026 14:43:01 +0000 Subject: [PATCH 4/6] Remove _Invite to community clinic_ button This is part of the soon to be removed _Sessions_ card on the _Child record_ tab, but is not present in the prototype for the new design. Jira-issue: MAV-3836 --- app/views/patients/programmes/show.html.erb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/patients/programmes/show.html.erb b/app/views/patients/programmes/show.html.erb index cf8bc5e6fe..05189f331c 100644 --- a/app/views/patients/programmes/show.html.erb +++ b/app/views/patients/programmes/show.html.erb @@ -19,8 +19,4 @@ <%= render AppCardComponent.new(section: true) do |card| %> <% card.with_heading { "Sessions" } %> <%= render AppPatientProgrammeSessionTableComponent.new(@patient, current_team:, programme_type: @programme.type) %> - - <% unless @in_generic_clinic %> - <%= govuk_button_to "Invite to community clinic", invite_to_clinic_patient_path(@patient), secondary: true %> - <% end %> <% end %> From 8a9236d94f3758a29937b173522a143a4dc5c429 Mon Sep 17 00:00:00 2001 From: Steve Hook Date: Wed, 4 Mar 2026 11:35:35 +0000 Subject: [PATCH 5/6] Apply suggestions from code review Proper scoping of vaccination records Co-authored-by: Thomas Leese --- .../app_patient_programme_session_table_component.rb | 3 ++- app/components/app_patient_session_table_component.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/app_patient_programme_session_table_component.rb b/app/components/app_patient_programme_session_table_component.rb index 96cc7eede1..1db8274f47 100644 --- a/app/components/app_patient_programme_session_table_component.rb +++ b/app/components/app_patient_programme_session_table_component.rb @@ -65,7 +65,8 @@ def sessions end def session_outcome_tag(session, programme_type) - vaccination_record = session.vaccination_records.where(programme_type:).last + vaccination_record = + session.vaccination_records.where(programme_type:, patient:).last return "No outcome" unless vaccination_record helpers.vaccination_record_status_tag(vaccination_record) diff --git a/app/components/app_patient_session_table_component.rb b/app/components/app_patient_session_table_component.rb index de17a5d27f..ce447bfd2a 100644 --- a/app/components/app_patient_session_table_component.rb +++ b/app/components/app_patient_session_table_component.rb @@ -8,7 +8,7 @@ class AppPatientSessionTableComponent < ViewComponent::Base <% head.with_row do |row| %> <% row.with_cell(text: "Location") %> <% row.with_cell(text: "Session dates") %> - <% row.with_cell(text: "Programme") %> + <% row.with_cell(text: "Programme") %> <% end %> <% end %> From 2eb44ca131e5fe23ff48c00eaaf6d4b17327198c Mon Sep 17 00:00:00 2001 From: Steve Hook Date: Wed, 4 Mar 2026 13:18:01 +0000 Subject: [PATCH 6/6] Use performed_at to work out the last vaccination `VaccinationRecord.last` orders by `id` so it won't return what we want by itself if vaccination records are added to the system out of sequence. --- .../app_patient_programme_session_table_component.rb | 6 +++++- ...app_patient_programme_session_table_component_spec.rb | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/components/app_patient_programme_session_table_component.rb b/app/components/app_patient_programme_session_table_component.rb index 1db8274f47..92c39dd876 100644 --- a/app/components/app_patient_programme_session_table_component.rb +++ b/app/components/app_patient_programme_session_table_component.rb @@ -66,7 +66,11 @@ def sessions def session_outcome_tag(session, programme_type) vaccination_record = - session.vaccination_records.where(programme_type:, patient:).last + session + .vaccination_records + .where(programme_type:, patient:) + .order(:performed_at_date, :performed_at_time) + .last return "No outcome" unless vaccination_record helpers.vaccination_record_status_tag(vaccination_record) diff --git a/spec/components/app_patient_programme_session_table_component_spec.rb b/spec/components/app_patient_programme_session_table_component_spec.rb index 8a859a57ae..522919351c 100644 --- a/spec/components/app_patient_programme_session_table_component_spec.rb +++ b/spec/components/app_patient_programme_session_table_component_spec.rb @@ -67,6 +67,15 @@ patient:, session:, outcome: :administered, + performed_at_date: 1.month.ago, + programme_type: + ) + create( + :vaccination_record, + patient:, + session:, + outcome: :refused, + performed_at_date: 1.year.ago, programme_type: ) end