Skip to content

Commit d20a8b6

Browse files
committed
Prescribers can create PSD instructions in bulk
This PR implements a new feature that allows prescribers (nurses, pharmacists) to bulk create Patient Specific Directions (PSDs) for patients who have given consent and don't require individual triage assessment. This significantly reduces manual work by enabling batch processing of PSDs instead of creating them one by one. What's changed? - New (basic) controller and page for the PSDs tab (no filters yet) - PSDs tab only appears if delegation feature is enabled - New controller to manage the bulk add PSDs
1 parent 4d51071 commit d20a8b6

File tree

13 files changed

+392
-3
lines changed

13 files changed

+392
-3
lines changed

app/assets/stylesheets/components/_action-list.scss

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,59 @@
3333
margin-right: 0;
3434
padding-right: 0;
3535
}
36+
37+
.nhsuk-action-link {
38+
@include nhsuk-responsive-margin(6, "bottom");
39+
}
40+
41+
.nhsuk-action-link__link {
42+
display: inline-block; // [1]
43+
padding-left: 38px; // [2]
44+
position: relative; // [3]
45+
text-decoration: none; // [4]
46+
47+
@include nhsuk-font(22, $weight: bold);
48+
49+
&:not(:focus):hover {
50+
.nhsuk-action-link__text {
51+
text-decoration: underline; // [6]
52+
}
53+
}
54+
55+
@include nhsuk-media-query($until: tablet) {
56+
padding-left: 26px; // [2]
57+
}
58+
59+
@include nhsuk-media-query($media-type: print) {
60+
color: $nhsuk-print-text-color;
61+
62+
&:visited {
63+
color: $nhsuk-print-text-color;
64+
}
65+
}
66+
67+
.nhsuk-icon__arrow-right-circle {
68+
// stylelint-disable-next-line declaration-no-important
69+
fill: $color_nhsuk-green !important;
70+
height: 36px;
71+
left: -3px;
72+
position: absolute;
73+
top: -3px;
74+
width: 36px;
75+
76+
@include nhsuk-print-color($nhsuk-print-text-color);
77+
78+
@include nhsuk-media-query($until: tablet) {
79+
height: 24px;
80+
left: -2px;
81+
margin-bottom: 0;
82+
top: 1px;
83+
width: 24px;
84+
}
85+
}
86+
87+
&:focus .nhsuk-icon__arrow-right-circle {
88+
// stylelint-disable-next-line declaration-no-important
89+
fill: $color_nhsuk-black !important;
90+
}
91+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# frozen_string_literal: true
2+
3+
class AppPatientSessionPsdSearchResultCardComponent < ViewComponent::Base
4+
erb_template <<-ERB
5+
<%= render AppCardComponent.new(heading_level: 4, compact: true) do |card| %>
6+
<% card.with_heading { link_to(patient.full_name_with_known_as, patient_path) } %>
7+
8+
<%= govuk_summary_list do |summary_list|
9+
summary_list.with_row do |row|
10+
row.with_key { "Date of birth" }
11+
row.with_value { helpers.patient_date_of_birth(patient) }
12+
end
13+
14+
summary_list.with_row do |row|
15+
row.with_key { "Year group" }
16+
row.with_value { helpers.patient_year_group(patient, academic_year:) }
17+
end
18+
19+
summary_list.with_row do |row|
20+
row.with_key { "PSD status" }
21+
row.with_value { psd_status_tag }
22+
end
23+
end %>
24+
<% end %>
25+
ERB
26+
27+
def initialize(patient_session)
28+
super
29+
30+
@patient_session = patient_session
31+
@patient = patient_session.patient
32+
@session = patient_session.session
33+
@programmes = patient_session.programmes
34+
end
35+
36+
private
37+
38+
attr_reader :patient_session, :patient, :session, :programmes
39+
40+
delegate :academic_year, to: :session
41+
42+
def patient_path
43+
session_patient_programme_path(
44+
session,
45+
patient,
46+
programmes.first,
47+
return_to: :patient_specific_directions
48+
)
49+
end
50+
51+
def psd_status_tag
52+
status = psd_status_for_programme(programmes.first)
53+
text = t(status, scope: %i[status psd label])
54+
colour = t(status, scope: %i[status psd colour])
55+
56+
tag.strong(text, class: ["nhsuk-tag nhsuk-tag--#{colour}"])
57+
end
58+
59+
def psd_status_for_programme(programme)
60+
if patient
61+
.patient_specific_directions
62+
.where(programme:, academic_year:)
63+
.exists?
64+
:added
65+
else
66+
:not_added
67+
end
68+
end
69+
end

app/components/app_search_results_component.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
class AppSearchResultsComponent < ViewComponent::Base
44
erb_template <<-ERB
5-
<h3 class="nhsuk-heading-m nhsuk-u-margin-bottom-2">Search results</h3>
5+
<h3 class="nhsuk-heading-m nhsuk-u-margin-bottom-2"><%= heading %></h3>
66
77
<p class="nhsuk-caption-m nhsuk-u-margin-bottom-4">
88
<% if has_results? %>
@@ -19,16 +19,17 @@ class AppSearchResultsComponent < ViewComponent::Base
1919
<% end %>
2020
ERB
2121

22-
def initialize(pagy, label:)
22+
def initialize(pagy, label:, heading: "Search results")
2323
super
2424

2525
@pagy = pagy
2626
@label = label
27+
@heading = heading
2728
end
2829

2930
private
3031

31-
attr_reader :pagy, :label
32+
attr_reader :pagy, :label, :heading
3233

3334
def has_results? = pagy.count.positive?
3435
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module PatientSpecificDirectionConcern
4+
extend ActiveSupport::Concern
5+
6+
def patient_sessions_allowed_psd
7+
@patient_sessions_allowed_psd ||=
8+
@session
9+
.patient_sessions
10+
.has_consent_status(:given, programme:)
11+
.joins(:patient)
12+
.where.not(
13+
patients: {
14+
id:
15+
PatientSpecificDirection.where(
16+
programme:,
17+
academic_year: @session.academic_year
18+
).select(:patient_id)
19+
}
20+
)
21+
end
22+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
class Sessions::BulkPatientSpecificDirectionsController < ApplicationController
4+
include PatientSpecificDirectionConcern
5+
6+
before_action :set_session
7+
8+
def show
9+
@eligible_for_bulk_psd_count = patient_sessions_allowed_psd.count
10+
end
11+
12+
def create
13+
ActiveRecord::Base.transaction do
14+
PatientSpecificDirection.import!(
15+
psds_to_create,
16+
on_duplicate_key_ignore: true
17+
)
18+
end
19+
20+
redirect_to session_psds_path(@session), flash: { success: "PSDs added" }
21+
end
22+
23+
private
24+
25+
def set_session
26+
@session = policy_scope(Session).find_by!(slug: params[:session_slug])
27+
end
28+
29+
def psds_to_create
30+
patient_sessions_allowed_psd.map do |patient_session|
31+
PatientSpecificDirection.new(
32+
academic_year: @session.academic_year,
33+
patient_id: patient_session.patient_id,
34+
programme_id: programme.id,
35+
vaccine_id: programme.vaccines.first.id,
36+
created_by_user_id: current_user.id,
37+
vaccine_method: :nasal,
38+
delivery_site: :nose,
39+
full_dose: true
40+
)
41+
end
42+
end
43+
44+
def programme
45+
@session.programmes.includes(:vaccines).first
46+
end
47+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
class Sessions::PatientSpecificDirectionsController < ApplicationController
4+
include PatientSearchFormConcern
5+
include PatientSpecificDirectionConcern
6+
7+
before_action :set_session
8+
before_action :set_patient_search_form
9+
10+
layout "full"
11+
12+
def show
13+
scope = @session.patient_sessions.includes_programmes.includes(:latest_note)
14+
@eligible_for_bulk_psd_count = patient_sessions_allowed_psd.count
15+
patient_sessions = @form.apply(scope)
16+
@pagy, @patient_sessions = pagy(patient_sessions)
17+
end
18+
19+
private
20+
21+
def set_session
22+
@session = policy_scope(Session).find_by!(slug: params[:session_slug])
23+
end
24+
25+
def programme
26+
@session.programmes.first
27+
end
28+
end

app/views/sessions/_header.html.erb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424
selected: request.path == session_consent_path(@session),
2525
)
2626

