From 15b7242d54a62cefbbeb0a825d1b20d09b48f691 Mon Sep 17 00:00:00 2001 From: Ryan Kendall Date: Tue, 12 Sep 2023 14:08:44 +0100 Subject: [PATCH 1/2] feat(fworks): simple register import and audit --- .rubocop_todo.yml | 3 + Gemfile | 2 + Gemfile.lock | 10 + app/assets/stylesheets/application.sass.scss | 4 +- app/assets/stylesheets/base/_misc.scss | 4 + .../stylesheets/components/_activity-log.scss | 20 ++ app/controllers/application_controller.rb | 15 +- .../engagement/application_controller.rb | 14 - .../frameworks/application_controller.rb | 1 + .../frameworks/frameworks_controller.rb | 35 +++ .../management/activity_logs_controller.rb | 11 + .../management/application_controller.rb | 5 + .../management/management_controller.rb | 3 + .../management/register_uploads_controller.rb | 19 ++ .../provider_contacts_controller.rb | 45 ++++ app/models/current.rb | 2 +- app/models/frameworks/activity.rb | 7 + app/models/frameworks/activity_log_item.rb | 12 + .../activity_log_item/presentable.rb | 11 + app/models/frameworks/activity_loggable.rb | 19 ++ .../activity_log_presentable.rb | 15 ++ .../frameworks/activity_loggable_version.rb | 8 + .../activity_loggable_version/presentable.rb | 23 ++ .../presentable_changes.rb | 20 ++ .../presentable_changes/field_change.rb | 33 +++ app/models/frameworks/framework.rb | 12 +- .../framework/activity_log_presentable.rb | 11 + .../frameworks/framework/faf_importable.rb | 21 -- app/models/frameworks/framework/filterable.rb | 18 ++ app/models/frameworks/framework/filtering.rb | 71 +++++ .../frameworks/framework/presentable.rb | 47 ++++ app/models/frameworks/framework/sortable.rb | 35 +++ app/models/frameworks/framework/sourceable.rb | 9 + .../framework/spreadsheet_importable.rb | 25 ++ .../spreadsheet_importable/row_mapping.rb | 68 +++++ .../frameworks/framework/status_changeable.rb | 7 +- app/models/frameworks/provider.rb | 1 + app/models/frameworks/provider_contact.rb | 13 + .../activity_log_presentable.rb | 7 + .../frameworks/provider_contact/filterable.rb | 13 + .../frameworks/provider_contact/filtering.rb | 38 +++ .../provider_contact/presentable.rb | 7 + .../frameworks/provider_contact/sortable.rb | 27 ++ app/models/support/agent.rb | 9 + app/models/support/concerns/scope_filter.rb | 8 +- app/policies/cms_portal_policy.rb | 12 +- app/services/support/sync_frameworks.rb | 20 +- .../_activity_log_item.html.erb | 4 + .../activity_log_items/_list.html.erb | 18 ++ .../activity/_activity_loggable_version.erb | 20 ++ .../frameworks/dashboards/index.html.erb | 26 +- .../frameworks/frameworks/_framework.html.erb | 54 ++++ .../frameworks/_framework_status.html.erb | 10 + .../frameworks/_index_filters.html.erb | 66 +++++ .../frameworks/frameworks/index.html.erb | 20 ++ app/views/frameworks/frameworks/new.html.erb | 0 app/views/frameworks/frameworks/show.html.erb | 38 +++ .../show/_framework_activity.html.erb | 2 + .../show/_framework_details.html.erb | 47 ++++ .../show/_framework_provider.html.erb | 26 ++ .../management/activity_logs/show.html.erb | 4 + .../management/management/index.html.erb | 11 + .../management/register_uploads/new.html.erb | 8 + .../provider_contacts/_index_filters.html.erb | 26 ++ .../_provider_contact.html.erb | 25 ++ .../provider_contacts/edit.html.erb | 21 ++ .../provider_contacts/index.html.erb | 20 ++ .../provider_contacts/show.html.erb | 40 +++ app/views/layouts/_navigation_links.html.erb | 10 + config/brakeman.ignore | 118 +++++++- config/initializers/simple_xlsx_reader.rb | 1 + config/routes.rb | 8 + ...0823132039_create_frameworks_frameworks.rb | 26 +- ...30823132322_create_frameworks_providers.rb | 1 + ...323_create_frameworks_provider_contacts.rb | 12 + ...41_create_frameworks_activity_log_items.rb | 15 ++ db/migrate/20230906100110_create_versions.rb | 38 +++ db/schema.rb | 253 ++++++++++-------- .../frameworks/activity_log_items.rb | 10 + .../factories/frameworks/provider_contacts.rb | 8 + ...ent_can_browse_frameworks_register_spec.rb | 45 ++++ ...agent_can_browse_provider_contacts_spec.rb | 52 ++++ .../frameworks/activity_loggable_spec.rb | 29 ++ .../presentable_changes_spec.rb | 68 +++++ .../framework/faf_importable_spec.rb | 73 ----- .../frameworks/framework/filterable_spec.rb | 64 +++++ .../frameworks/framework/sortable_spec.rb | 78 ++++++ spec/models/support/case_spec.rb | 2 +- spec/services/support/sync_frameworks_spec.rb | 4 - spec/support/shared/govuk_design_matchers.rb | 39 +++ spec/support/shared/support_agent.rb | 4 + 91 files changed, 2000 insertions(+), 264 deletions(-) create mode 100644 app/assets/stylesheets/components/_activity-log.scss create mode 100644 app/controllers/frameworks/frameworks_controller.rb create mode 100644 app/controllers/frameworks/management/activity_logs_controller.rb create mode 100644 app/controllers/frameworks/management/application_controller.rb create mode 100644 app/controllers/frameworks/management/management_controller.rb create mode 100644 app/controllers/frameworks/management/register_uploads_controller.rb create mode 100644 app/controllers/frameworks/provider_contacts_controller.rb create mode 100644 app/models/frameworks/activity.rb create mode 100644 app/models/frameworks/activity_log_item.rb create mode 100644 app/models/frameworks/activity_log_item/presentable.rb create mode 100644 app/models/frameworks/activity_loggable.rb create mode 100644 app/models/frameworks/activity_loggable/activity_log_presentable.rb create mode 100644 app/models/frameworks/activity_loggable_version.rb create mode 100644 app/models/frameworks/activity_loggable_version/presentable.rb create mode 100644 app/models/frameworks/activity_loggable_version/presentable_changes.rb create mode 100644 app/models/frameworks/activity_loggable_version/presentable_changes/field_change.rb create mode 100644 app/models/frameworks/framework/activity_log_presentable.rb delete mode 100644 app/models/frameworks/framework/faf_importable.rb create mode 100644 app/models/frameworks/framework/filterable.rb create mode 100644 app/models/frameworks/framework/filtering.rb create mode 100644 app/models/frameworks/framework/presentable.rb create mode 100644 app/models/frameworks/framework/sortable.rb create mode 100644 app/models/frameworks/framework/sourceable.rb create mode 100644 app/models/frameworks/framework/spreadsheet_importable.rb create mode 100644 app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb create mode 100644 app/models/frameworks/provider_contact.rb create mode 100644 app/models/frameworks/provider_contact/activity_log_presentable.rb create mode 100644 app/models/frameworks/provider_contact/filterable.rb create mode 100644 app/models/frameworks/provider_contact/filtering.rb create mode 100644 app/models/frameworks/provider_contact/presentable.rb create mode 100644 app/models/frameworks/provider_contact/sortable.rb create mode 100644 app/views/frameworks/activity_log_items/_activity_log_item.html.erb create mode 100644 app/views/frameworks/activity_log_items/_list.html.erb create mode 100644 app/views/frameworks/activity_log_items/activity/_activity_loggable_version.erb create mode 100644 app/views/frameworks/frameworks/_framework.html.erb create mode 100644 app/views/frameworks/frameworks/_framework_status.html.erb create mode 100644 app/views/frameworks/frameworks/_index_filters.html.erb create mode 100644 app/views/frameworks/frameworks/index.html.erb create mode 100644 app/views/frameworks/frameworks/new.html.erb create mode 100644 app/views/frameworks/frameworks/show.html.erb create mode 100644 app/views/frameworks/frameworks/show/_framework_activity.html.erb create mode 100644 app/views/frameworks/frameworks/show/_framework_details.html.erb create mode 100644 app/views/frameworks/frameworks/show/_framework_provider.html.erb create mode 100644 app/views/frameworks/management/activity_logs/show.html.erb create mode 100644 app/views/frameworks/management/management/index.html.erb create mode 100644 app/views/frameworks/management/register_uploads/new.html.erb create mode 100644 app/views/frameworks/provider_contacts/_index_filters.html.erb create mode 100644 app/views/frameworks/provider_contacts/_provider_contact.html.erb create mode 100644 app/views/frameworks/provider_contacts/edit.html.erb create mode 100644 app/views/frameworks/provider_contacts/index.html.erb create mode 100644 app/views/frameworks/provider_contacts/show.html.erb create mode 100644 config/initializers/simple_xlsx_reader.rb create mode 100644 db/migrate/20230823132323_create_frameworks_provider_contacts.rb create mode 100644 db/migrate/20230901142741_create_frameworks_activity_log_items.rb create mode 100644 db/migrate/20230906100110_create_versions.rb create mode 100644 spec/factories/frameworks/activity_log_items.rb create mode 100644 spec/factories/frameworks/provider_contacts.rb create mode 100644 spec/features/frameworks/register/agent_can_browse_frameworks_register_spec.rb create mode 100644 spec/features/frameworks/register/agent_can_browse_provider_contacts_spec.rb create mode 100644 spec/models/frameworks/activity_loggable_spec.rb create mode 100644 spec/models/frameworks/activity_loggable_version/presentable_changes_spec.rb delete mode 100644 spec/models/frameworks/framework/faf_importable_spec.rb create mode 100644 spec/models/frameworks/framework/filterable_spec.rb create mode 100644 spec/models/frameworks/framework/sortable_spec.rb create mode 100644 spec/support/shared/govuk_design_matchers.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 465769d12..6a75bbb2a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -119,3 +119,6 @@ Style/DateTime: - 'lib/microsoft_graph/transformer/attachment.rb' - 'lib/microsoft_graph/transformer/message.rb' - 'lib/microsoft_graph/transformer/update_message.rb' + +Rails/UniqueValidationWithoutIndex: + Enabled: false diff --git a/Gemfile b/Gemfile index 0bc6fa410..43ecaddc0 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,7 @@ gem "omniauth" gem "omniauth_openid_connect" gem "omniauth-rails_csrf_protection" gem "pandoc-ruby" +gem "paper_trail" gem "pg" gem "pg_search" gem "puma", "~> 6" @@ -51,6 +52,7 @@ gem "rollbar" gem "scenic" gem "sidekiq", "~> 6.4" gem "sidekiq-cron", "~> 1.10" +gem "simple_xlsx_reader" gem "slack-notifier" gem "sprockets-rails" gem "stimulus-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 1370b13a1..efc2a8983 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -390,6 +390,9 @@ GEM validate_url webfinger (~> 2.0) pandoc-ruby (2.1.7) + paper_trail (15.0.0) + activerecord (>= 6.1) + request_store (~> 1.4) parallel (1.23.0) parser (3.2.2.3) ast (~> 2.4.1) @@ -491,6 +494,8 @@ GEM redis-store (1.9.2) redis (>= 4, < 6) regexp_parser (2.8.1) + request_store (1.5.1) + rack (>= 1.4) rexml (3.2.6) rollbar (3.4.0) rouge (4.1.3) @@ -572,6 +577,9 @@ GEM fugit (~> 1.8) globalid (>= 1.0.1) sidekiq (>= 6) + simple_xlsx_reader (5.0.0) + nokogiri + rubyzip simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -705,6 +713,7 @@ DEPENDENCIES omniauth-rails_csrf_protection omniauth_openid_connect pandoc-ruby + paper_trail pg pg_search pry-byebug @@ -727,6 +736,7 @@ DEPENDENCIES shoulda-matchers sidekiq (~> 6.4) sidekiq-cron (~> 1.10) + simple_xlsx_reader simplecov slack-notifier spring diff --git a/app/assets/stylesheets/application.sass.scss b/app/assets/stylesheets/application.sass.scss index c51f295dc..2ddef00ed 100644 --- a/app/assets/stylesheets/application.sass.scss +++ b/app/assets/stylesheets/application.sass.scss @@ -2,6 +2,7 @@ @import "./base/frontend"; @import "./base/misc"; +@import "./components/activity-log"; @import "./components/supported/procurement-stage-colours"; @import "./components/govuk-radios-small"; @import "./components/autocomplete"; @@ -47,7 +48,7 @@ &.wide-container { max-width: 2000px; } - + @media (min-width: 1020px) { &.wide-container { margin-left: 30px; @@ -55,4 +56,3 @@ } } } - \ No newline at end of file diff --git a/app/assets/stylesheets/base/_misc.scss b/app/assets/stylesheets/base/_misc.scss index d4c34186e..709dd8c77 100644 --- a/app/assets/stylesheets/base/_misc.scss +++ b/app/assets/stylesheets/base/_misc.scss @@ -3,6 +3,10 @@ align-items: center; } +.pull-up-10 { + margin-top: -10px; +} + .spread-out { display: flex; align-items: center; diff --git a/app/assets/stylesheets/components/_activity-log.scss b/app/assets/stylesheets/components/_activity-log.scss new file mode 100644 index 000000000..c1b022a8b --- /dev/null +++ b/app/assets/stylesheets/components/_activity-log.scss @@ -0,0 +1,20 @@ +.activity-log-table { + @extend .govuk-\!-font-size-16; + + table { + @extend .govuk-\!-font-size-16; + } + + .activity-log-date { + width: 180px; + } + .activity-log-person { + width: 180px; + } + + .activity-log-change-arrow { + color: #a2a2a2; + margin-left: 5px; + margin-right: 5px; + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9dc8a6e83..1a1daa97d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base default_form_builder GOVUKDesignSystemFormBuilder::FormBuilder before_action :authenticate_user!, except: %i[health_check maintenance] + before_action :set_current_request_id protect_from_forgery @@ -24,7 +25,7 @@ def maintenance protected - helper_method :current_user, :cookie_policy, :record_ga?, :engagement_portal?, :support_portal?, :frameworks_portal? + helper_method :current_user, :cookie_policy, :record_ga?, :engagement_portal?, :support_portal?, :frameworks_portal?, :current_url_b64 # @return [User, Guest] # @@ -86,9 +87,17 @@ def cookie_policy CookiePolicy.new(cookies) end - def current_url_b64 - Base64.encode64(request.fullpath) + def current_url_b64(tab = nil) + Base64.encode64("#{request.fullpath}#{"##{tab.to_s.dasherize}" if tab.present?}") + end + + def back_link_param(back_to = params[:back_to]) + return if back_to.blank? + + Base64.decode64(back_to) end def tracking_base_properties = { user_id: current_user.id } + + def set_current_request_id = Current.request_id = request.request_id end diff --git a/app/controllers/engagement/application_controller.rb b/app/controllers/engagement/application_controller.rb index 9dbd2d1e3..73bd0ca26 100644 --- a/app/controllers/engagement/application_controller.rb +++ b/app/controllers/engagement/application_controller.rb @@ -2,20 +2,6 @@ module Engagement class ApplicationController < ::ApplicationController include SupportAgents - helper_method :current_url_b64 - - protected - - def back_link_param(back_to = params[:back_to]) - return if back_to.blank? - - Base64.decode64(back_to) - end - - def current_url_b64(tab = "") - Base64.encode64("#{request.fullpath}##{tab.to_s.dasherize}") - end - private def portal_namespace = :engagement diff --git a/app/controllers/frameworks/application_controller.rb b/app/controllers/frameworks/application_controller.rb index 160a39224..78cc45997 100644 --- a/app/controllers/frameworks/application_controller.rb +++ b/app/controllers/frameworks/application_controller.rb @@ -1,5 +1,6 @@ class Frameworks::ApplicationController < ApplicationController include SupportAgents + before_action { Current.actor = current_agent } private diff --git a/app/controllers/frameworks/frameworks_controller.rb b/app/controllers/frameworks/frameworks_controller.rb new file mode 100644 index 000000000..f9e8dca67 --- /dev/null +++ b/app/controllers/frameworks/frameworks_controller.rb @@ -0,0 +1,35 @@ +class Frameworks::FrameworksController < Frameworks::ApplicationController + before_action :redirect_to_register_tab, unless: :turbo_frame_request?, only: :index + before_action :set_back_url, only: %i[new show] + + def new; end + + def index + @filtering = Frameworks::Framework.filtering(filter_form_params) + @frameworks = @filtering.results.paginate(page: params[:frameworks_page]) + end + + def show + @framework = Frameworks::Framework.find(params[:id]) + @activity_log_items = @framework.activity_log_items.paginate(page: params[:activities_page]) + end + +private + + def filter_form_params + params.fetch(:frameworks_filter, {}).permit( + :sort_by, :sort_order, + status: [], provider: [], + e_and_o_lead: [], proc_ops_lead: [], + category: [], provider_contact: [] + ) + end + + def set_back_url + @back_url = back_link_param + end + + def redirect_to_register_tab + redirect_to frameworks_root_path(anchor: "frameworks-register", **request.params.except(:controller, :action)) + end +end diff --git a/app/controllers/frameworks/management/activity_logs_controller.rb b/app/controllers/frameworks/management/activity_logs_controller.rb new file mode 100644 index 000000000..ab57b44be --- /dev/null +++ b/app/controllers/frameworks/management/activity_logs_controller.rb @@ -0,0 +1,11 @@ +class Frameworks::Management::ActivityLogsController < Frameworks::Management::ApplicationController + before_action { @back_url = frameworks_management_path } + + def show + @activity_log_items = Frameworks::ActivityLogItem.paginate(page: params[:page], per_page: 50) + end + +private + + def authorize_agent_scope = [super, :manage_frameworks_activity_log?] +end diff --git a/app/controllers/frameworks/management/application_controller.rb b/app/controllers/frameworks/management/application_controller.rb new file mode 100644 index 000000000..bbfd07560 --- /dev/null +++ b/app/controllers/frameworks/management/application_controller.rb @@ -0,0 +1,5 @@ +class Frameworks::Management::ApplicationController < Frameworks::ApplicationController +private + + def authorize_agent_scope = [super, :access_admin_settings?] +end diff --git a/app/controllers/frameworks/management/management_controller.rb b/app/controllers/frameworks/management/management_controller.rb new file mode 100644 index 000000000..d841436f4 --- /dev/null +++ b/app/controllers/frameworks/management/management_controller.rb @@ -0,0 +1,3 @@ +class Frameworks::Management::ManagementController < Frameworks::Management::ApplicationController + def index; end +end diff --git a/app/controllers/frameworks/management/register_uploads_controller.rb b/app/controllers/frameworks/management/register_uploads_controller.rb new file mode 100644 index 000000000..4a91d5851 --- /dev/null +++ b/app/controllers/frameworks/management/register_uploads_controller.rb @@ -0,0 +1,19 @@ +class Frameworks::Management::RegisterUploadsController < Frameworks::Management::ApplicationController + before_action { @back_url = frameworks_management_path } + + def new; end + + def create + Frameworks::Framework.import_from_spreadsheet(register_upload_params[:spreadsheet]) + + redirect_to frameworks_management_path + end + +private + + def register_upload_params + params.require(:register_upload).permit(:spreadsheet) + end + + def authorize_agent_scope = [super, :manage_frameworks_register_upload?] +end diff --git a/app/controllers/frameworks/provider_contacts_controller.rb b/app/controllers/frameworks/provider_contacts_controller.rb new file mode 100644 index 000000000..fde1b487e --- /dev/null +++ b/app/controllers/frameworks/provider_contacts_controller.rb @@ -0,0 +1,45 @@ +class Frameworks::ProviderContactsController < Frameworks::ApplicationController + before_action :redirect_to_register_tab, unless: :turbo_frame_request?, only: :index + before_action :set_back_url, only: %i[show edit] + + def index + @filtering = Frameworks::ProviderContact.filtering(filter_form_params) + @provider_contacts = @filtering.results.paginate(page: params[:provider_contacts_page]) + end + + def show + @provider_contact = Frameworks::ProviderContact.find(params[:id]) + end + + def edit + @provider_contact = Frameworks::ProviderContact.find(params[:id]) + end + + def update + @provider_contact = Frameworks::ProviderContact.find(params[:id]) + + if @provider_contact.update(provider_contact_params) + redirect_to frameworks_provider_contact_path(@provider_contact) + else + render :edit + end + end + +private + + def set_back_url + @back_url = back_link_param + end + + def filter_form_params + params.fetch(:provider_contacts_filter, {}).permit(:sort_by, :sort_order, provider: []) + end + + def provider_contact_params + params.require(:frameworks_provider_contact).permit(:name, :email, :phone) + end + + def redirect_to_register_tab + redirect_to frameworks_root_path(anchor: "provider-contacts", **request.params.except(:controller, :action)) + end +end diff --git a/app/models/current.rb b/app/models/current.rb index e48bce946..601277fa1 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,3 +1,3 @@ class Current < ActiveSupport::CurrentAttributes - attribute :user, :agent + attribute :user, :agent, :actor, :request_id end diff --git a/app/models/frameworks/activity.rb b/app/models/frameworks/activity.rb new file mode 100644 index 000000000..9b7f86be1 --- /dev/null +++ b/app/models/frameworks/activity.rb @@ -0,0 +1,7 @@ +module Frameworks::Activity + extend ActiveSupport::Concern + + included do + has_one :activity_log_item, as: :activity, touch: true + end +end diff --git a/app/models/frameworks/activity_log_item.rb b/app/models/frameworks/activity_log_item.rb new file mode 100644 index 000000000..ac544a5fc --- /dev/null +++ b/app/models/frameworks/activity_log_item.rb @@ -0,0 +1,12 @@ +class Frameworks::ActivityLogItem < ApplicationRecord + include Presentable + + belongs_to :actor, polymorphic: true, optional: true + belongs_to :subject, polymorphic: true, optional: true + delegated_type :activity, types: %w[Frameworks::ActivityLoggableVersion] + + before_create do + self.guid ||= Current.request_id + self.actor ||= Current.actor + end +end diff --git a/app/models/frameworks/activity_log_item/presentable.rb b/app/models/frameworks/activity_log_item/presentable.rb new file mode 100644 index 000000000..7941fb2c8 --- /dev/null +++ b/app/models/frameworks/activity_log_item/presentable.rb @@ -0,0 +1,11 @@ +module Frameworks::ActivityLogItem::Presentable + extend ActiveSupport::Concern + + def display_created_at + created_at.strftime("%d/%m/%Y %H:%M:%S") + end + + def display_actor + actor.try(:full_name) + end +end diff --git a/app/models/frameworks/activity_loggable.rb b/app/models/frameworks/activity_loggable.rb new file mode 100644 index 000000000..349ae604d --- /dev/null +++ b/app/models/frameworks/activity_loggable.rb @@ -0,0 +1,19 @@ +module Frameworks::ActivityLoggable + extend ActiveSupport::Concern + include ActivityLogPresentable + + included do + has_paper_trail versions: { class_name: "Frameworks::ActivityLoggableVersion" } + + has_many :activity_log_items, as: :subject + + after_create :log_latest_version_in_activity_log + after_update :log_latest_version_in_activity_log + end + +private + + def log_latest_version_in_activity_log + Frameworks::ActivityLogItem.create!(subject: self, activity: versions.last, activity_type: "Frameworks::ActivityLoggableVersion") + end +end diff --git a/app/models/frameworks/activity_loggable/activity_log_presentable.rb b/app/models/frameworks/activity_loggable/activity_log_presentable.rb new file mode 100644 index 000000000..38d6cbfbc --- /dev/null +++ b/app/models/frameworks/activity_loggable/activity_log_presentable.rb @@ -0,0 +1,15 @@ +module Frameworks::ActivityLoggable::ActivityLogPresentable + extend ActiveSupport::Concern + + included do + delegate :version_at, to: :paper_trail + end + + def activity_log_display_name + try(:full_name) || try(:short_name) || try(:name) + end + + def activity_log_display_type + self.class.to_s.demodulize.underscore.humanize + end +end diff --git a/app/models/frameworks/activity_loggable_version.rb b/app/models/frameworks/activity_loggable_version.rb new file mode 100644 index 000000000..c2c315315 --- /dev/null +++ b/app/models/frameworks/activity_loggable_version.rb @@ -0,0 +1,8 @@ +class Frameworks::ActivityLoggableVersion < PaperTrail::Version + include Presentable + + def item_association_at_version_or_current(association:, id:, version_at: created_at) + current_version_of_association = item.class.reflections[association].klass.find_by(id:) + current_version_of_association.try(:version_at, version_at).presence || current_version_of_association + end +end diff --git a/app/models/frameworks/activity_loggable_version/presentable.rb b/app/models/frameworks/activity_loggable_version/presentable.rb new file mode 100644 index 000000000..3273294c7 --- /dev/null +++ b/app/models/frameworks/activity_loggable_version/presentable.rb @@ -0,0 +1,23 @@ +module Frameworks::ActivityLoggableVersion::Presentable + extend ActiveSupport::Concern + + def presentable_changes + Frameworks::ActivityLoggableVersion::PresentableChanges.new(self) + end + + def display_field_version(field:, value:) + display_value = + if field.ends_with?("_id") + display_field_version_for_association(association: field.split("_id").first, id: value) + else + item.try("display_field_version_#{field}", value) + end + + display_value.presence || value + end + + def display_field_version_for_association(association:, id:) + record = item_association_at_version_or_current(association:, id:) + record.try(:activity_log_display_name).presence || record.try(:full_name).presence || record.try(:name) + end +end diff --git a/app/models/frameworks/activity_loggable_version/presentable_changes.rb b/app/models/frameworks/activity_loggable_version/presentable_changes.rb new file mode 100644 index 000000000..1eec7cb73 --- /dev/null +++ b/app/models/frameworks/activity_loggable_version/presentable_changes.rb @@ -0,0 +1,20 @@ +class Frameworks::ActivityLoggableVersion::PresentableChanges + PresentableChange = Struct.new(:field, :from, :to, keyword_init: true) + + include Enumerable + + def initialize(version) + @version = version + end + + def each + return to_enum(:each) unless block_given? + + @version.object_changes.except("id", "created_at", "updated_at").each do |field, changes| + from = @version.display_field_version(field:, value: changes.first) + to = @version.display_field_version(field:, value: changes.last) + + yield PresentableChange.new(field:, from:, to:) + end + end +end diff --git a/app/models/frameworks/activity_loggable_version/presentable_changes/field_change.rb b/app/models/frameworks/activity_loggable_version/presentable_changes/field_change.rb new file mode 100644 index 000000000..350c50c13 --- /dev/null +++ b/app/models/frameworks/activity_loggable_version/presentable_changes/field_change.rb @@ -0,0 +1,33 @@ +class Frameworks::ActivityLoggableVersion::PresentableChanges::FieldChange + attr_reader :version, :item, :previous_item, :field, :changes + + def initialize(version, field, changes) + @version = version + @field = field + @changes = changes + @item = version.item + @previous_item = version.previous.try(:item) + end + + def field_name + field.humanize + end + + def display + "#{value_or_empty(from)} #{value_or_empty(to)}".html_safe + end + +private + + def to + item.activity_log_display(field) || changes.last + end + + def from + previous_item.try(:activity_log_display, field) || changes.first + end + + def value_or_empty(value) + value.nil? ? "empty" : value + end +end diff --git a/app/models/frameworks/framework.rb b/app/models/frameworks/framework.rb index d3d5a8178..430f8cf4c 100644 --- a/app/models/frameworks/framework.rb +++ b/app/models/frameworks/framework.rb @@ -1,6 +1,16 @@ class Frameworks::Framework < ApplicationRecord - include FafImportable + include Frameworks::ActivityLoggable include StatusChangeable + include SpreadsheetImportable + include Sourceable + include Presentable + include ActivityLogPresentable + include Filterable + include Sortable belongs_to :provider + belongs_to :provider_contact, optional: true + belongs_to :proc_ops_lead, class_name: "Support::Agent", optional: true + belongs_to :e_and_o_lead, class_name: "Support::Agent", optional: true + belongs_to :support_category, class_name: "Support::Category", optional: true end diff --git a/app/models/frameworks/framework/activity_log_presentable.rb b/app/models/frameworks/framework/activity_log_presentable.rb new file mode 100644 index 000000000..031c6d229 --- /dev/null +++ b/app/models/frameworks/framework/activity_log_presentable.rb @@ -0,0 +1,11 @@ +module Frameworks::Framework::ActivityLogPresentable + extend ActiveSupport::Concern + + def display_field_version_dfe_start_date(date) + Date.parse(date).strftime("%d/%m/%y") if date.present? + end + + def display_field_version_dfe_end_date(date) + display_field_version_dfe_start_date(date) + end +end diff --git a/app/models/frameworks/framework/faf_importable.rb b/app/models/frameworks/framework/faf_importable.rb deleted file mode 100644 index 026b2d5ae..000000000 --- a/app/models/frameworks/framework/faf_importable.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Frameworks::Framework::FafImportable - extend ActiveSupport::Concern - - class_methods do - def import_from_faf(framework_details) - framework = find_or_initialize_by(provider_reference: framework_details.provider_reference) - - provider = Frameworks::Provider.find_or_create_by!(name: framework_details.provider_name) - - framework.update!( - status: :dfe_approved, - name: framework_details.name, - provider_url: framework_details.provider_url, - ends_at: framework_details.ends_at, - description: framework_details.description, - published_on_faf: true, - provider:, - ) - end - end -end diff --git a/app/models/frameworks/framework/filterable.rb b/app/models/frameworks/framework/filterable.rb new file mode 100644 index 000000000..c26f0c24f --- /dev/null +++ b/app/models/frameworks/framework/filterable.rb @@ -0,0 +1,18 @@ +module Frameworks::Framework::Filterable + extend ActiveSupport::Concern + + included do + scope :by_status, ->(statuses) { where(status: Array(statuses)) } + scope :by_provider, ->(provider_ids) { where(provider_id: Array(provider_ids)) } + scope :by_category, ->(category_ids) { where(support_category_id: Array(category_ids)) } + scope :by_e_and_o_lead, ->(e_and_o_lead_ids) { where(e_and_o_lead_id: Array(e_and_o_lead_ids)) } + scope :by_proc_ops_lead, ->(proc_ops_lead_ids) { where(proc_ops_lead_id: Array(proc_ops_lead_ids)) } + scope :by_provider_contact, ->(provider_contact_ids) { where(provider_contact_id: Array(provider_contact_ids)) } + end + + class_methods do + def filtering(params = {}) + Frameworks::Framework::Filtering.new(params) + end + end +end diff --git a/app/models/frameworks/framework/filtering.rb b/app/models/frameworks/framework/filtering.rb new file mode 100644 index 000000000..e31eca699 --- /dev/null +++ b/app/models/frameworks/framework/filtering.rb @@ -0,0 +1,71 @@ +class Frameworks::Framework::Filtering + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + + attribute :scoped_frameworks, default: -> { Frameworks::Framework } + attribute :status, default: -> { [] } + attribute :provider, default: -> { [] } + attribute :e_and_o_lead, default: -> { [] } + attribute :proc_ops_lead, default: -> { [] } + attribute :category, default: -> { [] } + attribute :provider_contact, default: -> { [] } + attribute :sort_by + attribute :sort_order + + def results + frameworks = scoped_frameworks + + filters.each { |filter| frameworks = filter.filter(frameworks) } + + frameworks.sorted_by(sort_by:, sort_order:) + end + + def available_category_options + Support::Category.order("title ASC").where(id: Frameworks::Framework.pluck(:support_category_id)) + .map { |category| [category.title, category.id] } + end + + def available_provider_filter_options + Frameworks::Provider.order("short_name ASC").pluck(:short_name, :id) + end + + def available_e_and_o_lead_options + Support::Agent.by_first_name.where(id: Frameworks::Framework.pluck(:e_and_o_lead_id)) + .map { |agent| [agent.full_name, agent.id] } + end + + def available_proc_ops_lead_options + Support::Agent.by_first_name.where(id: Frameworks::Framework.pluck(:proc_ops_lead_id)) + .map { |agent| [agent.full_name, agent.id] } + end + + def available_status_filter_options + Frameworks::Framework.statuses.map { |label, _id| [label.humanize, label] } + end + + def available_provider_contact_options + Frameworks::ProviderContact.sort_by_name("ascending").pluck(:name, :id) + end + + def available_sort_options + Frameworks::Framework.available_sort_options + end + + def number_of_selected(field) + send(field.to_s.singularize).count(&:present?) + end + +private + + def filters + [ + Support::Concerns::ScopeFilter.new(status, scope: :by_status), + Support::Concerns::ScopeFilter.new(provider, scope: :by_provider), + Support::Concerns::ScopeFilter.new(e_and_o_lead, scope: :by_e_and_o_lead), + Support::Concerns::ScopeFilter.new(proc_ops_lead, scope: :by_proc_ops_lead), + Support::Concerns::ScopeFilter.new(category, scope: :by_category), + Support::Concerns::ScopeFilter.new(provider_contact, scope: :by_provider_contact), + ] + end +end diff --git a/app/models/frameworks/framework/presentable.rb b/app/models/frameworks/framework/presentable.rb new file mode 100644 index 000000000..e5e8822f3 --- /dev/null +++ b/app/models/frameworks/framework/presentable.rb @@ -0,0 +1,47 @@ +module Frameworks::Framework::Presentable + extend ActiveSupport::Concern + + def provider_name + provider.name || provider.short_name + end + + def reference_and_short_name + [reference, short_name].compact.join(" - ") + end + + def provider_framework_owner_phone + provider_contact&.phone + end + + def provider_framework_owner_name + provider_contact&.name + end + + def provider_framework_owner_email + provider_contact&.email + end + + def category_name + support_category&.title + end + + def proc_ops_lead_name + proc_ops_lead&.full_name + end + + def e_and_o_lead_name + e_and_o_lead&.full_name + end + + def display_status + ApplicationController.render(partial: "frameworks/frameworks/framework_status", locals: { status: }) + end + + def display_dfe_start_date + dfe_start_date.strftime("%d/%m/%Y") if dfe_start_date.present? + end + + def display_dfe_end_date + dfe_end_date.strftime("%d/%m/%Y") if dfe_end_date.present? + end +end diff --git a/app/models/frameworks/framework/sortable.rb b/app/models/frameworks/framework/sortable.rb new file mode 100644 index 000000000..bfe56710c --- /dev/null +++ b/app/models/frameworks/framework/sortable.rb @@ -0,0 +1,35 @@ +module Frameworks::Framework::Sortable + extend ActiveSupport::Concern + + included do + scope :sort_by_updated, ->(direction = "descending") { order("updated_at #{safe_direction(direction)}") } + scope :sort_by_dfe_start_date, ->(direction = "descending") { order("dfe_start_date #{safe_direction(direction)}") } + scope :sort_by_dfe_end_date, ->(direction = "descending") { order("dfe_end_date #{safe_direction(direction)}") } + scope :sort_by_provider_start_date, ->(direction = "descending") { order("provider_start_date #{safe_direction(direction)}") } + scope :sort_by_provider_end_date, ->(direction = "descending") { order("provider_end_date #{safe_direction(direction)}") } + scope :sort_by_reference, ->(direction = "descending") { order("reference #{safe_direction(direction)}") } + end + + class_methods do + def sorted_by(sort_by:, sort_order:) + return sort_by_updated("descending") unless sort_by.present? && sort_order.present? + + public_send("sort_by_#{sort_by}", sort_order) + end + + def available_sort_options + [ + ["Updated", "updated"], + ["Reference", "reference"], + ["DfE start date", "dfe_start_date"], + ["DfE end date", "dfe_end_date"], + ["Provider start date", "provider_start_date"], + ["Provider end date", "provider_end_date"], + ] + end + + def safe_direction(direction) + direction == "descending" ? "DESC" : "ASC" + end + end +end diff --git a/app/models/frameworks/framework/sourceable.rb b/app/models/frameworks/framework/sourceable.rb new file mode 100644 index 000000000..3dafc6a7d --- /dev/null +++ b/app/models/frameworks/framework/sourceable.rb @@ -0,0 +1,9 @@ +module Frameworks::Framework::Sourceable + extend ActiveSupport::Concern + + included do + enum source: { + spreadsheet_import: 0, + } + end +end diff --git a/app/models/frameworks/framework/spreadsheet_importable.rb b/app/models/frameworks/framework/spreadsheet_importable.rb new file mode 100644 index 000000000..7ce2e063a --- /dev/null +++ b/app/models/frameworks/framework/spreadsheet_importable.rb @@ -0,0 +1,25 @@ +module Frameworks::Framework::SpreadsheetImportable + extend ActiveSupport::Concern + + class_methods do + def import_from_spreadsheet(file) + frameworks_list = SimpleXlsxReader.open(file) + .sheets + .find { |potential_sheet| potential_sheet.name == "Recommended Framework List" } + + populated_rows = frameworks_list.rows + .each(headers: true) + .reject { |row| row[nil].nil? } + + populated_rows + .each { |row| import_from_spreadsheet_row(RowMapping.new(row)) } + end + + def import_from_spreadsheet_row(row_mapping) + framework = find_or_initialize_by(provider: row_mapping.provider, reference: row_mapping.reference) + framework.source = :spreadsheet_import + framework.attributes = row_mapping.attributes + framework.save! + end + end +end diff --git a/app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb b/app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb new file mode 100644 index 000000000..b85027e62 --- /dev/null +++ b/app/models/frameworks/framework/spreadsheet_importable/row_mapping.rb @@ -0,0 +1,68 @@ +class Frameworks::Framework::SpreadsheetImportable::RowMapping + attr_reader :row + + def initialize(row) + @row = row + end + + def provider = Frameworks::Provider.find_or_create_by!(short_name: row["FWK Provider"]) + + def reference = row["Framework Reference "] + + def attributes + { + provider:, + provider_contact:, + status:, + name:, + url:, + reference:, + dfe_start_date:, + dfe_end_date:, + sct_framework_provider_lead:, + proc_ops_lead:, + e_and_o_lead:, + } + end + +private + + def name = row["Framework Name"] + + def provider_contact + return nil if [ + String(row["PBO Framework Owner"]), + String(row["PBO Framework Email"]), + ].any? { |value| value.strip == "-" || value.empty? } + + Frameworks::ProviderContact.find_or_create_by!( + provider:, + name: row["PBO Framework Owner"], + email: row["PBO Framework Email"], + ) + end + + def status + recommended = String(row["Presently Recommended Y/N"]).downcase.inquiry + + if recommended.y? + :dfe_approved + elsif recommended.n? + :not_approved + elsif recommended.x? + :evaluating + end + end + + def url = (row["Framework Name"].url if row["Framework Name"].respond_to?(:url)) + + def dfe_start_date = row["Date when added on FaF"] + + def dfe_end_date = row["Contracted End Date on FaF as at 20.1.2023"] + + def sct_framework_provider_lead = row["Current PSBO Lead (Interim - To Be Confirmed)"] + + def proc_ops_lead = Support::Agent.find_or_create_by_full_name(row["Proc Ops Lead"]) + + def e_and_o_lead = Support::Agent.find_or_create_by_full_name(row["Engagement and Outreach Lead"]) +end diff --git a/app/models/frameworks/framework/status_changeable.rb b/app/models/frameworks/framework/status_changeable.rb index ea5ceab79..b268e5537 100644 --- a/app/models/frameworks/framework/status_changeable.rb +++ b/app/models/frameworks/framework/status_changeable.rb @@ -4,9 +4,10 @@ module Frameworks::Framework::StatusChangeable included do enum status: { pending_evaluation: 0, - not_approved: 1, - dfe_approved: 2, - cab_approved: 3, + evaluating: 1, + not_approved: 2, + dfe_approved: 3, + cab_approved: 4, } end end diff --git a/app/models/frameworks/provider.rb b/app/models/frameworks/provider.rb index f492d2bd0..345785986 100644 --- a/app/models/frameworks/provider.rb +++ b/app/models/frameworks/provider.rb @@ -1,2 +1,3 @@ class Frameworks::Provider < ApplicationRecord + include Frameworks::ActivityLoggable end diff --git a/app/models/frameworks/provider_contact.rb b/app/models/frameworks/provider_contact.rb new file mode 100644 index 000000000..b9d2890df --- /dev/null +++ b/app/models/frameworks/provider_contact.rb @@ -0,0 +1,13 @@ +class Frameworks::ProviderContact < ApplicationRecord + include Frameworks::ActivityLoggable + include ActivityLogPresentable + include Presentable + include Filterable + include Sortable + + belongs_to :provider + has_many :frameworks + + validates :name, presence: true + validates :email, presence: true, uniqueness: true +end diff --git a/app/models/frameworks/provider_contact/activity_log_presentable.rb b/app/models/frameworks/provider_contact/activity_log_presentable.rb new file mode 100644 index 000000000..5f611136b --- /dev/null +++ b/app/models/frameworks/provider_contact/activity_log_presentable.rb @@ -0,0 +1,7 @@ +module Frameworks::ProviderContact::ActivityLogPresentable + extend ActiveSupport::Concern + + def activity_log_display_name + "#{name} (#{email})" + end +end diff --git a/app/models/frameworks/provider_contact/filterable.rb b/app/models/frameworks/provider_contact/filterable.rb new file mode 100644 index 000000000..7029111d7 --- /dev/null +++ b/app/models/frameworks/provider_contact/filterable.rb @@ -0,0 +1,13 @@ +module Frameworks::ProviderContact::Filterable + extend ActiveSupport::Concern + + included do + scope :by_provider, ->(provider_ids) { where(provider_id: Array(provider_ids)) } + end + + class_methods do + def filtering(params = {}) + Frameworks::ProviderContact::Filtering.new(params) + end + end +end diff --git a/app/models/frameworks/provider_contact/filtering.rb b/app/models/frameworks/provider_contact/filtering.rb new file mode 100644 index 000000000..7e862aa09 --- /dev/null +++ b/app/models/frameworks/provider_contact/filtering.rb @@ -0,0 +1,38 @@ +class Frameworks::ProviderContact::Filtering + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + + attribute :scoped_provider_contacts, default: -> { Frameworks::ProviderContact } + attribute :provider, default: -> { [] } + attribute :sort_by + attribute :sort_order + + def results + provider_contacts = scoped_provider_contacts + + filters.each { |filter| provider_contacts = filter.filter(provider_contacts) } + + provider_contacts.sorted_by(sort_by:, sort_order:) + end + + def available_sort_options + Frameworks::ProviderContact.available_sort_options + end + + def available_provider_filter_options + Frameworks::Provider.order("short_name ASC").pluck(:short_name, :id) + end + + def number_of_selected(field) + send(field.to_s.singularize).count(&:present?) + end + +private + + def filters + [ + Support::Concerns::ScopeFilter.new(provider, scope: :by_provider), + ] + end +end diff --git a/app/models/frameworks/provider_contact/presentable.rb b/app/models/frameworks/provider_contact/presentable.rb new file mode 100644 index 000000000..b172cbf2d --- /dev/null +++ b/app/models/frameworks/provider_contact/presentable.rb @@ -0,0 +1,7 @@ +module Frameworks::ProviderContact::Presentable + extend ActiveSupport::Concern + + def provider_name + provider.name || provider.short_name + end +end diff --git a/app/models/frameworks/provider_contact/sortable.rb b/app/models/frameworks/provider_contact/sortable.rb new file mode 100644 index 000000000..6faf85285 --- /dev/null +++ b/app/models/frameworks/provider_contact/sortable.rb @@ -0,0 +1,27 @@ +module Frameworks::ProviderContact::Sortable + extend ActiveSupport::Concern + + included do + scope :sort_by_name, ->(direction = "descending") { order("name #{safe_direction(direction)}") } + scope :sort_by_provider_name, ->(direction = "descending") { joins(:provider).order("frameworks_providers.short_name #{safe_direction(direction)}") } + end + + class_methods do + def sorted_by(sort_by:, sort_order:) + return sort_by_name("descending") unless sort_by.present? && sort_order.present? + + public_send("sort_by_#{sort_by}", sort_order) + end + + def available_sort_options + [ + %w[Name name], + %w[Provider provider_name], + ] + end + + def safe_direction(direction) + direction == "descending" ? "DESC" : "ASC" + end + end +end diff --git a/app/models/support/agent.rb b/app/models/support/agent.rb index 8794b6ff0..379501bae 100644 --- a/app/models/support/agent.rb +++ b/app/models/support/agent.rb @@ -15,6 +15,7 @@ class Agent < ApplicationRecord internal: "Digital Team Staff Member", analyst: "Data Analyst", framework_evaluator: "Framework Evaluator", + framework_evaluator_admin: "Framework Evaluator Admin", }.freeze has_many :cases, class_name: "Support::Case" @@ -33,6 +34,14 @@ class Agent < ApplicationRecord caseworkers.where(sql, q: "#{query}%").limit(30) } + def self.find_or_create_by_full_name(full_name) + first_name, last_name = String(full_name).split(" ") + + return nil if first_name.blank? || last_name.blank? + + find_or_create_by!(first_name:, last_name:) + end + def self.find_or_create_by_user(user) agent = find_by(dsi_uid: user.dfe_sign_in_uid) diff --git a/app/models/support/concerns/scope_filter.rb b/app/models/support/concerns/scope_filter.rb index 4b4cf6f79..0b882fb35 100644 --- a/app/models/support/concerns/scope_filter.rb +++ b/app/models/support/concerns/scope_filter.rb @@ -6,11 +6,11 @@ def initialize(value, scope:, mapping: {}, multiple: true) @multiple = multiple end - def filter(cases) - return cases if selected_all? || !entered? - return cases.send("#{scope}_unspecified") if selected_unspecified? + def filter(records) + return records if selected_all? || !entered? + return records.send("#{scope}_unspecified") if selected_unspecified? - cases.send(scope, multiple ? values : values.first) + records.send(scope, multiple ? values : values.first) end def selected?(*checked_for_values) diff --git a/app/policies/cms_portal_policy.rb b/app/policies/cms_portal_policy.rb index 21b86e032..76c0890b5 100644 --- a/app/policies/cms_portal_policy.rb +++ b/app/policies/cms_portal_policy.rb @@ -9,14 +9,19 @@ def access_legacy_admin? = allow_any_of(%w[global_admin internal analyst]) def access_statistics? = allow_any_of(%w[global_admin internal procops_admin procops analyst]) def access_proc_ops_portal? = allow_any_of(%w[global_admin internal procops_admin procops]) def access_e_and_o_portal? = allow_any_of(%w[global_admin internal e_and_o_admin e_and_o]) - def access_admin_settings? = allow_any_of(%w[global_admin procops_admin e_and_o_admin]) + def access_admin_settings? = allow_any_of(%w[global_admin procops_admin e_and_o_admin framework_evaluator_admin]) def access_establishment_search? = access_proc_ops_portal? || access_e_and_o_portal? - def access_frameworks_portal? = allow_any_of(%w[global_admin framework_evaluator]) + def access_frameworks_portal? = allow_any_of(%w[global_admin framework_evaluator_admin framework_evaluator]) # Agent management def manage_agents? = allow_any_of(%w[global_admin procops_admin e_and_o_admin]) + # Frameworks management + + def manage_frameworks_register_upload? = allow_any_of(%w[global_admin framework_evaluator_admin]) + def manage_frameworks_activity_log? = allow_any_of(%w[global_admin framework_evaluator_admin]) + # Role management def grantable_roles = @grantable_roles ||= Support::Agent::ROLES.select { |role, _label| send("grant_#{role}_role?") } @@ -28,7 +33,8 @@ def grant_e_and_o_admin_role? = allow_any_of(%w[global_admin e_and_o_admin]) def grant_e_and_o_role? = allow_any_of(%w[global_admin e_and_o_admin]) def grant_internal_role? = allow_any_of(%w[global_admin]) def grant_analyst_role? = allow_any_of(%w[global_admin]) - def grant_framework_evaluator_role? = allow_any_of(%w[global_admin framework_evaluator]) + def grant_framework_evaluator_admin_role? = allow_any_of(%w[global_admin]) + def grant_framework_evaluator_role? = allow_any_of(%w[global_admin framework_evaluator_admin]) private diff --git a/app/services/support/sync_frameworks.rb b/app/services/support/sync_frameworks.rb index 28473aaff..e95ea7c3e 100644 --- a/app/services/support/sync_frameworks.rb +++ b/app/services/support/sync_frameworks.rb @@ -8,11 +8,7 @@ def initialize(endpoint: ENV["FAF_FRAMEWORK_ENDPOINT"]) def call fetch_frameworks - - if @frameworks.present? - upsert_frameworks - import_to_framework_register - end + upsert_frameworks if @frameworks.present? end private @@ -40,20 +36,6 @@ def upsert_frameworks ) end - def import_to_framework_register - @frameworks.each do |raw_framework| - framework = OpenStruct.new( - name: raw_framework["title"], - provider_reference: raw_framework["ref"], - provider_name: raw_framework["provider"].try(:[], "title"), - provider_url: raw_framework["url"], - ends_at: raw_framework["expiry"], - description: raw_framework["descr"], - ) - Frameworks::Framework.import_from_faf(framework) - end - end - def prepare_frameworks(frameworks) frameworks.select { |framework| framework["expiry"].present? }.map do |framework| { diff --git a/app/views/frameworks/activity_log_items/_activity_log_item.html.erb b/app/views/frameworks/activity_log_items/_activity_log_item.html.erb new file mode 100644 index 000000000..ecee3c40f --- /dev/null +++ b/app/views/frameworks/activity_log_items/_activity_log_item.html.erb @@ -0,0 +1,4 @@ +<%= render "frameworks/activity_log_items/activity/#{activity_log_item.activity_type.demodulize.underscore}", + activity_log_item:, + activity: activity_log_item.activity, + subject: activity_log_item.subject %> diff --git a/app/views/frameworks/activity_log_items/_list.html.erb b/app/views/frameworks/activity_log_items/_list.html.erb new file mode 100644 index 000000000..ed43c4f2a --- /dev/null +++ b/app/views/frameworks/activity_log_items/_list.html.erb @@ -0,0 +1,18 @@ + + + + + + + + + + <% activity_log_items.each do |activity_log_item| %> + + + + + + <% end %> + +
DatePersonDescription
<%= activity_log_item.display_created_at %><%= activity_log_item.display_actor %><%= render activity_log_item %>
diff --git a/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.erb b/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.erb new file mode 100644 index 000000000..c8451f02c --- /dev/null +++ b/app/views/frameworks/activity_log_items/activity/_activity_loggable_version.erb @@ -0,0 +1,20 @@ +<%= activity.event.humanize %>d <%= activity.item.activity_log_display_type %> +<%= link_to "[+]", "#", title: "See changes", class: "govuk-link", "data-component" => "toggle-panel-visibility", "data-panel" => activity_log_item.id %> + +
+ + + <% activity.presentable_changes.each do |changes| %> + + + + + <% end %> + +
<%= changes.field.humanize %> + <%= changes.from.presence || "empty" %> + + <%= changes.to.presence || "empty" %> +
+
+ diff --git a/app/views/frameworks/dashboards/index.html.erb b/app/views/frameworks/dashboards/index.html.erb index 816bbc57d..5069cb354 100644 --- a/app/views/frameworks/dashboards/index.html.erb +++ b/app/views/frameworks/dashboards/index.html.erb @@ -1 +1,25 @@ -

