diff --git a/app/components/app_sub_navigation_component.rb b/app/components/app_sub_navigation_component.rb
new file mode 100644
index 0000000000..6b85a1e805
--- /dev/null
+++ b/app/components/app_sub_navigation_component.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class AppSubNavigationComponent < ViewComponent::Base
+ renders_many :items, "Item"
+
+ def initialize(classes: nil, attributes: {})
+ @classes = ["app-sub-navigation", *Array(classes)].compact.join(" ")
+
+ @attributes =
+ attributes.merge(class: @classes, "aria-label": "Secondary menu")
+ end
+
+ def selected_item_text
+ selected_item = items.find(&:selected)
+ selected_item&.call
+ end
+
+ class Item < ViewComponent::Base
+ def initialize(href:, text: nil, selected: false)
+ @href = href
+ @text = html_escape(text)
+ @selected = selected
+ end
+
+ def call
+ content || @text || raise(ArgumentError, "no text or content")
+ end
+
+ attr_reader :href, :selected, :ticked
+ end
+end
diff --git a/app/components/app_team_navigation_component.rb b/app/components/app_team_navigation_component.rb
new file mode 100644
index 0000000000..bff1989562
--- /dev/null
+++ b/app/components/app_team_navigation_component.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class AppTeamNavigationComponent < ViewComponent::Base
+ def initialize(team:)
+ @team = team
+ end
+
+ def call
+ render AppSubNavigationComponent.new do |nav|
+ nav.with_item(
+ href: contact_details_team_path,
+ text: "Contact details",
+ selected: request.path.ends_with?("contact_details")
+ )
+ nav.with_item(
+ href: clinics_team_path,
+ text: "Clinics",
+ selected: request.path.ends_with?("clinics")
+ )
+ nav.with_item(
+ href: schools_team_path,
+ text: "Schools",
+ selected: request.path.ends_with?("schools")
+ )
+ nav.with_item(
+ href: sessions_team_path,
+ text: "Sessions",
+ selected: request.path.ends_with?("sessions")
+ )
+ end
+ end
+end
diff --git a/app/components/app_triage_form_component.rb b/app/components/app_triage_form_component.rb
index 252294e050..18b5c0eb3d 100644
--- a/app/components/app_triage_form_component.rb
+++ b/app/components/app_triage_form_component.rb
@@ -53,8 +53,8 @@ def fieldset_options
def patient_eligible_for_additional_dose?
next_dose =
- patient.vaccination_status(
- programme: programme,
+ patient.programme_status(
+ programme,
academic_year: session.academic_year
).dose_sequence
diff --git a/app/components/app_vaccination_record_summary_component.rb b/app/components/app_vaccination_record_summary_component.rb
index 33d4183d9a..671fffe9b1 100644
--- a/app/components/app_vaccination_record_summary_component.rb
+++ b/app/components/app_vaccination_record_summary_component.rb
@@ -51,7 +51,13 @@ def call
if @vaccine
row.with_value { vaccine_value }
- if (href = @change_links[:vaccine])
+ if @current_user.selected_team.has_upload_only_access?
+ row.with_action(
+ text: "Change",
+ href: @change_links[:batch],
+ visually_hidden_text: "vaccine"
+ )
+ elsif (href = @change_links[:vaccine])
row.with_action(
text: "Change",
visually_hidden_text: "vaccine",
@@ -89,6 +95,14 @@ def call
summary_list.with_row do |row|
row.with_key { "Batch expiry date" }
row.with_value { batch_expiry_value }
+
+ if @current_user.selected_team.has_upload_only_access?
+ row.with_action(
+ text: "Change",
+ href: @change_links[:batch],
+ visually_hidden_text: "batch expiry date"
+ )
+ end
end
end
@@ -280,12 +294,12 @@ def call
end
end
- sync_feature_flag_enabled =
- Programme.all.any? { Flipper.enabled?(:imms_api_sync_job, it) }
+ correct_feature_flags_enabled =
+ Programme.all.any? { Flipper.enabled?(:imms_api_sync_job, it) } &&
+ Flipper.enabled?(:imms_api_integration)
if @vaccination_record.respond_to?(:sync_status) &&
- sync_feature_flag_enabled &&
- Flipper.enabled?(:imms_api_integration) &&
- @vaccination_record&.sourced_from_service?
+ correct_feature_flags_enabled &&
+ @vaccination_record&.correct_source_for_nhs_immunisations_api?
summary_list.with_row do |row|
row.with_key { "Synced with NHS England?" }
row.with_value do
@@ -325,7 +339,13 @@ def programme_value
end
def vaccine_value
- highlight_if(@vaccine.brand, @vaccination_record.vaccine_id_changed?)
+ display_name =
+ if @current_user.selected_team.has_upload_only_access?
+ @vaccine.nivs_name.presence || @vaccine.brand
+ else
+ @vaccine.brand
+ end
+ highlight_if(display_name, @vaccination_record.vaccine_id_changed?)
end
def delivery_method_value
diff --git a/app/controllers/api/reporting/totals_controller.rb b/app/controllers/api/reporting/totals_controller.rb
index 10e8cb3476..e0911e7df0 100644
--- a/app/controllers/api/reporting/totals_controller.rb
+++ b/app/controllers/api/reporting/totals_controller.rb
@@ -14,13 +14,16 @@ class API::Reporting::TotalsController < API::Reporting::BaseController
GROUPS = {
local_authority: :patient_local_authority_code,
year_group: :patient_year_group,
- gender: :patient_gender
+ gender: :patient_gender,
+ school: %i[patient_school_urn patient_school_name]
}.freeze
GROUP_HEADERS = {
patient_local_authority_code: "Local Authority",
patient_year_group: "Year Group",
- patient_gender: "Gender"
+ patient_gender: "Gender",
+ patient_school_urn: "School URN",
+ patient_school_name: "School Name"
}.freeze
METRIC_HEADERS = {
@@ -99,15 +102,18 @@ def csv_headers(groups)
headers
end
+ def parse_groups
+ params[:group]
+ .to_s
+ .split(",")
+ .map { GROUPS[it.strip.to_sym] }
+ .compact
+ .flatten
+ .uniq
+ end
+
def render_format_csv
- groups =
- params[:group]
- .to_s
- .split(",")
- .map { GROUPS[it.strip.to_sym] }
- .compact
- .flatten
- .uniq
+ groups = parse_groups
scope = @totals_scope
scope = scope.group(groups).select(groups) if groups.any?
@@ -117,6 +123,27 @@ def render_format_csv
end
def render_format_json
+ groups = parse_groups
+
+ groups.any? ? render_grouped_json(groups) : render_totals_json
+ end
+
+ def render_grouped_json(groups)
+ records = @totals_scope.group(groups).select(groups).with_aggregate_metrics
+ render json: records.map { grouped_record_json(it, groups) }
+ end
+
+ def grouped_record_json(record, groups)
+ groups
+ .to_h { [it.to_s.delete_prefix("patient_").to_sym, record[it]] }
+ .merge(
+ cohort: record.cohort,
+ vaccinated: record.vaccinated,
+ not_vaccinated: record.not_vaccinated
+ )
+ end
+
+ def render_totals_json
cohort = @totals_scope.cohort_count
vaccinated = @totals_scope.vaccinated_count
diff --git a/app/controllers/concerns/navigation_concern.rb b/app/controllers/concerns/navigation_concern.rb
index 11f8f74e72..e6e509ad8f 100644
--- a/app/controllers/concerns/navigation_concern.rb
+++ b/app/controllers/concerns/navigation_concern.rb
@@ -59,14 +59,14 @@ def set_navigation_items
@navigation_items << {
title: t("imports.index.title_short"),
path: imports_path,
- count: @cached_counts.import_issues
+ count: (@cached_counts.import_issues if policy(%i[import issue]).index?)
}
end
if current_team&.has_poc_only_access?
@navigation_items << {
title: I18n.t("teams.show.title_short"),
- path: team_path
+ path: contact_details_team_path
}
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 8bb918487c..6a5dcd45a2 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -92,7 +92,7 @@ def set_secondary_items
@secondary_items << {
title: I18n.t("teams.show.title"),
- path: team_path,
+ path: contact_details_team_path,
description: I18n.t("teams.show.description")
}
end
diff --git a/app/controllers/draft_sessions_controller.rb b/app/controllers/draft_sessions_controller.rb
index 1f2cd8ead1..93f9ca36c0 100644
--- a/app/controllers/draft_sessions_controller.rb
+++ b/app/controllers/draft_sessions_controller.rb
@@ -130,7 +130,7 @@ def set_catch_up_patients_vaccinated_percentage
@draft_session
.patient_locations
.where(patient: { birth_academic_year: birth_academic_years })
- .includes(patient: :vaccination_statuses)
+ .includes(patient: :programme_statuses)
.map(&:patient)
total_count = catch_up_patients.count
@@ -141,7 +141,7 @@ def set_catch_up_patients_vaccinated_percentage
.programmes_for(patient:)
.all? do |programme|
if programme.is_catch_up?(year_group:)
- patient.vaccination_status(programme:, academic_year:).vaccinated?
+ patient.programme_status(programme, academic_year:).vaccinated?
else
true
end
@@ -415,13 +415,13 @@ def catch_up_year_group_has_high_unvaccinated_count?(programme, year_group)
@draft_session
.patient_locations
.where(patient: { birth_academic_year: })
- .includes(patient: :vaccination_statuses)
+ .includes(patient: :programme_statuses)
.map(&:patient)
total_count = catch_up_patients.count
vaccinated_count =
catch_up_patients.count do |patient|
- patient.vaccination_status(programme:, academic_year:).vaccinated?
+ patient.programme_status(programme, academic_year:).vaccinated?
end
vaccinated_count < total_count / 2
diff --git a/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb
index 2195bf39ed..b49b64ce1e 100644
--- a/app/controllers/draft_vaccination_records_controller.rb
+++ b/app/controllers/draft_vaccination_records_controller.rb
@@ -14,7 +14,16 @@ class DraftVaccinationRecordsController < ApplicationController
include WizardControllerConcern
before_action :validate_params, only: :update
- before_action :set_batches, if: -> { current_step == :batch }
+ before_action :set_batches,
+ if: -> do
+ current_step == :batch &&
+ !@draft_vaccination_record.bulk_upload_user_and_record?
+ end
+ before_action :set_vaccines,
+ if: -> do
+ current_step == :batch &&
+ @draft_vaccination_record.bulk_upload_user_and_record?
+ end
before_action :set_locations, if: -> { current_step == :location }
before_action :set_supplied_by_users, if: -> { current_step == :supplier }
before_action :set_back_link_path
@@ -73,6 +82,20 @@ def validate_params
@draft_vaccination_record.errors.add(:performed_at, :invalid)
render_wizard nil, status: :unprocessable_content
end
+ elsif current_step == :batch &&
+ @draft_vaccination_record.bulk_upload_user_and_record?
+ validator =
+ DateParamsValidator.new(
+ field_name: :batch_expiry,
+ object: @draft_vaccination_record,
+ params: update_params
+ )
+
+ unless validator.date_params_valid?
+ @draft_vaccination_record.errors.add(:batch_expiry, :invalid)
+ set_vaccines
+ render_wizard nil, status: :unprocessable_content
+ end
end
end
@@ -144,6 +167,8 @@ def handle_confirm
NextDoseTriageFactory.call(vaccination_record: @vaccination_record)
+ PatientTeamUpdater.call(patient_scope: Patient.where(id: @patient.id))
+
StatusUpdater.call(patient: @patient)
if should_notify_parents
@@ -174,7 +199,7 @@ def finish_wizard_path
def update_params
permitted_attributes = {
- batch: %i[batch_id],
+ batch: %i[batch_id vaccine_id batch_name batch_expiry],
confirm: @draft_vaccination_record.editing? ? [] : %i[notes],
date_and_time: %i[performed_at],
delivery: %i[delivery_site delivery_method],
@@ -229,6 +254,10 @@ def set_steps
self.steps = @draft_vaccination_record.wizard_steps
end
+ def set_vaccines
+ @vaccines = @programme.vaccines.select(&:nivs_name)
+ end
+
def set_batches
vaccines = vaccine_criteria.apply(@programme.vaccines)
diff --git a/app/controllers/patients_controller.rb b/app/controllers/patients_controller.rb
index 9e6cbf664a..0e0fa7a9a3 100644
--- a/app/controllers/patients_controller.rb
+++ b/app/controllers/patients_controller.rb
@@ -62,11 +62,18 @@ def pds_search_history
end
def invite_to_clinic
- PatientLocation.find_or_create_by!(
- patient: @patient,
- location: current_team.generic_clinic,
- academic_year: AcademicYear.pending
- )
+ ActiveRecord::Base.transaction do
+ PatientLocation.find_or_create_by!(
+ patient: @patient,
+ location: current_team.generic_clinic,
+ academic_year: AcademicYear.pending
+ )
+
+ PatientTeamUpdater.call(
+ patient_scope: Patient.where(id: @patient.id),
+ team_scope: Team.where(id: current_team.id)
+ )
+ end
redirect_to patient_path(@patient),
flash: {
diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb
index bdc2c09ca9..e353c31922 100644
--- a/app/controllers/teams_controller.rb
+++ b/app/controllers/teams_controller.rb
@@ -2,8 +2,47 @@
class TeamsController < ApplicationController
skip_after_action :verify_policy_scoped
+ before_action :set_team
+ before_action :set_schools, only: :schools
+ before_action :set_clinics, only: :clinics
- def show
+ layout "full"
+
+ def contact_details
+ end
+
+ def sessions
+ end
+
+ def schools
+ end
+
+ def clinics
+ end
+
+ private
+
+ def set_team
@team = authorize current_team
end
+
+ def set_schools
+ @schools =
+ @team
+ .schools
+ .joins(:team_locations)
+ .where(team_locations: { academic_year: AcademicYear.pending })
+ .distinct
+ .order(:name)
+ end
+
+ def set_clinics
+ @clinics =
+ @team
+ .community_clinics
+ .joins(:team_locations)
+ .where(team_locations: { academic_year: AcademicYear.pending })
+ .distinct
+ .order(:name)
+ end
end
diff --git a/app/forms/batch_form.rb b/app/forms/batch_form.rb
index f99a4e1f59..7ebc8b6d49 100644
--- a/app/forms/batch_form.rb
+++ b/app/forms/batch_form.rb
@@ -9,17 +9,7 @@ class BatchForm
attribute :name, :string
attribute :expiry, :date
- NAME_FORMAT = /\A[A-Za-z0-9]+\z/
-
- validates :name,
- presence: true,
- format: {
- with: NAME_FORMAT
- },
- length: {
- minimum: 2,
- maximum: 100
- }
+ validates :name, batch_name: true
validates :expiry,
comparison: {
diff --git a/app/forms/triage_form.rb b/app/forms/triage_form.rb
index 4e746b9be6..a9a293d032 100644
--- a/app/forms/triage_form.rb
+++ b/app/forms/triage_form.rb
@@ -94,14 +94,13 @@ def show_add_patient_specific_direction?(option)
end
def next_mmr_dose_date
- vaccination_status = patient.vaccination_status(programme:, academic_year:)
+ programme_status = patient.programme_status(programme, academic_year:)
- first_dose_date =
- if vaccination_status.eligible? || vaccination_status.due?
- vaccination_status.latest_date
- end
-
- (first_dose_date || Date.current) + 28.days
+ if programme_status.cannot_vaccinate_delay_vaccination?
+ programme_status.date
+ elsif (first_dose_date = programme_status.date)
+ (first_dose_date + 28.days).to_date
+ end
end
private
@@ -269,8 +268,8 @@ def associate_triage_with_vaccination_record(next_dose_delay_triage)
def patient_eligible_for_additional_dose?
next_dose =
- patient.vaccination_status(
- programme: programme,
+ patient.programme_status(
+ programme,
academic_year: session.academic_year
).dose_sequence
diff --git a/app/jobs/concerns/send_school_consent_notification_concern.rb b/app/jobs/concerns/send_school_consent_notification_concern.rb
index 07a04f8cc0..f70935a4c3 100644
--- a/app/jobs/concerns/send_school_consent_notification_concern.rb
+++ b/app/jobs/concerns/send_school_consent_notification_concern.rb
@@ -11,7 +11,7 @@ def patient_programmes_eligible_for_notification(session:)
session
.patient_locations
.includes(
- patient: %i[consent_notifications consent_statuses vaccination_statuses]
+ patient: %i[consent_notifications consent_statuses programme_statuses]
)
.find_each do |patient_location|
patient = patient_location.patient
@@ -33,8 +33,7 @@ def get_programmes_that_need_consent(patient:, session:, programmes:)
academic_year = session.academic_year
programmes.select do |programme|
- patient.consent_status(programme:, academic_year:).no_response? &&
- patient.vaccination_status(programme:, academic_year:).eligible?
+ patient.programme_status(programme, academic_year:).needs_consent?
end
end
diff --git a/app/jobs/send_clinic_initial_invitations_job.rb b/app/jobs/send_clinic_initial_invitations_job.rb
index 2820dd6c88..4585abfc60 100644
--- a/app/jobs/send_clinic_initial_invitations_job.rb
+++ b/app/jobs/send_clinic_initial_invitations_job.rb
@@ -57,7 +57,7 @@ def should_send_notification?(patient:, team:, academic_year:, programmes:)
return if already_invited
programmes.any? do |programme|
- !patient.vaccination_status(programme:, academic_year:).vaccinated? &&
+ !patient.programme_status(programme, academic_year:).vaccinated? &&
!patient.consent_status(programme:, academic_year:).refused?
end
end
diff --git a/app/jobs/send_clinic_subsequent_invitations_job.rb b/app/jobs/send_clinic_subsequent_invitations_job.rb
index ffd5a269a7..d9741231aa 100644
--- a/app/jobs/send_clinic_subsequent_invitations_job.rb
+++ b/app/jobs/send_clinic_subsequent_invitations_job.rb
@@ -57,7 +57,7 @@ def should_send_notification?(patient:, team:, academic_year:, programmes:)
return unless already_invited
programmes.any? do |programme|
- !patient.vaccination_status(programme:, academic_year:).vaccinated? &&
+ !patient.programme_status(programme, academic_year:).vaccinated? &&
!patient.consent_status(programme:, academic_year:).refused?
end
end
diff --git a/app/jobs/send_school_session_reminders_job.rb b/app/jobs/send_school_session_reminders_job.rb
index b56df1307c..35dd8db06d 100644
--- a/app/jobs/send_school_session_reminders_job.rb
+++ b/app/jobs/send_school_session_reminders_job.rb
@@ -47,7 +47,7 @@ def should_send_notification?(patient:, session:)
all_vaccinated =
programmes.all? do |programme|
- patient.vaccination_status(programme:, academic_year:).vaccinated?
+ patient.programme_status(programme, academic_year:).vaccinated?
end
return false if all_vaccinated
diff --git a/app/lib/already_had_notification_sender.rb b/app/lib/already_had_notification_sender.rb
index 465cd44030..acd6c71c94 100644
--- a/app/lib/already_had_notification_sender.rb
+++ b/app/lib/already_had_notification_sender.rb
@@ -58,7 +58,7 @@ def self.call(...) = new(...).call
attr_reader :vaccination_record
- delegate :patient, :programme, to: :vaccination_record
+ delegate :patient, :programme_type, to: :vaccination_record
def academic_year = AcademicYear.current
@@ -67,7 +67,7 @@ def other_vaccination_records
end
def would_still_be_vaccinated?
- # We're not using the existing `Patient::VaccinationStatus` instance here
+ # We're not using the existing `Patient::ProgrammeStatus` instance here
# because we want to know if the patient would still be vaccinated if we
# took away the vaccination record in question, to know whether to send
# the notification.
@@ -76,7 +76,7 @@ def would_still_be_vaccinated?
# although we're using the same status generator logic as elsewhere, we
# don't need to pass in the consents and triage as an optimisation.
StatusGenerator::Vaccination.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
vaccination_records: other_vaccination_records,
diff --git a/app/lib/clinic_patient_locations_factory.rb b/app/lib/clinic_patient_locations_factory.rb
index 2c277c7b27..ac7df5adfe 100644
--- a/app/lib/clinic_patient_locations_factory.rb
+++ b/app/lib/clinic_patient_locations_factory.rb
@@ -10,7 +10,7 @@ def create_patient_locations!
PatientLocation.import!(
patient_locations_to_create,
on_duplicate_key_ignore: true
- ).ids
+ )
PatientTeamUpdater.call(
patient_scope: patients_in_school,
diff --git a/app/lib/fhir_mapper/vaccination_record.rb b/app/lib/fhir_mapper/vaccination_record.rb
index d1dff09bf3..9f79a05461 100644
--- a/app/lib/fhir_mapper/vaccination_record.rb
+++ b/app/lib/fhir_mapper/vaccination_record.rb
@@ -18,10 +18,10 @@ def initialize(vaccination_record)
def fhir_record
immunisation = FHIR::Immunization.new(id: nhs_immunisations_api_id)
- if performed_by_user.present?
- immunisation.contained << performed_by_user.fhir_practitioner(
- reference_id: "Practitioner1"
- )
+ if performed_by.present?
+ immunisation.contained << FHIRMapper::User.new(
+ performed_by
+ ).fhir_practitioner(reference_id: "Practitioner1")
end
immunisation.contained << patient.fhir_record(reference_id: "Patient1")
@@ -44,10 +44,12 @@ def fhir_record
immunisation.site = fhir_site
immunisation.route = fhir_route
immunisation.doseQuantity = fhir_dose_quantity
- immunisation.performer = [
- fhir_user_performer(reference_id: "Practitioner1"),
- fhir_org_performer
- ]
+ if performed_by.present?
+ immunisation.performer << fhir_user_performer(
+ reference_id: "Practitioner1"
+ )
+ end
+ immunisation.performer << fhir_org_performer
immunisation.reasonCode = [fhir_reason_code]
immunisation.protocolApplied = [fhir_protocol_applied]
diff --git a/app/lib/govuk_notify_personalisation.rb b/app/lib/govuk_notify_personalisation.rb
index 711ba96f88..ac8d29ce68 100644
--- a/app/lib/govuk_notify_personalisation.rb
+++ b/app/lib/govuk_notify_personalisation.rb
@@ -199,10 +199,9 @@ def mmr_second_dose_message
return unless patient
return unless mmr_programme
- vaccination_status =
- patient.vaccination_status(programme: mmr_programme, academic_year:)
+ programme_status = patient.programme_status(mmr_programme, academic_year:)
- return "" if vaccination_status.vaccinated?
+ return "" if programme_status.vaccinated?
[
"## Your child still needs a second dose of the MMR vaccine",
@@ -262,15 +261,16 @@ def next_mmr_dose_date
return if patient.nil?
return if mmr_programme.nil?
- vaccination_status =
- patient.vaccination_status(programme: mmr_programme, academic_year:)
+ programme_status = patient.programme_status(mmr_programme, academic_year:)
- first_dose_date =
- if vaccination_status.eligible? || vaccination_status.due?
- vaccination_status.latest_date
+ date =
+ if programme_status.cannot_vaccinate_delay_vaccination?
+ programme_status.date
+ elsif (first_dose_date = programme_status.date)
+ (first_dose_date + 28.days).to_date
end
- (first_dose_date + 28.days).to_date.to_fs(:long) if first_dose_date
+ date.to_fs(:long)
end
def patient_eligible_for_additional_dose?
@@ -280,7 +280,7 @@ def patient_eligible_for_additional_dose?
next_dose =
patient
.reload
- .vaccination_status(programme: mmr_programme, academic_year:)
+ .programme_status(mmr_programme, academic_year:)
.dose_sequence
next_dose == mmr_programme.maximum_dose_sequence
diff --git a/app/lib/mavis_cli/clinics/add_to_team.rb b/app/lib/mavis_cli/clinics/add_to_team.rb
index 68b3ebc98b..ced88305f3 100644
--- a/app/lib/mavis_cli/clinics/add_to_team.rb
+++ b/app/lib/mavis_cli/clinics/add_to_team.rb
@@ -5,46 +5,58 @@ module Clinics
class AddToTeam < Dry::CLI::Command
desc "Add an existing clinic to a team"
- argument :workgroup, required: true, desc: "The ODS code of the team"
- argument :subteam, required: true, desc: "The subteam of the team"
+ argument :team_workgroup,
+ required: true,
+ desc: "The workgroup of the team"
+ argument :subteam_name, required: true, desc: "The name of the subteam"
argument :ods_codes,
type: :array,
required: true,
- desc: "The ODS codes of the clinics"
+ desc: "The ODS code of the clinic"
- def call(workgroup:, subteam:, ods_codes:, **)
+ def call(team_workgroup:, subteam_name:, ods_codes:, **)
MavisCLI.load_rails
- team = Team.find_by(workgroup:)
+ team = Team.find_by(workgroup: team_workgroup)
academic_year = AcademicYear.pending
if team.nil?
- warn "Could not find team."
+ warn "Could not find team with workgroup #{team_workgroup}."
return
end
- subteam = team.subteams.find_by(name: subteam)
+ subteam = team.subteams.find_by(name: subteam_name)
+
+ if subteam.nil?
+ warn "Could not find subteam with name #{subteam_name}."
+ return
+ end
ActiveRecord::Base.transaction do
ods_codes.each do |ods_code|
location = Location.clinic.find_by(ods_code:)
if location.nil?
- warn "Could not find location: #{ods_code}"
+ warn "Could not find clinic with ODS code #{ods_code}."
next
end
if (
existing_team_locations =
- location.team_locations.includes(:team).where(academic_year:)
+ location
+ .team_locations
+ .includes(:team, :subteam)
+ .where(academic_year:)
)
existing_team_locations.each do |existing_team_location|
- warn "#{ods_code} previously belonged to #{existing_team_location.name}"
+ warn "#{ods_code} previously belonged to #{existing_team_location.name}."
end
end
location.attach_to_team!(team, academic_year:, subteam:)
end
+
+ PatientTeamUpdater.call(team_scope: Team.where(id: team.id))
end
end
end
diff --git a/app/lib/mavis_cli/schools/add_to_team.rb b/app/lib/mavis_cli/schools/add_to_team.rb
index f907cd854d..c92cbdf933 100644
--- a/app/lib/mavis_cli/schools/add_to_team.rb
+++ b/app/lib/mavis_cli/schools/add_to_team.rb
@@ -5,28 +5,35 @@ module Schools
class AddToTeam < Dry::CLI::Command
desc "Add an existing school to a team"
- argument :workgroup, required: true, desc: "The ODS code of the team"
- argument :subteam, required: true, desc: "The subteam of the team"
+ argument :team_workgroup,
+ required: true,
+ desc: "The workgroup of the team"
+ argument :subteam_name, required: true, desc: "The name of the subteam"
argument :urns,
type: :array,
required: true,
- desc: "The URN of the school"
+ desc: "The URN of the school (including site, if applicable)"
option :programmes,
type: :array,
desc: "The programmes administered at the school"
- def call(workgroup:, subteam:, urns:, programmes: [], **)
+ def call(team_workgroup:, subteam_name:, urns:, programmes: [], **)
MavisCLI.load_rails
- team = Team.find_by(workgroup:)
+ team = Team.find_by(workgroup: team_workgroup)
if team.nil?
- warn "Could not find team."
+ warn "Could not find team with workgroup #{team_workgroup}."
return
end
- subteam = team.subteams.find_by(name: subteam)
+ subteam = team.subteams.find_by(name: subteam_name)
+
+ if subteam.nil?
+ warn "Could not find subteam with name #{subteam_name}."
+ return
+ end
programmes =
(programmes.empty? ? team.programmes : Programme.find_all(programmes))
@@ -38,16 +45,19 @@ def call(workgroup:, subteam:, urns:, programmes: [], **)
location = Location.school.find_by_urn_and_site(urn)
if location.nil?
- warn "Could not find location: #{urn}"
+ warn "Could not find school with URN #{urn}."
next
end
if (
existing_team_locations =
- location.team_locations.includes(:team).where(academic_year:)
+ location
+ .team_locations
+ .includes(:team, :subteam)
+ .where(academic_year:)
)
existing_team_locations.each do |existing_team_location|
- warn "#{ods_code} previously belonged to #{existing_team_location.name}"
+ warn "#{urn} previously belonged to #{existing_team_location.name}."
end
end
@@ -58,6 +68,8 @@ def call(workgroup:, subteam:, urns:, programmes: [], **)
academic_year:
)
end
+
+ PatientTeamUpdater.call(team_scope: Team.where(id: team.id))
end
end
end
diff --git a/app/lib/mavis_cli/schools/create_site.rb b/app/lib/mavis_cli/schools/create_site.rb
index 7af4e6335b..ea54a54d82 100644
--- a/app/lib/mavis_cli/schools/create_site.rb
+++ b/app/lib/mavis_cli/schools/create_site.rb
@@ -85,8 +85,8 @@ def call(
).subteam
MavisCLI::Schools::AddToTeam.new.call(
- workgroup: team.workgroup,
- subteam: subteam.name,
+ team_workgroup: team.workgroup,
+ subteam_name: subteam.name,
urns: [location.urn_and_site]
)
diff --git a/app/lib/mavis_cli/schools/remove_from_team.rb b/app/lib/mavis_cli/schools/remove_from_team.rb
index a1d1f55e10..80d57e51b3 100644
--- a/app/lib/mavis_cli/schools/remove_from_team.rb
+++ b/app/lib/mavis_cli/schools/remove_from_team.rb
@@ -43,15 +43,24 @@ def call(team_workgroup:, subteam_name:, urns:, academic_year: nil, **)
location = Location.school.find_by_urn_and_site(urn)
if location.nil?
- warn "Could not find location with URN #{urn}"
+ warn "Could not find school with URN #{urn}"
next
end
- team_location =
- TeamLocation
- .includes(:team)
- .where(team:, academic_year:, subteam:, location:)
- .sole
+ team_locations =
+ TeamLocation.includes(:team).where(
+ team:,
+ academic_year:,
+ subteam:,
+ location:
+ )
+
+ if team_locations.empty?
+ warn "Could not find team location for URN #{urn}"
+ next
+ else
+ team_location = team_locations.sole
+ end
unless team_location.safe_to_destroy?
warn "Location #{location.id} (URN: #{urn}) cannot be removed as it has associated records."
diff --git a/app/lib/next_dose_triage_factory.rb b/app/lib/next_dose_triage_factory.rb
index c6a1dffdf1..05370ad7b6 100644
--- a/app/lib/next_dose_triage_factory.rb
+++ b/app/lib/next_dose_triage_factory.rb
@@ -34,9 +34,7 @@ def should_create?
return false if next_date.past?
- status = patient.vaccination_status(programme:, academic_year:)
-
- !status.vaccinated?
+ !patient.programme_status(programme, academic_year:).vaccinated?
end
def next_date = vaccination_record.performed_at + 28.days
diff --git a/app/lib/notifier/consent.rb b/app/lib/notifier/consent.rb
index d4d54c8474..4e3b71a766 100644
--- a/app/lib/notifier/consent.rb
+++ b/app/lib/notifier/consent.rb
@@ -84,7 +84,7 @@ def patient_eligible_for_additional_dose?(session)
next_dose =
patient
.reload
- .vaccination_status(programme:, academic_year: session.academic_year)
+ .programme_status(programme, academic_year: session.academic_year)
.dose_sequence
next_dose == programme.maximum_dose_sequence
diff --git a/app/lib/patient_archiver.rb b/app/lib/patient_archiver.rb
index 1c7284b8a0..52e65e9d12 100644
--- a/app/lib/patient_archiver.rb
+++ b/app/lib/patient_archiver.rb
@@ -19,6 +19,8 @@ def call
patient.clear_pending_sessions!(team:)
destroy_school_moves!
+
+ update_patient_teams!
end
end
@@ -43,4 +45,11 @@ def destroy_school_moves!
.where("team_locations.team_id = ?", team.id)
.destroy_all
end
+
+ def update_patient_teams!
+ PatientTeamUpdater.call(
+ patient_scope: Patient.where(id: patient.id),
+ team_scope: Team.where(id: team.id)
+ )
+ end
end
diff --git a/app/lib/patient_programme_status_resolver.rb b/app/lib/patient_programme_status_resolver.rb
new file mode 100644
index 0000000000..020fbc7a9d
--- /dev/null
+++ b/app/lib/patient_programme_status_resolver.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+##
+# This class can be used to generate a hash suitable for use by the
+# `AppAttachedTagsComponent` used to render the various statuses of any
+# particular patient, programme and academic year combination.
+class PatientProgrammeStatusResolver
+ def initialize(
+ patient,
+ programme_type:,
+ academic_year:,
+ context_location_id: nil,
+ only_if_vaccinated: false
+ )
+ @patient = patient
+ @programme_type = programme_type
+ @academic_year = academic_year
+ @context_location_id = context_location_id
+ @only_if_vaccinated = only_if_vaccinated
+ end
+
+ def call
+ return false if only_if_vaccinated && !programme_status.vaccinated?
+
+ { prefix:, text:, colour:, details_text: }.compact
+ end
+
+ def self.call(...) = new(...).call
+
+ private_class_method :new
+
+ private
+
+ attr_reader :patient,
+ :programme_type,
+ :academic_year,
+ :context_location_id,
+ :only_if_vaccinated
+
+ def programme_status
+ @programme_status ||=
+ patient.programme_status(Programme.find(programme_type), academic_year:)
+ end
+
+ def prefix = programme_status.programme.name
+
+ def text
+ if programme_status.due? && (count = programme_status.dose_sequence)
+ "Due #{count.ordinalize} dose"
+ else
+ I18n.t(programme_status.status, scope: %i[status programme label])
+ end
+ end
+
+ def colour =
+ I18n.t(programme_status.status, scope: %i[status programme colour])
+
+ def details_text
+ text =
+ I18n.t(
+ programme_status.status,
+ scope: %i[status programme details],
+ default: nil
+ )
+
+ if programme_status.due?
+ translation_key = programme_status.vaccine_criteria.to_param
+ I18n.t(translation_key, scope: :vaccine_criteria).presence || text
+ elsif programme_status.cannot_vaccinate_delay_vaccination?
+ if (date = programme_status.date)
+ text + " until #{date.to_fs(:long)}"
+ else
+ text
+ end
+ elsif programme_status.vaccinated_fully? ||
+ programme_status.cannot_vaccinate?
+ (date = programme_status.date) ? text + " on #{date.to_fs(:long)}" : text
+ else
+ text
+ end
+ end
+end
diff --git a/app/lib/patient_status_resolver.rb b/app/lib/patient_status_resolver.rb
deleted file mode 100644
index 4b45736fd6..0000000000
--- a/app/lib/patient_status_resolver.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-##
-# This class can be used to generate a hash suitable for use by the
-# `AppAttachedTagsComponent` used to render the various statuses of any
-# particular patient, programme and academic year combination.
-class PatientStatusResolver
- def initialize(patient, programme:, academic_year:, context_location_id: nil)
- @patient = patient
- @programme = programme
- @academic_year = academic_year
- @context_location_id = context_location_id
- end
-
- def consent
- status =
- if consent_status.given?
- vaccine_method =
- triage_status.vaccine_method.presence ||
- consent_status.vaccine_methods.first
-
- without_gelatine =
- triage_status.without_gelatine || consent_status.without_gelatine
-
- parts = [
- "given",
- vaccine_method,
- without_gelatine ? "without_gelatine" : nil,
- without_gelatine && @programme.flu? ? "flu" : nil
- ]
-
- parts.compact_blank.join("_")
- else
- consent_status.status
- end
-
- tag_hash(status, context: :consent).merge(
- prefix: consent_status.programme.name
- )
- end
-
- def programme(only_if_vaccinated: false)
- return if only_if_vaccinated && !programme_status.vaccinated?
-
- hash = tag_hash(programme_status.status, context: :programme)
-
- if programme_status.due?
- if (count = programme_status.dose_sequence)
- hash[:text] = "Due #{count.ordinalize} dose"
- end
-
- translation_key = programme_status.vaccine_criteria.to_param
-
- if (
- details_text = I18n.t(translation_key, scope: :vaccine_criteria)
- ).present?
- hash[:details_text] = details_text
- end
- elsif programme_status.cannot_vaccinate_delay_vaccination?
- if (date = programme_status.date)
- hash[:details_text] += " until #{date.to_fs(:long)}"
- end
- elsif programme_status.vaccinated_fully? ||
- programme_status.cannot_vaccinate?
- if (date = programme_status.date)
- hash[:details_text] += " on #{date.to_fs(:long)}"
- end
- end
-
- hash.merge(prefix: programme_status.programme.name)
- end
-
- def triage
- status =
- if triage_status.safe_to_vaccinate?
- vaccine_method = triage_status.vaccine_method
- without_gelatine = triage_status.without_gelatine
-
- parts = [
- "safe_to_vaccinate",
- vaccine_method,
- without_gelatine ? "without_gelatine" : nil,
- without_gelatine && @programme.flu? ? "flu" : nil
- ]
-
- parts.compact_blank.join("_")
- else
- triage_status.status
- end
-
- tag_hash(status, context: :triage).merge(
- prefix: consent_status.programme.name
- )
- end
-
- private
-
- attr_reader :patient, :academic_year, :context_location_id
-
- def tag_hash(status, context:)
- text = I18n.t(status, scope: [:status, context, :label])
- colour = I18n.t(status, scope: [:status, context, :colour])
- details_text =
- I18n.t(status, scope: [:status, context, :details], default: nil)
- { text:, colour:, details_text: }.compact
- end
-
- def consent_status
- @consent_status ||=
- patient.consent_status(programme: @programme, academic_year:)
- end
-
- def programme_status
- @programme_status ||= patient.programme_status(@programme, academic_year:)
- end
-
- def triage_status
- @triage_status ||=
- patient.triage_status(programme: @programme, academic_year:)
- end
-end
diff --git a/app/lib/programme_grouper.rb b/app/lib/programme_grouper.rb
index 9cec47efda..c8b518ec69 100644
--- a/app/lib/programme_grouper.rb
+++ b/app/lib/programme_grouper.rb
@@ -30,7 +30,7 @@ def group(object)
if (value = GROUPS[key])
value
else
- raise UnsupportedProgramme, programme(object)
+ raise UnsupportedProgrammeType, key
end
end
diff --git a/app/lib/reports/careplus_exporter.rb b/app/lib/reports/careplus_exporter.rb
index 094f590ab2..e1b102b3a9 100644
--- a/app/lib/reports/careplus_exporter.rb
+++ b/app/lib/reports/careplus_exporter.rb
@@ -143,8 +143,8 @@ def rows(patient:, vaccination_records:)
records.first.performed_at.strftime("%H:%M"),
session.location.school? ? "SC" : "CL", # Venue Type
session.location.dfe_number || team.careplus_venue_code, # Venue Code
- "IN", # Staff Type
- "LW5PM", # Staff Code
+ team.careplus_staff_type,
+ team.careplus_staff_code,
"Y", # Attended; Did not attends do not get recorded on GP systems
"", # Reason Not Attended; Always blank
"", # Suspension End Date; Doesn't need to be used
diff --git a/app/lib/stats/session.rb b/app/lib/stats/session.rb
index 5136f28c86..d4237800bb 100644
--- a/app/lib/stats/session.rb
+++ b/app/lib/stats/session.rb
@@ -46,15 +46,6 @@ def self.call(...) = new(...).call
delegate :academic_year, :location, :team, to: :session
- def vaccinated_count
- @vaccinated_count ||=
- Patient::VaccinationStatus
- .vaccinated
- .for_programme(programme)
- .where(patient_id: patient_ids, academic_year:)
- .count
- end
-
def due_statuses
if programme.flu?
%w[due_nasal due_injection]
diff --git a/app/lib/status_generator/consent.rb b/app/lib/status_generator/consent.rb
index e612d15912..73456eeeef 100644
--- a/app/lib/status_generator/consent.rb
+++ b/app/lib/status_generator/consent.rb
@@ -2,19 +2,23 @@
class StatusGenerator::Consent
def initialize(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
vaccination_records:
)
- @programme = programme
+ @programme_type = programme_type
@academic_year = academic_year
@patient = patient
@consents = consents
@vaccination_records = vaccination_records
end
+ def programme
+ Programme.find(programme_type, disease_types:, patient:)
+ end
+
def status
if status_should_be_given?
:given
@@ -47,7 +51,7 @@ def disease_types
private
- attr_reader :programme,
+ attr_reader :programme_type,
:academic_year,
:patient,
:consents,
@@ -59,7 +63,7 @@ def vaccinated?
# in the consents and triage as an optimisation.
@vaccinated ||=
StatusGenerator::Vaccination.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
vaccination_records:,
@@ -134,10 +138,6 @@ def parental_consents
def latest_consents
@latest_consents ||=
- ConsentGrouper.call(
- consents,
- programme_type: programme.type,
- academic_year:
- )
+ ConsentGrouper.call(consents, programme_type:, academic_year:)
end
end
diff --git a/app/lib/status_generator/programme.rb b/app/lib/status_generator/programme.rb
index 58d8fbdcd6..b1e1c5f511 100644
--- a/app/lib/status_generator/programme.rb
+++ b/app/lib/status_generator/programme.rb
@@ -9,7 +9,7 @@ class StatusGenerator::Programme
# to already be sorted in reverse chronological order, meaning the most
# recent item is at the beginning of the array.
def initialize(
- programme:,
+ programme_type:,
academic_year:,
patient:,
patient_locations:,
@@ -18,7 +18,7 @@ def initialize(
attendance_record:,
vaccination_records:
)
- @programme = programme
+ @programme_type = programme_type
@academic_year = academic_year
@patient = patient
@patient_locations = patient_locations
@@ -28,6 +28,10 @@ def initialize(
@vaccination_records = vaccination_records
end
+ def programme
+ Programme.find(programme_type, disease_types:, patient:)
+ end
+
def status
if should_be_vaccinated_already?
:vaccinated_already
@@ -68,12 +72,7 @@ def status
end
end
- def dose_sequence
- if triage_generator.status.in?(%i[safe_to_vaccinate not_required]) &&
- consent_generator.status == :given
- vaccination_generator.dose_sequence
- end
- end
+ delegate :dose_sequence, to: :vaccination_generator
def without_gelatine
if vaccination_generator.status == :not_eligible ||
@@ -122,7 +121,7 @@ def location_id
private
- attr_reader :programme,
+ attr_reader :programme_type,
:academic_year,
:patient,
:patient_locations,
@@ -214,7 +213,7 @@ def should_be_needs_consent_request_not_scheduled?
def consent_generator
@consent_generator ||=
StatusGenerator::Consent.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
@@ -225,7 +224,7 @@ def consent_generator
def triage_generator
@triage_generator ||=
StatusGenerator::Triage.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
@@ -237,7 +236,7 @@ def triage_generator
def vaccination_generator
@vaccination_generator ||=
StatusGenerator::Vaccination.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
patient_locations:,
diff --git a/app/lib/status_generator/triage.rb b/app/lib/status_generator/triage.rb
index 44c93d21b8..9c2228e25f 100644
--- a/app/lib/status_generator/triage.rb
+++ b/app/lib/status_generator/triage.rb
@@ -2,14 +2,14 @@
class StatusGenerator::Triage
def initialize(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
triages:,
vaccination_records:
)
- @programme = programme
+ @programme_type = programme_type
@academic_year = academic_year
@patient = patient
@consents = consents
@@ -17,6 +17,10 @@ def initialize(
@vaccination_records = vaccination_records
end
+ def programme
+ Programme.find(programme_type, disease_types:, patient:)
+ end
+
def status
if status_should_be_safe_to_vaccinate?
:safe_to_vaccinate
@@ -41,6 +45,8 @@ def without_gelatine
latest_triage&.without_gelatine if status_should_be_safe_to_vaccinate?
end
+ delegate :disease_types, to: :consent_generator
+
def delay_vaccination_until_date
if status_should_be_delay_vaccination?
latest_triage&.delay_vaccination_until
@@ -66,22 +72,20 @@ def vaccination_history_requires_triage?
private
- attr_reader :programme,
+ attr_reader :programme_type,
:academic_year,
:patient,
:consents,
:triages,
:vaccination_records
- def programme_type = programme.type
-
def vaccinated?
# We only care about whether the patient is vaccinated so although we're
# using the same status generator logic as elsewhere we don't need to pass
# in the consents and triage as an optimisation.
@vaccinated ||=
StatusGenerator::Vaccination.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
vaccination_records:,
@@ -126,7 +130,7 @@ def status_should_be_required?
def consent_generator
@consent_generator ||=
StatusGenerator::Consent.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
diff --git a/app/lib/status_generator/vaccination.rb b/app/lib/status_generator/vaccination.rb
index 37c70e5102..a6c573f045 100644
--- a/app/lib/status_generator/vaccination.rb
+++ b/app/lib/status_generator/vaccination.rb
@@ -9,7 +9,7 @@ class StatusGenerator::Vaccination
# to already be sorted in reverse chronological order, meaning the most
# recent item is at the beginning of the array.
def initialize(
- programme:,
+ programme_type:,
academic_year:,
patient:,
patient_locations:,
@@ -18,7 +18,7 @@ def initialize(
attendance_record:,
vaccination_records:
)
- @programme = programme
+ @programme_type = programme_type
@academic_year = academic_year
@patient = patient
@patient_locations = patient_locations
@@ -28,8 +28,8 @@ def initialize(
@vaccination_records =
vaccination_records.select do
- it.patient_id == patient.id && it.programme_type == programme.type &&
- if programme.seasonal?
+ it.patient_id == patient.id && it.programme_type == programme_type &&
+ if seasonal?
it.academic_year == academic_year
else
it.academic_year <= academic_year
@@ -37,6 +37,10 @@ def initialize(
end
end
+ def programme
+ Programme.find(programme_type, disease_types:, patient:)
+ end
+
def status
if status_should_be_vaccinated?
:vaccinated
@@ -94,7 +98,7 @@ def latest_session_status
private
- attr_reader :programme,
+ attr_reader :programme_type,
:academic_year,
:patient,
:patient_locations,
@@ -141,7 +145,7 @@ def year_group = patient.year_group(academic_year:)
def valid_vaccination_records
@valid_vaccination_records ||=
- if programme.seasonal?
+ if seasonal?
vaccination_records.select { it.administered? || it.already_had? }
else
if (
@@ -152,14 +156,14 @@ def valid_vaccination_records
administered_records = vaccination_records.select(&:administered?)
- if programme.doubles?
+ if doubles?
filter_doubles_vaccination_records(administered_records)
- elsif programme.hpv?
+ elsif hpv?
filter_hpv_vaccination_records(administered_records)
- elsif programme.mmr?
+ elsif mmr?
filter_mmr_vaccination_records(administered_records)
else
- raise UnsupportedProgramme, programme
+ raise UnsupportedProgrammeType, programme.type
end
end
end
@@ -211,11 +215,11 @@ def vaccinated_vaccination_record
return already_had_record
end
- if programme.mmr?
- if valid_vaccination_records.count >= programme.maximum_dose_sequence
+ if mmr?
+ if valid_vaccination_records.count >= maximum_dose_sequence
valid_vaccination_records.first
end
- elsif programme.td_ipv?
+ elsif td_ipv?
valid_vaccination_records.find do
it.dose_sequence == 5 ||
(it.dose_sequence.nil? && it.sourced_from_service?)
@@ -232,16 +236,31 @@ def is_eligible?
.select { it.academic_year == academic_year }
.any? do |patient_location|
patient_location.location.location_programme_year_groups.any? do
- it.programme_type == programme.type &&
+ it.programme_type == programme_type &&
it.academic_year == academic_year && it.year_group == year_group
end
end
end
+ Programme::TYPES.each do |type|
+ define_method("#{type}?") { programme_type == type }
+ end
+
+ def doubles? = menacwy? || td_ipv?
+
+ def seasonal?
+ @seasonal ||= Programme.find(programme_type).seasonal?
+ end
+
+ def maximum_dose_sequence
+ @maximum_dose_sequence ||=
+ Programme.find(programme_type).maximum_dose_sequence
+ end
+
def consent_generator
@consent_generator ||=
StatusGenerator::Consent.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
@@ -252,7 +271,7 @@ def consent_generator
def triage_generator
@triage_generator ||=
StatusGenerator::Triage.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
diff --git a/app/lib/status_updater.rb b/app/lib/status_updater.rb
index 9015370bdc..efcc97a5ec 100644
--- a/app/lib/status_updater.rb
+++ b/app/lib/status_updater.rb
@@ -11,7 +11,6 @@ def call
update_programme_statuses!
update_registration_statuses!
update_triage_statuses!
- update_vaccination_statuses!
end
def self.call(...) = new(...).call
@@ -134,43 +133,6 @@ def update_triage_statuses!
end
end
- def update_vaccination_statuses!
- Patient::VaccinationStatus.import!(
- %i[patient_id programme_type academic_year],
- patient_statuses_to_import,
- on_duplicate_key_ignore: true
- )
-
- Patient::VaccinationStatus
- .then { patient ? it.where(patient:) : it }
- .where(academic_year: academic_years)
- .includes(
- :attendance_record,
- :consents,
- :patient,
- :patient_locations,
- :triages,
- :vaccination_records
- )
- .find_in_batches(batch_size: 10_000) do |batch|
- batch.each(&:assign_status)
-
- Patient::VaccinationStatus.import!(
- batch.select(&:changed?),
- on_duplicate_key_update: {
- conflict_target: [:id],
- columns: %i[
- dose_sequence
- latest_date
- latest_location_id
- latest_session_status
- status
- ]
- }
- )
- end
- end
-
def patient_statuses_to_import
@patient_statuses_to_import ||=
Patient
diff --git a/app/lib/unsupported_programme.rb b/app/lib/unsupported_programme.rb
deleted file mode 100644
index 4227c092d4..0000000000
--- a/app/lib/unsupported_programme.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class UnsupportedProgramme < RuntimeError
- def initialize(programme)
- super("Unsupported programme: #{programme.name}")
- end
-end
diff --git a/app/lib/unsupported_programme_type.rb b/app/lib/unsupported_programme_type.rb
new file mode 100644
index 0000000000..61d5e25d50
--- /dev/null
+++ b/app/lib/unsupported_programme_type.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class UnsupportedProgrammeType < StandardError
+ def initialize(programme_type)
+ super("Unsupported programme type: #{programme_type}")
+ end
+end
diff --git a/app/models/archive_reason.rb b/app/models/archive_reason.rb
index 712c0494e5..ec699368cd 100644
--- a/app/models/archive_reason.rb
+++ b/app/models/archive_reason.rb
@@ -28,8 +28,6 @@
# fk_rails_... (team_id => teams.id)
#
class ArchiveReason < ApplicationRecord
- include UpdatesPatientTeam
-
self.inheritance_column = nil
belongs_to :team
diff --git a/app/models/cis2_info.rb b/app/models/cis2_info.rb
index 194733efd1..a89adf3003 100644
--- a/app/models/cis2_info.rb
+++ b/app/models/cis2_info.rb
@@ -8,7 +8,7 @@ class CIS2Info
SUPPORT_ROLE = "S8001:G8005:R8015"
SUPPORT_WORKGROUP = "mavissupport"
- SUPPORT_ORGANISATION = "Y90128"
+ SUPPORT_ORGANISATION = Settings.cis2.support_organisation
ACCESS_SENSITIVE_FLAGGED_RECORDS_ACTIVITY_CODE = "B1611"
INDEPENDENT_PRESCRIBING_ACTIVITY_CODE = "B0420"
diff --git a/app/models/class_import.rb b/app/models/class_import.rb
index 82c61e50d0..85d04d8926 100644
--- a/app/models/class_import.rb
+++ b/app/models/class_import.rb
@@ -97,9 +97,7 @@ def postprocess_rows!
)
end
- @imported_school_move_ids ||= []
- @imported_school_move_ids |=
- SchoolMove.import!(school_moves, on_duplicate_key_ignore: true).ids
+ SchoolMove.import!(school_moves, on_duplicate_key_ignore: true)
valid_changesets.update_all(status: :processed) if valid_changesets
diff --git a/app/models/concerns/patient_import_concern.rb b/app/models/concerns/patient_import_concern.rb
index 96db0cf443..1a289543ae 100644
--- a/app/models/concerns/patient_import_concern.rb
+++ b/app/models/concerns/patient_import_concern.rb
@@ -70,11 +70,13 @@ def import_school_moves(changesets, import)
# the duplicates won't be persisted, so we can skip those
school_move.confirm! if school_move.patient.persisted?
end
+
school_move_import_records = importable_school_moves.to_a
+
SchoolMove.import!(
school_move_import_records,
on_duplicate_key_update: :all
- ).ids
+ )
end
def import_pds_search_results(changesets, import)
diff --git a/app/models/concerns/updates_patient_team.rb b/app/models/concerns/updates_patient_team.rb
deleted file mode 100644
index faae20c8da..0000000000
--- a/app/models/concerns/updates_patient_team.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module UpdatesPatientTeam
- extend ActiveSupport::Concern
-
- included do
- after_save :update_patient_team
- after_destroy :update_patient_team
- end
-
- private
-
- def update_patient_team
- if should_update_patient_team?
- PatientTeamUpdater.call(
- patient_scope: patient_scope_for_update_patient_team,
- team_scope: team_scope_for_update_patient_team
- )
- end
- end
-
- def should_update_patient_team?
- try(:patient_id_previous_change).present? ||
- try(:team_id_previous_change).present?
- end
-
- def patient_scope_for_update_patient_team
- if (previous_change = try(:patient_id_previous_change)).present?
- Patient.where(id: previous_change.compact)
- end
- end
-
- def team_scope_for_update_patient_team
- if (previous_change = try(:team_id_previous_change)).present?
- Team.where(id: previous_change.compact)
- end
- end
-end
diff --git a/app/models/draft_vaccination_record.rb b/app/models/draft_vaccination_record.rb
index 30fa232b08..662d5fb473 100644
--- a/app/models/draft_vaccination_record.rb
+++ b/app/models/draft_vaccination_record.rb
@@ -10,6 +10,8 @@ class DraftVaccinationRecord
include VaccinationRecordPerformedByConcern
attribute :batch_id, :integer
+ attribute :batch_name, :string
+ attribute :batch_expiry, :date
attribute :delivery_method, :string
attribute :delivery_site, :string
attribute :disease_types, array: true
@@ -34,6 +36,7 @@ class DraftVaccinationRecord
attribute :session_id, :integer
attribute :source, :string
attribute :supplied_by_user_id, :integer
+ attribute :vaccine_id, :integer
def initialize(current_user:, **attributes)
@current_user = current_user
@@ -83,7 +86,11 @@ def wizard_steps
end
on_wizard_step :batch, exact: true do
- validates :batch_id, presence: true
+ validates :batch_id, presence: true, unless: :bulk_upload_user_and_record?
+
+ validates :vaccine_id, presence: true, if: :bulk_upload_user_and_record?
+ validates :batch_name, batch_name: true, if: :bulk_upload_user_and_record?
+ validates :batch_expiry, presence: true, if: :bulk_upload_user_and_record?
end
on_wizard_step :dose, exact: true do
@@ -155,6 +162,17 @@ def already_had?
alias_method :administered, :administered?
def batch
+ if batch_expiry && batch_name && vaccine_id && bulk_upload_user_and_record?
+ return(
+ Batch.create_with(archived_at: Time.current).find_or_create_by!(
+ expiry: batch_expiry,
+ name: batch_name,
+ team_id: nil,
+ vaccine_id: vaccine_id
+ )
+ )
+ end
+
return nil if batch_id.nil?
Batch.find(batch_id)
end
@@ -236,8 +254,6 @@ def delivery_method=(value)
delegate :vaccine, to: :batch, allow_nil: true
- delegate :id, to: :vaccine, prefix: true, allow_nil: true
-
def vaccine_id_changed? = batch_id_changed?
def location_is_school
@@ -283,7 +299,7 @@ def vaccine_method_matches_consent_and_triage?
consent_generator =
StatusGenerator::Consent.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents: patient.consents,
@@ -292,7 +308,7 @@ def vaccine_method_matches_consent_and_triage?
triage_generator =
StatusGenerator::Triage.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents: patient.consents,
@@ -331,6 +347,24 @@ def bulk_upload_user_and_record?
sourced_from_bulk_upload?
end
+ def read_from!(vaccination_record)
+ self.batch_name = vaccination_record.batch&.name
+ self.batch_expiry = vaccination_record.batch&.expiry
+ self.vaccine_id = vaccination_record.vaccine&.id
+
+ super(vaccination_record)
+ end
+
+ def write_to!(vaccination_record)
+ super(vaccination_record)
+
+ if batch_expiry && batch_name && vaccine_id && bulk_upload_user_and_record?
+ vaccination_record.batch_id = batch&.id
+ end
+
+ vaccination_record.vaccine_id = batch&.vaccine_id
+ end
+
private
def readable_attribute_names
diff --git a/app/models/immunisation_import.rb b/app/models/immunisation_import.rb
index 0217d67e0a..9d8211a25d 100644
--- a/app/models/immunisation_import.rb
+++ b/app/models/immunisation_import.rb
@@ -76,13 +76,6 @@ def process_row(row)
count_column_to_increment = count_column(vaccination_record)
return count_column_to_increment unless vaccination_record
- # Instead of saving individually, we'll collect the records
- @vaccination_records_batch ||= Set.new
- @batches_batch ||= Set.new
- @patients_batch ||= Set.new
- @patient_locations_batch ||= Set.new
- @archive_reasons_batch ||= Set.new
-
@vaccination_records_batch.add(vaccination_record)
if (batch = vaccination_record.batch)
@batches_batch.add(batch)
@@ -103,6 +96,12 @@ def process_row(row)
def process_import!
counts = count_columns.index_with(0)
+ @vaccination_records_batch = Set.new
+ @batches_batch = Set.new
+ @patients_batch = Set.new
+ @patient_locations_batch = Set.new
+ @archive_reasons_batch = Set.new
+
ActiveRecord::Base.transaction do
rows.each do |row|
count_column_to_increment = process_row(row)
diff --git a/app/models/immunisation_import_row.rb b/app/models/immunisation_import_row.rb
index afaf522611..daed9f5b37 100644
--- a/app/models/immunisation_import_row.rb
+++ b/app/models/immunisation_import_row.rb
@@ -173,7 +173,11 @@ def to_vaccination_record
}
vaccination_record =
- if uuid.present?
+ if bulk?
+ VaccinationRecord.find_or_initialize_by(
+ attributes.merge(attributes_to_stage_if_already_exists)
+ )
+ elsif uuid.present?
VaccinationRecord
.find_by!(uuid: uuid.to_s)
.tap { it.stage_changes(attributes) }
@@ -630,7 +634,7 @@ def validate_batch_name
errors.add(batch_name.header, "must be at most 100 characters long")
elsif batch_name.to_s.length < 2
errors.add(batch_name.header, "must be at least 2 characters long")
- elsif batch_name.to_s !~ BatchForm::NAME_FORMAT
+ elsif batch_name.to_s !~ BatchNameValidator::FORMAT
errors.add(batch_name.header, "must be only letters and numbers")
end
elsif offline_recording? || bulk?
diff --git a/app/models/onboarding.rb b/app/models/onboarding.rb
index a3c7d3b8af..d2f5b2bf53 100644
--- a/app/models/onboarding.rb
+++ b/app/models/onboarding.rb
@@ -17,6 +17,8 @@ class Onboarding
ORGANISATION_ATTRIBUTES = %i[ods_code].freeze
TEAM_ATTRIBUTES = %i[
+ careplus_staff_code
+ careplus_staff_type
careplus_venue_code
days_before_consent_reminders
days_before_consent_requests
@@ -223,6 +225,8 @@ def save!(include_previous_academic_year: false)
academic_years.each do |academic_year|
GenericClinicFactory.call(team:, academic_year:)
end
+
+ PatientTeamUpdater.call(team_scope: Team.where(id: team.id))
end
end
diff --git a/app/models/patient.rb b/app/models/patient.rb
index 1450141321..4985b36a2a 100644
--- a/app/models/patient.rb
+++ b/app/models/patient.rb
@@ -86,7 +86,6 @@ class Patient < ApplicationRecord
has_many :triage_statuses
has_many :triages
has_many :vaccination_records, -> { kept }
- has_many :vaccination_statuses
has_many :locations, through: :patient_locations
has_many :parents, through: :parent_relationships
@@ -155,12 +154,7 @@ class Patient < ApplicationRecord
scope :includes_statuses,
-> do
- includes(
- :consent_statuses,
- :programme_statuses,
- :triage_statuses,
- vaccination_statuses: :latest_location
- )
+ includes(:consent_statuses, :programme_statuses, :triage_statuses)
end
scope :has_vaccination_records_dont_notify_parents,
@@ -407,8 +401,8 @@ class Patient < ApplicationRecord
return self if location.generic_clinic? && programme.seasonal?
- vaccinated_statuses =
- Patient::VaccinationStatus
+ programme_statuses =
+ Patient::ProgrammeStatus
.select("1")
.where("patient_id = patients.id")
.for_programme(programme)
@@ -416,18 +410,18 @@ class Patient < ApplicationRecord
not_eligible_criteria =
if location.generic_clinic?
- vaccinated_statuses.where(academic_year: academic_year - 1)
+ programme_statuses.where(academic_year: academic_year - 1)
else
scope =
- vaccinated_statuses.where(academic_year:).where(
- "latest_location_id IS NULL OR latest_location_id != ?",
+ programme_statuses.where(academic_year:).where(
+ "location_id IS NULL OR location_id != ?",
location.id
)
unless programme.seasonal?
scope =
scope.or(
- vaccinated_statuses.where(academic_year: academic_year - 1)
+ programme_statuses.where(academic_year: academic_year - 1)
)
end
@@ -618,10 +612,6 @@ def triage_status(programme:, academic_year:)
patient_status(triage_statuses, programme:, academic_year:)
end
- def vaccination_status(programme:, academic_year:)
- patient_status(vaccination_statuses, programme:, academic_year:)
- end
-
def has_patient_specific_direction?(team:, **kwargs)
patient_specific_directions.not_invalidated.where(team:, **kwargs).exists?
end
@@ -819,7 +809,7 @@ def archive_due_to_deceased!
conflict_target: %i[team_id patient_id],
columns: %i[type]
}
- ).ids
+ )
PatientTeamUpdater.call(
patient_scope: Patient.where(id:),
diff --git a/app/models/patient/consent_status.rb b/app/models/patient/consent_status.rb
index a031699f7a..772a9f71e8 100644
--- a/app/models/patient/consent_status.rb
+++ b/app/models/patient/consent_status.rb
@@ -58,7 +58,7 @@ def vaccine_method_nasal? = vaccine_methods.include?("nasal")
def generator
@generator ||=
StatusGenerator::Consent.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
diff --git a/app/models/patient/programme_status.rb b/app/models/patient/programme_status.rb
index 7a684c9ec3..b838ede8b0 100644
--- a/app/models/patient/programme_status.rb
+++ b/app/models/patient/programme_status.rb
@@ -123,7 +123,7 @@ class Patient::ProgrammeStatus < ApplicationRecord
scope :cannot_vaccinate, -> { where(status: CANNOT_VACCINATE_STATUSES.keys) }
- scope :fully_vaccinated, -> { where(status: VACCINATED_STATUSES.keys) }
+ scope :vaccinated, -> { where(status: VACCINATED_STATUSES.keys) }
def needs_consent? = status.in?(NEEDS_CONSENT_STATUSES.keys)
@@ -152,7 +152,7 @@ def vaccine_criteria = VaccineCriteria.from_programme_status(self)
def generator
@generator ||=
StatusGenerator::Programme.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
patient_locations:,
diff --git a/app/models/patient/triage_status.rb b/app/models/patient/triage_status.rb
index 5df2db6736..564ff3e6a1 100644
--- a/app/models/patient/triage_status.rb
+++ b/app/models/patient/triage_status.rb
@@ -73,7 +73,7 @@ def assign_status
def generator
@generator ||=
StatusGenerator::Triage.new(
- programme:,
+ programme_type:,
academic_year:,
patient:,
consents:,
diff --git a/app/models/patient/vaccination_status.rb b/app/models/patient/vaccination_status.rb
deleted file mode 100644
index f74642958e..0000000000
--- a/app/models/patient/vaccination_status.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-# == Schema Information
-#
-# Table name: patient_vaccination_statuses
-#
-# id :bigint not null, primary key
-# academic_year :integer not null
-# dose_sequence :integer
-# latest_date :date
-# latest_session_status :integer
-# programme_type :enum not null
-# status :integer default("not_eligible"), not null
-# latest_location_id :bigint
-# patient_id :bigint not null
-#
-# Indexes
-#
-# idx_on_academic_year_patient_id_9c400fc863 (academic_year,patient_id)
-# idx_on_patient_id_programme_type_academic_year_962639d2ac (patient_id,programme_type,academic_year) UNIQUE
-# index_patient_vaccination_statuses_on_latest_location_id (latest_location_id)
-# index_patient_vaccination_statuses_on_status (status)
-#
-# Foreign Keys
-#
-# fk_rails_... (latest_location_id => locations.id)
-# fk_rails_... (patient_id => patients.id) ON DELETE => cascade
-#
-class Patient::VaccinationStatus < ApplicationRecord
- include BelongsToProgramme
-
- belongs_to :patient
-
- belongs_to :latest_location, class_name: "Location", optional: true
-
- has_many :patient_locations,
- -> { includes(location: :location_programme_year_groups) },
- through: :patient
-
- has_many :consents,
- -> { not_invalidated.response_provided.includes(:parent, :patient) },
- through: :patient
-
- has_many :triages,
- -> { not_invalidated.order(created_at: :desc) },
- through: :patient
-
- has_many :vaccination_records,
- -> { kept.order(performed_at: :desc) },
- through: :patient
-
- has_one :attendance_record,
- -> { today },
- through: :patient,
- source: :attendance_records
-
- enum :status,
- { not_eligible: 0, eligible: 1, due: 2, vaccinated: 3 },
- default: :not_eligible,
- validate: true
-
- enum :latest_session_status,
- { refused: 0, absent: 1, unwell: 2, contraindicated: 3, already_had: 4 },
- prefix: true,
- validate: {
- allow_nil: true
- }
-
- def assign_status
- self.status = generator.status
- self.dose_sequence = generator.dose_sequence
- self.latest_date = generator.latest_date
- self.latest_location_id = generator.latest_location_id
- self.latest_session_status = generator.latest_session_status
- end
-
- private
-
- def generator
- @generator ||=
- StatusGenerator::Vaccination.new(
- programme:,
- academic_year:,
- patient:,
- patient_locations:,
- consents:,
- triages:,
- attendance_record:,
- vaccination_records:
- )
- end
-end
diff --git a/app/models/patient_location.rb b/app/models/patient_location.rb
index 4ba19b8dbc..19dbecefc3 100644
--- a/app/models/patient_location.rb
+++ b/app/models/patient_location.rb
@@ -25,8 +25,6 @@
#
class PatientLocation < ApplicationRecord
- include UpdatesPatientTeam
-
audited associated_with: :patient
has_associated_audits
diff --git a/app/models/reporting_api/total.rb b/app/models/reporting_api/total.rb
index 73de303a90..623a023376 100644
--- a/app/models/reporting_api/total.rb
+++ b/app/models/reporting_api/total.rb
@@ -11,6 +11,8 @@
# patient_gender :text
# patient_local_authority_code :string
# patient_school_local_authority_code :string
+# patient_school_name :text
+# patient_school_urn :string
# patient_year_group :integer
# programme_type :enum
# status :integer
diff --git a/app/models/school_move.rb b/app/models/school_move.rb
index b961489a5d..5efc974eb4 100644
--- a/app/models/school_move.rb
+++ b/app/models/school_move.rb
@@ -30,7 +30,6 @@
class SchoolMove < ApplicationRecord
include Schoolable
include SchoolMovesHelper
- include UpdatesPatientTeam
audited associated_with: :patient
diff --git a/app/models/team.rb b/app/models/team.rb
index 835a73ac08..e6c2d4ece8 100644
--- a/app/models/team.rb
+++ b/app/models/team.rb
@@ -5,7 +5,9 @@
# Table name: teams
#
# id :bigint not null, primary key
-# careplus_venue_code :string not null
+# careplus_staff_code :string
+# careplus_staff_type :string
+# careplus_venue_code :string
# days_before_consent_reminders :integer default(7), not null
# days_before_consent_requests :integer default(21), not null
# days_before_invitations :integer default(21), not null
@@ -81,7 +83,6 @@ class Team < ApplicationRecord
prefix: "has",
suffix: "access"
- validates :careplus_venue_code, presence: true
validates :email, notify_safe_email: true
validates :name, presence: true, uniqueness: true
validates :phone, presence: true, phone: true
@@ -100,4 +101,8 @@ def year_groups(academic_year: nil)
.where(location_year_group: { academic_year: })
.pluck_year_groups
end
+
+ def careplus_enabled? =
+ careplus_staff_code.present? && careplus_staff_type.present? &&
+ careplus_venue_code.present?
end
diff --git a/app/models/team_location.rb b/app/models/team_location.rb
index d59ee0e47f..ad31a1aeb1 100644
--- a/app/models/team_location.rb
+++ b/app/models/team_location.rb
@@ -27,8 +27,6 @@
#
class TeamLocation < ApplicationRecord
- include UpdatesPatientTeam
-
audited associated_with: :team
has_associated_audits
diff --git a/app/models/vaccination_record.rb b/app/models/vaccination_record.rb
index cbd0073bc2..02b0498163 100644
--- a/app/models/vaccination_record.rb
+++ b/app/models/vaccination_record.rb
@@ -81,7 +81,6 @@ class VaccinationRecord < ApplicationRecord
include HasDoseVolume
include Notable
include PendingChangesConcern
- include UpdatesPatientTeam
include VaccinationRecordPerformedByConcern
include VaccinationRecordSyncToNHSImmunisationsAPIConcern
diff --git a/app/models/vaccination_report.rb b/app/models/vaccination_report.rb
index 76d2880d43..172618f205 100644
--- a/app/models/vaccination_report.rb
+++ b/app/models/vaccination_report.rb
@@ -4,8 +4,6 @@ class VaccinationReport
include RequestSessionPersistable
include WizardStepConcern
- FILE_FORMATS = %w[mavis careplus systm_one].freeze
-
attribute :date_from, :date
attribute :date_to, :date
attribute :file_format, :string
@@ -22,7 +20,7 @@ def wizard_steps
end
on_wizard_step :file_format, exact: true do
- validates :file_format, inclusion: FILE_FORMATS
+ validates :file_format, inclusion: { in: :file_formats }
end
validates :programme_type,
@@ -30,7 +28,7 @@ def wizard_steps
:file_format,
presence: true,
on: :single_page
- validates :file_format, inclusion: { in: FILE_FORMATS }, on: :single_page
+ validates :file_format, inclusion: { in: :file_formats }, on: :single_page
def programme
Programme.find(programme_type) if programme_type
@@ -59,6 +57,15 @@ def csv_filename
"#{programme.name} - #{file_format} - #{from_str} - #{to_str}.csv"
end
+ def file_formats
+ common_file_formats = %w[mavis systm_one]
+ if @current_user.selected_team.careplus_enabled?
+ common_file_formats + ["careplus"]
+ else
+ common_file_formats
+ end
+ end
+
private
def exporter_class
diff --git a/app/policies/attendance_record_policy.rb b/app/policies/attendance_record_policy.rb
index 5392211d4c..8e0c05c2e6 100644
--- a/app/policies/attendance_record_policy.rb
+++ b/app/policies/attendance_record_policy.rb
@@ -23,7 +23,7 @@ def already_vaccinated?
session
.programmes_for(patient:)
.all? do |programme|
- patient.vaccination_status(programme:, academic_year:).vaccinated?
+ patient.programme_status(programme, academic_year:).vaccinated?
end
end
diff --git a/app/policies/import/issue_policy.rb b/app/policies/import/issue_policy.rb
index 29a9323cb8..433f4a3ed4 100644
--- a/app/policies/import/issue_policy.rb
+++ b/app/policies/import/issue_policy.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
class Import::IssuePolicy < ApplicationPolicy
- def index? = true
+ def index? = !team.has_upload_only_access?
- def create? = true
+ def create? = !team.has_upload_only_access?
- def show? = true
+ def show? = !team.has_upload_only_access?
- def update? = true
+ def update? = !team.has_upload_only_access?
end
diff --git a/app/policies/team_policy.rb b/app/policies/team_policy.rb
index 95c4c8b5de..8b7aaf6c8c 100644
--- a/app/policies/team_policy.rb
+++ b/app/policies/team_policy.rb
@@ -2,14 +2,16 @@
class TeamPolicy < ApplicationPolicy
def index? = false
-
def create? = false
+ def update? = false
+ def destroy? = false
def show? = team.has_poc_only_access? && record == team
- def update? = false
-
- def destroy? = false
+ alias_method :contact_details?, :show?
+ alias_method :schools?, :show?
+ alias_method :clinics?, :show?
+ alias_method :sessions?, :show?
class Scope < ApplicationPolicy::Scope
def resolve = scope.where(id: team.id)
diff --git a/app/policies/vaccination_record_policy.rb b/app/policies/vaccination_record_policy.rb
index 605f917c71..a4f59db86e 100644
--- a/app/policies/vaccination_record_policy.rb
+++ b/app/policies/vaccination_record_policy.rb
@@ -18,12 +18,8 @@ def create?
def show? = true
def record_already_vaccinated?
- return unless user.is_nurse? || user.is_prescriber?
- return if session.today?
-
- vaccination_status = patient.vaccination_status(programme:, academic_year:)
-
- vaccination_status.not_eligible? || vaccination_status.eligible?
+ (user.is_nurse? || user.is_prescriber?) && !session.today? &&
+ !patient.programme_status(programme, academic_year:).vaccinated?
end
def update?
diff --git a/app/validators/batch_name_validator.rb b/app/validators/batch_name_validator.rb
new file mode 100644
index 0000000000..4d911b4953
--- /dev/null
+++ b/app/validators/batch_name_validator.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class BatchNameValidator < ActiveModel::EachValidator
+ FORMAT = /\A[A-Za-z0-9]+\z/
+ MIN_LENGTH = 2
+ MAX_LENGTH = 100
+
+ def validate_each(record, attribute, value)
+ if value.blank?
+ record.errors.add(attribute, :blank)
+ elsif value.length < MIN_LENGTH
+ record.errors.add(attribute, :too_short, count: MIN_LENGTH)
+ elsif value.length > MAX_LENGTH
+ record.errors.add(attribute, :too_long, count: MAX_LENGTH)
+ elsif value !~ FORMAT
+ record.errors.add(attribute, :invalid)
+ end
+ end
+end
diff --git a/app/views/draft_vaccination_records/batch.html.erb b/app/views/draft_vaccination_records/batch.html.erb
index 0e5eb7ea3e..3bb7a1d8e1 100644
--- a/app/views/draft_vaccination_records/batch.html.erb
+++ b/app/views/draft_vaccination_records/batch.html.erb
@@ -7,6 +7,25 @@
<%= form_with model: @draft_vaccination_record, url: wizard_path, method: :put do |f| %>
<%= f.govuk_error_summary %>
+<% if @draft_vaccination_record.bulk_upload_user_and_record? %>
+
+ <%= @patient.full_name %>
+ <%= h1 "Which vaccine and batch did you use?" %>
+
+ <%= f.govuk_radio_buttons_fieldset :vaccine_id,
+ legend: { text: "Vaccine", size: "m" } do %>
+ <% @vaccines.each do |vaccine| %>
+ <%= f.govuk_radio_button :vaccine_id, vaccine.id, label: { text: vaccine.nivs_name } %>
+ <% end %>
+ <% end %>
+
+ <%= f.govuk_text_field :batch_name,
+ label: { text: "Batch number", size: "m" }, width: 10, class: "nhsuk-input--code" %>
+
+ <%= f.govuk_date_field :batch_expiry,
+ legend: { text: "Batch expiry date", size: "m" },
+ hint: { text: "For example, 27 10 2025" } %>
+<% else %>
<%= f.govuk_radio_buttons_fieldset :batch_id,
caption: { text: @patient.full_name, size: "l" },
legend: { size: "l", tag: "h1", text: "Which batch did you use?" } do %>
@@ -31,6 +50,7 @@
<% end %>
<% end %>
<% end %>
+<% end %>
<%= f.govuk_submit "Continue" %>
<% end %>
diff --git a/app/views/imports/_header.html.erb b/app/views/imports/_header.html.erb
index 61deda8769..7934513d10 100644
--- a/app/views/imports/_header.html.erb
+++ b/app/views/imports/_header.html.erb
@@ -1,18 +1,19 @@
<%= h1 t("imports.index.title"), size: "xl" %>
-<% if current_team.has_upload_only_access? %>
-
- Use this page to upload and import vaccination records.
-
-<% else %>
-
+
+ <% if policy(ClassImport).new? && policy(CohortImport).new? %>
Use this page to upload and import child, class list and vaccination records.
-
-<% end %>
+ <% else %>
+ Use this page to upload and import vaccination records.
+ <% end %>
+
After import, files move to the Completed imports tab.
- Any close matches to resolve will appear in the Issues tab.
+
+ <% if policy(%i[import issue]).index? %>
+ Any close matches to resolve will appear in the Issues tab.
+ <% end %>
diff --git a/app/views/parent_relationships/_fields.html.erb b/app/views/parent_relationships/_fields.html.erb
new file mode 100644
index 0000000000..d2df7a5a4d
--- /dev/null
+++ b/app/views/parent_relationships/_fields.html.erb
@@ -0,0 +1,38 @@
+<%= f.fields_for :parent do |parent_f| %>
+ <%= parent_f.govuk_text_field :full_name, label: { text: "Name" } %>
+<% end %>
+
+<%= f.govuk_radio_buttons_fieldset :type, legend: { text: "Relationship to child", size: "s" } do %>
+ <%= f.govuk_radio_button :type, :mother, label: { text: "Mum" }, link_errors: true %>
+ <%= f.govuk_radio_button :type, :father, label: { text: "Dad" } %>
+ <%= f.govuk_radio_button :type, :guardian, label: { text: "Guardian" } %>
+ <%= f.govuk_radio_button :type, :other, label: { text: "Other" } do %>
+ <%= f.govuk_text_field :other_name, label: { text: "Relationship to the child" }, hint: { text: "For example, carer" } %>
+ <% end %>
+<% end %>
+
+<%= f.fields_for :parent do |parent_f| %>
+ <%= parent_f.govuk_text_field :email, label: { text: "Email address" } %>
+ <%= parent_f.govuk_text_field :phone, label: { text: "Phone number" } %>
+
+ <%= parent_f.govuk_check_boxes_fieldset :phone_receive_updates, multiple: false, legend: nil do %>
+ <%= parent_f.govuk_check_box :phone_receive_updates, 1, 0, multiple: false, link_errors: true, label: { text: "Get updates by text message" } %>
+ <% end %>
+
+ <%= parent_f.govuk_radio_buttons_fieldset :contact_method_type,
+ legend: { text: "Does the parent have any specific needs?", size: "s" } do %>
+ <%= parent_f.govuk_radio_button :contact_method_type, "text",
+ label: { text: "They can only receive text messages" },
+ link_errors: true %>
+ <%= parent_f.govuk_radio_button :contact_method_type, "voice",
+ label: { text: "They can only receive voice calls" } %>
+ <%= parent_f.govuk_radio_button :contact_method_type, "other",
+ label: { text: "Other" } do %>
+ <%= parent_f.govuk_text_area :contact_method_other_details,
+ label: { text: "Give details" } %>
+ <% end %>
+ <%= parent_f.govuk_radio_divider %>
+ <%= parent_f.govuk_radio_button :contact_method_type, "any",
+ label: { text: "They do not have specific needs" } %>
+ <% end %>
+<% end %>
diff --git a/app/views/parent_relationships/edit.html.erb b/app/views/parent_relationships/edit.html.erb
index bfe3fb8d92..195e86fbf3 100644
--- a/app/views/parent_relationships/edit.html.erb
+++ b/app/views/parent_relationships/edit.html.erb
@@ -11,44 +11,7 @@
<%= page_title %>
<% end %>
- <%= f.fields_for :parent do |parent_f| %>
- <%= parent_f.govuk_text_field :full_name, label: { text: "Name" } %>
- <% end %>
-
- <%= f.govuk_radio_buttons_fieldset :type, legend: { text: "Relationship to child", size: "s" } do %>
- <%= f.govuk_radio_button :type, :mother, label: { text: "Mum" }, link_errors: true %>
- <%= f.govuk_radio_button :type, :father, label: { text: "Dad" } %>
- <%= f.govuk_radio_button :type, :guardian, label: { text: "Guardian" } %>
- <%= f.govuk_radio_button :type, :other, label: { text: "Other" } do %>
- <%= f.govuk_text_field :other_name, label: { text: "Relationship to the child" }, hint: { text: "For example, carer" } %>
- <% end %>
- <% end %>
-
- <%= f.fields_for :parent do |parent_f| %>
- <%= parent_f.govuk_text_field :email, label: { text: "Email address" } %>
- <%= parent_f.govuk_text_field :phone, label: { text: "Phone number" } %>
-
- <%= parent_f.govuk_check_boxes_fieldset :phone_receive_updates, multiple: false, legend: nil do %>
- <%= parent_f.govuk_check_box :phone_receive_updates, 1, 0, multiple: false, link_errors: true, label: { text: "Get updates by text message" } %>
- <% end %>
-
- <%= parent_f.govuk_radio_buttons_fieldset :contact_method_type,
- legend: { text: "Does the parent have any specific needs?", size: "s" } do %>
- <%= parent_f.govuk_radio_button :contact_method_type, "text",
- label: { text: "They can only receive text messages" },
- link_errors: true %>
- <%= parent_f.govuk_radio_button :contact_method_type, "voice",
- label: { text: "They can only receive voice calls" } %>
- <%= parent_f.govuk_radio_button :contact_method_type, "other",
- label: { text: "Other" } do %>
- <%= parent_f.govuk_text_area :contact_method_other_details,
- label: { text: "Give details" } %>
- <% end %>
- <%= parent_f.govuk_radio_divider %>
- <%= parent_f.govuk_radio_button :contact_method_type, "any",
- label: { text: "They do not have specific needs" } %>
- <% end %>
- <% end %>
+ <%= render "fields", f: %>
<%= f.govuk_submit "Continue" %>
<% end %>
diff --git a/app/views/parent_relationships/new.html.erb b/app/views/parent_relationships/new.html.erb
index ae78909638..67b742e01e 100644
--- a/app/views/parent_relationships/new.html.erb
+++ b/app/views/parent_relationships/new.html.erb
@@ -14,44 +14,7 @@
<%= page_title %>
<% end %>
- <%= f.fields_for :parent do |parent_f| %>
- <%= parent_f.govuk_text_field :full_name, label: { text: "Name" } %>
- <% end %>
-
- <%= f.govuk_radio_buttons_fieldset :type, legend: { text: "Relationship to child", size: "s" } do %>
- <%= f.govuk_radio_button :type, :mother, label: { text: "Mum" }, link_errors: true %>
- <%= f.govuk_radio_button :type, :father, label: { text: "Dad" } %>
- <%= f.govuk_radio_button :type, :guardian, label: { text: "Guardian" } %>
- <%= f.govuk_radio_button :type, :other, label: { text: "Other" } do %>
- <%= f.govuk_text_field :other_name, label: { text: "Relationship to the child" }, hint: { text: "For example, carer" } %>
- <% end %>
- <% end %>
-
- <%= f.fields_for :parent do |parent_f| %>
- <%= parent_f.govuk_text_field :email, label: { text: "Email address" } %>
- <%= parent_f.govuk_text_field :phone, label: { text: "Phone number" } %>
-
- <%= parent_f.govuk_check_boxes_fieldset :phone_receive_updates, multiple: false, legend: nil do %>
- <%= parent_f.govuk_check_box :phone_receive_updates, 1, 0, multiple: false, link_errors: true, label: { text: "Get updates by text message" } %>
- <% end %>
-
- <%= parent_f.govuk_radio_buttons_fieldset :contact_method_type,
- legend: { text: "Does the parent have any specific needs?", size: "s" } do %>
- <%= parent_f.govuk_radio_button :contact_method_type, "text",
- label: { text: "They can only receive text messages" },
- link_errors: true %>
- <%= parent_f.govuk_radio_button :contact_method_type, "voice",
- label: { text: "They can only receive voice calls" } %>
- <%= parent_f.govuk_radio_button :contact_method_type, "other",
- label: { text: "Other" } do %>
- <%= parent_f.govuk_text_area :contact_method_other_details,
- label: { text: "Give details" } %>
- <% end %>
- <%= parent_f.govuk_radio_divider %>
- <%= parent_f.govuk_radio_button :contact_method_type, "any",
- label: { text: "They do not have specific needs" } %>
- <% end %>
- <% end %>
+ <%= render "fields", f: %>
<%= f.govuk_submit "Save" %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/patient_sessions/_header.html.erb b/app/views/patient_sessions/_header.html.erb
index fd3767522f..a5eba5732e 100644
--- a/app/views/patient_sessions/_header.html.erb
+++ b/app/views/patient_sessions/_header.html.erb
@@ -69,7 +69,7 @@
href: session_patient_programme_path(@session, @patient, programme, return_to: params[:return_to]),
text: programme.name,
selected: @programme == programme,
- ticked: @patient.vaccination_status(programme:, academic_year: @academic_year).vaccinated?,
+ ticked: @patient.programme_status(programme, academic_year: @academic_year).vaccinated?,
)
end
diff --git a/app/views/teams/clinics.html.erb b/app/views/teams/clinics.html.erb
new file mode 100644
index 0000000000..f5ca50c28f
--- /dev/null
+++ b/app/views/teams/clinics.html.erb
@@ -0,0 +1,38 @@
+<%= h1 t("teams.show.title") do %>
+ <%= @team.name %>
+ <%= t("teams.show.title") %>
+<% end %>
+
+