diff --git a/app/controllers/support/cases/email_evaluators_controller.rb b/app/controllers/support/cases/email_evaluators_controller.rb new file mode 100644 index 000000000..1e9f38802 --- /dev/null +++ b/app/controllers/support/cases/email_evaluators_controller.rb @@ -0,0 +1,99 @@ +module Support + class Cases::EmailEvaluatorsController < Cases::ApplicationController + before_action :set_current_case + before_action :set_email_addresses + before_action :set_documents + before_action :current_email + before_action :set_template + before_action :category_name + before_action :organisation_name + before_action { @back_url = support_case_path(@current_case, anchor: "tasklist") } + + def edit + @draft = Email::Draft.new( + default_content: default_template, + default_subject:, + template_id: @template_id, + ticket: current_case.to_model, + to_recipients: @to_recipients, + ).save_draft! + + @support_email_id = @draft.id + @email_evaluators = Email::Draft.find(@support_email_id) + parse_template + end + + def update + @email_evaluators = Email::Draft.find(params[:id]) + @email_evaluators.attributes = form_params + parse_template + if @email_evaluators.valid?(:new_message) + @email_evaluators.save_draft! + @email_evaluators.deliver_as_new_message + + @current_case.update!(sent_email_to_evaluators: true) + + redirect_to @back_url + else + render :edit + end + end + + private + + def set_current_case + @current_case = Support::Case.find(params[:case_id]) + end + + def category_name + @category_name = Support::Category.find(current_case.category_id).try(:title) + end + + def organisation_name + @organisation_name = Support::Organisation.find(current_case.organisation_id).try(:name) + end + + def set_email_addresses + @evaluators = @current_case.evaluators.all + @email_addresses = @evaluators.map(&:email) + @to_recipients = @email_addresses.to_json + end + + def set_documents + @documents = @current_case.upload_documents + end + + def form_params + params.require(:email_evaluators).permit(:html_content) + end + + def draft_email_params + params.require(:email_evaluators).permit(:id) + end + + def current_email + @current_email = current_case.email + end + + def default_subject = "Case #{current_case.ref} - invitation to complete procurement evaluation" + + def default_template = render_to_string(partial: "support/cases/email_evaluators/form_template") + + def set_template + template = Support::EmailTemplate.find_by(title: "Invitation to complete procurement evaluation") + @template_id = template.id if template + end + + def parse_template + variables = { + "organisation_name" => @organisation_name.to_s, + "sub_category" => @category_name.to_s, + "unique_case_specific_link" => "unique case-specific link", + "evaluation_due_date" => current_case.evaluation_due_date.strftime("%d %b %y"), + } + + @parse_template = Liquid::Template.parse(@email_evaluators.body, error_mode: :strict).render(variables) + @email_evaluators.html_content = @parse_template + end + end +end diff --git a/app/models/email/template_parser.rb b/app/models/email/template_parser.rb index afa8bcc51..4ab606bdc 100644 --- a/app/models/email/template_parser.rb +++ b/app/models/email/template_parser.rb @@ -2,6 +2,10 @@ class Email::TemplateParser def initialize(variables: {}) @variables = { "caseworker_full_name" => Current.actor.try(:full_name), + "organisation_name" => "{{organisation_name}}", + "sub_category" => "{{sub_category}}", + "unique_case_specific_link" => "{{unique_case_specific_link}}", + "evaluation_due_date" => "{{evaluation_due_date}}", }.merge(variables) end diff --git a/app/views/support/cases/email_evaluators/_document_list.html.erb b/app/views/support/cases/email_evaluators/_document_list.html.erb new file mode 100644 index 000000000..92bf69d8b --- /dev/null +++ b/app/views/support/cases/email_evaluators/_document_list.html.erb @@ -0,0 +1,12 @@ +
+

<%= I18n.t("support.cases.email_evaluators.documents") %>

+ +
+ <% @documents.each do |document| %> +
+
<%= link_to document.file_name, support_document_download_path(document, type: document.class), class: "govuk-link", target: "_blank" %>
+
+ <% end %> +
+ +
diff --git a/app/views/support/cases/email_evaluators/_email_list.html.erb b/app/views/support/cases/email_evaluators/_email_list.html.erb new file mode 100644 index 000000000..86a3512b9 --- /dev/null +++ b/app/views/support/cases/email_evaluators/_email_list.html.erb @@ -0,0 +1,12 @@ +
+

<%= I18n.t("support.cases.email_evaluators.sharing_with") %>