Frameworks Portal

+<%= content_for :title, "GHBS | Frameworks" %> + +
+ + + <%= turbo_frame_tag "frameworks-register-frame", src: frameworks_frameworks_path(**request.params.except(:controller, :action)) do %> +
+

Loading...

+
+ <% end %> + + <%= turbo_frame_tag "provider-contacts-frame", src: frameworks_provider_contacts_path(**request.params.except(:controller, :action)) do %> +
+

Loading...

+
+ <% end %> +
diff --git a/app/views/frameworks/frameworks/_framework.html.erb b/app/views/frameworks/frameworks/_framework.html.erb new file mode 100644 index 000000000..4f7d1fdc3 --- /dev/null +++ b/app/views/frameworks/frameworks/_framework.html.erb @@ -0,0 +1,54 @@ +
+
+
+
+
Framework Ref
+
<%= link_to framework.reference, frameworks_framework_path(framework, back_to: current_url_b64(:frameworks_register)), class: "govuk-link", "data-turbo" => false %>
+
+ +
+
Framework Name
+
<%= framework.name %>
+
+ +
+
Framework Provider
+
<%= framework.provider_name %>
+
+ +
+
Category
+
<%= framework.category_name %>
+
+
+
+ +
+
+
+
Is approved?
+
<%= framework.display_status %>
+
+ +
+
DfE Start date
+
<%= framework.display_dfe_start_date %>
+
+ +
+
DfE End date
+
<%= framework.display_dfe_end_date %>
+
+ +
+
Proc Ops Lead
+
<%= framework.proc_ops_lead_name %>
+
+ +
+
E&O Lead
+
<%= framework.e_and_o_lead_name %>
+
+
+
+
diff --git a/app/views/frameworks/frameworks/_framework_status.html.erb b/app/views/frameworks/frameworks/_framework_status.html.erb new file mode 100644 index 000000000..f71d64c24 --- /dev/null +++ b/app/views/frameworks/frameworks/_framework_status.html.erb @@ -0,0 +1,10 @@ +<%# Required local: either literal status or a framework record %> +<% the_status = (defined?(status) ? status : framework.status).inquiry %> + + + <%= "govuk-tag--grey" if the_status.not_approved? %> + <%= "govuk-tag--purple" if the_status.evaluating? %> +"> + <%= the_status.humanize %> + diff --git a/app/views/frameworks/frameworks/_index_filters.html.erb b/app/views/frameworks/frameworks/_index_filters.html.erb new file mode 100644 index 000000000..2c7d498da --- /dev/null +++ b/app/views/frameworks/frameworks/_index_filters.html.erb @@ -0,0 +1,66 @@ +
+

