diff --git a/app/controllers/test/users_controller.rb b/app/controllers/test/users_controller.rb index b54a53fb0d2..4f28830ee7f 100644 --- a/app/controllers/test/users_controller.rb +++ b/app/controllers/test/users_controller.rb @@ -15,7 +15,8 @@ class Test::UsersController < ApplicationController name: "Queue", links: { your_queue: "/queue", - assignment_queue: "/queue/USER_CSS_ID/assign" # USER_CSS_ID is then updated in TestUsers file + assignment_queue: "/queue/USER_CSS_ID/assign", # USER_CSS_ID is then updated in TestUsers file + case_distribution_dashboard: "/acd-controls/test" } }, { @@ -65,7 +66,6 @@ class Test::UsersController < ApplicationController admin: "/admin", test_veterans: "/test/data", metrics_dashboard: "/metrics/dashboard", - case_distribution_dashboard: "/acd-controls/test", swagger: "/api-docs" } } diff --git a/app/jobs/push_priority_appeals_to_judges_job.rb b/app/jobs/push_priority_appeals_to_judges_job.rb index 6cf23f80928..7907a9eca08 100644 --- a/app/jobs/push_priority_appeals_to_judges_job.rb +++ b/app/jobs/push_priority_appeals_to_judges_job.rb @@ -21,7 +21,7 @@ def perform perform_later_or_now(UpdateAppealAffinityDatesJob) - send_job_report + slack_service.send_notification(generate_report.join("\n"), self.class.name) rescue StandardError => error start_time ||= Time.zone.now # temporary fix to get this job to succeed duration = time_ago_in_words(start_time) @@ -32,55 +32,6 @@ def perform metrics_service_report_runtime(metric_group_name: "priority_appeal_push_job") end - def send_job_report - slack_service.send_notification(slack_report.join("\n"), self.class.name) - end - - def slack_report # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - report = [] - if use_by_docket_date? - total_cases = @genpop_distributions.map(&:distributed_cases_count).sum - report << "*Number of cases distributed*: " \ - "#{total_cases}" - else - tied_distributions_sum = @tied_distributions.map(&:distributed_cases_count).sum - genpop_distributions_sum = @genpop_distributions.map(&:distributed_cases_count).sum - report << "*Number of cases tied to judges distributed*: " \ - "#{tied_distributions_sum}" - report << "*Number of general population cases distributed*: " \ - "#{genpop_distributions_sum}" - end - - appeals_not_distributed = docket_coordinator.dockets.map do |docket_type, docket| - report << "*Age of oldest #{docket_type} case*: #{docket.oldest_priority_appeal_days_waiting} days" - [docket_type, docket.ready_priority_appeal_ids] - end.to_h - - report << "*Total Number of appeals _not_ distributed*: #{appeals_not_distributed.values.flatten.count}" - docket_coordinator.dockets.each_pair do |sym, docket| - report << "*Number of #{sym} appeals _not_ distributed*: #{docket.count(priority: true, ready: true)}" - end - report << "*Number of Legacy Hearing Non Genpop appeals _not_ distributed*: #{legacy_not_genpop_count}" - - report << "" - report << "*Debugging information*" - report << "Priority Target: #{priority_target}" - report << "Previous monthly distributions {judge_id=>count}: #{priority_distributions_this_month_for_eligible_judges}" # rubocop:disable Layout/LineLength - - if appeals_not_distributed.values.flatten.any? - add_stuck_appeals_to_report(report, appeals_not_distributed) - end - - report - end - - def add_stuck_appeals_to_report(report, appeals) - report.unshift("[WARN]") - report << COPY::PRIORITY_PUSH_WARNING_MESSAGE - report << "AMA appeals not distributed: `Appeal.where(uuid: #{appeals.values.drop(1).flatten})`" - report << "Legacy appeals not distributed: `LegacyAppeal.where(vacols_id: #{appeals[:legacy]})`" - end - # Distribute all priority cases tied to a judge without limit def distribute_non_genpop_priority_appeals eligible_judges.map do |judge| @@ -182,7 +133,82 @@ def use_by_docket_date? FeatureToggle.enabled?(:acd_distribute_by_docket_date, user: RequestStore.store[:current_user]) end + # + # Reporting methods + # + + def generate_report + report = [] + + num_of_cases_distributed(report) + + report << "Priority Target: #{priority_target}" + + appeals_not_distributed = age_of_oldest_by_docket(report) + num_of_appeals_not_distributed(report, appeals_not_distributed) + num_of_appeals_not_distributed_by_affinity_date(report) + + report << "" + report << "*Debugging information*" + + excluded_judges_reporting(report) + + report << "Previous monthly distributions {judge_id=>count}: #{priority_distributions_this_month_for_eligible_judges}" # rubocop:disable Layout/LineLength + report + end + + def num_of_cases_distributed(report) + if use_by_docket_date? + total_cases = @genpop_distributions.map(&:distributed_cases_count).sum + report << "*Number of cases distributed*: " \ + "#{total_cases}" + else + tied_distributions_sum = @tied_distributions.map(&:distributed_cases_count).sum + genpop_distributions_sum = @genpop_distributions.map(&:distributed_cases_count).sum + report << "*Number of cases tied to judges distributed*: " \ + "#{tied_distributions_sum}" + report << "*Number of general population cases distributed*: " \ + "#{genpop_distributions_sum}" + end + end + + # :reek:FeatureEnvy + def age_of_oldest_by_docket(report) + docket_coordinator.dockets.map do |docket_type, docket| + report << "*Age of oldest #{docket_type} case*: #{docket.oldest_priority_appeal_days_waiting} days" + [docket_type, docket.ready_priority_appeal_ids] + end.to_h + end + + # :reek:FeatureEnvy + def num_of_appeals_not_distributed(report, appeals_not_distributed) + report << "" + report << "*Total Number of appeals _not_ distributed*: #{appeals_not_distributed.values.flatten.count}" + + docket_coordinator.dockets.each_pair do |sym, docket| + report << "*Number of #{sym} appeals _not_ distributed*: #{docket.count(priority: true, ready: true)}" + end + + report << "*Number of Legacy Hearing Non Genpop appeals _not_ distributed*: #{legacy_not_genpop_count}" + end + def legacy_not_genpop_count docket_coordinator.dockets[:legacy].not_genpop_priority_count end + + # :reek:FeatureEnvy + def num_of_appeals_not_distributed_by_affinity_date(report) + report << "" + docket_coordinator.dockets.each_pair do |sym, docket| + report << "*Number of #{sym} appeals in affinity date window*: " \ + "#{docket.affinity_date_count(true, true)}" + report << "*Number of #{sym} appeals out of affinity date window*: " \ + "#{docket.affinity_date_count(false, true)}" + end + end + + def excluded_judges_reporting(report) + excluded_judges = JudgeTeam.judges_with_exclude_appeals_from_affinity.pluck(:css_id) + report << "*Excluded Judges*: #{excluded_judges}" + end end diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 00876e8100e..4691f672563 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -251,11 +251,15 @@ def contested_claim? category_substrings = %w[Contested Apportionment] - request_issues.active.any? do |request_issue| - category_substrings.any? do |substring| - request_issues.active.include?(request_issue) && request_issue.nonrating_issue_category&.include?(substring) + request_issues.each do |request_issue| + category_substrings.each do |substring| + if request_issue.active? && request_issue.nonrating_issue_category&.include?(substring) + return true + end end end + + false end # :reek:RepeatedConditionals diff --git a/app/models/concerns/automatic_case_distribution.rb b/app/models/concerns/automatic_case_distribution.rb index 3f9a4cc191a..f23a4ae9139 100644 --- a/app/models/concerns/automatic_case_distribution.rb +++ b/app/models/concerns/automatic_case_distribution.rb @@ -90,18 +90,19 @@ def distribute_limited_priority_appeals_from_all_dockets(limit, style: "push") def ama_statistics sct_appeals_counts = @appeals.count { |appeal| appeal.try(:sct_appeal) } { - batch_size: @appeals.count, - total_batch_size: total_batch_size, - priority_count: priority_count, - direct_review_due_count: direct_review_due_count, - legacy_hearing_backlog_count: VACOLS::CaseDocket.nonpriority_hearing_cases_for_judge_count(judge), - legacy_proportion: docket_proportions[:legacy], - direct_review_proportion: docket_proportions[:direct_review], - evidence_submission_proportion: docket_proportions[:evidence_submission], - hearing_proportion: docket_proportions[:hearing], - nonpriority_iterations: @nonpriority_iterations, - algorithm: "proportions", - sct_appeals: sct_appeals_counts + statistics: { + batch_size: @appeals.count, + total_batch_size: total_batch_size, + priority_count: priority_count, + direct_review_due_count: direct_review_due_count, + legacy_hearing_backlog_count: VACOLS::CaseDocket.nonpriority_hearing_cases_for_judge_count(judge), + legacy_proportion: docket_proportions[:legacy], + direct_review_proportion: docket_proportions[:direct_review], + evidence_submission_proportion: docket_proportions[:evidence_submission], + hearing_proportion: docket_proportions[:hearing], + nonpriority_iterations: @nonpriority_iterations, + sct_appeals: sct_appeals_counts + } } end diff --git a/app/models/concerns/by_docket_date_distribution.rb b/app/models/concerns/by_docket_date_distribution.rb index 2b1b787f725..593c8322a91 100644 --- a/app/models/concerns/by_docket_date_distribution.rb +++ b/app/models/concerns/by_docket_date_distribution.rb @@ -57,23 +57,47 @@ def distribute_nonpriority_appeals_from_all_dockets_by_age_to_limit(limit, style end end - # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def ama_statistics - priority_counts = { count: priority_count } - nonpriority_counts = { count: nonpriority_count } + docket_counts = { + direct_review_priority_stats: {}, + direct_review_stats: {}, + evidence_submission_priority_stats: {}, + evidence_submission_stats: {}, + hearing_priority_stats: {}, + hearing_stats: {}, + legacy_priority_stats: {}, + legacy_stats: {} + } dockets.each_pair do |sym, docket| - priority_counts[sym] = docket.count(priority: true, ready: true) - nonpriority_counts[sym] = docket.count(priority: false, ready: true) + docket_counts["#{sym}_priority_stats".to_sym] = { + count: docket.count(priority: true, ready: true), + affinity_date: { + in_window: docket.affinity_date_count(true, true), + out_of_window: docket.affinity_date_count(false, true) + } + } + + docket_counts["#{sym}_stats".to_sym] = { + count: docket.count(priority: false, ready: true), + affinity_date: { + in_window: docket.affinity_date_count(true, false), + out_of_window: docket.affinity_date_count(false, false) + } + } end - priority_counts[:legacy_hearing_tied_to] = legacy_hearing_priority_count(judge) - nonpriority_counts[:legacy_hearing_tied_to] = legacy_hearing_nonpriority_count(judge) - - nonpriority_counts[:iterations] = @nonpriority_iterations + docket_counts[:legacy_priority_stats][:legacy_hearing_tied_to] = legacy_hearing_priority_count(judge) + docket_counts[:legacy_stats][:legacy_hearing_tied_to] = legacy_hearing_nonpriority_count(judge) sct_appeals_counts = @appeals.count { |appeal| appeal.try(:sct_appeal) } + cases_tied_to_ineligible_judges = { + ama: ama_distributed_cases_tied_to_ineligible_judges, + legacy: distributed_cases_tied_to_ineligible_judges + } + settings = {} feature_toggles = [ :specialty_case_team_distribution @@ -82,29 +106,39 @@ def ama_statistics settings[sym] = FeatureToggle.enabled?(sym, user: RequestStore.store[:current_user]) end - { - batch_size: @appeals.count, - total_batch_size: total_batch_size, - priority_target: @push_priority_target || @request_priority_count, - priority: priority_counts, - nonpriority: nonpriority_counts, - sct_appeals: sct_appeals_counts, - distributed_cases_tied_to_ineligible_judges: { - ama: ama_distributed_cases_tied_to_ineligible_judges, - legacy: distributed_cases_tied_to_ineligible_judges - }, - algorithm: "by_docket_date", - settings: settings - } + docket_counts.merge( + { + ineligible_judge_stats: { + distributed_cases_tied_to_ineligible_judges: cases_tied_to_ineligible_judges + }, + judge_stats: { + team_size: team_size, + ama_judge_assigned_tasks: judge_tasks.length, + legacy_assigned_tasks: judge_legacy_tasks.length, + settings: settings + }, + statistics: { + batch_size: @appeals.count, + total_batch_size: total_batch_size, + priority_target: @push_priority_target || @request_priority_count, + priority_count: priority_count, + nonpriority_count: nonpriority_count, + nonpriority_iterations: @nonpriority_iterations, + sct_appeals: sct_appeals_counts + } + } + ) rescue StandardError => error # There always needs to be a batch_size value for a completed distribution, else the priority push job will error { - batch_size: @appeals.count, - message: "Distribution successful, but there was an error generating statistics: \ - #{error.class}: #{error.message}, #{error.backtrace.first}" + statistics: { + batch_size: @appeals.count, + message: "Distribution successful, but there was an error generating statistics: \ + #{error.class}: #{error.message}, #{error.backtrace.first}" + } } end - # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize def ama_distributed_cases_tied_to_ineligible_judges @appeals.filter_map do |appeal| diff --git a/app/models/concerns/distribution_scopes.rb b/app/models/concerns/distribution_scopes.rb index f256c7cbc08..39f1db08e4b 100644 --- a/app/models/concerns/distribution_scopes.rb +++ b/app/models/concerns/distribution_scopes.rb @@ -54,7 +54,16 @@ def genpop OR original_judge_task.assigned_to_id in (?)", Constants.AMA_STREAM_TYPES.court_remand, CaseDistributionLever.cavc_affinity_days.days.ago, - JudgeTeam.judges_with_exclude_appeals_from_affinity + JudgeTeam.judge_ids_with_exclude_appeals_from_affinity + ) + end + + def genpop_by_affinity_start_date + with_appeal_affinities + .with_original_appeal_and_judge_task + .where( + "appeal_affinities.affinity_start_date <= ?", + CaseDistributionLever.cavc_affinity_days.days.ago ) end @@ -89,6 +98,14 @@ def non_genpop_for_judge(judge) .where(original_judge_task: { assigned_to_id: judge&.id }) end + def non_genpop_by_affinity_start_date + with_appeal_affinities + .with_original_appeal_and_judge_task + .where("appeal_affinities.affinity_start_date > ? or appeal_affinities.affinity_start_date is null", + CaseDistributionLever.cavc_affinity_days.days.ago) + .where.not(original_judge_task: { assigned_to_id: nil }) + end + def ordered_by_distribution_ready_date joins(:tasks) .group("appeals.id") @@ -134,7 +151,7 @@ def tied_to_ineligible_judge def tied_to_judges_with_exclude_appeals_from_affinity with_appeal_affinities - .where(hearings: { disposition: "held", judge_id: JudgeTeam.judges_with_exclude_appeals_from_affinity }) + .where(hearings: { disposition: "held", judge_id: JudgeTeam.judge_ids_with_exclude_appeals_from_affinity }) end # If an appeal has exceeded the affinity, it should be returned to genpop. diff --git a/app/models/decision_review.rb b/app/models/decision_review.rb index 7e1306187f4..6ecd2e07278 100644 --- a/app/models/decision_review.rb +++ b/app/models/decision_review.rb @@ -125,7 +125,11 @@ def veteran_full_name end def number_of_issues - request_issues.active.count + request_issues.count(&:active?) + end + + def issue_categories + request_issues.select(&:active?).map(&:nonrating_issue_category) end def external_id diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 2568a5582f1..37045d03fbf 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -121,17 +121,21 @@ def assigned_tasks end end - def batch_size - team_batch_size = JudgeTeam.for_judge(judge)&.attorneys&.size + def team_size + @team_size ||= JudgeTeam.for_judge(judge)&.attorneys&.size + end - return CaseDistributionLever.alternative_batch_size if team_batch_size.nil? || team_batch_size == 0 + def batch_size + return CaseDistributionLever.alternative_batch_size if team_size.nil? || team_size == 0 - team_batch_size * CaseDistributionLever.batch_size_per_attorney + team_size * CaseDistributionLever.batch_size_per_attorney end def error_statistics(error) { - error: error&.full_message + statistics: { + error: error&.full_message + } } end @@ -150,15 +154,12 @@ def process_error(error) # need to store batch_size in the statistics column for use within the PushPriorityAppealsToJudgesJob def completed_statistics(stats) { - batch_size: stats[:batch_size], + batch_size: stats[:statistics][:batch_size], info: "See related row in distribution_stats for additional stats" } end def record_distribution_stats(stats) - create_distribution_stats!( - statistics: stats, - levers: CaseDistributionLever.snapshot - ) + create_distribution_stats!(stats.merge(levers: CaseDistributionLever.snapshot)) end end diff --git a/app/models/docket.rb b/app/models/docket.rb index dcc314580af..a16d35fded9 100644 --- a/app/models/docket.rb +++ b/app/models/docket.rb @@ -161,6 +161,23 @@ def self.nonpriority_decisions_per_year .pluck(:id).size end + # used for distribution_stats + # :reek:ControlParameter + # :reek:FeatureEnvy + def affinity_date_count(in_window, priority) + scope = docket_appeals.ready_for_distribution + + scope = if in_window + scope.non_genpop_by_affinity_start_date + else + scope.genpop_by_affinity_start_date + end + + return scoped_for_priority(scope).ids.size if priority + + scope.nonpriority.ids.size + end + def calculate_days_for_time_goal_with_prior_to_goal return 0 unless docket_time_goal > 0 diff --git a/app/models/dockets/legacy_docket.rb b/app/models/dockets/legacy_docket.rb index 0a8c05cb5c9..96c3eb30f87 100644 --- a/app/models/dockets/legacy_docket.rb +++ b/app/models/dockets/legacy_docket.rb @@ -137,6 +137,12 @@ def distribute_nonpriority_appeals(distribution, end # rubocop:enable Metrics/ParameterLists + # used for distribution_stats + # change parameters to in_window, priority once implemented + def affinity_date_count(*) + "not implemented" + end + private def save_dist_case(dist_case) diff --git a/app/models/organizations/judge_team.rb b/app/models/organizations/judge_team.rb index aab4b5aad7c..b92c01510fc 100644 --- a/app/models/organizations/judge_team.rb +++ b/app/models/organizations/judge_team.rb @@ -21,10 +21,14 @@ def create_for_judge(user, ama_only_push = false, ama_only_request = false) end end + def judge_ids_with_exclude_appeals_from_affinity + judges_with_exclude_appeals_from_affinity.pluck(:id) + end + def judges_with_exclude_appeals_from_affinity return [] unless FeatureToggle.enabled?(:acd_exclude_from_affinity) - active.where(exclude_appeals_from_affinity: true).flat_map(&:judge).compact.pluck(:id) + active.where(exclude_appeals_from_affinity: true).flat_map(&:judge).compact end end diff --git a/app/models/organizations/supervisory_senior_council.rb b/app/models/organizations/supervisory_senior_counsel.rb similarity index 52% rename from app/models/organizations/supervisory_senior_council.rb rename to app/models/organizations/supervisory_senior_counsel.rb index 6a4e798f792..ee6db7c0c84 100644 --- a/app/models/organizations/supervisory_senior_council.rb +++ b/app/models/organizations/supervisory_senior_counsel.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -class SupervisorySeniorCouncil < Organization +class SupervisorySeniorCounsel < Organization alias_attribute :full_name, :name def self.singleton - SupervisorySeniorCouncil.first || SupervisorySeniorCouncil.create( - name: "Supervisory Senior Council", - url: "supervisory-senior-council" + SupervisorySeniorCounsel.first || SupervisorySeniorCounsel.create( + name: "Supervisory Senior Counsel", + url: "supervisory-senior-counsel" ) end diff --git a/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb b/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb index 92fb7acc108..0b7924744d9 100644 --- a/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb +++ b/app/models/queue_tabs/specialty_case_team_unassigned_tasks_tab.rb @@ -21,13 +21,31 @@ def tasks assigned_tasks end + # Override task_includes to optimize the queue a bit + def task_includes + [ + { appeal: [ + :request_issues, + :available_hearing_locations, + :claimants, + :work_mode, + :latest_informal_hearing_presentation_task + ] }, + :assigned_by, + :assigned_to, + :children, + :parent, + :attorney_case_reviews + ] + end + def column_names SpecialtyCaseTeam::COLUMN_NAMES end # This only affects bulk assign on the standard queue tab view def allow_bulk_assign? - true + false end def hide_from_queue_table_view @@ -37,8 +55,4 @@ def hide_from_queue_table_view def no_task_limit false end - - def custom_task_limit - 60 - end end diff --git a/app/models/serializers/work_queue/task_column_serializer.rb b/app/models/serializers/work_queue/task_column_serializer.rb index e1ad582a7e2..da5cc6bee81 100644 --- a/app/models/serializers/work_queue/task_column_serializer.rb +++ b/app/models/serializers/work_queue/task_column_serializer.rb @@ -90,11 +90,7 @@ def self.serialize_attribute?(params, columns) columns = [Constants.QUEUE_CONFIG.COLUMNS.ISSUE_TYPES.name] if serialize_attribute?(params, columns) - if object.appeal.is_a?(LegacyAppeal) - object.appeal.issue_categories - else - object.appeal.request_issues.active.map(&:nonrating_issue_category) - end.join(",") + object.appeal.issue_categories.join(",") end end diff --git a/app/models/serializers/work_queue/task_serializer.rb b/app/models/serializers/work_queue/task_serializer.rb index 4af2adfddb8..7bb06311269 100644 --- a/app/models/serializers/work_queue/task_serializer.rb +++ b/app/models/serializers/work_queue/task_serializer.rb @@ -146,11 +146,7 @@ class WorkQueue::TaskSerializer end attribute :issue_types do |object| - if object.appeal.is_a?(LegacyAppeal) - object.appeal.issue_categories - else - object.appeal.request_issues.active.map(&:nonrating_issue_category) - end.join(",") + object.appeal.issue_categories.join(",") end attribute :external_hearing_id do |object| diff --git a/app/models/user.rb b/app/models/user.rb index 4e4650f18c6..c2e562a9b55 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -124,7 +124,7 @@ def can_split_appeal?(appeal) end def member_of_cob_or_ssc? - member_of_organization?(ClerkOfTheBoard.singleton) || member_of_organization?(SupervisorySeniorCouncil.singleton) + member_of_organization?(ClerkOfTheBoard.singleton) || member_of_organization?(SupervisorySeniorCounsel.singleton) end def can_edit_issues? diff --git a/app/policies/appeal_request_issues_policy.rb b/app/policies/appeal_request_issues_policy.rb index 9035112c95b..727072ccceb 100644 --- a/app/policies/appeal_request_issues_policy.rb +++ b/app/policies/appeal_request_issues_policy.rb @@ -30,7 +30,7 @@ def editable_by_cavc_team_member? end def editable_by_ssc_team_member? - SupervisorySeniorCouncil.singleton.users.include?(user) && + SupervisorySeniorCounsel.singleton.users.include?(user) && FeatureToggle.enabled?(:split_appeal_workflow) end diff --git a/app/services/slack_service.rb b/app/services/slack_service.rb index 565452e328e..ba61aa1f07d 100644 --- a/app/services/slack_service.rb +++ b/app/services/slack_service.rb @@ -23,12 +23,16 @@ def initialize(url: ENV["SLACK_DISPATCH_ALERT_URL"]) attr_reader :url def send_notification(msg, title = "", channel = DEFAULT_CHANNEL) - return unless url && (aws_env == "uat" || aws_env == "prodtest" || aws_env == "prod") - slack_msg = format_slack_msg(msg, title, channel) - params = { body: slack_msg.to_json, headers: { "Content-Type" => "application/json" } } - http_service.post(url, params) + if url && (Rails.deploy_env?(:uat) || Rails.deploy_env?(:prodtest) || Rails.deploy_env?(:prod)) + params = { body: slack_msg.to_json, headers: { "Content-Type" => "application/json" } } + http_service.post(url, params) + else + # rubocop:disable Rails/Output + Rails.logger.info(pp(slack_msg)) + # rubocop:enable Rails/Output + end end private @@ -61,7 +65,7 @@ def format_slack_msg(msg, title, channel) channel.prepend("#") unless channel.match?(/^#/) { - username: "Caseflow (#{aws_env})", + username: "Caseflow (#{Rails.deploy_env})", channel: channel, attachments: [ { @@ -72,8 +76,4 @@ def format_slack_msg(msg, title, channel) ] } end - - def aws_env - ENV.fetch("DEPLOY_ENV", "development") - end end diff --git a/client/COPY.json b/client/COPY.json index 5735eec2290..e3b596368c3 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -1221,7 +1221,6 @@ "URL_HOST_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid URL host", "URL_PATH_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid URL path", "BULK_REASSIGN_INSTRUCTIONS": "This task has been %s due to the reassignment of all tasks previously assigned to %s.", - "PRIORITY_PUSH_WARNING_MESSAGE": "Some cases are ready to distribute but could not be. This could be due to the tied judge being an acting judge or an attorney (`!user.judge_in_vacols?`). These will be handled manually by the board. The tied judge could also have priority distribution turned off and should not be pushed cases (`JudgeTeam.for_judge(user).accepts_priority_pushed_cases?`). There could also be a duplicate distributed case blocking distribution (`DistributedCase.find_by(case_id: case_id`) or our validations for redistributing legacy appeals failed (https://github.com/department-of-veterans-affairs/caseflow/blob/master/app/services/redistributed_case.rb#L26)", "INVALID_IHP_DRAFT_PATH": "Path to IHP is invalid. This path should be of the format '\\\\vacoappbva3.dva.va.gov\\DMDI$\\VBMS Paperless IHPs\\VSO\\902\\VetName 12345.pdf' for legacy appeals and '\\\\vacoappbva3.dva.va.gov\\DMDI$\\VBMS Paperless IHPs\\VSO\\AMA IHPs\\VetName 12345.pdf' for AMA appeals. Hold SHIFT and right click the file to copy the path to the drafted IHP.", "UNSCHEDULED_HEARING_TITLE": "Unscheduled hearing", "SAVE_UNSCHEDULED_NOTES_SUCCESS_MESSAGE": "You updated the notes for %s’s hearing", diff --git a/client/app/queue/QueueActions.js b/client/app/queue/QueueActions.js index f5fb69d3dec..d4cc8ddf1d2 100644 --- a/client/app/queue/QueueActions.js +++ b/client/app/queue/QueueActions.js @@ -14,6 +14,7 @@ import ApiUtil from '../util/ApiUtil'; import { getMinutesToMilliseconds } from '../util/DateUtil'; import pluralize from 'pluralize'; import { keyBy, pick } from 'lodash'; +import { removeTaskIdsFromCache } from './caching/queueTableCache.slice'; export const onReceiveQueue = ( { tasks, amaTasks, appeals } @@ -419,12 +420,13 @@ export const fetchTasksAndAppealsOfAttorney = (attorneyId, params) => (dispatch) }; export const setSelectionOfTaskOfUser = - ({ userId, taskId, selected }) => ({ + ({ userId, taskId, selected, task }) => ({ type: ACTIONS.SET_SELECTION_OF_TASK_OF_USER, payload: { userId, taskId, - selected + selected, + task } }); @@ -507,6 +509,9 @@ export const initialSpecialtyCaseTeamAssignTasksToUser = ({ then((resp) => { const receievedTasks = prepareAllTasksForStore(resp.tasks.data); + // Removes tasks from queue table cache if using redux caching instead of local component state + dispatch(removeTaskIdsFromCache({ taskIds })); + dispatch(onReceiveTasks(pick(receievedTasks, ['tasks', 'amaTasks']))); dispatch(incrementTaskCountForAttorney({ diff --git a/client/app/queue/QueueTable.jsx b/client/app/queue/QueueTable.jsx index 4267a27a259..42d4098bf55 100644 --- a/client/app/queue/QueueTable.jsx +++ b/client/app/queue/QueueTable.jsx @@ -353,10 +353,21 @@ export default class QueueTable extends React.PureComponent { if (this.props.rowObjects.length) { this.setState({ cachedResponses: { ...this.state.cachedResponses, [this.requestUrl()]: firstResponse } }); + + if (this.props.useReduxCache) { + this.props.updateReduxCache({ key: this.requestUrl(), value: firstResponse }); + } + } }; componentDidUpdate = (previousProps, previousState) => { + if (this.props.useReduxCache && + (this.props.reduxCache[this.requestUrl()]?.tasks?.length !== + previousProps.reduxCache[this.requestUrl()]?.tasks?.length)) { + this.setState({ tasksFromApi: this.props.reduxCache[this.requestUrl()].tasks }); + } + // Only refetch if the search query text changes if (this.props.tabPaginationOptions && previousState.querySearchText !== this.props.tabPaginationOptions[QUEUE_CONFIG.SEARCH_QUERY_REQUEST_PARAM]) { @@ -556,9 +567,29 @@ export default class QueueTable extends React.PureComponent { deepLink = () => { const base = `${window.location.origin}${window.location.pathname}`; - const tab = this.props.taskPagesApiEndpoint.split('?')[1]; + const currentParams = new URLSearchParams(window.location.search); + const tableParams = new URLSearchParams(this.requestQueryString()); + const tabParams = new URLSearchParams(this.props.taskPagesApiEndpoint.split('?')[1]); + + // List of parameters that should be cleared if not present in tableParams + const paramsToClear = [ + QUEUE_CONFIG.SEARCH_QUERY_REQUEST_PARAM, + `${QUEUE_CONFIG.FILTER_COLUMN_REQUEST_PARAM}[]`, + ]; + + // Remove paramsToClear from currentParams if they are not in tableParams + paramsToClear.forEach((param) => { + if (!tableParams.has(param)) { + currentParams.delete(param); + } + }); + + // Merge tableParams and tabParams into currentParams, overwriting any duplicate keys + for (const [key, value] of [...tabParams.entries(), ...tableParams.entries()]) { + currentParams.set(key, value); + } - return `${base}?${tab}${this.requestQueryString()}`; + return `${base}?${currentParams.toString()}`; }; // /organizations/vlj-support-staff/tasks?tab=on_hold @@ -622,7 +653,8 @@ export default class QueueTable extends React.PureComponent { const endpointUrl = this.requestUrl(); // If we already have the tasks cached then we set the state and return early. - const responseFromCache = this.state.cachedResponses[endpointUrl]; + const responseFromCache = this.props.useReduxCache ? this.props.reduxCache[endpointUrl] : + this.state.cachedResponses[endpointUrl]; if (responseFromCache) { this.setState({ tasksFromApi: responseFromCache.tasks }); @@ -643,11 +675,21 @@ export default class QueueTable extends React.PureComponent { const preparedResponse = Object.assign(response.body, { tasks: preparedTasks }); this.setState({ - cachedResponses: { ...this.state.cachedResponses, [endpointUrl]: preparedResponse }, + // cachedResponses: { ...this.state.cachedResponses, [endpointUrl]: preparedResponse }, + ...(!this.props.useReduxCache && { + cachedResponses: { + ...this.state.cachedResponses, + [endpointUrl]: preparedResponse + } + }), tasksFromApi: preparedTasks, loadingComponent: null }); + if (this.props.useReduxCache) { + this.props.updateReduxCache({ key: endpointUrl, value: preparedResponse }); + } + this.updateAddressBar(); }). catch(() => this.setState({ loadingComponent: null })); @@ -669,7 +711,9 @@ export default class QueueTable extends React.PureComponent { styling, bodyStyling, enablePagination, - useTaskPagesApi + useTaskPagesApi, + reduxCache, + useReduxCache } = this.props; let { totalTaskCount, numberOfPages, rowObjects, casesPerPage } = this.props; @@ -682,7 +726,7 @@ export default class QueueTable extends React.PureComponent { // If we already have the response cached then use the attributes of the response to set the pagination vars. const endpointUrl = this.requestUrl(); - const responseFromCache = this.state.cachedResponses[endpointUrl]; + const responseFromCache = useReduxCache ? reduxCache[endpointUrl] : this.state.cachedResponses[endpointUrl]; if (responseFromCache) { numberOfPages = responseFromCache.task_page_count; @@ -843,6 +887,9 @@ HeaderRow.propTypes = FooterRow.propTypes = Row.propTypes = BodyRows.propTypes = }), onHistoryUpdate: PropTypes.func, preserveFilter: PropTypes.bool, + useReduxCache: PropTypes.bool, + reduxCache: PropTypes.object, + updateReduxCache: PropTypes.func }; Row.propTypes.rowObjects = PropTypes.arrayOf(PropTypes.object); diff --git a/client/app/queue/QueueTableBuilder.jsx b/client/app/queue/QueueTableBuilder.jsx index 0de35805a32..15d1077b12d 100644 --- a/client/app/queue/QueueTableBuilder.jsx +++ b/client/app/queue/QueueTableBuilder.jsx @@ -10,38 +10,12 @@ import QueueTable from './QueueTable'; import TabWindow from '../components/TabWindow'; import Link from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/Link'; import QueueOrganizationDropdown from './components/QueueOrganizationDropdown'; -import { - assignedToColumn, - assignedByColumn, - badgesColumn, - boardIntakeColumn, - completedToNameColumn, - daysOnHoldColumn, - daysSinceLastActionColumn, - daysSinceIntakeColumn, - receiptDateColumn, - daysWaitingColumn, - detailsColumn, - docketNumberColumn, - documentIdColumn, - lastActionColumn, - issueCountColumn, - issueTypesColumn, - readerLinkColumn, - readerLinkColumnWithNewDocsIcon, - regionalOfficeColumn, - taskColumn, - taskOwnerColumn, - taskCompletedDateColumn, - typeColumn, - vamcOwnerColumn -} from './components/TaskTableColumns'; import { tasksWithAppealsFromRawTasks } from './utils'; import COPY from '../../COPY'; -import QUEUE_CONFIG from '../../constants/QUEUE_CONFIG'; import { css } from 'glamor'; import { isActiveOrganizationVHA } from '../queue/selectors'; +import { columnsFromConfig } from './queueTableUtils'; const rootStyles = css({ '.usa-alert + &': { @@ -86,94 +60,6 @@ const QueueTableBuilder = (props) => { return config; }; - const filterValuesForColumn = (column) => - column && column.filterable && column.filter_options; - - const createColumnObject = (column, config, tasks) => { - - const { requireDasRecord } = props; - const filterOptions = filterValuesForColumn(column); - const functionForColumn = { - [QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name]: typeColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.BADGES.name]: badgesColumn(tasks), - [QUEUE_CONFIG.COLUMNS.CASE_DETAILS_LINK.name]: detailsColumn( - tasks, - requireDasRecord, - config.userRole - ), - [QUEUE_CONFIG.COLUMNS.DAYS_ON_HOLD.name]: daysOnHoldColumn( - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_LAST_ACTION.name]: daysSinceLastActionColumn( - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DAYS_WAITING.name]: daysWaitingColumn( - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DOCKET_NUMBER.name]: docketNumberColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.DOCUMENT_COUNT_READER_LINK.name]: readerLinkColumn( - requireDasRecord, - true - ), - [QUEUE_CONFIG.COLUMNS.DOCUMENT_ID.name]: documentIdColumn(), - [QUEUE_CONFIG.COLUMNS.ISSUE_COUNT.name]: issueCountColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.ISSUE_TYPES.name]: issueTypesColumn( - tasks, - filterOptions, - requireDasRecord - ), - [QUEUE_CONFIG.COLUMNS.READER_LINK_WITH_NEW_DOCS_ICON. - name]: readerLinkColumnWithNewDocsIcon(requireDasRecord), - [QUEUE_CONFIG.COLUMNS.REGIONAL_OFFICE.name]: regionalOfficeColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name]: assignedToColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.BOARD_INTAKE.name]: boardIntakeColumn( - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.LAST_ACTION.name]: lastActionColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.TASK_OWNER.name]: taskOwnerColumn( - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.VAMC_OWNER.name]: vamcOwnerColumn( - tasks, - filterOptions - ), - [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNER.name]: completedToNameColumn(), - [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNED_BY.name]: assignedByColumn(), - [QUEUE_CONFIG.COLUMNS.TASK_CLOSED_DATE.name]: taskCompletedDateColumn(), - [QUEUE_CONFIG.COLUMNS.TASK_TYPE.name]: taskColumn(tasks, filterOptions), - [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_INTAKE.name]: daysSinceIntakeColumn(requireDasRecord), - [QUEUE_CONFIG.COLUMNS.RECEIPT_DATE_INTAKE.name]: receiptDateColumn(), - }; - - return functionForColumn[column.name]; - }; - - const columnsFromConfig = (config, tabConfig, tasks) => - (tabConfig.columns || []).map((column) => - createColumnObject(column, config, tasks) - ); - const taskTableTabFactory = (tabConfig, config) => { const savedPaginationOptions = storedPaginationOptions; const tasks = tasksWithAppealsFromRawTasks(tabConfig.tasks); diff --git a/client/app/queue/UnassignedCasesPage.jsx b/client/app/queue/UnassignedCasesPage.jsx index 2c2230feee6..34810f5261e 100644 --- a/client/app/queue/UnassignedCasesPage.jsx +++ b/client/app/queue/UnassignedCasesPage.jsx @@ -29,6 +29,8 @@ import Alert from '../components/Alert'; import LoadingContainer from '../components/LoadingContainer'; import { LOGO_COLORS } from '../constants/AppConstants'; import { css } from 'glamor'; +import querystring from 'querystring'; +import { columnsFromConfig } from './queueTableUtils'; import { DEFAULT_QUEUE_TABLE_SORT } from './constants'; const assignSectionStyling = css({ marginTop: '30px' }); @@ -77,6 +79,49 @@ class UnassignedCasesPage extends React.PureComponent { const HeadingTag = userIsSCTCoordinator ? 'h1' : 'h2'; + // Setup for backend paginated task retrieval + const tabPaginationOptions = querystring.parse(window.location.search.slice(1)); + const queueConfig = this.props.queueConfig; + const tabConfig = queueConfig.tabs?.[0] || {}; + const tabColumns = columnsFromConfig(queueConfig, tabConfig, []); + + if (tabConfig) { + // Order all of the columns from the backend except badges so any included columns will be in front + tabColumns.slice(1).forEach((obj) => { + obj.order = 1; + }); + + // // Setup default sorting for the SCT assign page. + // If there is no sort by column in the pagination options, then use the tab config default sort + if (!tabPaginationOptions.sort_by) { + tabPaginationOptions.sort_by = tabConfig.defaultSort?.sortColName; + tabPaginationOptions.order = tabConfig.defaultSort?.sortAscending ? 'asc' : 'desc'; + } + + } + + const includedColumnProps = { + includeBadges: true, + includeSelect: true, + includeDetailsLink: true, + includeType: true, + includeDocketNumber: true, + includeIssueCount: true, + includeDaysWaiting: true, + includeIssueTypes: Boolean(userIsCamoEmployee), + includeReaderLink: true, + includeNewDocsIcon: true, + }; + + const specialtyCaseTeamProps = { + includeSelect: true, + customColumns: tabColumns, + taskPagesApiEndpoint: tabConfig.task_page_endpoint_base_path, + useTaskPagesApi: true, + tabPaginationOptions, + useReduxCache: true, + }; + return {JUDGE_QUEUE_UNASSIGNED_CASES_PAGE_TITLE} {error && } @@ -97,17 +142,9 @@ class UnassignedCasesPage extends React.PureComponent { } {!this.props.distributionCompleteCasesLoading && { const { queue: { isTaskAssignedToUserSelected, - pendingDistribution + pendingDistribution, + queueConfig }, ui: { userIsCamoEmployee, @@ -136,6 +174,7 @@ const mapStateToProps = (state, ownProps) => { } = state; let taskSelector = judgeAssignTasksSelector(state); + let fromReduxTasks = true; if (userIsCamoEmployee && isVhaCamoOrg(state)) { taskSelector = camoAssignTasksSelector(state); @@ -143,15 +182,17 @@ const mapStateToProps = (state, ownProps) => { if (userIsSCTCoordinator && isSpecialtyCaseTeamOrg(state)) { taskSelector = specialtyCaseTeamAssignTasksSelector(state); + fromReduxTasks = false; } return { tasks: taskSelector, isTaskAssignedToUserSelected, pendingDistribution, + queueConfig, distributionLoading: pendingDistribution !== null, distributionCompleteCasesLoading: pendingDistribution && pendingDistribution.status === 'completed', - selectedTasks: selectedTasksSelector(state, ownProps.userId), + selectedTasks: selectedTasksSelector(state, ownProps.userId, fromReduxTasks), success, error, userIsCamoEmployee: isVhaCamoOrg(state), @@ -178,6 +219,7 @@ UnassignedCasesPage.propTypes = { title: PropTypes.string, detail: PropTypes.string }), + queueConfig: PropTypes.object, userIsCamoEmployee: PropTypes.bool, userIsSCTCoordinator: PropTypes.bool, isVhaCamoOrg: PropTypes.bool, diff --git a/client/app/queue/caching/cachingReducer.js b/client/app/queue/caching/cachingReducer.js new file mode 100644 index 00000000000..7cb4860ea94 --- /dev/null +++ b/client/app/queue/caching/cachingReducer.js @@ -0,0 +1,8 @@ +import { combineReducers } from '@reduxjs/toolkit'; +import queueTableCacheReducer from './queueTableCache.slice'; + +const cachingReducer = combineReducers({ + queueTable: queueTableCacheReducer, +}); + +export default cachingReducer; diff --git a/client/app/queue/caching/queueTableCache.slice.js b/client/app/queue/caching/queueTableCache.slice.js new file mode 100644 index 00000000000..a61beb8f00b --- /dev/null +++ b/client/app/queue/caching/queueTableCache.slice.js @@ -0,0 +1,119 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + cachedResponses: {}, +}; + +// Helper functions for removing tasks from the cachedResponses. +const generateKeyFromUrl = (urlString) => { + // Split the url to only have get params from the cached url key + const params = new URLSearchParams(urlString.split('?')[1]); + const filter = params.get('filter'); + const tab = params.get('tab'); + + // Create a group key based on filter and tab values + const groupKey = `filter=${filter}&tab=${tab}`; + + return groupKey; +}; + +const groupUrlKeysByParams = (urlKeys) => { + const groups = {}; + + Object.keys(urlKeys).forEach((urlString) => { + const groupKey = generateKeyFromUrl(urlString); + + // Initialize the group if it doesn't exist + if (!groups[groupKey]) { + groups[groupKey] = { + urls: [], + count: 0 + }; + } + + // Add the URL key to the corresponding group + groups[groupKey].urls.push(urlString); + }); + + return groups; +}; + +const addToUrlCount = (groupedUrls, urlString, number) => { + const groupKey = generateKeyFromUrl(urlString); + + // Keep a grouped count of shared tasks between similar cached task pages + if (groupedUrls[groupKey]) { + groupedUrls[groupKey].count += number; + } +}; + +const createUrlCountObject = (groupedUrls) => { + const urlCountObject = {}; + + Object.values(groupedUrls).forEach((group) => { + group.urls.forEach((urlString) => { + urlCountObject[urlString] = group.count; + }); + }); + + return urlCountObject; +}; + +const calculatePages = (itemsPerPage, totalItems) => { + return Math.ceil(totalItems / itemsPerPage); +}; +// End of helper functions + +const resetState = () => ({ ...initialState }); + +const queueTableCacheSlice = createSlice({ + name: 'queueTableCache', + initialState, + reducers: { + reset: resetState, + updateQueueTableCache: (state, action) => { + const { key, value } = action.payload; + + state.cachedResponses[key] = value; + }, + removeTaskIdsFromCache: (state, action) => { + const { taskIds } = action.payload; + + const groupedKeys = groupUrlKeysByParams(state.cachedResponses); + + Object.keys(state.cachedResponses).forEach((key) => { + const cachedResponse = state.cachedResponses[key]; + + if (cachedResponse.tasks && Array.isArray(cachedResponse.tasks)) { + const filteredTasks = cachedResponse.tasks.filter((task) => !taskIds.includes(task.id)); + const numberOfExcludedRecords = cachedResponse.tasks.length - filteredTasks.length; + + state.cachedResponses[key].tasks = filteredTasks; + + addToUrlCount(groupedKeys, key, numberOfExcludedRecords); + } + }); + + const urlsWithRemovedTaskCount = createUrlCountObject(groupedKeys); + + Object.keys(state.cachedResponses).forEach((key) => { + const cachedResponse = state.cachedResponses[key]; + const itemsPerPage = cachedResponse.tasks_per_page; + + if (cachedResponse.tasks && Array.isArray(cachedResponse.tasks)) { + state.cachedResponses[key].total_task_count -= urlsWithRemovedTaskCount[key]; + state.task_page_count = calculatePages(itemsPerPage, state.cachedResponses[key].total_task_count); + } + }); + + } + }, +}); + +export const { + reset, + updateQueueTableCache, + removeTaskIdsFromCache +} = queueTableCacheSlice.actions; + +export default queueTableCacheSlice.reducer; diff --git a/client/app/queue/components/TaskTable.jsx b/client/app/queue/components/TaskTable.jsx index f5d84a4b422..ed44160b886 100644 --- a/client/app/queue/components/TaskTable.jsx +++ b/client/app/queue/components/TaskTable.jsx @@ -25,6 +25,7 @@ import { import { setSelectionOfTaskOfUser } from '../QueueActions'; import { hasDASRecord } from '../utils'; import COPY from '../../../COPY'; +import { updateQueueTableCache } from '../caching/queueTableCache.slice'; export class TaskTableUnconnected extends React.PureComponent { getKeyForRow = (rowNumber, object) => object.uniqueId @@ -34,7 +35,7 @@ export class TaskTableUnconnected extends React.PureComponent { } const isTaskSelected = this.props.isTaskAssignedToUserSelected[this.props.userId] || {}; - return isTaskSelected[uniqueId] || false; + return isTaskSelected[uniqueId]?.selected || false; } taskHasDASRecord = (task) => { @@ -57,7 +58,8 @@ export class TaskTableUnconnected extends React.PureComponent { onChange={(selected) => this.props.setSelectionOfTaskOfUser({ userId: this.props.userId, taskId: task.uniqueId, - selected + selected, + task })} /> } : null; } @@ -119,20 +121,23 @@ export class TaskTableUnconnected extends React.PureComponent { } } - render = () => - (this.taskHasDASRecord(task) || !this.props.requireDasRecord) ? null : 'usa-input-error'} - taskPagesApiEndpoint={this.props.taskPagesApiEndpoint} - useTaskPagesApi={this.props.useTaskPagesApi} - tabPaginationOptions={this.props.tabPaginationOptions} - />; + render = () => + (this.taskHasDASRecord(task) || !this.props.requireDasRecord) ? null : 'usa-input-error'} + taskPagesApiEndpoint={this.props.taskPagesApiEndpoint} + useTaskPagesApi={this.props.useTaskPagesApi} + tabPaginationOptions={this.props.tabPaginationOptions} + useReduxCache={this.props.useReduxCache} + reduxCache={this.props.queueTableResponseCache} + updateReduxCache={this.props.updateQueueTableCache} + />; } TaskTableUnconnected.propTypes = { @@ -165,17 +170,21 @@ TaskTableUnconnected.propTypes = { tabPaginationOptions: PropTypes.object, onHistoryUpdate: PropTypes.func, preserveQueueFilter: PropTypes.bool, + queueTableResponseCache: PropTypes.object, + updateQueueTableCache: PropTypes.func, + useReduxCache: PropTypes.bool, }; const mapStateToProps = (state) => ({ isTaskAssignedToUserSelected: state.queue.isTaskAssignedToUserSelected, userIsVsoEmployee: state.ui.userIsVsoEmployee, userRole: state.ui.userRole, - organizationId: state.ui.activeOrganization.id + organizationId: state.ui.activeOrganization.id, + queueTableResponseCache: state.caching.queueTable.cachedResponses }); const mapDispatchToProps = (dispatch) => ( - bindActionCreators({ setSelectionOfTaskOfUser }, dispatch) + bindActionCreators({ setSelectionOfTaskOfUser, updateQueueTableCache }, dispatch) ); export default (connect(mapStateToProps, mapDispatchToProps)(TaskTableUnconnected)); diff --git a/client/app/queue/queueTableUtils.js b/client/app/queue/queueTableUtils.js new file mode 100644 index 00000000000..2c7748c7257 --- /dev/null +++ b/client/app/queue/queueTableUtils.js @@ -0,0 +1,114 @@ +import { + assignedToColumn, + assignedByColumn, + badgesColumn, + boardIntakeColumn, + completedToNameColumn, + daysOnHoldColumn, + daysSinceLastActionColumn, + daysSinceIntakeColumn, + receiptDateColumn, + daysWaitingColumn, + detailsColumn, + docketNumberColumn, + documentIdColumn, + lastActionColumn, + issueCountColumn, + issueTypesColumn, + readerLinkColumn, + readerLinkColumnWithNewDocsIcon, + regionalOfficeColumn, + taskColumn, + taskOwnerColumn, + taskCompletedDateColumn, + typeColumn, + vamcOwnerColumn +} from './components/TaskTableColumns'; +import QUEUE_CONFIG from '../../constants/QUEUE_CONFIG'; + +const filterValuesForColumn = (column) => + column && column.filterable && column.filter_options; + +export const createColumnObject = (column, config, tasks, requireDasRecord) => { + + const filterOptions = filterValuesForColumn(column); + const functionForColumn = { + [QUEUE_CONFIG.COLUMNS.APPEAL_TYPE.name]: typeColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.BADGES.name]: badgesColumn(tasks), + [QUEUE_CONFIG.COLUMNS.CASE_DETAILS_LINK.name]: detailsColumn( + tasks, + requireDasRecord, + config.userRole + ), + [QUEUE_CONFIG.COLUMNS.DAYS_ON_HOLD.name]: daysOnHoldColumn( + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_LAST_ACTION.name]: daysSinceLastActionColumn( + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DAYS_WAITING.name]: daysWaitingColumn( + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DOCKET_NUMBER.name]: docketNumberColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.DOCUMENT_COUNT_READER_LINK.name]: readerLinkColumn( + requireDasRecord, + true + ), + [QUEUE_CONFIG.COLUMNS.DOCUMENT_ID.name]: documentIdColumn(), + [QUEUE_CONFIG.COLUMNS.ISSUE_COUNT.name]: issueCountColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.ISSUE_TYPES.name]: issueTypesColumn( + tasks, + filterOptions, + requireDasRecord + ), + [QUEUE_CONFIG.COLUMNS.READER_LINK_WITH_NEW_DOCS_ICON. + name]: readerLinkColumnWithNewDocsIcon(requireDasRecord), + [QUEUE_CONFIG.COLUMNS.REGIONAL_OFFICE.name]: regionalOfficeColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name]: assignedToColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.BOARD_INTAKE.name]: boardIntakeColumn( + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.LAST_ACTION.name]: lastActionColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.TASK_OWNER.name]: taskOwnerColumn( + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.VAMC_OWNER.name]: vamcOwnerColumn( + tasks, + filterOptions + ), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNER.name]: completedToNameColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNED_BY.name]: assignedByColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_CLOSED_DATE.name]: taskCompletedDateColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_TYPE.name]: taskColumn(tasks, filterOptions), + [QUEUE_CONFIG.COLUMNS.DAYS_SINCE_INTAKE.name]: daysSinceIntakeColumn(requireDasRecord), + [QUEUE_CONFIG.COLUMNS.RECEIPT_DATE_INTAKE.name]: receiptDateColumn(), + }; + + return functionForColumn[column.name]; +}; + +export const columnsFromConfig = (config, tabConfig, tasks) => + (tabConfig.columns || []).map((column) => + createColumnObject(column, config, tasks) + ); diff --git a/client/app/queue/reducers.js b/client/app/queue/reducers.js index 1fa4850fa4c..eb705894b9e 100644 --- a/client/app/queue/reducers.js +++ b/client/app/queue/reducers.js @@ -23,6 +23,7 @@ import caseSelectReducer from '../reader/CaseSelect/CaseSelectReducer'; import editClaimantReducer from './editAppellantInformation/editAppellantInformationSlice'; import cavcDashboardReducer from './cavcDashboard/cavcDashboardReducer'; +import cachingReducer from './caching/cachingReducer'; export const initialState = { judges: {}, @@ -550,7 +551,11 @@ const errorTasksAndAppealsOfAttorney = (state, action) => { const setSelectionOfTaskOfUser = (state, action) => { const isTaskSelected = update(state.isTaskAssignedToUserSelected[action.payload.userId] || {}, { [action.payload.taskId]: { - $set: action.payload.selected + $set: { + selected: action.payload.selected, + task: action.payload.task + }, + } }); @@ -807,7 +812,8 @@ const rootReducer = combineReducers({ substituteAppellant: substituteAppellantReducer, cavcRemand: editCavRemandReducer, editClaimantReducer, - cavcDashboard: cavcDashboardReducer + cavcDashboard: cavcDashboardReducer, + caching: cachingReducer }); export default timeFunction( diff --git a/client/app/queue/selectors.js b/client/app/queue/selectors.js index e0f61f64c6c..bb6640a9290 100644 --- a/client/app/queue/selectors.js +++ b/client/app/queue/selectors.js @@ -10,13 +10,20 @@ import COPY from '../../COPY'; const moment = extendMoment(Moment); -export const selectedTasksSelector = (state, userId) => { - return map(state.queue.isTaskAssignedToUserSelected[userId] || {}, (selected, id) => { - if (!selected) { +export const selectedTasksSelector = (state, userId, fromReduxTasks = true) => { + return map(state.queue.isTaskAssignedToUserSelected[userId] || {}, (task, id) => { + if (!task.selected) { return; } - return state.queue.tasks[id] || state.queue.amaTasks[id]; + // If you are pulling the tasks from the serialized redux tasks + if (fromReduxTasks) { + return state.queue.tasks[id] || state.queue.amaTasks[id]; + } + + // Grabbing tasks from the selectedTasksReduxStore instead of the queue tasks store + return task.task; + }).filter(Boolean); }; diff --git a/db/migrate/20240506140249_add_cols_to_distribution_stats.rb b/db/migrate/20240506140249_add_cols_to_distribution_stats.rb new file mode 100644 index 00000000000..704235a9f45 --- /dev/null +++ b/db/migrate/20240506140249_add_cols_to_distribution_stats.rb @@ -0,0 +1,19 @@ +class AddColsToDistributionStats < ActiveRecord::Migration[6.0] + def change + safety_assured do + add_column :distribution_stats, :legacy_priority_stats, :json, comment: "Priority statistics for any VACOLS Docket" + add_column :distribution_stats, :hearing_priority_stats, :json, comment: "Priority statistics for Hearings Docket" + add_column :distribution_stats, :direct_review_priority_stats, :json, comment: "Priority statistics for Direct Review Docket" + add_column :distribution_stats, :evidence_submission_priority_stats, :json, comment: "Priority statistics for Evidence Submission Docket" + + add_column :distribution_stats, :legacy_stats, :json, comment: "Statistics for any VACOLS Docket" + add_column :distribution_stats, :hearing_stats, :json, comment: "Statistics for Hearings Docket" + add_column :distribution_stats, :direct_review_stats, :json, comment: "Statistics for Direct Review Docket" + add_column :distribution_stats, :evidence_submission_stats, :json, comment: "Statistics for Evidence Submission Docket" + + add_column :distribution_stats, :judge_stats, :json, comment: "Statistics that are specific to judge" + add_column :distribution_stats, :ineligible_judge_stats, :json, comment: "Statistics about appeals tied to ineligible judges" + end + end +end + diff --git a/db/migrate/20240802154252_update_senior_counsel.rb b/db/migrate/20240802154252_update_senior_counsel.rb new file mode 100644 index 00000000000..26ad771c7a4 --- /dev/null +++ b/db/migrate/20240802154252_update_senior_counsel.rb @@ -0,0 +1,41 @@ +class UpdateSeniorCounsel < ActiveRecord::Migration[6.0] + def up + ActiveRecord::Base.transaction do + # Use direct SQL update to bypass STI mechanism + ActiveRecord::Base.connection.execute( + "UPDATE organizations SET type = 'SupervisorySeniorCounsel' WHERE name = 'Supervisory Senior Council'" + ) + + # Fetch the updated organization + organization = Organization.find_by(name: SupervisorySeniorCounsel.first.name) + + # Update attributes + organization.update!( + name: "Supervisory Senior Counsel", + url: "supervisory-senior-counsel" + ) + rescue StandardError => error + puts "Error updating names, types, and URLs: #{error.message}" + raise ActiveRecord::Rollback + end + end + def down + ActiveRecord::Base.transaction do + + # Revert type update using direct SQL + ActiveRecord::Base.connection.execute( + "UPDATE organizations SET type = 'Supervisory Senior Council' WHERE name = 'Supervisory Senior Counsel'" + ) + + # Revert name and url update + organization = Organization.find_by(name: SupervisorySeniorCounsel.first.name) + organization.update!( + name: "Supervisory Senior Council", + url: "supervisory-senior-council" + ) + rescue StandardError => error + puts "Error reverting names, types, and URL: #{error.message}" + raise ActiveRecord::Rollback + end + end +end diff --git a/db/schema.rb b/db/schema.rb index baf01d1ce03..92354b54582 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.define(version: 2024_07_17_145856) do +ActiveRecord::Schema.define(version: 2024_08_02_154252) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -722,7 +722,17 @@ create_table "distribution_stats", comment: "A database table to store a snapshot of variables used during a case distribution event", force: :cascade do |t| t.datetime "created_at", null: false + t.json "direct_review_priority_stats", comment: "Priority statistics for Direct Review Docket" + t.json "direct_review_stats", comment: "Statistics for Direct Review Docket" t.bigint "distribution_id", comment: "ID of the associated Distribution" + t.json "evidence_submission_priority_stats", comment: "Priority statistics for Evidence Submission Docket" + t.json "evidence_submission_stats", comment: "Statistics for Evidence Submission Docket" + t.json "hearing_priority_stats", comment: "Priority statistics for Hearings Docket" + t.json "hearing_stats", comment: "Statistics for Hearings Docket" + t.json "ineligible_judge_stats", comment: "Statistics about appeals tied to ineligible judges" + t.json "judge_stats", comment: "Statistics that are specific to judge" + t.json "legacy_priority_stats", comment: "Priority statistics for any VACOLS Docket" + t.json "legacy_stats", comment: "Statistics for any VACOLS Docket" t.json "levers", comment: "Indicates a snapshot of lever values and is_toggle_active for a distribution" t.json "statistics", comment: "Indicates a snapshot of variables used during the distribution" t.datetime "updated_at", null: false diff --git a/db/seeds/users.rb b/db/seeds/users.rb index b52c83b8c3b..e36698f4269 100644 --- a/db/seeds/users.rb +++ b/db/seeds/users.rb @@ -431,25 +431,25 @@ def create_case_search_only_user def create_split_appeals_test_users ussc = create(:user, css_id: "SPLTAPPLSNOW", - full_name: "Jon SupervisorySeniorCouncilUser Snow", + full_name: "Jon SupervisorySeniorCounselUser Snow", roles: ["Hearing Prep"]) - SupervisorySeniorCouncil.singleton.add_user(ussc) + SupervisorySeniorCounsel.singleton.add_user(ussc) ussc2 = create(:user, css_id: "SPLTAPPLTARGARYEN", - full_name: "Daenerys SupervisorySeniorCouncilUser Targaryen", + full_name: "Daenerys SupervisorySeniorCounselUser Targaryen", roles: ["Hearing Prep"]) - SupervisorySeniorCouncil.singleton.add_user(ussc2) + SupervisorySeniorCounsel.singleton.add_user(ussc2) ussccr = create(:user, css_id: "SPLTAPPLLANNISTER", - full_name: "Jaime SupervisorySeniorCouncilCaseReviewUser Lannister", + full_name: "Jaime SupervisorySeniorCounselCaseReviewUser Lannister", roles: ["Hearing Prep"]) - SupervisorySeniorCouncil.singleton.add_user(ussccr) + SupervisorySeniorCounsel.singleton.add_user(ussccr) CaseReview.singleton.add_user(ussccr) ussccr2 = create(:user, css_id: "SPLTAPPLSTARK", - full_name: "Ned SupervisorySeniorCouncilCaseReviewUser Stark", + full_name: "Ned SupervisorySeniorCounselCaseReviewUser Stark", roles: ["Hearing Prep"]) - SupervisorySeniorCouncil.singleton.add_user(ussccr2) + SupervisorySeniorCounsel.singleton.add_user(ussccr2) CaseReview.singleton.add_user(ussccr2) end @@ -577,7 +577,7 @@ def create_qa_test_users def create_qa_ssc_avlj_attorney atty = create(:user, css_id: "QSSCAVLJ", full_name: "QA SSC_AVLJ Attorney", roles: ["Hearing Prep"]) - SupervisorySeniorCouncil.singleton.add_user(atty) + SupervisorySeniorCounsel.singleton.add_user(atty) create(:staff, user: atty, sattyid: "9999", smemgrp: "9999") end diff --git a/lib/tasks/add_admins.rake b/lib/tasks/add_admins.rake index 630e7648407..bfe439eead4 100644 --- a/lib/tasks/add_admins.rake +++ b/lib/tasks/add_admins.rake @@ -72,39 +72,39 @@ namespace :add_admins do desc "create SSC org and test users for UAT testing" task :create_ssc_and_users do # rubocop:disable Rails/RakeEnvironment STDOUT.puts("Creating the SSC org and all test users") - SupervisorySeniorCouncil.singleton + SupervisorySeniorCounsel.singleton ussc = User.create!( station_id: 101, css_id: "SPLTAPPLJERRY", - full_name: "Jerry SupervisorySeniorCouncilUser", + full_name: "Jerry SupervisorySeniorCounselUser", roles: ["Hearing Prep"] ) - SupervisorySeniorCouncil.singleton.add_user(ussc) + SupervisorySeniorCounsel.singleton.add_user(ussc) STDOUT.puts("Created user #{ussc.css_id}") ussc2 = User.create!( station_id: 101, css_id: "SPLTAPPLTOM", - full_name: "Tom SupervisorySeniorCouncilUser", + full_name: "Tom SupervisorySeniorCounselUser", roles: ["Hearing Prep"] ) - SupervisorySeniorCouncil.singleton.add_user(ussc2) + SupervisorySeniorCounsel.singleton.add_user(ussc2) STDOUT.puts("Created user #{ussc2.css_id}") ussccr = User.create!( station_id: 101, css_id: "SPLTAPPLBILLY", - full_name: "Billy SupervisorySeniorCouncilCaseReviewUser", + full_name: "Billy SupervisorySeniorCounselCaseReviewUser", roles: ["Hearing Prep"] ) - SupervisorySeniorCouncil.singleton.add_user(ussccr) + SupervisorySeniorCounsel.singleton.add_user(ussccr) STDOUT.puts("Created user #{ussccr.css_id}") CaseReview.singleton.add_user(ussccr) ussccr2 = User.create!( station_id: 101, css_id: "SPLTAPPLSUSAN", - full_name: "Susan SupervisorySeniorCouncilCaseReviewUser", + full_name: "Susan SupervisorySeniorCounselCaseReviewUser", roles: ["Hearing Prep"] ) - SupervisorySeniorCouncil.singleton.add_user(ussccr2) + SupervisorySeniorCounsel.singleton.add_user(ussccr2) CaseReview.singleton.add_user(ussccr2) STDOUT.puts("Created user #{ussccr2.css_id}") @@ -189,7 +189,7 @@ namespace :add_admins do ihp = IhpDraft.create!( appeal_id: appeal.id, appeal_type: "Appeal", - organization_id: Organization.find_by(name: "Supervisory Senior Council").id, + organization_id: Organization.find_by(name: "Supervisory Senior Counsel").id, path: path ) diff --git a/spec/factories/task.rb b/spec/factories/task.rb index a184edca648..3478613c729 100644 --- a/spec/factories/task.rb +++ b/spec/factories/task.rb @@ -602,6 +602,20 @@ def self.find_first_task_or_create(appeal, task_type, **kwargs) associated_attorney { nil } end + trait :advanced_on_docket do + appeal do + create(:appeal, + :with_vha_issue, + :with_post_intake_tasks, + :direct_review_docket, + :advanced_on_docket_due_to_age) + end + end + + trait :cavc_type do + appeal { create(:appeal, :type_cavc_remand) } + end + trait :action_required do after(:create) do |task, evaluator| task.update(status: Constants.TASK_STATUSES.in_progress) diff --git a/spec/feature/intake/appeal/edit_spec.rb b/spec/feature/intake/appeal/edit_spec.rb index e8fa74a765c..91f30044041 100644 --- a/spec/feature/intake/appeal/edit_spec.rb +++ b/spec/feature/intake/appeal/edit_spec.rb @@ -391,11 +391,11 @@ end end - context "User is a member of the Supervisory Senior Council" do + context "User is a member of the Supervisory Senior Counsel" do before do User.authenticate!(user: current_user) FeatureToggle.enable!(:split_appeal_workflow) - OrganizationsUser.make_user_admin(current_user, SupervisorySeniorCouncil.singleton) + OrganizationsUser.make_user_admin(current_user, SupervisorySeniorCounsel.singleton) end after { FeatureToggle.disable!(:split_appeal_workflow) } diff --git a/spec/feature/queue/case_details_spec.rb b/spec/feature/queue/case_details_spec.rb index 579ab417382..36a2e856a26 100644 --- a/spec/feature/queue/case_details_spec.rb +++ b/spec/feature/queue/case_details_spec.rb @@ -1112,10 +1112,10 @@ def wait_for_page_render end end - context "When a current user is a member of Supervisory Senior Council organization" do + context "When a current user is a member of Supervisory Senior Counsel organization" do let(:appeal) { create(:appeal) } let(:current_user) { create(:user) } - let!(:organization) { SupervisorySeniorCouncil.singleton } + let!(:organization) { SupervisorySeniorCounsel.singleton } let!(:organization_user) { OrganizationsUser.make_user_admin(current_user, organization) } let(:receipt_date) { Time.zone.today - 20.days } let(:profile_date) { (receipt_date - 30.days).to_datetime } @@ -1143,7 +1143,7 @@ def wait_for_page_render end end - context "When a user isn't a member of the Supervisory Senior Council" do + context "When a user isn't a member of the Supervisory Senior Counsel" do let(:appeal) { create(:appeal) } let(:current_user) { create(:user) } let(:receipt_date) { Time.zone.today - 20.days } diff --git a/spec/jobs/push_priority_appeals_to_judges_job_spec.rb b/spec/jobs/push_priority_appeals_to_judges_job_spec.rb index c6a2bae3ea1..f46cb7d0826 100644 --- a/spec/jobs/push_priority_appeals_to_judges_job_spec.rb +++ b/spec/jobs/push_priority_appeals_to_judges_job_spec.rb @@ -25,7 +25,7 @@ def to_judge_hash(arr) expect_any_instance_of(PushPriorityAppealsToJudgesJob) .to receive(:distribute_genpop_priority_appeals).and_return([]) expect_any_instance_of(PushPriorityAppealsToJudgesJob) - .to receive(:send_job_report).and_return([]) + .to receive(:generate_report).and_return([]) end after { FeatureToggle.disable!(:acd_distribute_by_docket_date) } @@ -435,7 +435,7 @@ def to_judge_hash(arr) end end - context ".slack_report" do + context ".generate_report" do let!(:job) { PushPriorityAppealsToJudgesJob.new } let(:previous_distributions) { to_judge_hash([4, 3, 2, 1, 0]) } let!(:judge) { create(:user, :judge, :with_vacols_judge_record) } @@ -517,9 +517,10 @@ def to_judge_hash(arr) end end - subject { job.slack_report } + subject { job.generate_report } before do + FeatureToggle.disable!(:acd_distribute_by_docket_date) job.instance_variable_set(:@tied_distributions, distributed_cases) job.instance_variable_set(:@genpop_distributions, distributed_cases) job.instance_variable_set(:@distributions, distributed_cases) @@ -532,63 +533,86 @@ def to_judge_hash(arr) after { FeatureToggle.disable!(:acd_distribute_by_docket_date) } it "using Automatic Case Distribution module" do - expect(subject.second).to eq "*Number of cases tied to judges distributed*: 10" - expect(subject.third).to eq "*Number of general population cases distributed*: 10" - today = Time.zone.now.to_date legacy_days_waiting = (today - legacy_priority_case.bfdloout.to_date).to_i - expect(subject[3]).to eq "*Age of oldest legacy case*: #{legacy_days_waiting} days" direct_review_days_waiting = (today - ready_priority_direct_case.ready_for_distribution_at.to_date).to_i - expect(subject[4]).to eq "*Age of oldest direct_review case*: #{direct_review_days_waiting} days" evidence_submission_days_waiting = (today - ready_priority_evidence_case.ready_for_distribution_at.to_date).to_i - expect(subject[5]).to eq "*Age of oldest evidence_submission case*: #{evidence_submission_days_waiting} days" hearing_days_waiting = (today - ready_priority_hearing_case.ready_for_distribution_at.to_date).to_i - expect(subject[6]).to eq "*Age of oldest hearing case*: #{hearing_days_waiting} days" - - expect(subject[7]).to eq "*Total Number of appeals _not_ distributed*: 4" - expect(subject[8]).to eq "*Number of legacy appeals _not_ distributed*: 1" - expect(subject[9]).to eq "*Number of direct_review appeals _not_ distributed*: 1" - expect(subject[10]).to eq "*Number of evidence_submission appeals _not_ distributed*: 1" - expect(subject[11]).to eq "*Number of hearing appeals _not_ distributed*: 1" - expect(subject[12]).to eq "*Number of Legacy Hearing Non Genpop appeals _not_ distributed*: 1" - - expect(subject[15]).to eq "Priority Target: 6" - expect(subject[16]).to eq "Previous monthly distributions {judge_id=>count}: #{previous_distributions}" - expect(subject[17]).to eq COPY::PRIORITY_PUSH_WARNING_MESSAGE - expect(subject[18].include?(ready_priority_hearing_case.uuid)).to be true - expect(subject[18].include?(ready_priority_evidence_case.uuid)).to be true - expect(subject[18].include?(ready_priority_direct_case.uuid)).to be true - expect(subject[19].include?(legacy_priority_case.bfkey)).to be true + excluded_judges = JudgeTeam.judges_with_exclude_appeals_from_affinity.pluck(:css_id) + + [ + "*Number of cases tied to judges distributed*: 10", + "*Number of general population cases distributed*: 10", + "Priority Target: 6", + "*Age of oldest legacy case*: #{legacy_days_waiting} days", + "*Age of oldest direct_review case*: #{direct_review_days_waiting} days", + "*Age of oldest evidence_submission case*: #{evidence_submission_days_waiting} days", + "*Age of oldest hearing case*: #{hearing_days_waiting} days", + "", + "*Total Number of appeals _not_ distributed*: 4", + "*Number of legacy appeals _not_ distributed*: 1", + "*Number of direct_review appeals _not_ distributed*: 1", + "*Number of evidence_submission appeals _not_ distributed*: 1", + "*Number of hearing appeals _not_ distributed*: 1", + "*Number of Legacy Hearing Non Genpop appeals _not_ distributed*: 1", + "", + "*Number of legacy appeals in affinity date window*: not implemented", + "*Number of legacy appeals out of affinity date window*: not implemented", + "*Number of direct_review appeals in affinity date window*: 0", + "*Number of direct_review appeals out of affinity date window*: 0", + "*Number of evidence_submission appeals in affinity date window*: 0", + "*Number of evidence_submission appeals out of affinity date window*: 0", + "*Number of hearing appeals in affinity date window*: 0", + "*Number of hearing appeals out of affinity date window*: 0", + "", + "*Debugging information*", + "*Excluded Judges*: #{excluded_judges}", + "Previous monthly distributions {judge_id=>count}: #{previous_distributions}" + ].each_with_index do |line, index| + expect(subject[index]).to eq line + end end it "using By Docket Date Distribution module" do FeatureToggle.enable!(:acd_distribute_by_docket_date) - expect(subject.second).to eq "*Number of cases distributed*: 10" today = Time.zone.now.to_date legacy_days_waiting = (today - legacy_priority_case.bfd19.to_date).to_i - expect(subject[2]).to eq "*Age of oldest legacy case*: #{legacy_days_waiting} days" direct_review_days_waiting = (today - ready_priority_direct_case.receipt_date).to_i - expect(subject[3]).to eq "*Age of oldest direct_review case*: #{direct_review_days_waiting} days" evidence_submission_days_waiting = (today - ready_priority_evidence_case.receipt_date).to_i - expect(subject[4]).to eq "*Age of oldest evidence_submission case*: #{evidence_submission_days_waiting} days" hearing_days_waiting = (today - ready_priority_hearing_case.receipt_date).to_i - expect(subject[5]).to eq "*Age of oldest hearing case*: #{hearing_days_waiting} days" - - expect(subject[6]).to eq "*Total Number of appeals _not_ distributed*: 4" - expect(subject[7]).to eq "*Number of legacy appeals _not_ distributed*: 1" - expect(subject[8]).to eq "*Number of direct_review appeals _not_ distributed*: 1" - expect(subject[9]).to eq "*Number of evidence_submission appeals _not_ distributed*: 1" - expect(subject[10]).to eq "*Number of hearing appeals _not_ distributed*: 1" - expect(subject[11]).to eq "*Number of Legacy Hearing Non Genpop appeals _not_ distributed*: 1" - - expect(subject[14]).to eq "Priority Target: 6" - expect(subject[15]).to eq "Previous monthly distributions {judge_id=>count}: #{previous_distributions}" - expect(subject[16]).to eq COPY::PRIORITY_PUSH_WARNING_MESSAGE - expect(subject[17].include?(ready_priority_hearing_case.uuid)).to be true - expect(subject[17].include?(ready_priority_evidence_case.uuid)).to be true - expect(subject[17].include?(ready_priority_direct_case.uuid)).to be true - expect(subject[18].include?(legacy_priority_case.bfkey)).to be true + excluded_judges = JudgeTeam.judges_with_exclude_appeals_from_affinity.pluck(:css_id) + + [ + "*Number of cases distributed*: 10", + "Priority Target: 6", + "*Age of oldest legacy case*: #{legacy_days_waiting} days", + "*Age of oldest direct_review case*: #{direct_review_days_waiting} days", + "*Age of oldest evidence_submission case*: #{evidence_submission_days_waiting} days", + "*Age of oldest hearing case*: #{hearing_days_waiting} days", + "", + "*Total Number of appeals _not_ distributed*: 4", + "*Number of legacy appeals _not_ distributed*: 1", + "*Number of direct_review appeals _not_ distributed*: 1", + "*Number of evidence_submission appeals _not_ distributed*: 1", + "*Number of hearing appeals _not_ distributed*: 1", + "*Number of Legacy Hearing Non Genpop appeals _not_ distributed*: 1", + "", + "*Number of legacy appeals in affinity date window*: not implemented", + "*Number of legacy appeals out of affinity date window*: not implemented", + "*Number of direct_review appeals in affinity date window*: 0", + "*Number of direct_review appeals out of affinity date window*: 0", + "*Number of evidence_submission appeals in affinity date window*: 0", + "*Number of evidence_submission appeals out of affinity date window*: 0", + "*Number of hearing appeals in affinity date window*: 0", + "*Number of hearing appeals out of affinity date window*: 0", + "", + "*Debugging information*", + "*Excluded Judges*: #{excluded_judges}", + "Previous monthly distributions {judge_id=>count}: #{previous_distributions}" + ].each_with_index do |line, index| + expect(subject[index]).to eq line + end end end diff --git a/spec/models/concerns/by_docket_date_distribution_spec.rb b/spec/models/concerns/by_docket_date_distribution_spec.rb index 49d56ea6454..46da43129e6 100644 --- a/spec/models/concerns/by_docket_date_distribution_spec.rb +++ b/spec/models/concerns/by_docket_date_distribution_spec.rb @@ -11,6 +11,18 @@ class ByDocketDateDistributionTest def batch_size 12 end + + def team_size + 5 + end + + def judge_tasks + [] + end + + def judge_legacy_tasks + [] + end end before(:each) do @@ -192,31 +204,56 @@ def add_dates_to_date_array(num) context "#ama_statistics" do before do + FeatureToggle.enable!(:acd_distribute_by_docket_date) + create(:case_distribution_lever, :cavc_affinity_days) @new_acd.instance_variable_set(:@appeals, []) end + after { FeatureToggle.disable!(:acd_distribute_by_docket_date) } + it "returns a hash with keys" do - statistics = @new_acd.send(:ama_statistics) + ama_statistics = @new_acd.send(:ama_statistics) + statistics = ama_statistics[:statistics] + + expect(statistics).to have_key(:batch_size) + expect(statistics).to have_key(:total_batch_size) + expect(statistics).to have_key(:priority_target) + expect(statistics).to have_key(:priority_count) + expect(statistics).to have_key(:nonpriority_count) + expect(statistics).to have_key(:nonpriority_iterations) + expect(statistics).to have_key(:sct_appeals) - expect(statistics).to include(:batch_size) - expect(statistics).to include(:total_batch_size) - expect(statistics).to include(:priority_target) - expect(statistics).to include(:priority) - expect(statistics).to include(:nonpriority) - expect(statistics).to include(:algorithm) + ineligible_judge_stats = ama_statistics[:ineligible_judge_stats] + expect(ineligible_judge_stats).to have_key(:distributed_cases_tied_to_ineligible_judges) - priority_stats = statistics[:priority] - nonpriority_stats = statistics[:nonpriority] + judge_stats = ama_statistics[:judge_stats] - expect(priority_stats).to include(:count) - expect(priority_stats).to include(:legacy_hearing_tied_to) - expect(nonpriority_stats).to include(:count) - expect(nonpriority_stats).to include(:legacy_hearing_tied_to) - expect(nonpriority_stats).to include(:iterations) + expect(judge_stats).to have_key(:team_size) + expect(judge_stats).to have_key(:ama_judge_assigned_tasks) + expect(judge_stats).to have_key(:legacy_assigned_tasks) + expect(judge_stats).to have_key(:settings) @new_acd.dockets.each_key do |sym| - expect(priority_stats).to include(sym) - expect(nonpriority_stats).to include(sym) + # priority stats + expect(ama_statistics).to have_key("#{sym}_priority_stats".to_sym) + + priority_stats = ama_statistics["#{sym}_priority_stats".to_sym] + expect(priority_stats).to have_key(:count) + expect(priority_stats).to have_key(:affinity_date) + + priority_affinity_date = priority_stats[:affinity_date] + expect(priority_affinity_date).to have_key(:in_window) + expect(priority_affinity_date).to have_key(:out_of_window) + + # non priority stats + expect(ama_statistics).to have_key("#{sym}_stats".to_sym) + nonpriority_stats = ama_statistics["#{sym}_stats".to_sym] + expect(nonpriority_stats).to have_key(:count) + expect(nonpriority_stats).to have_key(:affinity_date) + + nonpriority_affinity_date = nonpriority_stats[:affinity_date] + expect(nonpriority_affinity_date).to have_key(:in_window) + expect(nonpriority_affinity_date).to have_key(:out_of_window) end end diff --git a/spec/models/distribution_spec.rb b/spec/models/distribution_spec.rb index fe5118272e2..66aebd9302d 100644 --- a/spec/models/distribution_spec.rb +++ b/spec/models/distribution_spec.rb @@ -152,10 +152,19 @@ context "#distribute!" do let(:statistics) do { - batch_size: 0, direct_review_due_count: 0, direct_review_proportion: 0, - evidence_submission_proportion: 0, hearing_proportion: 0, legacy_hearing_backlog_count: 0, - legacy_proportion: 0.0, nonpriority_iterations: 0, priority_count: 0, total_batch_size: 0, - algorithm: "proportions", sct_appeals: 0 + statistics: { + batch_size: 0, + direct_review_due_count: 0, + direct_review_proportion: 0, + evidence_submission_proportion: 0, + hearing_proportion: 0, + legacy_hearing_backlog_count: 0, + legacy_proportion: 0.0, + nonpriority_iterations: 0, + priority_count: 0, + total_batch_size: 0, + sct_appeals: 0 + } } end let(:result_stats) do @@ -198,7 +207,7 @@ it "calls requested_distribution" do expect(new_distribution).to receive(:requested_distribution) - allow(new_distribution).to receive(:ama_statistics).and_return({}) + allow(new_distribution).to receive(:ama_statistics).and_return(statistics) new_distribution.distribute! expect(new_distribution.reload.status).to eq "completed" end @@ -209,7 +218,7 @@ it "calls priority_push_distribution" do expect(new_distribution).to receive(:priority_push_distribution) - allow(new_distribution).to receive(:ama_statistics).and_return({}) + allow(new_distribution).to receive(:ama_statistics).and_return(statistics) new_distribution.distribute! expect(new_distribution.reload.status).to eq "completed" end diff --git a/spec/models/organizations/judge_team_spec.rb b/spec/models/organizations/judge_team_spec.rb index 5d0601ae3e4..7deec9f7187 100644 --- a/spec/models/organizations/judge_team_spec.rb +++ b/spec/models/organizations/judge_team_spec.rb @@ -192,7 +192,7 @@ end end - describe ".judges_with_exclude_appeals_from_affinity" do + describe ".judge_ids_with_exclude_appeals_from_affinity" do before { FeatureToggle.enable!(:acd_exclude_from_affinity) } after { FeatureToggle.disable!(:acd_exclude_from_affinity) } @@ -203,7 +203,7 @@ before { populate_judge_team_for_testing(judge_team, judge, [create(:user), user]) } it "returns nil as the default value for exclude_appeals_from_affinity is false" do - expect(JudgeTeam.judges_with_exclude_appeals_from_affinity).to eq([]) + expect(JudgeTeam.judge_ids_with_exclude_appeals_from_affinity).to eq([]) expect(judge_team.exclude_appeals_from_affinity).to be_falsey expect(judge_team.status).to eq("active") end @@ -226,12 +226,12 @@ judge_team.reload expect(judge_team.exclude_appeals_from_affinity).to be true expect(judge_team.status).to eq("active") - expect(JudgeTeam.judges_with_exclude_appeals_from_affinity).to eq([judge.id]) + expect(JudgeTeam.judge_ids_with_exclude_appeals_from_affinity).to eq([judge.id]) judge_team2.reload expect(judge_team2.exclude_appeals_from_affinity).to be true expect(judge_team2.status).to eq("inactive") - expect(JudgeTeam.judges_with_exclude_appeals_from_affinity).to_not include(judge2.id) + expect(JudgeTeam.judge_ids_with_exclude_appeals_from_affinity).to_not include(judge2.id) end end end diff --git a/spec/models/organizations/supervisory_senior_council_spec.rb b/spec/models/organizations/supervisory_senior_council_spec.rb deleted file mode 100644 index d9e56e9655a..00000000000 --- a/spec/models/organizations/supervisory_senior_council_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -describe SupervisorySeniorCouncil do - describe ".singleton" do - it "is named correctly" do - expect(SupervisorySeniorCouncil.singleton).to have_attributes(name: "Supervisory Senior Council") - end - - it "will only have one SupervisorySeniorCouncil no matter how many times it is run" do - SupervisorySeniorCouncil.singleton - SupervisorySeniorCouncil.singleton - expect(Organization.where(name: "Supervisory Senior Council").count).to eq(1) - end - - it "will have the correct url name" do - expect(SupervisorySeniorCouncil.singleton).to have_attributes(url: "supervisory-senior-council") - end - end - - describe ".users_can_create_mail_task?" do - it "should always be true" do - expect(SupervisorySeniorCouncil.singleton.users_can_create_mail_task?).to eq(true) - end - end - - describe ".can_receive_task?" do - it "returns false because the COB hasn't started using Queue yet" do - expect(SupervisorySeniorCouncil.singleton.can_receive_task?(nil)).to eq(false) - end - end -end diff --git a/spec/models/organizations/supervisory_senior_counsel_spec.rb b/spec/models/organizations/supervisory_senior_counsel_spec.rb new file mode 100644 index 00000000000..66bdf691dea --- /dev/null +++ b/spec/models/organizations/supervisory_senior_counsel_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +describe SupervisorySeniorCounsel do + describe ".singleton" do + it "is named correctly" do + expect(SupervisorySeniorCounsel.singleton).to have_attributes(name: "Supervisory Senior Counsel") + end + + it "will only have one SupervisorySeniorCounsel no matter how many times it is run" do + SupervisorySeniorCounsel.singleton + SupervisorySeniorCounsel.singleton + expect(Organization.where(name: "Supervisory Senior Counsel").count).to eq(1) + end + + it "will have the correct url name" do + expect(SupervisorySeniorCounsel.singleton).to have_attributes(url: "supervisory-senior-counsel") + end + end + + describe ".users_can_create_mail_task?" do + it "should always be true" do + expect(SupervisorySeniorCounsel.singleton.users_can_create_mail_task?).to eq(true) + end + end + + describe ".can_receive_task?" do + it "returns false because the COB hasn't started using Queue yet" do + expect(SupervisorySeniorCounsel.singleton.can_receive_task?(nil)).to eq(false) + end + end +end diff --git a/spec/models/regional_office_spec.rb b/spec/models/regional_office_spec.rb index 74da341153b..2659f39e49c 100644 --- a/spec/models/regional_office_spec.rb +++ b/spec/models/regional_office_spec.rb @@ -73,22 +73,6 @@ end end - context ".valid?" do - RegionalOffice.all.each do |ro| - it "regional office (#{ro.key}) is valid?" do - expect(ro.valid?).to eq true - end - end - end - - context ".facility_id" do - RegionalOffice.all.each do |ro| - it "regional office (#{ro.key}) does not throw when facility id is called" do - expect { ro.facility_id }.not_to raise_error - end - end - end - context ".ro_facility_ids" do subject { RegionalOffice.ro_facility_ids } @@ -106,12 +90,6 @@ end context ".street_address" do - RegionalOffice.all.each do |ro| - it "regional office (#{ro.key}) does not throw when street_address is called" do - expect { ro.street_address }.not_to raise_error - end - end - it "RO87 has nil address" do ro = RegionalOffice.find!("RO87") @@ -126,12 +104,6 @@ end context ".full_address" do - RegionalOffice.all.each do |ro| - it "regional office (#{ro.key}) does not throw when full_address is called" do - expect { ro.full_address }.not_to raise_error - end - end - it "RO87 has nil address" do ro = RegionalOffice.find!("RO87") @@ -151,17 +123,14 @@ end end - context ".name" do + context "individual RO attribute accessors do not raise errors" do RegionalOffice.all.each do |ro| - it "regional office (#{ro.key}) does not throw when name is called" do + it "regional office (#{ro.key})" do + expect(ro.valid?).to eq true + expect { ro.facility_id }.not_to raise_error + expect { ro.street_address }.not_to raise_error + expect { ro.full_address }.not_to raise_error expect { ro.name }.not_to raise_error - end - end - end - - context ".zip_code" do - RegionalOffice.all.each do |ro| - it "regional office (#{ro.key}) does not throw when zip_code is called" do expect { ro.zip_code }.not_to raise_error end end diff --git a/spec/models/serializers/work_queue/appeal_serializer_spec.rb b/spec/models/serializers/work_queue/appeal_serializer_spec.rb index 790efc935c4..1423db5f1e4 100644 --- a/spec/models/serializers/work_queue/appeal_serializer_spec.rb +++ b/spec/models/serializers/work_queue/appeal_serializer_spec.rb @@ -60,10 +60,10 @@ end end - context "when an appeal has a user that is part of the Supervisory Senior Council" do + context "when an appeal has a user that is part of the Supervisory Senior Counsel" do let!(:appeal) { create(:appeal) } let!(:current_user) { create(:user) } - let!(:organization) { SupervisorySeniorCouncil.singleton } + let!(:organization) { SupervisorySeniorCounsel.singleton } let!(:organization_user) { OrganizationsUser.make_user_admin(current_user, organization) } before do