diff --git a/app/controllers/admin/cx_action_plans_controller.rb b/app/controllers/admin/cx_action_plans_controller.rb new file mode 100644 index 000000000..d2dff6c4e --- /dev/null +++ b/app/controllers/admin/cx_action_plans_controller.rb @@ -0,0 +1,63 @@ +class Admin::CxActionPlansController < AdminController + before_action :set_cx_action_plan, only: %i[ show edit update destroy ] + + def index + @cx_action_plans = CxActionPlan.all + end + + def show + end + + def new + @service_providers = ServiceProvider.all.includes(:organization).order('organizations.name', 'service_providers.name') + @cx_action_plan = CxActionPlan.new + end + + def edit + @service_providers = ServiceProvider.all.includes(:organization).order('organizations.name', 'service_providers.name') + end + + def create + @cx_action_plan = CxActionPlan.new(cx_action_plan_params) + + respond_to do |format| + if @cx_action_plan.save + format.html { redirect_to admin_cx_action_plan_url(@cx_action_plan), notice: "Cx action plan was successfully created." } + format.json { render :show, status: :created, location: @cx_action_plan } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @cx_action_plan.errors, status: :unprocessable_entity } + end + end + end + + def update + respond_to do |format| + if @cx_action_plan.update(cx_action_plan_params) + format.html { redirect_to admin_cx_action_plan_url(@cx_action_plan), notice: "Cx action plan was successfully updated." } + format.json { render :show, status: :ok, location: @cx_action_plan } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @cx_action_plan.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @cx_action_plan.destroy + + respond_to do |format| + format.html { redirect_to cx_action_plans_url, notice: "Cx action plan was successfully destroyed." } + format.json { head :no_content } + end + end + + private + def set_cx_action_plan + @cx_action_plan = CxActionPlan.find(params[:id]) + end + + def cx_action_plan_params + params.require(:cx_action_plan).permit(:service_provider_id, :year, :delivered_current_year, :to_deliver_next_year) + end +end diff --git a/app/controllers/api/v1/cx_action_plans_controller.rb b/app/controllers/api/v1/cx_action_plans_controller.rb new file mode 100644 index 000000000..90f0be1bc --- /dev/null +++ b/app/controllers/api/v1/cx_action_plans_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Api + module V1 + class CxActionPlansController < ::ApiController + def index + respond_to do |format| + format.json do + render json: CxActionPlan.order(:id), each_serializer: CxActionPlanSerializer + end + end + end + + def show + respond_to do |format| + format.json do + render json: CxActionPlan.find(params[:id]), serializer: CxActionPlanSerializer + end + end + end + end + end +end diff --git a/app/models/cscrm_data_collection2.rb b/app/models/cscrm_data_collection2.rb index 006a5980c..a669fd341 100644 --- a/app/models/cscrm_data_collection2.rb +++ b/app/models/cscrm_data_collection2.rb @@ -409,6 +409,147 @@ def self.established_process_information_sharing_options } end + + # + # + # Custom logic applied to fields for data export + # example: 8 checkbox values being consolidated into a value between 1-3 + # + # + + def self.export_conversion_question_10(field) + return nil if field.length == 1 + + # 0 = Not Identified + # 1 = 1 or 2 identified + # 2 = All but suppliers identified + # 3 = All identified + # Note: Critical item selections not scored, for info only; however, + # If critical items were selected, assumption was that items had been identified + + question_option_selections = YAML.load(field) # parse the string encoded as an array, to an array + question_option_selections_without_not_identified = question_option_selections - ["Not identified"] # remove Not Identified option + question_option_selections_without_suppliers = question_option_selections_without_not_identified - ["Critical Suppliers Identified"] # remove Not Identified and Suppliers option + + if question_option_selections_without_not_identified.size == 8 # if all are selected + 3 + elsif question_option_selections_without_suppliers.size == 7 + 2 + elsif (1..2).include?(question_option_selections_without_not_identified.size) + 1 + elsif question_option_selections.include?("Not identified") + 0 + else + "not scored" + end + end + + def self.export_conversion_question_12(field) + return nil if field.length == 1 + + # 0 = Not Considered + # 1 = up to 2 selections + # 2 = 3 to 6 selections + # 3 = All + + question_option_selections = YAML.load(field) + question_option_selections_without_not_considered = question_option_selections - ["Not considered"] + question_option_selections_without_other = question_option_selections_without_not_considered - ["Other"] + + if question_option_selections_without_other.size == 7 # if all are selected + 3 + elsif (3..6).include?(question_option_selections_without_other.size) + 2 + elsif (1..2).include?(question_option_selections_without_other.size) + 1 + elsif question_option_selections.include?("Not considered") + 0 + else + "not scored" + end + end + + def self.export_conversion_question_14(field) + return nil if field.length == 1 + + # 0 = Not Considered + # 1 = Some Products and/or Services + # 2 = Some Products/All Services or All + # Products/Some Services + # 3 = All Product and Services + # Note: If 1 “all” option selected score = 2 + + question_option_selections = YAML.load(field) + question_option_selections_without_not_conducted = question_option_selections - ["Not conducted"] + + if question_option_selections_without_not_conducted.include?("Conducted for all prioritized products") && + question_option_selections_without_not_conducted.include?("Conducted for all prioritized services") + 3 + elsif question_option_selections_without_not_conducted.include?("Conducted for all prioritized products") || + question_option_selections_without_not_conducted.include?("Conducted for all prioritized services") + 2 + elsif question_option_selections_without_not_conducted.include?("Conducted for some prioritized products") || + question_option_selections_without_not_conducted.include?("Conducted for some prioritized services") + 1 + elsif question_option_selections.include?("Not conducted") + 0 + else + "not scored" + end + end + + def self.export_conversion_question_16(field) + # 0 = Not established + # 1 = Partial/in-Process Internal process + # 2 = Internal Process established and/or FASC process planned/in-process + # 3 = Internal and FASC process established + + if field == "5" + 3 + elsif field == "2" || + field == "3" || + field == "4" + 2 + elsif field == "1" + 1 + elsif field == "0" + 0 + else + "not scored" + end + end + + def self.export_conversion_question_17(field) + return nil if field.length == 1 + + # 0 = Not Considered; + # 1 = Response option(s), other than SCRAs; + # 2 = Response options includes “SCRAs” but not “mitigations” + # 3 = ”SCRAs” and “Mitigations” options selected + + question_option_selections = YAML.load(field) + + if question_option_selections.include?("SCRAs are conducted for critical suppliers") && + question_option_selections.include?("Mitigations to improve resilience/address assessed risks associated with critical suppliers are identified and implemented") + 3 + elsif question_option_selections.include?("SCRAs are conducted for critical suppliers") && + !question_option_selections.include?("Mitigations to improve resilience/address assessed risks associated with critical suppliers are identified and implemented") + 2 + elsif question_option_selections.include?("Critical Suppliers are identified in COOP and Recovery plans") || + question_option_selections.include?("Business Impact Analysis considers supplier and product dependency risks and resiliency requirements") + 1 + elsif question_option_selections.include?("Not considered") + 0 + else + "not scored" + end + end + + # + # end custom export logic + # + + def self.to_csv collections = CscrmDataCollection2.order('year, quarter') @@ -453,29 +594,34 @@ def self.to_csv "clearly_defined_roles_value", "clearly_defined_roles", "clearly_defined_roles_comments", - "identified_assets_and_essential_functions_value", "identified_assets_and_essential_functions", + "identified_assets_and_essential_functions_value", + "identified_assets_and_essential_functions_translated_value", "identified_assets_and_essential_functions_comments", "prioritization_process_value", "prioritization_process", "prioritization_process_comments", - "considerations_in_procurement_processes_value", "considerations_in_procurement_processes", + "considerations_in_procurement_processes_value", + "considerations_in_procurement_processes_translated_value", "considerations_in_procurement_processes_comments", "documented_methodology_value", "documented_methodology", "documented_methodology_comments", - "conducts_scra_for_prioritized_products_and_services_value", "conducts_scra_for_prioritized_products_and_services", + "conducts_scra_for_prioritized_products_and_services_value", + "conducts_scra_for_prioritized_products_and_services_translated_value", "conducts_scra_for_prioritized_products_and_services_comments", "personnel_required_to_complete_training_value", "personnel_required_to_complete_training", "personnel_required_to_complete_training_comments", - "established_process_information_sharing_with_fasc_value", "established_process_information_sharing_with_fasc", + "established_process_information_sharing_with_fasc_value", + "established_process_information_sharing_with_fasc_translated_value", "established_process_information_sharing_with_fasc_comments", - "cybersecurity_supply_chain_risk_considerations_value", "cybersecurity_supply_chain_risk_considerations", + "cybersecurity_supply_chain_risk_considerations_value", + "cybersecurity_supply_chain_risk_considerations_translated_value", "cybersecurity_supply_chain_risk_considerations_comments", "process_for_product_authenticity_value", "process_for_product_authenticity", @@ -534,30 +680,44 @@ def self.to_csv CscrmDataCollection2.question_9[:options].key(collection.clearly_defined_roles.to_i), collection.clearly_defined_roles, collection.clearly_defined_roles_comments, + CscrmDataCollection2.question_10[:options].key(collection.identified_assets_and_essential_functions.to_i), collection.identified_assets_and_essential_functions, + export_conversion_question_10(collection.identified_assets_and_essential_functions), collection.identified_assets_and_essential_functions_comments, + CscrmDataCollection2.question_11[:options].key(collection.prioritization_process.to_i), collection.prioritization_process, collection.prioritization_process_comments, + CscrmDataCollection2.question_12[:options].key(collection.considerations_in_procurement_processes.to_i), collection.considerations_in_procurement_processes, + export_conversion_question_12(collection.considerations_in_procurement_processes), collection.considerations_in_procurement_processes_comments, + CscrmDataCollection2.question_13[:options].key(collection.documented_methodology.to_i), collection.documented_methodology, collection.documented_methodology_comments, + CscrmDataCollection2.question_14[:options].key(collection.conducts_scra_for_prioritized_products_and_services.to_i), collection.conducts_scra_for_prioritized_products_and_services, + export_conversion_question_14(collection.conducts_scra_for_prioritized_products_and_services), collection.conducts_scra_for_prioritized_products_and_services_comments, + CscrmDataCollection2.question_15[:options].key(collection.personnel_required_to_complete_training.to_i), collection.personnel_required_to_complete_training, collection.personnel_required_to_complete_training_comments, + CscrmDataCollection2.question_16[:options].key(collection.established_process_information_sharing_with_fasc.to_i), collection.established_process_information_sharing_with_fasc, + export_conversion_question_16(collection.established_process_information_sharing_with_fasc), collection.established_process_information_sharing_with_fasc_comments, + CscrmDataCollection2.question_17[:options].key(collection.cybersecurity_supply_chain_risk_considerations.to_i), collection.cybersecurity_supply_chain_risk_considerations, + export_conversion_question_17(collection.cybersecurity_supply_chain_risk_considerations), collection.cybersecurity_supply_chain_risk_considerations_comments, + CscrmDataCollection2.question_18[:options].key(collection.process_for_product_authenticity.to_i), collection.process_for_product_authenticity, collection.process_for_product_authenticity_comments, diff --git a/app/models/cx_action_plan.rb b/app/models/cx_action_plan.rb new file mode 100644 index 000000000..c92e21b38 --- /dev/null +++ b/app/models/cx_action_plan.rb @@ -0,0 +1,20 @@ +class CxActionPlan < ApplicationRecord + belongs_to :service_provider + + + def organization_id + self.service_provider.organization_id + end + + def organization_name + self.service_provider.organization.name + end + + def service_provider_name + self.service_provider.name + end + + def services + self.service_provider.services + end +end diff --git a/app/serializers/cx_action_plan_serializer.rb b/app/serializers/cx_action_plan_serializer.rb new file mode 100644 index 000000000..d3898ddc6 --- /dev/null +++ b/app/serializers/cx_action_plan_serializer.rb @@ -0,0 +1,16 @@ +class CxActionPlanSerializer < ActiveModel::Serializer + attributes :id, + :organization_id, + :organization_name, + :service_provider_id, + :service_provider_name, + :year, + :delivered_current_year, + :to_deliver_next_year, + :services + + + def services + ActiveModel::Serializer::CollectionSerializer.new(object.services, serializer: ServiceSerializer) + end +end diff --git a/app/views/admin/cscrm_data_collections2/edit.html.erb b/app/views/admin/cscrm_data_collections2/edit.html.erb index a7484c466..da45687dc 100644 --- a/app/views/admin/cscrm_data_collections2/edit.html.erb +++ b/app/views/admin/cscrm_data_collections2/edit.html.erb @@ -3,7 +3,7 @@ <% end %>
- <%= link_to admin_cscrm_data_collections2_index_path(@cscrm_data_collection) do %> + <%= link_to admin_cscrm_data_collections2_path(@cscrm_data_collection) do %> Back to CSCRM Data Collection <% end %> diff --git a/app/views/admin/cx_action_plans/_cx_action_plan.html.erb b/app/views/admin/cx_action_plans/_cx_action_plan.html.erb new file mode 100644 index 000000000..689c5c99a --- /dev/null +++ b/app/views/admin/cx_action_plans/_cx_action_plan.html.erb @@ -0,0 +1,22 @@ +
+ Service provider: + <%= cx_action_plan.service_provider_id %> +
+ ++ Year: + <%= cx_action_plan.year %> +
+ ++ Delivered current year: + <%= cx_action_plan.delivered_current_year %> +
+ ++ To deliver next year: + <%= cx_action_plan.to_deliver_next_year %> +
+ ++ <%= form.submit class: "usa-button" %> +
+<% end %> diff --git a/app/views/admin/cx_action_plans/edit.html.erb b/app/views/admin/cx_action_plans/edit.html.erb new file mode 100644 index 000000000..f826fa8ef --- /dev/null +++ b/app/views/admin/cx_action_plans/edit.html.erb @@ -0,0 +1,13 @@ +<% content_for :navigation_title do %> + Editing CX Action Plan +<% end %> + ++ <%= link_to admin_cx_action_plan_path(@cx_action_plan) do %> + + Back to CX Action Plan + <% end %> +
+ + +<%= render "form", cx_action_plan: @cx_action_plan %> diff --git a/app/views/admin/cx_action_plans/index.html.erb b/app/views/admin/cx_action_plans/index.html.erb new file mode 100644 index 000000000..640d2a596 --- /dev/null +++ b/app/views/admin/cx_action_plans/index.html.erb @@ -0,0 +1,44 @@ +<% content_for :navigation_title do %> + CX Action Plans + + <%= link_to new_admin_cx_action_plan_path, class: "usa-button usa-button-inverted float-right" do %> + + New CX Action Plan + <% end %> +<% end %> + ++ Organization name + | ++ Service Provider name + | ++ Year + | ++ | +
---|---|---|---|
+ <%= cx_action_plan.service_provider.organization.name %> + | ++ <%= cx_action_plan.service_provider.name %> + | ++ <%= cx_action_plan.year %> + | ++ <%= link_to 'View', admin_cx_action_plan_path(cx_action_plan) %> + | +
+ <%= link_to admin_cx_action_plans_path do %> + + Back to CX Action Plans + <% end %> +
+ +<%= render "form", cx_action_plan: @cx_action_plan %> diff --git a/app/views/admin/cx_action_plans/show.html.erb b/app/views/admin/cx_action_plans/show.html.erb new file mode 100644 index 000000000..edd16a422 --- /dev/null +++ b/app/views/admin/cx_action_plans/show.html.erb @@ -0,0 +1,46 @@ +<% content_for :navigation_title do %> + CX Action Plan + <%= link_to edit_admin_cx_action_plan_path(@cx_action_plan), class: "usa-button usa-button-inverted float-right" do %> + + Edit + <% end %> +<% end %> ++ <%= link_to admin_cx_action_plans_path do %> + + Back to CX Action Plans + <% end %> +
+ ++ Organization name: + <%= @cx_action_plan.service_provider.organization.name %> +
+ ++ Service provider: + <%= link_to @cx_action_plan.service_provider.name, admin_service_provider_path(@cx_action_plan.service_provider) %> +
+ ++ Year: + <%= @cx_action_plan.year %> +
+ ++ Delivered current year: + <%= to_markdown(@cx_action_plan.delivered_current_year) %> +
+ ++ To deliver next year: + <%= to_markdown(@cx_action_plan.to_deliver_next_year) %> +
+- A red asterisk (*) indicates a required field. + <%= t :required_text_html %>
<%= render 'components/forms/flash', form: form %> <%= render partial: "components/forms/custom", locals: { form: form, questions: form.questions } %> diff --git a/app/views/components/widget/_no_modal.html.erb b/app/views/components/widget/_no_modal.html.erb index 341a911e4..7c3151907 100644 --- a/app/views/components/widget/_no_modal.html.erb +++ b/app/views/components/widget/_no_modal.html.erb @@ -12,20 +12,22 @@ <% unless form.delivery_method == "inline" %> - × + × <% end %> <% if form.instructions? %><%= sanitize(form.instructions) %>
- <% end %> + <% end -%>- A red asterisk (*) indicates a required field. + <%= t :required_field_html %>
-