<%= I18n.t("support.case.label.filters") %>

+ + <%= form_with model: @filtering, scope: :frameworks_filter, method: :get, url: frameworks_frameworks_path, html: { "data-controller" => "case-filters" } do |form| %> + <%= link_to I18n.t("support.case.filter.buttons.clear"), frameworks_frameworks_path, class: "govuk-button govuk-button--secondary", role: "button" %> + + <%= form.govuk_select :sort_by, + options_for_select(@filtering.available_sort_options, @filtering.sort_by), + label: { text: I18n.t("support.case.label.sort_by"), size: "s" } %> + + <%= form.govuk_collection_radio_buttons :sort_order, + available_sort_orders, + :id, + :title, + legend: nil, + small: true %> + + <%= expander title: "Status", subtitle: "#{@filtering.number_of_selected(:statuses)} selected", expanded: true do %> + <%= form.govuk_check_boxes_fieldset :status, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %> + <% @filtering.available_status_filter_options.each do |status| %> + <%= form.govuk_check_box :status, status.last, exclusive: false, label: { text: status.first } %> + <% end %> + <% end %> + <% end %> + + <%= expander title: "Category", subtitle: "#{@filtering.number_of_selected(:categories)} selected", expanded: @filtering.number_of_selected(:categories).positive? do %> + <%= form.govuk_check_boxes_fieldset :category, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %> + <% @filtering.available_category_options.each do |option| %> + <%= form.govuk_check_box :category, option.last, exclusive: false, label: { text: option.first } %> + <% end %> + <% end %> + <% end %> + + <%= expander title: "Provider", subtitle: "#{@filtering.number_of_selected(:providers)} selected", expanded: @filtering.number_of_selected(:providers).positive? do %> + <%= form.govuk_check_boxes_fieldset :provider, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %> + <% @filtering.available_provider_filter_options.each do |option| %> + <%= form.govuk_check_box :provider, option.last, exclusive: false, label: { text: option.first } %> + <% end %> + <% end %> + <% end %> + + <%= expander title: "Proc-Ops Lead", subtitle: "#{@filtering.number_of_selected(:proc_ops_leads)} selected", expanded: @filtering.number_of_selected(:proc_ops_leads).positive? do %> + <%= form.govuk_check_boxes_fieldset :proc_ops_lead, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %> + <% @filtering.available_proc_ops_lead_options.each do |option| %> + <%= form.govuk_check_box :proc_ops_lead, option.last, exclusive: false, label: { text: option.first } %> + <% end %> + <% end %> + <% end %> + + <%= expander title: "E&O Lead", subtitle: "#{@filtering.number_of_selected(:e_and_o_leads)} selected", expanded: @filtering.number_of_selected(:e_and_o_leads).positive? do %> + <%= form.govuk_check_boxes_fieldset :e_and_o_lead, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %> + <% @filtering.available_e_and_o_lead_options.each do |option| %> + <%= form.govuk_check_box :e_and_o_lead, option.last, exclusive: false, label: { text: option.first } %> + <% end %> + <% end %> + <% end %> + + <%= expander title: "Provider Contact", subtitle: "#{@filtering.number_of_selected(:provider_contacts)} selected", expanded: @filtering.number_of_selected(:provider_contacts).positive? do %> + <%= form.govuk_check_boxes_fieldset :e_and_o_lead, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %> + <% @filtering.available_provider_contact_options.each do |option| %> + <%= form.govuk_check_box :provider_contact, option.last, exclusive: false, label: { text: option.first } %> + <% end %> + <% end %> + <% end %> + <% end %> +
diff --git a/app/views/frameworks/frameworks/index.html.erb b/app/views/frameworks/frameworks/index.html.erb new file mode 100644 index 000000000..f97c72f70 --- /dev/null +++ b/app/views/frameworks/frameworks/index.html.erb @@ -0,0 +1,20 @@ +<%= turbo_frame_tag "frameworks-register-frame" do %> +
+
+
+ <%= render "index_filters" %> +
+ +
+ <%= render @frameworks %> + + <% unless @frameworks.any? %> +

