diff --git a/app/assets/stylesheets/components/_action-list.scss b/app/assets/stylesheets/components/_action-list.scss index e803d575d5..fc20935bde 100644 --- a/app/assets/stylesheets/components/_action-list.scss +++ b/app/assets/stylesheets/components/_action-list.scss @@ -33,3 +33,59 @@ margin-right: 0; padding-right: 0; } + +.nhsuk-action-link { + @include nhsuk-responsive-margin(6, "bottom"); +} + +.nhsuk-action-link__link { + display: inline-block; // [1] + padding-left: 38px; // [2] + position: relative; // [3] + text-decoration: none; // [4] + + @include nhsuk-font(22, $weight: bold); + + &:not(:focus):hover { + .nhsuk-action-link__text { + text-decoration: underline; // [6] + } + } + + @include nhsuk-media-query($until: tablet) { + padding-left: 26px; // [2] + } + + @include nhsuk-media-query($media-type: print) { + color: $nhsuk-print-text-color; + + &:visited { + color: $nhsuk-print-text-color; + } + } + + .nhsuk-icon__arrow-right-circle { + // stylelint-disable-next-line declaration-no-important + fill: $color_nhsuk-green !important; + height: 36px; + left: -3px; + position: absolute; + top: -3px; + width: 36px; + + @include nhsuk-print-color($nhsuk-print-text-color); + + @include nhsuk-media-query($until: tablet) { + height: 24px; + left: -2px; + margin-bottom: 0; + top: 1px; + width: 24px; + } + } + + &:focus .nhsuk-icon__arrow-right-circle { + // stylelint-disable-next-line declaration-no-important + fill: $color_nhsuk-black !important; + } +} diff --git a/app/components/app_patient_search_form_component.rb b/app/components/app_patient_search_form_component.rb index 62836144eb..f9ae716595 100644 --- a/app/components/app_patient_search_form_component.rb +++ b/app/components/app_patient_search_form_component.rb @@ -79,6 +79,19 @@ class AppPatientSearchFormComponent < ViewComponent::Base <% end %> <% end %> <% end %> + + <% if patient_specific_direction_statuses.any? %> + <%= f.govuk_radio_buttons_fieldset :patient_specific_direction_status, legend: { text: "PSD status", size: "s" } do %> + <%= f.govuk_radio_button :patient_specific_direction_status, "", checked: form.patient_specific_direction_status.blank?, label: { text: "Any" } %> + + <% patient_specific_direction_statuses.each do |status| %> + <%= f.govuk_radio_button :patient_specific_direction_status, + status, + checked: form.patient_specific_direction_status == status, + label: { text: t(status, scope: %i[status patient_specific_direction label]) } %> + <% end %> + <% end %> + <% end %> <% if vaccine_methods.any? %> <%= f.govuk_radio_buttons_fieldset :vaccine_method, legend: { text: "Vaccination method", size: "s" } do %> @@ -182,6 +195,7 @@ def initialize( register_statuses: [], triage_statuses: [], vaccination_statuses: [], + patient_specific_direction_statuses: [], vaccine_methods: [], year_groups: [], heading_level: 3, @@ -197,6 +211,7 @@ def initialize( @register_statuses = register_statuses @triage_statuses = triage_statuses @vaccination_statuses = vaccination_statuses + @patient_specific_direction_statuses = patient_specific_direction_statuses @vaccine_methods = vaccine_methods @year_groups = year_groups @heading_level = heading_level @@ -212,6 +227,7 @@ def initialize( :register_statuses, :triage_statuses, :vaccination_statuses, + :patient_specific_direction_statuses, :vaccine_methods, :year_groups, :heading_level, diff --git a/app/components/app_patient_session_record_component.rb b/app/components/app_patient_session_record_component.rb index 6322d761d1..2a39ecc89f 100644 --- a/app/components/app_patient_session_record_component.rb +++ b/app/components/app_patient_session_record_component.rb @@ -2,18 +2,18 @@ class AppPatientSessionRecordComponent < ViewComponent::Base erb_template <<-ERB -

<%= heading %>

- - <% if helpers.policy(VaccinationRecord).new? %> + <% if helpers.policy(vaccination_record).new? %> +

<%= heading %>

<%= render AppVaccinateFormComponent.new(vaccinate_form) %> <% end %> ERB - def initialize(patient_session, programme:, vaccinate_form: nil) + def initialize(patient_session, programme:, current_user:, vaccinate_form:) super @patient_session = patient_session @programme = programme + @current_user = current_user @vaccinate_form = vaccinate_form || default_vaccinate_form end @@ -28,16 +28,21 @@ def render? private - attr_reader :patient_session, :programme, :vaccinate_form + attr_reader :patient_session, :current_user, :programme, :vaccinate_form delegate :patient, :session, to: :patient_session delegate :academic_year, to: :session + def vaccination_record + VaccinationRecord.new(patient:, session:, programme:) + end + def default_vaccinate_form pre_screening_confirmed = patient.pre_screenings.today.exists?(programme:) session_date = session.session_dates.today.first VaccinateForm.new( + current_user:, patient:, session_date:, programme:, diff --git a/app/components/app_patient_session_search_result_card_component.rb b/app/components/app_patient_session_search_result_card_component.rb index c468368432..960268de68 100644 --- a/app/components/app_patient_session_search_result_card_component.rb +++ b/app/components/app_patient_session_search_result_card_component.rb @@ -37,7 +37,7 @@ class AppPatientSessionSearchResultCardComponent < ViewComponent::Base end end - if (note = patient_session.latest_note) + if context != :patient_specific_direction && (note = patient_session.latest_note) summary_list.with_row do |row| row.with_key { "Notes" } row.with_value { render note_to_log_event(note) } @@ -57,7 +57,16 @@ class AppPatientSessionSearchResultCardComponent < ViewComponent::Base def initialize(patient_session, context:, programmes: []) super - unless context.in?(%i[patients consent triage register record]) + unless context.in?( + %i[ + patients + consent + triage + register + record + patient_specific_direction + ] + ) raise "Unknown context: #{context}" end @@ -157,6 +166,8 @@ def status_tags [consent_status_tag] when :triage [triage_status_tag] + when :patient_specific_direction + [patient_specific_direction_status_tag] else [vaccination_status_tag] end @@ -203,8 +214,9 @@ def register_status_tag key: :register, value: render( - AppRegisterStatusTagComponent.new( - patient_session.registration_status&.status || "unknown" + AppStatusTagComponent.new( + patient_session.registration_status&.status || "unknown", + context: :register ) ) } @@ -240,6 +252,19 @@ def triage_status_value(triage_status, programme) { status: status } end + def patient_specific_direction_status_tag + { + key: :patient_specific_direction, + value: + render( + AppStatusTagComponent.new( + psd_exists?(programmes.first) ? :added : :not_added, + context: :patient_specific_direction + ) + ) + } + end + def note_to_log_event(note) truncated_body = note.body.truncate_words(80, omission: "…") @@ -260,4 +285,10 @@ def note_to_log_event(note) AppLogEventComponent.new(body:, at: note.created_at, by: note.created_by) end + + def psd_exists?(programme) + patient.patient_specific_directions.any? do + it.programme_id == programme.id && it.academic_year == academic_year + end + end end diff --git a/app/components/app_register_status_tag_component.rb b/app/components/app_register_status_tag_component.rb deleted file mode 100644 index 5e64d859a0..0000000000 --- a/app/components/app_register_status_tag_component.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class AppRegisterStatusTagComponent < ViewComponent::Base - def initialize(status) - super - - @status = status - end - - def call = tag.strong(text, class: ["nhsuk-tag nhsuk-tag--#{colour}"]) - - private - - def text - I18n.t(@status, scope: %i[status register label]) - end - - def colour - I18n.t(@status, scope: %i[status register colour]) - end -end diff --git a/app/components/app_search_results_component.rb b/app/components/app_search_results_component.rb index f68a3d004e..e87016dbb2 100644 --- a/app/components/app_search_results_component.rb +++ b/app/components/app_search_results_component.rb @@ -2,7 +2,7 @@ class AppSearchResultsComponent < ViewComponent::Base erb_template <<-ERB -

Search results

+

<%= heading %>

<% if has_results? %> @@ -19,16 +19,17 @@ class AppSearchResultsComponent < ViewComponent::Base <% end %> ERB - def initialize(pagy, label:) + def initialize(pagy, label:, heading: "Search results") super @pagy = pagy @label = label + @heading = heading end private - attr_reader :pagy, :label + attr_reader :pagy, :label, :heading def has_results? = pagy.count.positive? end diff --git a/app/components/app_status_tag_component.rb b/app/components/app_status_tag_component.rb new file mode 100644 index 0000000000..80c6a59357 --- /dev/null +++ b/app/components/app_status_tag_component.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AppStatusTagComponent < ViewComponent::Base + def initialize(status, context:) + super + + @status = status + @context = context + end + + def call = tag.strong(text, class: ["nhsuk-tag nhsuk-tag--#{colour}"]) + + private + + def text + I18n.t(@status, scope: [:status, @context, :label]) + end + + def colour + I18n.t(@status, scope: [:status, @context, :colour]) + end +end diff --git a/app/components/app_triage_form_component.html.erb b/app/components/app_triage_form_component.html.erb index d69912ce52..56799dacde 100644 --- a/app/components/app_triage_form_component.html.erb +++ b/app/components/app_triage_form_component.html.erb @@ -3,7 +3,15 @@ <%= f.govuk_radio_buttons_fieldset :status_and_vaccine_method, **fieldset_options do %> <% triage_form.safe_to_vaccinate_options.each do |option| %> - <%= f.govuk_radio_button :status_and_vaccine_method, option %> + <%= f.govuk_radio_button :status_and_vaccine_method, option do %> + <% if show_psd_options?(option) %> + <%= f.govuk_radio_buttons_fieldset :psd_action, + legend: { text: "Do you want to add a PSD?", size: "s" } do %> + <%= f.govuk_radio_button :add_psd, "true", label: { text: "Yes" } %> + <%= f.govuk_radio_button :add_psd, "false", label: { text: "No" } %> + <% end %> + <% end %> + <% end %> <% end %> <%= f.govuk_radio_divider %> <% triage_form.other_options.each do |option| %> diff --git a/app/components/app_triage_form_component.rb b/app/components/app_triage_form_component.rb index fa48c813cc..42bfd69cbd 100644 --- a/app/components/app_triage_form_component.rb +++ b/app/components/app_triage_form_component.rb @@ -26,6 +26,12 @@ def initialize( def builder = GOVUKDesignSystemFormBuilder::FormBuilder + def show_psd_options?(option) + patient_session.session.psd_enabled? && + option == "safe_to_vaccinate_nasal" && + helpers.policy(PatientSpecificDirection).create? + end + def fieldset_options text = "Is it safe to vaccinate #{patient.given_name}?" hint = diff --git a/app/components/app_vaccinate_form_component.html.erb b/app/components/app_vaccinate_form_component.html.erb index ebbdd14b73..afd935956a 100644 --- a/app/components/app_vaccinate_form_component.html.erb +++ b/app/components/app_vaccinate_form_component.html.erb @@ -1,5 +1,5 @@ <%= form_with( - model: vaccinate_form, + model: form, url:, method: :post, class: "nhsuk-card", @@ -54,6 +54,20 @@ <% end %> <%= f.govuk_text_area :pre_screening_notes, label: { text: "Pre-screening notes (optional)" }, rows: 3 %> + + <% if form.requires_supplied_by_user_id? %> + <%= f.govuk_select :supplied_by_user_id, + label: { text: "Which nurse identified and pre-screened the child and supplied the vaccine?" }, + data: { module: "autocomplete" } do %> + <%= tag.option "", value: "" %> + <% form.supplied_by_users.each do |user| %> + <%= tag.option user.full_name, + value: user.id, + selected: user.id == form.supplied_by_user_id, + data: { hint: user.email } %> + <% end %> + <% end %> + <% end %>


diff --git a/app/components/app_vaccinate_form_component.rb b/app/components/app_vaccinate_form_component.rb index 06dc19a26c..f5e0a46287 100644 --- a/app/components/app_vaccinate_form_component.rb +++ b/app/components/app_vaccinate_form_component.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true class AppVaccinateFormComponent < ViewComponent::Base - def initialize(vaccinate_form) + def initialize(form) super - @vaccinate_form = vaccinate_form + @form = form end private - attr_reader :vaccinate_form + attr_reader :form - delegate :patient, :session, :programme, to: :vaccinate_form + delegate :patient, :session, :programme, to: :form delegate :academic_year, to: :session def url diff --git a/app/components/app_vaccination_record_summary_component.rb b/app/components/app_vaccination_record_summary_component.rb index 46bc5f9fcf..43268356f9 100644 --- a/app/components/app_vaccination_record_summary_component.rb +++ b/app/components/app_vaccination_record_summary_component.rb @@ -201,6 +201,20 @@ def call end end + if @vaccination_record.supplied_by.present? + summary_list.with_row do |row| + row.with_key { "Supplier" } + row.with_value { supplier_value } + if (href = @change_links[:supplier]) + row.with_action( + text: "Change", + visually_hidden_text: "supplier", + href: + ) + end + end + end + if @vaccination_record.performed_by.present? summary_list.with_row do |row| row.with_key { "Vaccinator" } @@ -331,6 +345,13 @@ def time_value ) end + def supplier_value + highlight_if( + @vaccination_record.supplied_by&.full_name, + @vaccination_record.supplied_by_user_id_changed? + ) + end + def vaccinator_value value = if @vaccination_record.performed_by == @current_user diff --git a/app/controllers/concerns/authentication_concern.rb b/app/controllers/concerns/authentication_concern.rb index ffff28bc18..9e67bb530c 100644 --- a/app/controllers/concerns/authentication_concern.rb +++ b/app/controllers/concerns/authentication_concern.rb @@ -50,7 +50,8 @@ def selected_cis2_workgroup_is_valid? def selected_cis2_role_is_valid? cis2_info.is_admin? || cis2_info.is_nurse? || - cis2_info.is_healthcare_assistant? || cis2_info.is_superuser? + cis2_info.is_healthcare_assistant? || cis2_info.is_superuser? || + cis2_info.is_prescriber? end def storable_location? diff --git a/app/controllers/concerns/patient_search_form_concern.rb b/app/controllers/concerns/patient_search_form_concern.rb index 073bc33ef5..1c92036518 100644 --- a/app/controllers/concerns/patient_search_form_concern.rb +++ b/app/controllers/concerns/patient_search_form_concern.rb @@ -28,6 +28,7 @@ def patient_search_form_params :date_of_birth_year, :missing_nhs_number, :vaccination_status, + :patient_specific_direction_status, :q, :register_status, :triage_status, diff --git a/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb index f6f14ff349..215532a948 100644 --- a/app/controllers/draft_vaccination_records_controller.rb +++ b/app/controllers/draft_vaccination_records_controller.rb @@ -17,6 +17,7 @@ class DraftVaccinationRecordsController < ApplicationController before_action :validate_params, only: :update before_action :set_batches, if: -> { current_step == :batch } before_action :set_locations, if: -> { current_step == :location } + before_action :set_supplied_by_users, if: -> { current_step == :supplier } before_action :set_back_link_path after_action :verify_authorized @@ -165,7 +166,8 @@ def update_params ], location: %i[location_id], notes: %i[notes], - outcome: %i[outcome] + outcome: %i[outcome], + supplier: %i[supplied_by_user_id] }.fetch(current_step) params @@ -193,7 +195,12 @@ def set_programme def set_vaccination_record @vaccination_record = - @draft_vaccination_record.vaccination_record || VaccinationRecord.new + @draft_vaccination_record.vaccination_record || + VaccinationRecord.new( + patient: @patient, + session: @session, + programme: @programme + ) end def set_steps @@ -226,6 +233,10 @@ def set_locations @locations = policy_scope(Location).community_clinic end + def set_supplied_by_users + @supplied_by_users = current_team.users.show_in_suppliers + end + def set_back_link_path @back_link_path = if @draft_vaccination_record.editing? diff --git a/app/controllers/patient_sessions/programmes_controller.rb b/app/controllers/patient_sessions/programmes_controller.rb index 75f617552c..669b2e9278 100644 --- a/app/controllers/patient_sessions/programmes_controller.rb +++ b/app/controllers/patient_sessions/programmes_controller.rb @@ -8,11 +8,11 @@ def show end def record_already_vaccinated - unless @patient_session.can_record_as_already_vaccinated?( - programme: @programme - ) - redirect_to session_patient_path and return - end + authorize VaccinationRecord.new( + patient: @patient, + session: @session, + programme: @programme + ) draft_vaccination_record = DraftVaccinationRecord.new(request_session: session, current_user:) diff --git a/app/controllers/patient_sessions/triages_controller.rb b/app/controllers/patient_sessions/triages_controller.rb index 1bb20d24cc..4a0b78818a 100644 --- a/app/controllers/patient_sessions/triages_controller.rb +++ b/app/controllers/patient_sessions/triages_controller.rb @@ -46,6 +46,8 @@ def create ) .each { send_triage_confirmation(@patient_session, @programme, it) } + ensure_psd_exists if @triage_form.add_psd? + redirect_to redirect_path, flash: { success: "Triage outcome updated" } else render "patient_sessions/programmes/show", @@ -57,7 +59,7 @@ def create private def triage_form_params - params.expect(triage_form: %i[status_and_vaccine_method notes]) + params.expect(triage_form: %i[status_and_vaccine_method notes add_psd]) end def redirect_path @@ -68,4 +70,24 @@ def redirect_path return_to: "triage" ) end + + def ensure_psd_exists + # TODO: Handle programmes with multiple nasal vaccines. + vaccine = @programme.vaccines.nasal.first + + psd_attributes = { + academic_year: @academic_year, + delivery_site: "nose", + patient: @patient, + programme: @programme, + vaccine:, + vaccine_method: "nasal" + } + + return if PatientSpecificDirection.exists?(**psd_attributes) + + PatientSpecificDirection.create!( + psd_attributes.merge(created_by: current_user) + ) + end end diff --git a/app/controllers/patient_sessions/vaccinations_controller.rb b/app/controllers/patient_sessions/vaccinations_controller.rb index 72a9b2268c..762f859e0f 100644 --- a/app/controllers/patient_sessions/vaccinations_controller.rb +++ b/app/controllers/patient_sessions/vaccinations_controller.rb @@ -10,7 +10,11 @@ class PatientSessions::VaccinationsController < PatientSessions::BaseController after_action :verify_authorized def create - authorize VaccinationRecord + authorize VaccinationRecord.new( + patient: @patient, + session: @session, + programme: @programme + ) draft_vaccination_record = DraftVaccinationRecord.new(request_session: session, current_user:) @@ -28,9 +32,10 @@ def create if @vaccinate_form.save(draft_vaccination_record:) steps = draft_vaccination_record.wizard_steps - steps.delete(:notes) # this is on the confirmation page - steps.delete(:identity) # this can only be changed from confirmation page steps.delete(:dose) # this can only be changed from confirmation page + steps.delete(:identity) # this can only be changed from confirmation page + steps.delete(:notes) # this is on the confirmation page + steps.delete(:supplier) # this can only be changed from confirmation page steps.delete(:date_and_time) steps.delete(:outcome) if draft_vaccination_record.administered? @@ -65,6 +70,7 @@ def vaccinate_form_params identity_check_confirmed_by_patient pre_screening_confirmed pre_screening_notes + supplied_by_user_id vaccine_id vaccine_method ] diff --git a/app/controllers/session_dates_controller.rb b/app/controllers/session_dates_controller.rb index 1c05a0f1ff..276a0abefb 100644 --- a/app/controllers/session_dates_controller.rb +++ b/app/controllers/session_dates_controller.rb @@ -34,7 +34,7 @@ def update if any_destroyed? session_dates_path(@session) else - edit_session_path(@session) + session_edit_path(@session) end ) end diff --git a/app/controllers/sessions/edit_controller.rb b/app/controllers/sessions/edit_controller.rb index a3e41e4277..4f92bd9fdf 100644 --- a/app/controllers/sessions/edit_controller.rb +++ b/app/controllers/sessions/edit_controller.rb @@ -3,28 +3,47 @@ class Sessions::EditController < ApplicationController before_action :set_session - def edit_programmes + before_action :authorize_session_edit, + except: %i[ + update_programmes + update_send_consent_requests_at + update_send_invitations_at + update_weeks_before_consent_reminders + update_register_attendance + update_delegation + ] + before_action :authorize_session_update, + only: %i[ + update_programmes + update_send_consent_requests_at + update_send_invitations_at + update_weeks_before_consent_reminders + update_register_attendance + update_delegation + ] + + def show + end + + def programmes @form = SessionProgrammesForm.new( session: @session, programme_ids: @session.programme_ids ) - - render :programmes end def update_programmes @form = SessionProgrammesForm.new(session: @session, **programmes_params) if @form.save - redirect_to edit_session_path(@session) + redirect_to session_edit_path(@session) else render :programmes, status: :unprocessable_content end end - def edit_send_consent_requests_at - render :send_consent_requests_at + def send_consent_requests_at end def update_send_consent_requests_at @@ -35,12 +54,11 @@ def update_send_consent_requests_at elsif !@session.update(send_consent_requests_at_params) render :send_consent_requests_at, status: :unprocessable_content else - redirect_to edit_session_path(@session) + redirect_to session_edit_path(@session) end end - def edit_send_invitations_at - render :send_invitations_at + def send_invitations_at end def update_send_invitations_at @@ -51,26 +69,55 @@ def update_send_invitations_at elsif !@session.update(send_invitations_at_params) render :send_invitations_at, status: :unprocessable_content else - redirect_to edit_session_path(@session) + redirect_to session_edit_path(@session) end end - def edit_weeks_before_consent_reminders - render :weeks_before_consent_reminders + def weeks_before_consent_reminders end def update_weeks_before_consent_reminders if @session.update(weeks_before_consent_reminders_params) - redirect_to edit_session_path(@session) + redirect_to session_edit_path(@session) else render :weeks_before_consent_reminders, status: :unprocessable_content end end + def register_attendance + end + + def update_register_attendance + if @session.update(register_attendance_params) + redirect_to session_edit_path(@session) + else + render :register_attendance, status: :unprocessable_content + end + end + + def delegation + end + + def update_delegation + if @session.update(delegation_params) + redirect_to session_edit_path(@session) + else + render :delegation, status: :unprocessable_content + end + end + private def set_session - @session = policy_scope(Session).find_by!(slug: params[:slug]) + @session = policy_scope(Session).find_by!(slug: params[:session_slug]) + end + + def authorize_session_edit + authorize @session, :edit? + end + + def authorize_session_update + authorize @session, :update? end def programmes_params @@ -106,4 +153,12 @@ def send_invitations_at_params def weeks_before_consent_reminders_params params.expect(session: :weeks_before_consent_reminders) end + + def register_attendance_params + params.expect(session: :requires_registration) + end + + def delegation_params + params.expect(session: %i[psd_enabled national_protocol_enabled]) + end end diff --git a/app/controllers/sessions/patient_specific_directions_controller.rb b/app/controllers/sessions/patient_specific_directions_controller.rb new file mode 100644 index 0000000000..c6ec73747e --- /dev/null +++ b/app/controllers/sessions/patient_specific_directions_controller.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +class Sessions::PatientSpecificDirectionsController < ApplicationController + include PatientSearchFormConcern + + before_action :set_session + before_action :set_programme + before_action :set_vaccine + before_action :set_patient_search_form + + def show + scope = + @session.patient_sessions.includes_programmes.includes( + patient: { + patient_specific_directions: :programme + } + ) + @eligible_for_bulk_psd_count = patient_sessions_allowed_psd.count + patient_sessions = @form.apply(scope) + @pagy, @patient_sessions = pagy(patient_sessions) + + render layout: "full" + end + + def new + @eligible_for_bulk_psd_count = patient_sessions_allowed_psd.count + end + + def create + PatientSpecificDirection.import!( + psds_to_create, + on_duplicate_key_ignore: true + ) + + redirect_to session_patient_specific_directions_path(@session), + flash: { + success: "PSDs added" + } + end + + private + + def set_session + @session = policy_scope(Session).find_by!(slug: params[:session_slug]) + end + + def set_programme + # TODO: Handle PSDs in sessions with multiple programmes. + @programme = @session.programmes.supports_delegation.first + end + + def set_vaccine + # TODO: Handle programmes with multiple vaccines. + @vaccine = @programme.vaccines.nasal.first + end + + def psds_to_create + patient_sessions_allowed_psd.map do |patient_session| + PatientSpecificDirection.new( + academic_year: @session.academic_year, + created_by: current_user, + delivery_site: "nose", + patient_id: patient_session.patient_id, + programme: @programme, + vaccine: @vaccine, + vaccine_method: "nasal" + ) + end + end + + def patient_sessions_allowed_psd + @patient_sessions_allowed_psd ||= + @session + .patient_sessions + .has_consent_status("given", programme: @programme) + .has_triage_status("not_required", programme: @programme) + .without_patient_specific_direction(programme: @programme) + end +end diff --git a/app/controllers/sessions/record_controller.rb b/app/controllers/sessions/record_controller.rb index ffbd77d3c7..de56fff23b 100644 --- a/app/controllers/sessions/record_controller.rb +++ b/app/controllers/sessions/record_controller.rb @@ -25,6 +25,22 @@ def show scope = scope.has_registration_status(%w[attending completed]) end + @vaccine_methods = @session.vaccine_methods_for(user: current_user) + + if @vaccine_methods != @session.vaccine_methods + scope = + if @vaccine_methods.empty? + scope.none + else + @vaccine_methods.reduce(scope) do |accumulator, vaccine_method| + accumulator.has_vaccine_method( + vaccine_method, + programme: @session.programmes + ) + end + end + end + patient_sessions = @form.apply(scope).consent_given_and_ready_to_vaccinate( programmes: @form.programmes, diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1152ea4091..0c2c07519e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -51,9 +51,6 @@ def show end end - def edit - end - def import draft_import = DraftImport.new(request_session: session, current_user:) diff --git a/app/forms/patient_search_form.rb b/app/forms/patient_search_form.rb index 85393fd6e4..9a782d1243 100644 --- a/app/forms/patient_search_form.rb +++ b/app/forms/patient_search_form.rb @@ -12,6 +12,7 @@ class PatientSearchForm < SearchForm attribute :date_of_birth_year, :integer attribute :missing_nhs_number, :boolean attribute :vaccination_status, :string + attribute :patient_specific_direction_status, :string attribute :programme_types, array: true attribute :q, :string attribute :register_status, :string @@ -59,6 +60,7 @@ def apply(scope) scope = filter_register_status(scope) scope = filter_triage_status(scope) scope = filter_vaccine_method(scope) + scope = filter_patient_specific_direction_status(scope) scope.order_by_name end @@ -168,6 +170,19 @@ def filter_vaccination_statuses(scope) end end + def filter_patient_specific_direction_status(scope) + return scope if (status = patient_specific_direction_status&.to_sym).blank? + + case status + when :added + scope.has_patient_specific_direction(programme: programmes) + when :not_added + scope.without_patient_specific_direction(programme: programmes) + else + scope + end + end + def filter_register_status(scope) if (status = register_status&.to_sym).present? scope.has_registration_status(status) diff --git a/app/forms/triage_form.rb b/app/forms/triage_form.rb index c891186a77..374aebed89 100644 --- a/app/forms/triage_form.rb +++ b/app/forms/triage_form.rb @@ -7,6 +7,7 @@ class TriageForm attr_accessor :patient_session, :programme, :current_user attribute :status_and_vaccine_method, :string + attribute :add_psd, :boolean attribute :notes, :string attribute :vaccine_methods, array: true, default: [] @@ -15,6 +16,13 @@ class TriageForm in: :status_and_vaccine_method_options } validates :notes, length: { maximum: 1000 } + validates :add_psd, + inclusion: { + in: [true, false] + }, + if: -> { add_psd.present? } + + def add_psd? = add_psd def triage=(triage) self.status_and_vaccine_method = diff --git a/app/forms/vaccinate_form.rb b/app/forms/vaccinate_form.rb index c354de203b..4e97ba8ee0 100644 --- a/app/forms/vaccinate_form.rb +++ b/app/forms/vaccinate_form.rb @@ -19,6 +19,8 @@ class VaccinateForm attribute :pre_screening_confirmed, :boolean attribute :pre_screening_notes, :string + attribute :supplied_by_user_id, :integer + attribute :vaccine_method, :string attribute :delivery_site, :string attribute :dose_sequence, :integer @@ -37,10 +39,16 @@ class VaccinateForm maximum: 300 } - validates :vaccine_method, inclusion: { in: :vaccine_method_options } validates :pre_screening_notes, length: { maximum: 1000 } - validates :pre_screening_confirmed, presence: true, if: :administered? + + validates :supplied_by_user_id, + inclusion: { + in: :supplied_by_user_id_values + }, + if: :requires_supplied_by_user_id? + + validates :vaccine_method, inclusion: { in: :vaccine_method_options } validates :delivery_site, inclusion: { in: :delivery_site_options @@ -65,6 +73,12 @@ def delivery_site super end + def supplied_by_users + current_user.selected_team.users.show_in_suppliers + end + + def requires_supplied_by_user_id? = !current_user.show_in_suppliers + def save(draft_vaccination_record:) return nil if invalid? @@ -99,6 +113,7 @@ def save(draft_vaccination_record:) draft_vaccination_record.patient_id = patient.id draft_vaccination_record.performed_at = Time.current draft_vaccination_record.performed_by_user = current_user + draft_vaccination_record.supplied_by_user_id = supplied_by_user_id draft_vaccination_record.performed_ods_code = organisation.ods_code draft_vaccination_record.programme = programme draft_vaccination_record.session_id = session.id @@ -112,6 +127,8 @@ def save(draft_vaccination_record:) def administered? = vaccine_method != "none" + def supplied_by_user_id_values = supplied_by_users.pluck(:id) + def vaccine_method_options programme.vaccine_methods + ["none"] end diff --git a/app/lib/mavis_cli.rb b/app/lib/mavis_cli.rb index a7ef1acb53..05a2a33850 100644 --- a/app/lib/mavis_cli.rb +++ b/app/lib/mavis_cli.rb @@ -43,6 +43,7 @@ def self.progress_bar(total) require_relative "mavis_cli/schools/create" require_relative "mavis_cli/schools/move_patients" require_relative "mavis_cli/schools/remove_programme_year_group" +require_relative "mavis_cli/sessions/configure" require_relative "mavis_cli/stats/consents_by_school" require_relative "mavis_cli/stats/organisations" require_relative "mavis_cli/stats/vaccinations" diff --git a/app/lib/mavis_cli/sessions/configure.rb b/app/lib/mavis_cli/sessions/configure.rb new file mode 100644 index 0000000000..f330c3d0e1 --- /dev/null +++ b/app/lib/mavis_cli/sessions/configure.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module MavisCLI + module Sessions + class Configure < Dry::CLI::Command + desc "Configure options in bulk" + + argument :workgroup, required: true, desc: "Workgroup of the team" + argument :programme_type, + required: true, + desc: "Find sessions that administer this programme" + + option :requires_registration, + type: :boolean, + desc: "Whether the session requires registration" + option :psd_enabled, + type: :boolean, + desc: "Use patient specific direction (PSD)" + option :national_protocol_enabled, + type: :boolean, + desc: "Use national protocol" + + def call( + workgroup:, + programme_type:, + requires_registration: nil, + psd_enabled: nil, + national_protocol_enabled: nil + ) + MavisCLI.load_rails + + team = Team.find_by!(workgroup:) + programme = Programme.find_by!(type: programme_type) + + attributes = { + requires_registration:, + psd_enabled:, + national_protocol_enabled: + }.compact + + team + .sessions + .includes(:location) + .where(academic_year: AcademicYear.pending) + .has_programmes([programme]) + .find_each do |session| + session.assign_attributes(attributes) + + if session.changed? + session.save! + puts "Updated #{session.slug}: #{session.location.name}" + end + end + end + end + end + + register "sessions" do |prefix| + prefix.register "configure", Sessions::Configure + end +end diff --git a/app/models/cis2_info.rb b/app/models/cis2_info.rb index 4d6e620c0c..c3052ba68e 100644 --- a/app/models/cis2_info.rb +++ b/app/models/cis2_info.rb @@ -8,6 +8,7 @@ class CIS2Info SUPERUSER_WORKGROUP = "mavissuperusers" + INDEPENDENT_PRESCRIBING_ACTIVITY_CODE = "B0420" PERSONAL_MEDICATION_ADMINISTRATION_ACTIVITY_CODE = "B0428" attribute :organisation_name @@ -50,6 +51,10 @@ def is_healthcare_assistant? activity_codes.include?(PERSONAL_MEDICATION_ADMINISTRATION_ACTIVITY_CODE) end + def is_prescriber? + activity_codes.include?(INDEPENDENT_PRESCRIBING_ACTIVITY_CODE) + end + def is_superuser? workgroups.include?(SUPERUSER_WORKGROUP) end diff --git a/app/models/draft_vaccination_record.rb b/app/models/draft_vaccination_record.rb index 5be3d2e840..fb80ae67dc 100644 --- a/app/models/draft_vaccination_record.rb +++ b/app/models/draft_vaccination_record.rb @@ -11,8 +11,8 @@ class DraftVaccinationRecord attribute :delivery_method, :string attribute :delivery_site, :string attribute :dose_sequence, :integer + attribute :first_active_wizard_step, :string attribute :full_dose, :boolean - attribute :protocol, :string attribute :identity_check_confirmed_by_other_name, :string attribute :identity_check_confirmed_by_other_relationship, :string attribute :identity_check_confirmed_by_patient, :boolean @@ -27,8 +27,9 @@ class DraftVaccinationRecord attribute :performed_by_user_id, :integer attribute :performed_ods_code, :string attribute :programme_id, :integer + attribute :protocol, :string attribute :session_id, :integer - attribute :first_active_wizard_step, :string + attribute :supplied_by_user_id, :integer def initialize(current_user:, **attributes) @current_user = current_user @@ -47,6 +48,7 @@ def wizard_steps :notes, :date_and_time, (:outcome if can_change_outcome?), + (:supplier if requires_supplied_by?), (:delivery if administered?), (:dose if administered? && can_be_half_dose?), (:batch if administered?), @@ -187,13 +189,27 @@ def programme=(value) def session return nil if session_id.nil? - SessionPolicy::Scope.new(@current_user, Session).resolve.find(session_id) + SessionPolicy::Scope + .new(@current_user, Session) + .resolve + .includes(:programmes) + .find(session_id) end def session=(value) self.session_id = value.id end + def supplied_by + return nil if supplied_by_user_id.nil? + + User.find(supplied_by_user_id) + end + + def supplied_by=(value) + self.supplied_by_user_id = value.id + end + def vaccination_record return nil if editing_id.nil? @@ -272,7 +288,6 @@ def writable_attribute_names delivery_site dose_sequence full_dose - protocol identity_check location_id location_name @@ -285,7 +300,9 @@ def writable_attribute_names performed_by_user_id performed_ods_code programme_id + protocol session_id + supplied_by_user_id vaccine_id ] end @@ -344,6 +361,10 @@ def can_change_outcome? outcome != "already_had" || editing? || session.nil? || session.today? end + def requires_supplied_by? + performed_by_user && !performed_by_user&.show_in_suppliers + end + def delivery_site_matches_delivery_method return if delivery_method.blank? diff --git a/app/models/patient.rb b/app/models/patient.rb index eac95b4e5b..d1eaf0d017 100644 --- a/app/models/patient.rb +++ b/app/models/patient.rb @@ -78,6 +78,7 @@ class Patient < ApplicationRecord has_many :triages has_many :vaccination_records, -> { kept } has_many :vaccination_statuses + has_many :patient_specific_directions has_many :gillick_assessments has_many :parents, through: :parent_relationships diff --git a/app/models/patient_session.rb b/app/models/patient_session.rb index 15f6339b2f..df06f8f2b3 100644 --- a/app/models/patient_session.rb +++ b/app/models/patient_session.rb @@ -257,6 +257,30 @@ class PatientSession < ApplicationRecord end end + scope :without_patient_specific_direction, + ->(programme:) do + joins(:session).where.not( + PatientSpecificDirection + .where("patient_id = patient_sessions.patient_id") + .where("academic_year = sessions.academic_year") + .where(programme:) + .arel + .exists + ) + end + + scope :has_patient_specific_direction, + ->(programme:) do + joins(:session).where( + PatientSpecificDirection + .where("patient_id = patient_sessions.patient_id") + .where("academic_year = sessions.academic_year") + .where(programme:) + .arel + .exists + ) + end + scope :destroy_all_if_safe, -> do includes( @@ -268,6 +292,13 @@ class PatientSession < ApplicationRecord delegate :academic_year, to: :session + def psd_added?(programme:) + patient + .patient_specific_directions + .where(programme:, academic_year:) + .exists? + end + def safe_to_destroy? vaccination_records.empty? && gillick_assessments.empty? && session_attendances.none?(&:attending?) @@ -277,11 +308,6 @@ def destroy_if_safe! destroy! if safe_to_destroy? end - def can_record_as_already_vaccinated?(programme:) - !session.today? && - patient.vaccination_status(programme:, academic_year:).none_yet? - end - def programmes = session.programmes_for(patient:, academic_year:) def todays_attendance diff --git a/app/models/patient_specific_direction.rb b/app/models/patient_specific_direction.rb index 571be4e7ef..e121f0e805 100644 --- a/app/models/patient_specific_direction.rb +++ b/app/models/patient_specific_direction.rb @@ -7,7 +7,6 @@ # id :bigint not null, primary key # academic_year :integer not null # delivery_site :integer not null -# full_dose :boolean not null # vaccine_method :integer not null # created_at :datetime not null # updated_at :datetime not null @@ -39,8 +38,6 @@ class PatientSpecificDirection < ApplicationRecord belongs_to :programme belongs_to :vaccine - validates :full_dose, inclusion: { in: [true, false] } - enum :delivery_site, { left_arm_upper_position: 2, diff --git a/app/models/session.rb b/app/models/session.rb index 765452bed1..9f907c491f 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -205,7 +205,7 @@ def year_groups end def vaccine_methods - programmes.flat_map(&:vaccine_methods).uniq.sort + @vaccine_methods ||= programmes.flat_map(&:vaccine_methods).uniq.sort end def programmes_for(year_group: nil, patient: nil, academic_year: nil) @@ -218,6 +218,16 @@ def programmes_for(year_group: nil, patient: nil, academic_year: nil) end end + def vaccine_methods_for(user:) + if user.is_nurse? + vaccine_methods + elsif user.is_healthcare_assistant? && pgd_supply_enabled? + %w[nasal] + else + [] + end + end + def dates session_dates.map(&:value).compact end diff --git a/app/models/user.rb b/app/models/user.rb index 75dad8c76e..5f6b4bcaef 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -72,6 +72,8 @@ class User < ApplicationRecord scope :recently_active, -> { where(last_sign_in_at: 1.week.ago..Time.current) } + scope :show_in_suppliers, -> { where(show_in_suppliers: true) } + enum :fallback_role, { nurse: 0, @@ -126,6 +128,8 @@ def role_description role = if is_healthcare_assistant? "Healthcare Assistant" + elsif is_prescriber? + "Prescriber" elsif is_nurse? "Nurse" else @@ -151,6 +155,10 @@ def is_healthcare_assistant? end end + def is_prescriber? + cis2_enabled? ? cis2_info.is_prescriber? : fallback_role_prescriber? + end + def is_superuser? cis2_enabled? ? cis2_info.is_superuser? : fallback_role_superuser? end diff --git a/app/models/vaccination_record.rb b/app/models/vaccination_record.rb index beae07c967..bb38288ba4 100644 --- a/app/models/vaccination_record.rb +++ b/app/models/vaccination_record.rb @@ -34,6 +34,7 @@ # performed_by_user_id :bigint # programme_id :bigint not null # session_id :bigint +# supplied_by_user_id :bigint # vaccine_id :bigint # # Indexes @@ -46,6 +47,7 @@ # index_vaccination_records_on_performed_by_user_id (performed_by_user_id) # index_vaccination_records_on_programme_id (programme_id) # index_vaccination_records_on_session_id (session_id) +# index_vaccination_records_on_supplied_by_user_id (supplied_by_user_id) # index_vaccination_records_on_uuid (uuid) UNIQUE # index_vaccination_records_on_vaccine_id (vaccine_id) # @@ -56,6 +58,7 @@ # fk_rails_... (performed_by_user_id => users.id) # fk_rails_... (programme_id => programmes.id) # fk_rails_... (session_id => sessions.id) +# fk_rails_... (supplied_by_user_id => users.id) # fk_rails_... (vaccine_id => vaccines.id) # class VaccinationRecord < ApplicationRecord @@ -87,9 +90,14 @@ class VaccinationRecord < ApplicationRecord belongs_to :batch, optional: true belongs_to :vaccine, optional: true - belongs_to :performed_by_user, class_name: "User", optional: true belongs_to :programme + belongs_to :performed_by_user, class_name: "User", optional: true + belongs_to :supplied_by, + class_name: "User", + foreign_key: :supplied_by_user_id, + optional: true + has_and_belongs_to_many :immunisation_imports belongs_to :location, optional: true diff --git a/app/policies/patient_specific_direction_policy.rb b/app/policies/patient_specific_direction_policy.rb new file mode 100644 index 0000000000..a6071735c8 --- /dev/null +++ b/app/policies/patient_specific_direction_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class PatientSpecificDirectionPolicy < ApplicationPolicy + def create? + user.is_nurse? + end +end diff --git a/app/policies/programme_policy.rb b/app/policies/programme_policy.rb index 90ca12d5a5..e6b7cb86a9 100644 --- a/app/policies/programme_policy.rb +++ b/app/policies/programme_policy.rb @@ -9,7 +9,12 @@ def consent_form? = show? class Scope < ApplicationPolicy::Scope def resolve - scope.where(id: user.selected_team.programmes.ids) + scope + .joins(:team_programmes) + .where(team_programmes: { team: user.selected_team }) + .then do |scope| + user.is_healthcare_assistant? ? scope.supports_delegation : scope + end end end end diff --git a/app/policies/session_policy.rb b/app/policies/session_policy.rb index 46ef0b9caa..acfc8b073f 100644 --- a/app/policies/session_policy.rb +++ b/app/policies/session_policy.rb @@ -1,13 +1,21 @@ # frozen_string_literal: true class SessionPolicy < ApplicationPolicy + def update? + user.is_nurse? || user.is_admin? + end + def import? = show? def make_in_progress? = edit? class Scope < ApplicationPolicy::Scope def resolve - scope.where(team: user.selected_team) + scope + .where(team: user.selected_team) + .then do |scope| + user.is_healthcare_assistant? ? scope.supports_delegation : scope + end end end end diff --git a/app/policies/vaccination_record_policy.rb b/app/policies/vaccination_record_policy.rb index 8148875788..2f8cfd9ddb 100644 --- a/app/policies/vaccination_record_policy.rb +++ b/app/policies/vaccination_record_policy.rb @@ -2,25 +2,32 @@ class VaccinationRecordPolicy < ApplicationPolicy def create? - user.is_nurse? + user.is_nurse? || + ( + patient.approved_vaccine_methods(programme:, academic_year:) & + session.vaccine_methods_for(user:) + ).present? end - def new? - create? + def new? = create? + + def record_already_vaccinated? + user.is_nurse? && !session.today? && + patient.vaccination_status(programme:, academic_year:).none_yet? end def edit? - user.is_nurse? && record.session_id.present? && + (record.performed_by_user_id == user.id || user.is_nurse?) && + record.recorded_in_service? && record.performed_ods_code == user.selected_organisation.ods_code end - def update? - edit? - end + def update? = edit? - def destroy? - user.is_superuser? - end + def destroy? = user.is_superuser? + + delegate :patient, :session, :programme, to: :record + delegate :academic_year, to: :session class Scope < ApplicationPolicy::Scope def resolve diff --git a/app/views/draft_vaccination_records/confirm.html.erb b/app/views/draft_vaccination_records/confirm.html.erb index b47e62c642..55ef1f9b40 100644 --- a/app/views/draft_vaccination_records/confirm.html.erb +++ b/app/views/draft_vaccination_records/confirm.html.erb @@ -20,6 +20,7 @@ delivery_site: wizard_path("delivery"), dose_volume: @draft_vaccination_record.wizard_steps.include?(:dose) ? wizard_path("dose") : nil, identity: wizard_path("identity"), + supplier: wizard_path("supplier"), location: @draft_vaccination_record.wizard_steps.include?(:location) ? wizard_path("location") : nil, notes: wizard_path("notes"), outcome: @draft_vaccination_record.wizard_steps.include?(:outcome) ? wizard_path("outcome") : nil, diff --git a/app/views/draft_vaccination_records/supplier.html.erb b/app/views/draft_vaccination_records/supplier.html.erb new file mode 100644 index 0000000000..95727b5879 --- /dev/null +++ b/app/views/draft_vaccination_records/supplier.html.erb @@ -0,0 +1,22 @@ +<% content_for :before_main do %> + <%= govuk_back_link(href: @back_link_path) %> +<% end %> + +<% title = "Which nurse identified and pre-screened the child and supplied the vaccine?" %> +<% content_for :page_title, title %> + +<%= form_with model: @draft_vaccination_record, url: wizard_path, method: :put do |f| %> + <%= f.govuk_error_summary %> + + <%= f.govuk_radio_buttons_fieldset :supplied_by_user_id, + caption: { text: @patient.full_name, size: "l" }, + legend: { size: "l", tag: "h1", + text: title } do %> + + <% @supplied_by_users.each do |user| %> + <%= f.govuk_radio_button :supplied_by_user_id, user.id, label: { text: user.full_name } %> + <% end %> + <% end %> + + <%= f.govuk_submit "Continue" %> +<% end %> diff --git a/app/views/patient_sessions/_header.html.erb b/app/views/patient_sessions/_header.html.erb index 0cc87b922a..ad573a5a15 100644 --- a/app/views/patient_sessions/_header.html.erb +++ b/app/views/patient_sessions/_header.html.erb @@ -9,9 +9,13 @@ ].compact) %> <% end %> -<% if policy(VaccinationRecord).new? && (outstanding_programmes = @patient_session.outstanding_programmes).any? %> +<% if (outstanding_programmes = @patient_session.outstanding_programmes).any? %> + <% programmes_can_record_vaccination = outstanding_programmes.filter do |programme| + policy(VaccinationRecord.new(patient: @patient, session: @session, programme:)).new? + end %> + <%= govuk_notification_banner(title_text: "Important") do |notification_banner| %> - <% notification_banner.with_heading(text: "You still need to record an outcome for #{outstanding_programmes.map(&:name).to_sentence}.") %> + <% notification_banner.with_heading(text: "You still need to record an outcome for #{programmes_can_record_vaccination.map(&:name).to_sentence}.") %> <% end %> <% end %> @@ -25,8 +29,14 @@