+ +
+ <% @evaluators.each do |item| %> +
+
<%= item.email %>
+
+ <% end %> +
+ +
diff --git a/app/views/support/cases/email_evaluators/_form.html.erb b/app/views/support/cases/email_evaluators/_form.html.erb new file mode 100644 index 000000000..7c27549bc --- /dev/null +++ b/app/views/support/cases/email_evaluators/_form.html.erb @@ -0,0 +1,22 @@ +<%= form_with model: @email_evaluators, scope: :email_evaluators, url: support_case_email_evaluators_path(@current_case, id: @support_email_id), method: :patch do |form| %> + +<%= render "email_list" %> +<%= render "document_list"%> + +

<%= I18n.t("support.cases.email_evaluators.preview_template") %>

+<%= form.govuk_text_area :html_content, label: { text: I18n.t("support.case.label.messages.reply.body"), class: "govuk-!-display-none" }, rows: 5, class: "message-reply-box", + "data-component" => "tinymce", + "data-tinymce-profile" => "basic", + "data-tinymce-selector" => ".message-reply-box", + value: form.object.body +%> + +<%= form.hidden_field :to_recipients, value: @to_recipients %> +<%= form.hidden_field :id, value: @support_email_id %> + +
+ <%= form.submit I18n.t("support.cases.email_evaluators.submit"), class: "govuk-button" %> + <%= link_to I18n.t("generic.button.cancel"), @back_url, class: "govuk-link govuk-link--no-visited-state" %> +
+ +<% end %> \ No newline at end of file diff --git a/app/views/support/cases/email_evaluators/_form_template.html.erb b/app/views/support/cases/email_evaluators/_form_template.html.erb new file mode 100644 index 000000000..f8fea8043 --- /dev/null +++ b/app/views/support/cases/email_evaluators/_form_template.html.erb @@ -0,0 +1,67 @@ +

{{organisation_name}} has asked you to participate in the evaluation process for a {{sub_category}} procurement.

+ +

Follow the steps below to complete this evaluation:

+ +

1. Complete the declaration of interest form

+

If you do not have a conflict of interest with any of the following suppliers, then sign the attached declaration and reply to this email with the completed declaration as an attachment.

+ + + + +

What happens if I identify a conflict of interest?

+ +

In some situations, it is possible to mitigate a conflict of interest. When this is not possible a new evaluator can be selected.

+ +

Reply to this email if you have a conflict of interest so the procurement is not delayed.

+ +

2. Watch the evaluation training video (10 minutes)

+The video provides necessary knowledge for scoring and justifying your assessments. Evaluators are required to be appropriately trained to evaluate tenders in a fair, transparent, and compliant manner.   +
+

You must watch the evaluation training video before starting the evaluation process. The video link is: https://youtu.be/cbL65QlOas4?si=3fA-vKVPRGn9TEJ_

+
+ +

3. Sign in to complete the evaluation

+

Click the {{unique_case_specific_link}} to complete your evaluation with your DfE Sign-in account. If you do not have a DfE Sign-in account, you will need to create one.

+
+ +

4. Complete the evaluation

+

Complete the tasks required by {{evaluation_due_date}} or the procurement could be delayed.

+ +
+ +

Help completing the evaluation

+ +

Bids must be evaluated using the pre-populated evaluation sheet with scores allocated using the scoring methodology and reasoning shared. Bids must be evaluated independently of each other and other evaluators. Insufficient justification for scores will result in evaluations being returned, which could delay the procurement.

+ +

Reply to this email if you have any questions or need help.

+ +
+ +

5. Attend the moderation meeting

+ +

The moderation meeting is scheduled for [Day] [Date] [Time]

+ +
+ +

{{caseworker_full_name}}
+Procurement Specialist
+Get help buying for schools +

+ +

Reply to this email within 15 working days or by the date agreed. Late replies can delay the procurement process.

+ +

We'll close the case if we don't hear back from you. You can reopen the case at any time by replying to this email.

+ +
+ +

GET HELP BUYING FOR SCHOOLS SERVICE DISCLAIMER

+ +

Please note that the expertise and advice being provided by the Service shall not in any way affect your obligations to comply with all relevant procurement law and the Department's guidance, including but not limited to the Public Contracts Regulations, Academy Trust Handbook, ESFA Funding Agreement and the Schemes for Financing Local Authority Maintained schools.

+ +

For the avoidance of doubt, the Department shall bear no liability as a result of using our Service.

\ No newline at end of file diff --git a/app/views/support/cases/email_evaluators/edit.html.erb b/app/views/support/cases/email_evaluators/edit.html.erb new file mode 100644 index 000000000..66baa6b79 --- /dev/null +++ b/app/views/support/cases/email_evaluators/edit.html.erb @@ -0,0 +1,14 @@ +<%= render partial: "support/cases/components/case_header", locals: { current_case: @current_case } %> +