No frameworks found

+

Try clearing or changing your filters.

+ <% end %> + + <%= render "components/pagination", records: @frameworks, page_param_name: :frameworks_page, pagination_params: { anchor: "frameworks-register" } %> +
+
+
+<% end %> diff --git a/app/views/frameworks/frameworks/new.html.erb b/app/views/frameworks/frameworks/new.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/frameworks/frameworks/show.html.erb b/app/views/frameworks/frameworks/show.html.erb new file mode 100644 index 000000000..5ad3707fe --- /dev/null +++ b/app/views/frameworks/frameworks/show.html.erb @@ -0,0 +1,38 @@ +<%= content_for :title, "GHBS | Frameworks | #{@framework.name}" %> + +<%= @framework.reference_and_short_name %> +

<%= @framework.name %>

+ +

<%= @framework.display_status %>

+ +
+ + + <%= turbo_frame_tag "framework-details-frame" do %> +
+ <%= render "frameworks/frameworks/show/framework_details" %> +
+ <% end %> + + <%= turbo_frame_tag "framework-provider-frame" do %> +
+ <%= render "frameworks/frameworks/show/framework_provider" %> +
+ <% end %> + + <%= turbo_frame_tag "framework-activity-frame" do %> +
+ <%= render "frameworks/frameworks/show/framework_activity" %> +
+ <% end %> +
diff --git a/app/views/frameworks/frameworks/show/_framework_activity.html.erb b/app/views/frameworks/frameworks/show/_framework_activity.html.erb new file mode 100644 index 000000000..820c7082d --- /dev/null +++ b/app/views/frameworks/frameworks/show/_framework_activity.html.erb @@ -0,0 +1,2 @@ +<%= render "frameworks/activity_log_items/list", activity_log_items: @activity_log_items %> +<%= render "components/pagination", records: @activity_log_items, page_param_name: 'activities_page', pagination_params: { anchor: "framework-activity" } %> diff --git a/app/views/frameworks/frameworks/show/_framework_details.html.erb b/app/views/frameworks/frameworks/show/_framework_details.html.erb new file mode 100644 index 000000000..1ca41c151 --- /dev/null +++ b/app/views/frameworks/frameworks/show/_framework_details.html.erb @@ -0,0 +1,47 @@ + +
+
+
Url
+
<%= link_to @framework.url, @framework.url, target: "_blank", class: "govuk-link" if @framework.url.present? %>
+
+ +
+
Category
+
<%= @framework.category_name %>
+
+ +
+
SCT Framework Owner
+
<%= @framework.sct_framework_owner %>
+
+ +
+
Procurement Operations Lead
+
<%= @framework.proc_ops_lead_name %>
+
+ +
+
Engagement and Outreach Lead
+
<%= @framework.e_and_o_lead_name %>
+
+ +
+
Provider start date
+
<%= @framework.provider_start_date %>
+
+ +
+
Provider End Date
+
<%= @framework.provider_end_date %>
+
+ +
+
DfE Start Date
+
<%= @framework.display_dfe_start_date %>
+
+ +
+
DfE End Date
+
<%= @framework.display_dfe_end_date %>
+
+
diff --git a/app/views/frameworks/frameworks/show/_framework_provider.html.erb b/app/views/frameworks/frameworks/show/_framework_provider.html.erb new file mode 100644 index 000000000..1a1d72b18 --- /dev/null +++ b/app/views/frameworks/frameworks/show/_framework_provider.html.erb @@ -0,0 +1,26 @@ +
+
+
Provider
+
<%= @framework.provider_name %>
+
+ +
+
SCT Provider Lead
+
<%= @framework.sct_framework_provider_lead %>
+
+ +
+
Framework Owner
+
<%= @framework.provider_framework_owner_name %>
+
+ +
+
Email
+
<%= @framework.provider_framework_owner_email %>
+
+ +
+
Phone
+
<%= @framework.provider_framework_owner_phone %>
+
+
diff --git a/app/views/frameworks/management/activity_logs/show.html.erb b/app/views/frameworks/management/activity_logs/show.html.erb new file mode 100644 index 000000000..d15c0ec4f --- /dev/null +++ b/app/views/frameworks/management/activity_logs/show.html.erb @@ -0,0 +1,4 @@ +

