Skip to content

Commit 1ea785e

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 1ea785e

File tree

14 files changed

+398
-3
lines changed

14 files changed

+398
-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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
.without_patient_specific_direction(programme:)
12+
end
13+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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_patient_specific_directions_path(@session),
21+
flash: {
22+
success: "PSDs added"
23+
}
24+
end
25+
26+
private
27+
28+
def set_session
29+
@session = policy_scope(Session).find_by!(slug: params[:session_slug])
30+
end
31+
32+
def psds_to_create
33+
patient_sessions_allowed_psd.map do |patient_session|
34+
PatientSpecificDirection.new(
35+
academic_year: @session.academic_year,
36+
patient_id: patient_session.patient_id,
37+
programme_id: programme.id,
38+
vaccine_id: programme.vaccines.first.id,
39+
created_by_user_id: current_user.id,
40+
vaccine_method: :nasal,
41+
delivery_site: :nose,
42+
full_dose: true
43+
)
44+
end
45+
end
46+
47+
def programme
48+
@session.programmes.includes(:vaccines).first
49+
end
50+
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/models/patient_session.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ class PatientSession < ApplicationRecord
254254
end
255255
end
256256

257+
scope :without_patient_specific_direction,
258+
->(programme:) do
259+
joins(:session).where.not(
260+
PatientSpecificDirection
261+
.where("patient_id = patient_sessions.patient_id")
262+
.where("academic_year = sessions.academic_year")
263+
.where(programme:)
264+
.arel
265+
.exists
266+
)
267+
end
268+
257269
scope :destroy_all_if_safe,
258270
-> do
259271
includes(

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>

0 commit comments

Comments
 (0)