27+
if Flipper.enabled?(:delegation) && @session.programmes.first.flu?
28+
nav.with_item(
29+
href: session_patient_specific_directions_path(@session),
30+
text: t("sessions.tabs.patient_specific_directions"),
31+
selected: request.path == session_patient_specific_directions_path(@session),
32+
)
33+
end
34+
2735
nav.with_item(
2836
href: session_triage_path(@session),
2937
text: t("sessions.tabs.triage"),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<% content_for :before_main do %>
2+
<%= render AppBreadcrumbComponent.new(items: [
3+
{ text: t("dashboard.index.title"), href: dashboard_path },
4+
{ text: t("sessions.index.title"), href: sessions_path },
5+
{ text: @session.location.name, href: session_path(@session) },
6+
]) %>
7+
<% end %>
8+
9+
<%= h1 "Are you sure you want to add #{@eligible_for_bulk_psd_count} new PSDs?" %>
10+
11+
<div class="nhsuk-grid-row">
12+
<div class="nhsuk-grid-column-two-thirds">
13+
<p class="nhsuk-body">This cannot be undone.</p>
14+
<div class="app-button-group">
15+
<%= govuk_button_to "Yes, add PSDs", session_bulk_patient_specific_directions_path(@session) %>
16+
<%= govuk_link_to "No, return to session",
17+
session_bulk_patient_specific_directions_path(@session),
18+
method: :post,
19+
prevent_double_click: true %>
20+
</div>
21+
</div>
22+
</div>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<% content_for :before_main do %>
2+
<%= render AppBreadcrumbComponent.new(items: [
3+
{ text: t("dashboard.index.title"), href: dashboard_path },
4+
{ text: t("sessions.index.title"), href: sessions_path },
5+
{ text: @session.location.name, href: session_path(@session) },
6+
]) %>
7+
<% end %>
8+
9+
<%= render "sessions/header" %>
10+
11+
<%= govuk_inset_text do %>
12+
<span class="nhsuk-u-visually-hidden">Information: </span>
13+
<p class="nhsuk-body">
14+
There are <%= @eligible_for_bulk_psd_count %> children with consent for the nasal flu vaccine
15+
who do not require triage and do not yet have a PSD in place.
16+
</p>
17+
<div class="nhsuk-action-link">
18+
<%= link_to session_bulk_patient_specific_directions_path(@session), class: "nhsuk-action-link__link" do %>
19+
<svg class="nhsuk-icon nhsuk-icon__arrow-right-circle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" width="36" height="36">
20+
<path d="M0 0h24v24H0z" fill="none"></path>
21+
<path d="M12 2a10 10 0 0 0-9.95 9h11.64L9.74 7.05a1 1 0 0 1 1.41-1.41l5.66 5.65a1 1 0 0 1 0 1.42l-5.66 5.65a1 1 0 0 1-1.41 0 1 1 0 0 1 0-1.41L13.69 13H2.05A10 10 0 1 0 12 2z"></path>
22+
</svg>
23+
<span class="nhsuk-action-link__text">Add new PSDs</span>
24+
<% end %>
25+
</div>
26+
<% end %>
27+
28+
<div class="nhsuk-grid-row">
29+
<div class="app-grid-column-filters"></div>
30+
31+
<div class="app-grid-column-results">
32+
<%= render AppSessionNeedsReviewWarningComponent.new(session: @session) %>
33+
<%= render AppSearchResultsComponent.new(@pagy, label: "children", heading: "Review PSDs") do %>
34+
<% @patient_sessions.each do |patient_session| %>
35+
<%= render AppPatientSessionPsdSearchResultCardComponent.new(patient_session) %>
36+
<% end %>
37+
<% end %>
38+
</div>
39+
</div>

config/locales/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ en:
685685
record: Record vaccinations
686686
register: Register
687687
triage: Triage
688+
patient_specific_directions: PSDs
688689
table:
689690
no_filtered_results: We couldn’t find any children that matched your filters.
690691
no_results: No results

0 commit comments

Comments
 (0)