Activity Log

+ +<%= render "frameworks/activity_log_items/list", activity_log_items: @activity_log_items %> +<%= render "components/pagination", records: @activity_log_items %> diff --git a/app/views/frameworks/management/management/index.html.erb b/app/views/frameworks/management/management/index.html.erb new file mode 100644 index 000000000..7b7523fda --- /dev/null +++ b/app/views/frameworks/management/management/index.html.erb @@ -0,0 +1,11 @@ +

Frameworks Portal Management

+ + diff --git a/app/views/frameworks/management/register_uploads/new.html.erb b/app/views/frameworks/management/register_uploads/new.html.erb new file mode 100644 index 000000000..fe207c39d --- /dev/null +++ b/app/views/frameworks/management/register_uploads/new.html.erb @@ -0,0 +1,8 @@ +

Upload Frameworks Register xlsx

+ +<%= form_with url: frameworks_management_register_upload_path, method: :post, scope: :register_upload do |f| %> + <%= f.govuk_file_field :spreadsheet, + label: { text: 'Register Spreadsheet (.xlsx)' } %> + + <%= f.govuk_submit "Upload" %> +<% end %> diff --git a/app/views/frameworks/provider_contacts/_index_filters.html.erb b/app/views/frameworks/provider_contacts/_index_filters.html.erb new file mode 100644 index 000000000..f0a57af14 --- /dev/null +++ b/app/views/frameworks/provider_contacts/_index_filters.html.erb @@ -0,0 +1,26 @@ +
+