<%= I18n.t("support.cases.email_evaluators.header") %>

+ +

<%= I18n.t("support.cases.email_evaluators.hint") %>

+ +

<%= I18n.t("support.cases.email_evaluators.check_list.header") %>

+ + + +<%= render "form" %> \ No newline at end of file diff --git a/app/views/support/cases/show/_tasklist.html.erb b/app/views/support/cases/show/_tasklist.html.erb index 352157ac9..8bd8038c9 100644 --- a/app/views/support/cases/show/_tasklist.html.erb +++ b/app/views/support/cases/show/_tasklist.html.erb @@ -20,9 +20,17 @@ end task_list.with_item(title: I18n.t("support.case.label.tasklist.item.upload_documents"), href: edit_support_case_document_uploads_path(@current_case), status: document_upload_Status) - task_list.with_item(title: I18n.t("support.case.label.tasklist.item.email_evaluators")) do | item | - item.with_status(text: I18n.t("support.case.label.tasklist.status.cannot_start"), cannot_start_yet: true) + + if (@current_case.evaluators.count > 0 && @current_case.evaluation_due_date && @current_case.has_uploaded_documents && @current_case.sent_email_to_evaluators) + task_list.with_item(title: I18n.t("support.case.label.tasklist.item.email_evaluators"), href: edit_support_case_email_evaluators_path(@current_case), status: govuk_tag(text: I18n.t("support.case.label.tasklist.status.complete"), colour: "green")) + elsif (@current_case.evaluators.count > 0 && @current_case.evaluation_due_date && @current_case.has_uploaded_documents && @current_case.sent_email_to_evaluators == false) + task_list.with_item(title: I18n.t("support.case.label.tasklist.item.email_evaluators"), href: edit_support_case_email_evaluators_path(@current_case), status: govuk_tag(text: I18n.t("support.case.label.tasklist.status.to_do"))) + else + task_list.with_item(title: I18n.t("support.case.label.tasklist.item.email_evaluators")) do | item | + item.with_status(text: I18n.t("support.case.label.tasklist.status.cannot_start"), cannot_start_yet: true) + end end + task_list.with_item(title: I18n.t("support.case.label.tasklist.item.review_evaluations")) do | item | item.with_status(text: I18n.t("support.case.label.tasklist.status.cannot_start"), cannot_start_yet: true) end diff --git a/config/locales/en.yml b/config/locales/en.yml index f89f3fa81..e937f31d6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1795,6 +1795,19 @@ en: destroyed: "%{name} successfully removed" delete_confirmation: Are you sure you want to delete %{name}? is_document_uploaded: Please select uploaded option + email_evaluators: + header: Email evaluators + hint: Check and then send the emails to notify people that the files have been shared. Files will only be accessible to email addresses added. + check_list: + header: "You should check the:" + item: + email_addresses: email addresses + documents_available: documents available to download + email_template: email template + sharing_with: Sharing with + documents: Documents + preview_template: Preview template + submit: Send email and continue emails: attachments: attach_files: Attach files diff --git a/config/routes.rb b/config/routes.rb index 67a05aa1b..a1aabe704 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -237,6 +237,7 @@ resources :evaluators, except: %i[show] resource :evaluation_due_dates, only: %i[edit update] resource :document_uploads, except: %i[show] + resource :email_evaluators, except: %i[show] resource :email, only: %i[create] do scope module: :emails do resources :content, only: %i[show], param: :template diff --git a/db/migrate/20241211171744_add_sent_email_to_evaluators_to_case.rb b/db/migrate/20241211171744_add_sent_email_to_evaluators_to_case.rb new file mode 100644 index 000000000..eabd26949 --- /dev/null +++ b/db/migrate/20241211171744_add_sent_email_to_evaluators_to_case.rb @@ -0,0 +1,5 @@ +class AddSentEmailToEvaluatorsToCase < ActiveRecord::Migration[7.2] + def change + add_column :support_cases, :sent_email_to_evaluators, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 105518eea..4f3c4014c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_12_09_104836) do +ActiveRecord::Schema[7.2].define(version: 2024_12_11_171744) do create_sequence "evaluation_refs" create_sequence "framework_refs" @@ -637,6 +637,7 @@ t.boolean "is_evaluator", default: false t.date "evaluation_due_date" t.boolean "has_uploaded_documents" + t.boolean "sent_email_to_evaluators", default: false t.index ["category_id"], name: "index_support_cases_on_category_id" t.index ["existing_contract_id"], name: "index_support_cases_on_existing_contract_id" t.index ["new_contract_id"], name: "index_support_cases_on_new_contract_id" diff --git a/spec/features/support/agent_can_email_to_evaluators_spec.rb b/spec/features/support/agent_can_email_to_evaluators_spec.rb new file mode 100644 index 000000000..65f5762c7 --- /dev/null +++ b/spec/features/support/agent_can_email_to_evaluators_spec.rb @@ -0,0 +1,52 @@ +require "rails_helper" + +describe "Agent can email to evaluators", :js, :with_csrf_protection do + include_context "with an agent" + + let(:support_case) { create(:support_case) } + let(:file_1) { fixture_file_upload(Rails.root.join("spec/fixtures/support/text-file.txt"), "text/plain") } + let(:file_2) { fixture_file_upload(Rails.root.join("spec/fixtures/support/another-text-file.txt"), "text/plain") } + let(:document_uploader) { support_case.document_uploader(files: [file_1, file_2]) } + + specify "When add evaluators, set due date and upload documents status are not complete" do + visit support_case_path(support_case, anchor: "tasklist") + + expect(find("#complete-evaluation-1-status")).to have_text("To do") + expect(find("#complete-evaluation-2-status")).to have_text("To do") + expect(find("#complete-evaluation-3-status")).to have_text("To do") + expect(find("#complete-evaluation-4-status")).to have_text("Cannot start") + end + + specify "When add evaluators or set due date or upload documents status are not complete" do + support_case.update!(evaluation_due_date: Date.tomorrow) + visit support_case_path(support_case, anchor: "tasklist") + + expect(find("#complete-evaluation-1-status")).to have_text("To do") + expect(find("#complete-evaluation-2-status")).to have_text("Complete") + expect(find("#complete-evaluation-3-status")).to have_text("To do") + expect(find("#complete-evaluation-4-status")).to have_text("Cannot start") + end + + specify "When add evaluators, set due date and upload documents status are complete" do + support_case.update!(evaluation_due_date: Date.tomorrow, has_uploaded_documents: true) + support_case.evaluators.create!(first_name: "Momo", last_name: "Taro", email: "email@address") + document_uploader.save! + create(:support_email_template, title: "Invitation to complete procurement evaluation", subject: "about energy", body: "energy body") + support_case.reload + + visit support_case_path(support_case, anchor: "tasklist") + + expect(find("#complete-evaluation-4-status")).to have_text("To do") + + visit edit_support_case_email_evaluators_path(support_case) + + expect(page).to have_content("Email evaluators") + + create(:support_email, :inbox, ticket: support_case, outlook_conversation_id: "OCID1", subject: "Email Vealuators", recipients: [{ "name" => "Test 1", "address" => "test1@email.com" }], unique_body: "Email 1", is_read: false) + support_case.update!(sent_email_to_evaluators: true) + + visit support_case_path(support_case, anchor: "tasklist") + + expect(find("#complete-evaluation-4-status")).to have_text("Complete") + end +end diff --git a/spec/models/support/case_spec.rb b/spec/models/support/case_spec.rb index 58a209031..de6870e4b 100644 --- a/spec/models/support/case_spec.rb +++ b/spec/models/support/case_spec.rb @@ -59,7 +59,7 @@ describe "#to_csv" do it "includes headers" do expect(described_class.to_csv).to eql( - "id,ref,category_id,request_text,support_level,status,state,created_at,updated_at,agent_id,first_name,last_name,email,phone_number,source,organisation_id,existing_contract_id,new_contract_id,procurement_id,savings_status,savings_estimate_method,savings_actual_method,savings_estimate,savings_actual,action_required,organisation_type,value,closure_reason,extension_number,other_category,other_query,procurement_amount,confidence_level,special_requirements,query_id,exit_survey_sent,detected_category_id,creation_source,user_selected_category,created_by_id,procurement_stage_id,initial_request_text,with_school,next_key_date,next_key_date_description,discovery_method,discovery_method_other_text,project,other_school_urns,is_evaluator,evaluation_due_date,has_uploaded_documents\n", + "id,ref,category_id,request_text,support_level,status,state,created_at,updated_at,agent_id,first_name,last_name,email,phone_number,source,organisation_id,existing_contract_id,new_contract_id,procurement_id,savings_status,savings_estimate_method,savings_actual_method,savings_estimate,savings_actual,action_required,organisation_type,value,closure_reason,extension_number,other_category,other_query,procurement_amount,confidence_level,special_requirements,query_id,exit_survey_sent,detected_category_id,creation_source,user_selected_category,created_by_id,procurement_stage_id,initial_request_text,with_school,next_key_date,next_key_date_description,discovery_method,discovery_method_other_text,project,other_school_urns,is_evaluator,evaluation_due_date,has_uploaded_documents,sent_email_to_evaluators\n", ) end end