<%= I18n.t("support.case.label.filters") %>

+ + <%= form_with model: @filtering, scope: :provider_contacts_filter, method: :get, url: frameworks_provider_contacts_path, html: { "data-controller" => "case-filters" } do |form| %> + <%= link_to I18n.t("support.case.filter.buttons.clear"), frameworks_provider_contacts_path, class: "govuk-button govuk-button--secondary", role: "button" %> + + <%= form.govuk_select :sort_by, + options_for_select(@filtering.available_sort_options, @filtering.sort_by), + label: { text: I18n.t("support.case.label.sort_by"), size: "s" } %> + + <%= form.govuk_collection_radio_buttons :sort_order, + available_sort_orders, + :id, + :title, + legend: nil, + small: true %> + + <%= expander title: "Provider", subtitle: "#{@filtering.number_of_selected(:providers)} selected", expanded: true do %> + <%= form.govuk_check_boxes_fieldset :provider, legend: nil, small: true, form_group: { class: "govuk-!-margin-bottom-0" } do %> + <% @filtering.available_provider_filter_options.each do |option| %> + <%= form.govuk_check_box :provider, option.last, exclusive: false, label: { text: option.first } %> + <% end %> + <% end %> + <% end %> + <% end %> +
diff --git a/app/views/frameworks/provider_contacts/_provider_contact.html.erb b/app/views/frameworks/provider_contacts/_provider_contact.html.erb new file mode 100644 index 000000000..3d7d30090 --- /dev/null +++ b/app/views/frameworks/provider_contacts/_provider_contact.html.erb @@ -0,0 +1,25 @@ +
+
+
+
+
Name
+
<%= link_to provider_contact.name, frameworks_provider_contact_path(provider_contact, back_to: current_url_b64(:provider_contacts)), class: "govuk-link", "data-turbo" => false %>
+
+ +
+
Email
+
<%= provider_contact.email %>
+
+ +
+
Phone
+
<%= provider_contact.phone %>
+
+ +
+
Provider
+
<%= provider_contact.provider_name %>
+
+
+
+
diff --git a/app/views/frameworks/provider_contacts/edit.html.erb b/app/views/frameworks/provider_contacts/edit.html.erb new file mode 100644 index 000000000..d6af6a289 --- /dev/null +++ b/app/views/frameworks/provider_contacts/edit.html.erb @@ -0,0 +1,21 @@ +<%= content_for :title, "GHBS | Provider Contacts | Edit #{@provider_contact.name}" %> + +<%= turbo_frame_tag dom_id(@provider_contact) do %> + Edit Framework Provider Contact +

<%= @provider_contact.name %>

+ +

Details

+ +
+ <%= form_for @provider_contact do |f| %> + <%= f.govuk_text_field :name %> + <%= f.govuk_text_field :email %> + <%= f.govuk_text_field :phone %> + +
+ <%= f.govuk_submit "Save changes" %> + <%= link_to "Cancel", @provider_contact, class: "govuk-link govuk-link--no-visited-state" %> +
+ <% end %> +
+<% end %> diff --git a/app/views/frameworks/provider_contacts/index.html.erb b/app/views/frameworks/provider_contacts/index.html.erb new file mode 100644 index 000000000..9babb80a3 --- /dev/null +++ b/app/views/frameworks/provider_contacts/index.html.erb @@ -0,0 +1,20 @@ +<%= turbo_frame_tag "provider-contacts-frame" do %> +
+
+
+ <%= render "index_filters" %> +
+ +
+ <%= render @provider_contacts %> + + <% unless @provider_contacts.any? %> +

No contacts found

+

Try clearing or changing your filters.

+ <% end %> + + <%= render "components/pagination", records: @provider_contacts, page_param_name: :provider_contacts_page, pagination_params: { anchor: "provider-contacts" } %> +
+
+
+<% end %> diff --git a/app/views/frameworks/provider_contacts/show.html.erb b/app/views/frameworks/provider_contacts/show.html.erb new file mode 100644 index 000000000..33c431ffc --- /dev/null +++ b/app/views/frameworks/provider_contacts/show.html.erb @@ -0,0 +1,40 @@ +<%= content_for :title, "GHBS | Provider Contacts | #{@provider_contact.name}" %> + +<%= turbo_frame_tag dom_id(@provider_contact) do %> + Framework Provider Contact +

<%= @provider_contact.name %>

+ +

Details

+
+
+
Name
+
<%= @provider_contact.name %>
+
+ +
+
Email
+
<%= @provider_contact.email %>
+
+ +
+
Phone
+
<%= @provider_contact.phone %>
+
+ +
+
Provider
+
<%= @provider_contact.provider_name %>
+
+
+ +

Actions

+ + +

History

+
+ <%= render "frameworks/activity_log_items/list", activity_log_items: @provider_contact.activity_log_items %> +
+<% end %> diff --git a/app/views/layouts/_navigation_links.html.erb b/app/views/layouts/_navigation_links.html.erb index 0bc5efa87..1a00f5afe 100644 --- a/app/views/layouts/_navigation_links.html.erb +++ b/app/views/layouts/_navigation_links.html.erb @@ -49,6 +49,16 @@ <% end %> <% if frameworks_portal? %> + + + <% if policy(:cms_portal).access_admin_settings? %> + + <% end %> + <% if policy(:cms_portal).access_proc_ops_portal? %>