diff --git a/.fasterer.yml b/.fasterer.yml index 6e0821c009a..b679a509670 100644 --- a/.fasterer.yml +++ b/.fasterer.yml @@ -14,3 +14,7 @@ exclude_paths: - "db/seeds.rb" - "vendor/**/*.rb" - "app/mappers/zip_code_to_lat_lng_mapper.rb" + + # Flagged for using sort instead of using sort_by - however due to complexity of sorting + # conditionally makes more sense to use sort + - "app/services/auto_assignable_user_finder.rb" diff --git a/Gemfile b/Gemfile index 17eec027d43..71763cae1b5 100644 --- a/Gemfile +++ b/Gemfile @@ -114,8 +114,8 @@ group :test, :development, :demo do gem "rails-erd" gem "rb-readline" gem "rspec" - gem "rspec-rails" # For CircleCI test metadata analysis + gem "rspec-rails" gem "rspec_junit_formatter" gem "rswag-specs" gem "rubocop", "= 0.83", require: false diff --git a/Makefile.example b/Makefile.example index e27b6efcb99..81c8c29d740 100644 --- a/Makefile.example +++ b/Makefile.example @@ -113,6 +113,10 @@ realclean: clean ## TODO rm -rf client/node_modules rm -f client/package-lock.json +precompile: ## Precompiles assets for testing + bundle exec rake assets:precompile + + facols-bash: ## Connect to the docker FACOLS instance docker exec --tty -i VACOLS_DB bash @@ -286,6 +290,8 @@ client-build-all client-all: client-test client-demo ## Builds webpack for both one-test: ## run the rspec test passed in bundle exec rspec $(RUN_ARGS) +clean-test: clean precompile one-test ## Cleans and precompiles assets for testing, then runs spec test passed in + run-all-queues: ## start shoryuken with all queues bundle exec shoryuken -q caseflow_development_send_notifications caseflow_development_high_priority caseflow_development_low_priority -R diff --git a/app/controllers/appeals_controller.rb b/app/controllers/appeals_controller.rb index cdbb1a6e236..0fed6bf1502 100644 --- a/app/controllers/appeals_controller.rb +++ b/app/controllers/appeals_controller.rb @@ -42,7 +42,7 @@ def show_case_list format.html { render template: "queue/index" } format.json do result = CaseSearchResultsForCaseflowVeteranId.new( - caseflow_veteran_ids: params[:veteran_ids]&.split(","), user: current_user + caseflow_veteran_ids: appeals_controller_params[:veteran_ids]&.split(","), user: current_user ).search_call render_search_results_as_json(result) @@ -52,7 +52,7 @@ def show_case_list # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def fetch_notification_list - appeals_id = params[:appeals_id] + appeals_id = appeals_controller_params[:appeals_id] respond_to do |format| format.json do results = find_notifications_by_appeals_id(appeals_id) @@ -138,7 +138,7 @@ def show format.html { render template: "queue/index" } format.json do if appeal.accessible? - id = params[:appeal_id] + id = url_appeal_uuid MetricsService.record("Get appeal information for ID #{id}", service: :queue, name: "AppealsController.show") do @@ -162,16 +162,15 @@ def edit helper_method :appeal, :url_appeal_uuid def appeal - @appeal ||= Appeal.find_appeal_by_uuid_or_find_or_create_legacy_appeal_by_vacols_id(params[:appeal_id]) + @appeal ||= Appeal.find_appeal_by_uuid_or_find_or_create_legacy_appeal_by_vacols_id(url_appeal_uuid) end def url_appeal_uuid - params[:appeal_id] + params.permit(:appeal_id)[:appeal_id] end def update if appeal.is_a?(LegacyAppeal) && feature_enabled?(:legacy_mst_pact_identification) - legacy_mst_pact_updates elsif request_issues_update.perform! set_flash_success_message @@ -187,8 +186,50 @@ def update end end + def active_evidence_submissions + appeal = Appeal.find(url_appeal_uuid) + render json: appeal.evidence_submission_task + end + private + REQUEST_ISSUE_PARAMS = %w[ + request_issue_id + rating_issue_reference_id + rating_decision_reference_id + rating_issue_profile_date + notes + ramp_claim_id + vacols_id + vacols_sequence_id + contested_decision_issue_id + vbms_mst_status + vbms_pact_status + rating_issue_diagnostic_code + mst_status_update_reason_notes + pact_status_update_reason_notes + benefit_type + nonrating_issue_category + decision_text + decision_date + ineligible_due_to_id + ineligible_reason + withdrawal_date + is_predocket_needed + mst_status + pact_status + ].freeze + + def appeals_controller_params + params.permit( + :appeal_id, + :any, + :appeals_id, + :veteran_ids, + request_issues: REQUEST_ISSUE_PARAMS + ) + end + def create_subtasks! # if cc appeal, create SendInitialNotificationLetterTask if appeal.contested_claim? && feature_enabled?(:cc_appeal_workflow) @@ -216,7 +257,7 @@ def request_issues_update @request_issues_update ||= RequestIssuesUpdate.new( user: current_user, review: appeal, - request_issues_data: params[:request_issues] + request_issues_data: appeals_controller_params[:request_issues] ) end @@ -284,7 +325,7 @@ def mst_and_pact_edited_issues pact_removed = 0 # get edited issues from params and reject new issues without id if !appeal.is_a?(LegacyAppeal) - existing_issues = params[:request_issues].reject { |iss| iss[:request_issue_id].nil? } + existing_issues = appeals_controller_params[:request_issues].reject { |iss| iss[:request_issue_id].nil? } # get added issues new_issues = request_issues_update.after_issues - request_issues_update.before_issues @@ -448,7 +489,7 @@ def create_legacy_issue_update_task(before_issue, current_issue) # close out any tasks that might be open open_issue_task = Task.where( assigned_to: SpecialIssueEditTeam.singleton - ).where(status: "assigned").where(appeal: appeal) + ).where(status: Constants.TASK_STATUSES.assigned).where(appeal: appeal) open_issue_task[0].delete unless open_issue_task.empty? task = IssuesUpdateTask.create!( diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c34519195e4..615d9971158 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,7 +9,7 @@ class ApplicationController < ApplicationBaseController before_action :set_raven_user before_action :verify_authentication before_action :set_paper_trail_whodunnit - before_action :deny_vso_access, except: [:unauthorized, :feedback] + before_action :deny_vso_access, except: [:unauthorized, :feedback, :under_construction] before_action :set_no_cache_headers rescue_from StandardError do |e| @@ -196,7 +196,12 @@ def defult_menu_items def manage_teams_menu_items current_user.administered_teams.map do |team| { - title: "#{team.name} team management", + title: + if team.type == InboundOpsTeam.singleton.type + "#{team.name} management" + else + "#{team.name} team management" + end, link: team.user_admin_path } end diff --git a/app/controllers/concerns/correspondence_controller_concern.rb b/app/controllers/concerns/correspondence_controller_concern.rb new file mode 100644 index 00000000000..8b6148ffe9b --- /dev/null +++ b/app/controllers/concerns/correspondence_controller_concern.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/ModuleLength +# :reek:DataClump + +# Contains most of the logic inside of CorrespondenceController +module CorrespondenceControllerConcern + private + + # :reek:FeatureEnvy + def process_tasks_if_applicable(mail_team_user, task_ids, tab) + # candidate for refactor using PATCH request + return unless mail_team_user && task_ids.present? + + # Instantiate AutoAssignableUserFinder with current_user + permission_checker = AutoAssignableUserFinder.new(mail_team_user) + + # iterate through each task and check if the user can work the correspondence + task_ids.each do |id| + correspondence = Task.find(id)&.correspondence + check_result = permission_checker.can_user_work_this_correspondence?( + user: mail_team_user, + correspondence: correspondence + ) + + # assign the task if the user can work the correspondence + update_task(mail_team_user, id) if check_result + end + + # use permission_checker.unassignable_reasons errors to generate the banner + set_banner_params( + mail_team_user, + permission_checker.unassignable_reasons, + task_ids.count, + tab + ) + end + + def update_task(mail_team_user, task_id) + task = Task.find_by(id: task_id) + task.update( + assigned_to_id: mail_team_user.id, + assigned_to_type: "User", + status: Constants.TASK_STATUSES.assigned + ) + end + + # :reek:LongParameterList + def set_banner_params(user, errors, task_count, tab) + template = message_template(user, errors, task_count, tab) + @response_type = errors.empty? ? "success" : "warning" + @response_header = template[:header] + @response_message = template[:message] + end + + # :reek:ControlParameter and :reek:LongParameterList + def message_template(user, errors, task_count, tab) + case tab + when "correspondence_unassigned" + if task_count == 1 + single_assignment_banner_text(user, errors, task_count) + else + multiple_assignment_banner_text(user, errors, task_count) + end + when "correspondence_team_assigned" + if task_count == 1 + single_assignment_banner_text(user, errors, task_count, action_prefix: "re") + else + multiple_assignment_banner_text(user, errors, task_count, action_prefix: "re") + end + end + end + + # :reek:FeatureEnvy + def single_assignment_banner_text(*args, action_prefix: "") + success_header_unassigned = "You have successfully #{action_prefix}"\ + "assigned #{args[2]} Correspondence to #{args[0].css_id}." + failure_header_unassigned = "Correspondence was not #{action_prefix}assigned to #{args[0].css_id}" + success_message = "Please go to your individual queue to see any self-assigned correspondence." + + failure_message = build_single_error_message(action_prefix, error_reason(args[1][0])) + + { + header: args[1].empty? ? success_header_unassigned : failure_header_unassigned, + message: args[1].empty? ? success_message : failure_message + } + end + + # :reek:FeatureEnvy + def multiple_assignment_banner_text(*args, action_prefix: "") + success_header = "You have successfully #{action_prefix}"\ + "assigned #{args[2]} Correspondences to #{args[0].css_id}." + success_message = "Please go to your individual queue to see any self-assigned correspondences." + failure_header = "Not all correspondence were #{action_prefix}assigned to #{args[0].css_id}" + + failure_message = build_multi_error_message(args[1], action_prefix) + + # return JSON message + { + header: args[1].blank? ? success_header : failure_header, + message: args[1].blank? ? success_message : failure_message.join(" \n") + } + end + + # :reek:FeatureEnvy + def build_multi_error_message(errors, action_prefix) + failure_message = [] + + # Get error counts + error_counts = { + Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.NOD_ERROR => errors.count( + Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.NOD_ERROR + ), + Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.SENSITIVITY_ERROR => errors.count( + Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.SENSITIVITY_ERROR + ), + Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.CAPACITY_ERROR => errors.count( + Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.CAPACITY_ERROR + ) + } + + error_counts.each do |error, count| + if count.positive? + multiple_errors = error_counts.values.count(&:positive?) > 1 + failure_message << build_error_message(count, action_prefix, error_reason(error), multiple_errors) + end + end + + failure_message + end + + def error_reason(error) + return "" unless error.is_a?(String) + + case error + when Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.NOD_ERROR then "of NOD permissions settings" + when Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.SENSITIVITY_ERROR then "of sensitivity level mismatch" + when Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.CAPACITY_ERROR then "maximum capacity has been reached for user's + queue" + end + end + + def build_single_error_message(action_prefix, reason) + # Build error message for single correspondence based on error types + "Case was not #{action_prefix}assigned to user because #{reason}." + end + + def build_error_message(*args) + # Build error message for multiple correspondence based on error types + message = "#{args[0]} cases were not #{args[1]}assigned to user" + message = "• #{message}" if args[3].present? + message += " because #{args[2]}." unless args[0].zero? + message + end + + def set_flash_intake_success_message + # intake error message is handled in client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx + vet = veteran_by_correspondence + flash[:correspondence_intake_success] = [ + "You have successfully submitted a correspondence record for #{vet.name}(#{vet.file_number})", + "The mail package has been uploaded to the Veteran's eFolder as well." + ] + end + + # :reek:ControlParameter + def intake_cancel_message(action_type) + vet = veteran_by_correspondence + if action_type == "cancel_intake" + @response_header = "You have successfully cancelled the intake form" + @response_message = "#{vet.name}'s correspondence (ID: #{correspondence.id}) "\ + "has been returned to the supervisor's queue for assignment." + else + @response_header = "You have successfully saved the intake form" + @response_message = "You can continue from step three of the intake form for #{vet.name}'s "\ + "correspondence (ID: #{correspondence.id}) at a later date." + end + @response_type = "success" + end + + def general_information + vet = veteran_by_correspondence + { + notes: correspondence.notes, + file_number: vet.file_number, + veteran_name: vet.name, + correspondence_type_id: correspondence.correspondence_type_id, + correspondence_tasks: correspondence.tasks.map do |task| + WorkQueue::CorrespondenceTaskSerializer.new(task).serializable_hash[:data][:attributes] + end + } + end + + def correspondence + @correspondence = Correspondence.find_by(uuid: params[:correspondence_uuid]) + end + + def veteran_by_correspondence + return nil if correspondence&.veteran_id.blank? + + @veteran_by_correspondence ||= Veteran.find_by(id: correspondence.veteran_id) + end + + def veterans_with_correspondences + veterans = Veteran.includes(:correspondences).where(correspondences: { id: Correspondence.select(:id) }) + veterans.map { |veteran| vet_info_serializer(veteran, veteran.correspondences.last) } + end + + def auto_texts + @auto_texts ||= AutoText.all.pluck(:name) + end + + def vet_info_serializer(veteran, correspondence) + { + firstName: veteran.first_name, + lastName: veteran.last_name, + fileNumber: veteran.file_number, + correspondenceUuid: correspondence.uuid, + packageDocumentType: correspondence.correspondence_type_id + } + end + + def correspondence_intake_processor + @correspondence_intake_processor ||= CorrespondenceIntakeProcessor.new + end + + def correspondence_documents_efolder_uploader + @correspondence_documents_efolder_uploader ||= CorrespondenceDocumentsEfolderUploader.new + end + + def upload_documents_to_claim_evidence + rpt = ReviewPackageTask.find_by(appeal_id: correspondence.id, type: ReviewPackageTask.name) + correspondence_documents_efolder_uploader.upload_documents_to_claim_evidence(correspondence, current_user, rpt) + end +end +# rubocop:enable Metrics/ModuleLength diff --git a/app/controllers/concerns/explain_timeline_concern.rb b/app/controllers/concerns/explain_timeline_concern.rb index a72722a4354..0c19c27bb1a 100644 --- a/app/controllers/concerns/explain_timeline_concern.rb +++ b/app/controllers/concerns/explain_timeline_concern.rb @@ -9,6 +9,7 @@ module ExplainTimelineConcern # :reek:FeatureEnvy def timeline_data return "(LegacyAppeals are not yet supported)".to_json if legacy_appeal? + return "(Correspondences are not yet supported)".to_json if correspondence? (tasks_timeline_data + intake_timeline_data + hearings_timeline_data).map(&:as_json) end diff --git a/app/controllers/concerns/task_pagination_concern.rb b/app/controllers/concerns/task_pagination_concern.rb index eb2ce725391..e162a59bf1d 100644 --- a/app/controllers/concerns/task_pagination_concern.rb +++ b/app/controllers/concerns/task_pagination_concern.rb @@ -17,8 +17,28 @@ def pagination_json } end + def correspondence_pagination_json + { + tasks: correspondence_json_tasks(correspondence_task_pager.paged_tasks), + task_page_count: correspondence_task_pager.task_page_count, + total_task_count: correspondence_task_pager.total_task_count, + tasks_per_page: CorrespondenceTaskPager::TASKS_PER_PAGE + } + end + private + def correspondence_task_pager + @correspondence_task_pager ||= CorrespondenceTaskPager.new( + assignee: assignee, + tab_name: params[Constants.QUEUE_CONFIG.TAB_NAME_REQUEST_PARAM.to_sym], + page: params[Constants.QUEUE_CONFIG.PAGE_NUMBER_REQUEST_PARAM.to_sym], + sort_order: params[Constants.QUEUE_CONFIG.SORT_DIRECTION_REQUEST_PARAM.to_sym], + sort_by: params[Constants.QUEUE_CONFIG.SORT_COLUMN_REQUEST_PARAM.to_sym], + filters: params[Constants.QUEUE_CONFIG.FILTER_COLUMN_REQUEST_PARAM.to_sym] + ) + end + def task_pager @task_pager ||= TaskPager.new( assignee: assignee, @@ -30,6 +50,16 @@ def task_pager ) end + def correspondence_json_tasks(tasks) + tab = CorrespondenceQueueTab.from_name(params[Constants.QUEUE_CONFIG.TAB_NAME_REQUEST_PARAM.to_sym]) + + { data: WorkQueue::CorrespondenceTaskColumnSerializer.new( + tasks, + is_collection: true, + params: { columns: tab.column_names } + ).serializable_hash[:data] } + end + def json_tasks(tasks) tasks = AppealRepository.eager_load_legacy_appeals_for_tasks(tasks) params = { user: current_user } diff --git a/app/controllers/correspondence_controller.rb b/app/controllers/correspondence_controller.rb new file mode 100644 index 00000000000..76f71e3f276 --- /dev/null +++ b/app/controllers/correspondence_controller.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# :reek:RepeatedConditional +class CorrespondenceController < ApplicationController + include CorrespondenceControllerConcern + include RunAsyncable + + before_action :verify_correspondence_access + before_action :verify_feature_toggle + before_action :correspondence + before_action :auto_texts + + def auto_assign_correspondences + batch = BatchAutoAssignmentAttempt.create!( + user: current_user, + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.started + ) + + job_args = { + current_user_id: current_user.id, + batch_auto_assignment_attempt_id: batch.id + } + + begin + perform_later_or_now(AutoAssignCorrespondenceJob, job_args) + render json: { batch_auto_assignment_attempt_id: batch&.id }, status: :ok + rescue StandardError => error + Rails.logger.error(error.full_message) + render json: { success: false, error: error } + end + end + + # :reek:FeatureEnvy + def auto_assign_status + batch = BatchAutoAssignmentAttempt.includes(:individual_auto_assignment_attempts) + .find_by!(user: current_user, id: corr_controller_params[:batch_auto_assignment_attempt_id]) + + num_assigned = batch.individual_auto_assignment_attempts + .where(status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.completed).count + + status_details = { + error_message: batch.error_info, + status: batch.status, + number_assigned: num_assigned, + number_attempted: batch.individual_auto_assignment_attempts.count + } + + render json: status_details, status: :ok + end + + private + + def corr_controller_params + params.permit(:batch_auto_assignment_attempt_id) + end + + def verify_correspondence_access + return true if InboundOpsTeam.singleton.user_has_access?(current_user) + + redirect_to "/unauthorized" + end + + def verify_feature_toggle + if !FeatureToggle.enabled?(:correspondence_queue) && verify_correspondence_access + redirect_to "/under_construction" + elsif !FeatureToggle.enabled?(:correspondence_queue) || !verify_correspondence_access + redirect_to "/unauthorized" + end + end +end diff --git a/app/controllers/correspondence_details_controller.rb b/app/controllers/correspondence_details_controller.rb new file mode 100644 index 00000000000..4bec6d2d5ab --- /dev/null +++ b/app/controllers/correspondence_details_controller.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +class CorrespondenceDetailsController < CorrespondenceController + include CorrespondenceControllerConcern + + def correspondence_details + set_instance_variables + + # Sort the response letters + @correspondence_response_letters = sort_response_letters( + @correspondence_details[:correspondence][:correspondenceResponseLetters] + ) + + respond_to do |format| + format.html + format.json { render json: build_json_response, status: :ok } + end + end + + def set_instance_variables + @correspondence = serialized_correspondence + + # Group related variables into a single hash + @correspondence_details = { + organizations: current_user.organizations.pluck(:name), + correspondence: @correspondence, + correspondence_documents: @correspondence[:correspondenceDocuments], + general_information: general_information, + mail_tasks: mail_tasks, + appeals_information: appeals, + inbound_ops_team_users: User.inbound_ops_team_users.select(:css_id).pluck(:css_id), + correspondence_types: CorrespondenceType.all + } + end + + def serialized_correspondence + WorkQueue::CorrespondenceSerializer + .new(correspondence) + .serializable_hash[:data][:attributes] + .merge(general_information) + .merge(mail_tasks) + .merge(appeals) + .merge(all_correspondences) + .merge(prior_mail) + end + + def build_json_response + { + correspondence: @correspondence_details[:correspondence], + general_information: @correspondence_details[:general_information], + mailTasks: @correspondence_details[:mail_tasks], + corres_docs: @correspondence_details[:correspondence_documents] + } + end + + # overriding method to allow users to access the correspondence details page + def verify_correspondence_access + true + end + + private + + def sort_response_letters(response_letters) + response_letters.sort_by do |letter| + days_left = letter[:days_left] + + sort_key = if days_left.match?(/Expired on/) + expiration_date = Date.strptime(days_left.split("Expired on ").last, "%m/%d/%Y") + [0, expiration_date, letter[:date_sent].to_date, letter[:title]] + elsif days_left.match?(/No response window/) + [2, letter[:date_sent].to_date, letter[:title]] + else + expiration_date_str = days_left.split(" (").first + expiration_date = Date.strptime(expiration_date_str, "%m/%d/%Y") + [1, expiration_date, letter[:date_sent].to_date, letter[:title]] + end + sort_key + end + end + + def appeals + case_search_results = CaseSearchResultsForCaseflowVeteranId.new( + caseflow_veteran_ids: [@correspondence.veteran_id], user: current_user + ).search_call + + { appeals_information: case_search_results.extra[:case_search_results] } + end + + def mail_tasks + { + mailTasks: @correspondence.correspondence_mail_tasks.completed.map(&:label) + } + end + + def all_correspondences + { all_correspondences: serialized_correspondences } + end + + def serialized_correspondences + serialized_data.map { |correspondence| correspondence[:attributes] } + end + + def serialized_data + serializer = WorkQueue::CorrespondenceSerializer.new(ordered_correspondences) + serializer.serializable_hash[:data] + end + + def ordered_correspondences + @correspondence.veteran.correspondences.order(va_date_of_receipt: :asc) + end + + def prior_mail + prior_mail = Correspondence.prior_mail(veteran_by_correspondence.id, correspondence.uuid).order(:va_date_of_receipt) + .select { |corr| corr.status == "Completed" || corr.status == "Pending" } + serialized_mail = prior_mail.map do |correspondence| + WorkQueue::CorrespondenceSerializer.new(correspondence).serializable_hash[:data][:attributes] + end + + { prior_mail: serialized_mail } + end +end diff --git a/app/controllers/correspondence_document_controller.rb b/app/controllers/correspondence_document_controller.rb new file mode 100644 index 00000000000..129160d4fff --- /dev/null +++ b/app/controllers/correspondence_document_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "paper_trail" + +class CorrespondenceDocumentController < ApplicationController + def update_document + document = CorrespondenceDocument.find(corr_document_params[:id]) + document.update!(corr_document_params) + render json: { correspondence: serialized_correspondence(document.correspondence) } + end + + private + + def corr_document_params + params.permit(:id, :vbms_document_type_id) + end + + def serialized_correspondence(correspondence) + WorkQueue::CorrespondenceSerializer + .new(correspondence) + .serializable_hash[:data][:attributes] + end +end diff --git a/app/controllers/correspondence_intake_controller.rb b/app/controllers/correspondence_intake_controller.rb new file mode 100644 index 00000000000..61a0799bf80 --- /dev/null +++ b/app/controllers/correspondence_intake_controller.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +class CorrespondenceIntakeController < CorrespondenceController + before_action :verify_correspondence_intake_access + def intake + # If correspondence intake was started, json data from the database will + # be loaded into the page when user returns to intake + @redux_store ||= CorrespondenceIntake.find_by( + task: correspondence&.open_intake_task + )&.redux_store + @prior_mail = prior_mail.map do |correspondence| + WorkQueue::CorrespondenceSerializer.new(correspondence).serializable_hash[:data][:attributes] + end + @correspondence = WorkQueue::CorrespondenceSerializer + .new(correspondence) + .serializable_hash[:data][:attributes] + .merge(general_information) + end + + def current_step + intake = CorrespondenceIntake.find_by(task: correspondence&.open_intake_task) || + CorrespondenceIntake.new(task: correspondence&.open_intake_task) + intake.update( + current_step: corr_intake_params[:current_step], + redux_store: redux_store + ) + + if intake.valid? + intake.save! + + render(json: {}, status: :ok) && return + else + render(json: intake.errors.full_messages, status: :unprocessable_entity) && return + end + end + + def intake_update + begin + correspondence.cancel_task_tree_for_appeal_intake + upload_documents_to_claim_evidence if FeatureToggle.enabled?(:ce_api_demo_toggle) + render json: { correspondence: correspondence } + rescue StandardError => error + Rails.logger.error(error.to_s) + Raven.capture_exception(error) + render json: {}, status: :bad_request + end + end + + def process_intake + if correspondence_intake_processor.process_intake(params, current_user) + set_flash_intake_success_message + render json: {}, status: :created + else + render json: { error: "Failed to update records" }, status: :bad_request + end + end + + def cancel_intake + begin + # find the correspondence intake task even if it isn't assigned to the user + intake_task = CorrespondenceIntakeTask.open.find_by(appeal_id: correspondence.id) + intake_task.update!(status: Constants.TASK_STATUSES.cancelled) + ReviewPackageTask.find_or_create_by!( + parent_id: intake_task.parent_id, + assigned_to: intake_task.assigned_to, + status: Constants.TASK_STATUSES.assigned, + appeal_id: correspondence.id, + appeal_type: "Correspondence" + ) + render json: {}, status: :ok + rescue StandardError + render json: { error: "Failed to update records" }, status: :bad_request + end + end + + private + + def corr_intake_params + params.permit(:current_step, :correspondence_uuid) + end + + def redux_store + params.require(:redux_store) + end + + def correspondence_uuid + params.permit(:correspondence_uuid)[:correspondence_uuid] + end + + def prior_mail + Correspondence.prior_mail(veteran_by_correspondence.id, corr_intake_params[:correspondence_uuid]) + .select { |corr| corr.status == "Completed" || corr.status == "Pending" } + end + + def verify_correspondence_intake_access + active_intake_task = CorrespondenceIntakeTask.open.find_by(appeal_id: correspondence.id) + # route if no active task + route_user unless active_intake_task && user_can_work_intake(active_intake_task) + end + + # always allow supervisors and superusers to acccess intakes not assigned to them. + def user_can_work_intake(task) + (task.assigned_to == current_user) || + (current_user.inbound_ops_team_supervisor? || current_user.inbound_ops_team_superuser?) + end + + # redirect if no access + def route_user + if current_user.inbound_ops_team_user? + redirect_to "/queue/correspondence" + elsif current_user.inbound_ops_team_superuser? || current_user.inbound_ops_team_supervisor? + redirect_to "/queue/correspondence/team" + else + redirect_to "/unauthorized" + end + end +end diff --git a/app/controllers/correspondence_queue_controller.rb b/app/controllers/correspondence_queue_controller.rb new file mode 100644 index 00000000000..d4ad41d618c --- /dev/null +++ b/app/controllers/correspondence_queue_controller.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +class CorrespondenceQueueController < CorrespondenceController + def correspondence_cases + if current_user.inbound_ops_team_supervisor? + redirect_to "/queue/correspondence/team" + elsif current_user.inbound_ops_team_superuser? || current_user.inbound_ops_team_user? + intake_cancel_message(action_type) if %w[continue_later cancel_intake].include?(action_type) + respond_to do |format| + format.html {} + format.json do + render json: { correspondence_config: CorrespondenceConfig.new(assignee: current_user) } + end + end + else + redirect_to "/unauthorized" + end + end + + def correspondence_team + if current_user.inbound_ops_team_superuser? || current_user.inbound_ops_team_supervisor? + correspondence_team_response + elsif current_user.inbound_ops_team_user? + redirect_to "/queue/correspondence" + else + redirect_to "/unauthorized" + end + end + + private + + def correspondence_team_response + inbound_ops_team_user = User.find_by(css_id: params[:user].strip) if params[:user].present? + task_ids = params[:task_ids]&.split(",") if params[:task_ids].present? + tab = params[:tab] if params[:tab].present? + + respond_to do |format| + format.html do + @inbound_ops_team_users = User.inbound_ops_team_users.pluck(:css_id) + @inbound_ops_team_non_admin = User.inbound_ops_team_users.select(&:inbound_ops_team_user?).pluck(:css_id) + correspondence_team_html_response(inbound_ops_team_user, task_ids, tab) + end + format.json { correspondence_team_json_response } + end + end + + def correspondence_team_json_response + render json: { correspondence_config: CorrespondenceConfig.new(assignee: InboundOpsTeam.singleton) } + end + + def correspondence_team_html_response(inbound_ops_team_user, task_ids, tab) + if inbound_ops_team_user && task_ids.present? + # candidate for refactor using PATCH request + process_tasks_if_applicable(inbound_ops_team_user, task_ids, tab) + elsif %w[continue_later cancel_intake].include?(action_type) + intake_cancel_message(action_type) + end + end + + def action_type + params[:userAction].strip if params[:userAction].present? + end +end diff --git a/app/controllers/correspondence_response_letters_controller.rb b/app/controllers/correspondence_response_letters_controller.rb new file mode 100644 index 00000000000..ae32f0ac12b --- /dev/null +++ b/app/controllers/correspondence_response_letters_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CorrespondenceResponseLettersController < ApplicationController + def create + correspondence_response_letter = CorrespondenceResponseLetter.new(correspondence_response_letter_params) + if correspondence_response_letter.save + render json: correspondence_response_letter, status: :ok + else + render json: correspondence_response_letter.errors.full_messages, status: :unprocessable_entity + end + end + + def correspondence_response_letter_params + params.require(:correspondence_response_letter).permit(:title, :date_sent, :letter_type, :subcategory, :reason, + :response_window, :user_id) + end +end diff --git a/app/controllers/correspondence_review_package_controller.rb b/app/controllers/correspondence_review_package_controller.rb new file mode 100644 index 00000000000..814906bbe82 --- /dev/null +++ b/app/controllers/correspondence_review_package_controller.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +class CorrespondenceReviewPackageController < CorrespondenceController + def review_package + set_instance_variables + + respond_to do |format| + format.html + format.json { render json: build_json_response, status: :ok } + end + end + + def update + unless update_veteran_on_correspondence + return render(json: { error: "Please enter a valid Veteran ID" }, status: :unprocessable_entity) + end + + if update_open_review_package_tasks + # return the new JSON response to update the frontend with changes + render json: { correspondence: serialized_correspondence, status: :ok } + else + render json: { error: "Failed to update tasks" }, status: :internal_server_error + end + end + + def update_cmp + correspondence.update( + va_date_of_receipt: params["VADORDate"].in_time_zone + ) + + correspondence.tasks.map do |task| + if task.type == "ReviewPackageTask" + task.status = "in_progress" + task.save + end + end + render json: { status: 200, correspondence: correspondence } + end + + def document_type_correspondence + data = vbms_document_types + render json: { data: data } + end + + def pdf + # Hard-coding Document access until CorrespondenceDocuments are uploaded to S3Bucket + document = Document.limit(200)[pdf_params[:pdf_id].to_i] + + document_disposition = "inline" + if pdf_params[:download] + document_disposition = "attachment; filename='#{pdf_params[:type]}-#{pdf_params[:id]}.pdf'" + end + + # The line below enables document caching for a month. + expires_in 30.days, public: true + send_file( + document.serve, + type: "application/pdf", + disposition: document_disposition + ) + end + + private + + def set_instance_variables + @inbound_ops_team_users = User.inbound_ops_team_users.select(:css_id).pluck(:css_id) + @correspondence_types = CorrespondenceType.all + @has_efolder_failed_task = correspondence_has_efolder_failed_task? + @correspondence = serialized_correspondence + end + + def serialized_correspondence + WorkQueue::CorrespondenceSerializer + .new(correspondence) + .serializable_hash[:data][:attributes] + .merge(general_information) + end + + def build_json_response + { + correspondence: @correspondence, + general_information: general_information, + user_can_edit_vador: current_user.inbound_ops_team_supervisor?, + corres_docs: @correspondence[:correspondenceDocuments], + taskInstructions: task_instructions + } + end + + def task_instructions + CorrespondenceTask.package_action_tasks.open + .find_by(appeal_id: @correspondence[:id])&.instructions || "" + end + + def correspondence_has_efolder_failed_task? + correspondence.tasks.active.where(type: EfolderUploadFailedTask.name).exists? + end + + def correspondence_params + params.require(:correspondence).permit(:correspondence, :notes, :correspondence_type_id, :va_date_of_receipt) + .merge(params.require(:veteran).permit(:file_number, :first_name, :last_name)) + end + + def pdf_params + params.permit(pdf: [:pdf_id, :type, :id, :download]) + end + + def update_veteran_on_correspondence + veteran = Veteran.find_by(file_number: correspondence_params[:file_number]) + if veteran + correspondence.update!( + veteran_id: veteran.id, + notes: correspondence_params[:notes], + correspondence_type_id: correspondence_params[:correspondence_type_id], + va_date_of_receipt: correspondence_params[:va_date_of_receipt] + ) + true + else + false + end + end + + # :reek:FeatureEnvy + def update_open_review_package_tasks + begin + ActiveRecord::Base.transaction do + correspondence.tasks.open.where(type: ReviewPackageTask.name).find_each do |task| + task.update!(status: Constants.TASK_STATUSES.in_progress) + end + end + true + rescue ActiveRecord::RecordInvalid => error + Rails.logger.error "Failed to update task due to validation error: #{error.message}" + false + rescue StandardError => error + Rails.logger.error "Failed to update tasks due to an unexpected error: #{error.message}" + false + end + end + + # :reek:FeatureEnvy + def vbms_document_types + begin + data = ExternalApi::ClaimEvidenceService.document_types + rescue StandardError => error + Rails.logger.error(error.full_message) + data ||= demo_data + end + data.map { |document_type| { id: document_type["id"], name: document_type["description"] } } + end + + def demo_data + json_file_path = "./lib/fakes/constants/VBMS_DOC_TYPES.json" + JSON.parse(File.read(json_file_path)) + end +end diff --git a/app/controllers/correspondence_task_pages_controller.rb b/app/controllers/correspondence_task_pages_controller.rb new file mode 100644 index 00000000000..e210215aaec --- /dev/null +++ b/app/controllers/correspondence_task_pages_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class CorrespondenceTaskPagesController < ApplicationController + include TaskPaginationConcern + + def index + render json: correspondence_pagination_json + end + + def assignee + @assignee ||= find_assignee + end + + private + + def task_pages_params + params.permit(:user_id, :organization_id) + end + + def find_assignee + if task_pages_params[:user_id] + User.find(task_pages_params[:user_id]) + elsif task_pages_params[:organization_id] + Organization.find(task_pages_params[:organization_id]) + end + end +end diff --git a/app/controllers/correspondence_tasks_controller.rb b/app/controllers/correspondence_tasks_controller.rb new file mode 100644 index 00000000000..131273d262d --- /dev/null +++ b/app/controllers/correspondence_tasks_controller.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +class CorrespondenceTasksController < TasksController + PACKAGE_ACTION_TYPES = [ + SplitPackageTask: SplitPackageTask, + MergePackageTask: MergePackageTask, + RemovePackageTask: RemovePackageTask, + ReassignPackageTask: ReassignPackageTask + ].freeze + + def create_package_action_task + review_package_task = ReviewPackageTask.find_by(appeal_id: correspondence_tasks_params[:correspondence_id]) + if review_package_task.children.open.present? + render json: + { message: "Existing package action request. Only one package action request may be made at a time" }, + status: :bad_request + else + task = task_to_create + task_params = { + parent_id: review_package_task.id, + instructions: correspondence_tasks_params[:instructions], + assigned_to: InboundOpsTeam.singleton, + appeal_id: correspondence_tasks_params[:correspondence_id], + appeal_type: "Correspondence", + status: Constants.TASK_STATUSES.assigned, + type: task.name + } + + ReviewPackageTask.create_from_params(task_params, current_user) + review_package_task.update!(assigned_to: InboundOpsTeam.singleton, status: :on_hold) + render json: { status: :ok } + end + end + + def create_correspondence_intake_task + review_package_task = ReviewPackageTask.open.find_by(appeal_id: correspondence_tasks_params[:id]) + return render json: { message: "Correspondence Root Task not found" }, status: :not_found unless review_package_task + + current_parent = review_package_task.parent + current_cit = CorrespondenceIntakeTask.open.find_by(parent_id: current_parent.id) + + if current_cit.present? + review_package_task.update!(assigned_to: current_user) + current_cit.update!(assigned_to: current_user) + render json: { status: :ok } + else + cit = CorrespondenceIntakeTask.create_from_params(current_parent, current_user) + if cit.present? + review_package_task.update!(assigned_to: current_user, status: :completed) + render json: { status: :ok } + else + render json: + { message: "No exist Correspondence Intake Task" }, + status: :bad_request + end + end + end + + def update + process_package_action_decision(correspondence_tasks_params[:decision]) + end + + def cancel + task = CorrespondenceTask.find(correspondence_tasks_params[:task_id]) + task.update!(status: Constants.TASK_STATUSES.cancelled) + end + + def complete + task = CorrespondenceTask.find(correspondence_tasks_params[:task_id]) + task.update!(status: Constants.TASK_STATUSES.completed) + end + + def change_task_type + @task = CorrespondenceTask.find(correspondence_tasks_params[:task_id]) + @task.update!( + type: change_task_type_params[:type], + instructions: change_task_type_params[:instructions] + ) + end + + private + + def correspondence_tasks_params + params.permit( + :correspondence_id, + :id, + :decision, + :task_id, + :new_assignee, + :decision_reason, + :action_type, + :type, + :correspondence_uuid, + instructions: [] + ) + end + + def change_task_type_params + change_type_params = params.require(:task).permit(:type, :instructions) + change_type_params[:instructions] = @task.flattened_instructions(change_type_params) + change_type_params + end + + def process_package_action_decision(decision) + task = CorrespondenceTask.find(correspondence_tasks_params[:task_id]) + requesting_user_name = task.assigned_by&.display_name + begin + case decision + when COPY::CORRESPONDENCE_QUEUE_PACKAGE_ACTION_DECISION_OPTIONS["APPROVE"] + if task.is_a?(ReassignPackageTask) + task.approve(current_user, User.find_by(css_id: correspondence_tasks_params[:new_assignee])) + elsif task.is_a?(RemovePackageTask) + task.approve(current_user) + end + when COPY::CORRESPONDENCE_QUEUE_PACKAGE_ACTION_DECISION_OPTIONS["REJECT"] + task.reject(current_user, correspondence_tasks_params[:decision_reason]) + end + package_action_flash(decision.upcase, requesting_user_name) + rescue StandardError + flash_error_banner(requesting_user_name) + end + end + + def package_action_flash(decision, user_name) + action = correspondence_tasks_params[:action_type].upcase + flash[:custom] = { + title: format( + COPY::CORRESPONDENCE_QUEUE_PACKAGE_ACTION_SUCCESS[action][decision]["TITLE"], + user_name + ), + message: COPY::CORRESPONDENCE_QUEUE_PACKAGE_ACTION_SUCCESS[action][decision]["MESSAGE"] + } + end + + def flash_error_banner(user_name) + operation_verb = (correspondence_tasks_params[:action_type] == "approve") ? "approved" : "rejected" + flash[:custom_error] = { + title: "Package request for #{user_name} could not be #{operation_verb}", + message: "Please try again at a later time or contact the Help Desk." + } + end + + def task_to_create + case correspondence_tasks_params[:type] + when "removePackage" + RemovePackageTask + when "mergePackage" + MergePackageTask + when "splitPackage" + SplitPackageTask + when "reassignPackage" + ReassignPackageTask + else + fail NotImplementedError "Type not implemented" + end + end +end diff --git a/app/controllers/explain_controller.rb b/app/controllers/explain_controller.rb index c1fb2b6ae2a..827cae0a9b0 100644 --- a/app/controllers/explain_controller.rb +++ b/app/controllers/explain_controller.rb @@ -36,7 +36,7 @@ def access_allowed? Rails.env.development? end - helper_method :legacy_appeal?, :appeal, :appeal_affinity, + helper_method :legacy_appeal?, :correspondence?, :appeal, :appeal_affinity, :show_pii_query_param, :fields_query_param, :sections_query_param, :treee_fields, :enabled_sections, :available_fields, @@ -51,15 +51,22 @@ def appeal_object_id end def explain_as_text - [ - "show_pii = #{show_pii_query_param}", - task_tree_as_text, - intake_as_text, - hearing_as_text, - JSON.pretty_generate(event_table_data), - JSON.pretty_generate(timeline_data), - JSON.pretty_generate(network_graph_data) - ].join("\n\n") + if correspondence? + [ + "show_pii = #{show_pii_query_param}", + appeal.tree(*treee_fields) + ].join("\n\n") + else + [ + "show_pii = #{show_pii_query_param}", + task_tree_as_text, + intake_as_text, + hearing_as_text, + JSON.pretty_generate(event_table_data), + JSON.pretty_generate(timeline_data), + JSON.pretty_generate(network_graph_data) + ].join("\n\n") + end end def available_fields @@ -127,6 +134,7 @@ def hearing_as_text def sanitized_json return "(LegacyAppeals are not yet supported)".to_json if legacy_appeal? + return "(Correspondences are not yet supported)".to_json if correspondence? SanitizedJsonExporter.new(appeal, sanitize: !show_pii_query_param, verbosity: 0).file_contents end @@ -143,11 +151,17 @@ def legacy_appeal? appeal.is_a?(LegacyAppeal) end + def correspondence? + appeal.is_a?(Correspondence) + end + def appeal @appeal ||= fetch_appeal end def appeal_affinity + return if correspondence? + @appeal_affinity ||= if legacy_appeal? VACOLS::Case.find_by(bfkey: appeal.vacols_id)&.appeal_affinity else @@ -156,7 +170,9 @@ def appeal_affinity end def fetch_appeal - if appeal_id.start_with?("ama-") + if params[:correspondence_uuid].present? + Correspondence.find_by_uuid(params[:correspondence_uuid]) + elsif appeal_id.start_with?("ama-") record_id = appeal_id.delete_prefix("ama-") Appeal.find_by_id(record_id) elsif appeal_id.start_with?("legacy-") diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 34f4569dbf4..76e2e092fae 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -75,7 +75,7 @@ def create_legacy_issue_update_task(issue) # close out any tasks that might be open open_issue_task = Task.where( assigned_to: SpecialIssueEditTeam.singleton - ).where(status: "assigned").where(appeal: appeal) + ).where(status: Constants.TASK_STATUSES.assigned).where(appeal: appeal) open_issue_task[0].delete unless open_issue_task.empty? task = IssuesUpdateTask.create!( diff --git a/app/controllers/organizations/users_controller.rb b/app/controllers/organizations/users_controller.rb index 6a125576a84..61c1beddfee 100644 --- a/app/controllers/organizations/users_controller.rb +++ b/app/controllers/organizations/users_controller.rb @@ -2,11 +2,16 @@ class Organizations::UsersController < OrganizationsController def index + @permissions = organization.organization_permissions.select( + :permission, :description, :enabled, :parent_permission_id, :default_for_admin, :id + ) + + @user_permissions = user_permissions + respond_to do |format| format.html { render template: "queue/index" } format.json do organization_users = organization.users - render json: { organization_name: organization.name, judge_team: organization.type == JudgeTeam.name, @@ -19,6 +24,26 @@ def index end end + def modify_user_permission + user_id, permission_name = user_permission_params + + org_permission = organization.organization_permissions.find_by(permission: permission_name) + target_user = organization.organizations_users.find_by(user_id: user_id) + + if org_user_permission_checker.can?( + permission_name: org_permission.permission, + organization: organization, + user: target_user.user + ) + disable_permission(user_id: user_id, org_permission: org_permission, target_user: target_user) + render json: { checked: false } + + else + enable_permission(user_id: user_id, org_permission: org_permission, target_user: target_user) + render json: { checked: true } + end + end + def create organization.add_user(user_to_modify, current_user) @@ -55,6 +80,38 @@ def verify_role_access private + def user_permissions + organization.organizations_users.sort_by(&:user_id).as_json( + only: [:user_id], + include: [ + organization_user_permissions: { include: [organization_permission: { only: [:permission, :permitted] }] } + ] + ) + end + + def disable_permission(user_id:, org_permission:, target_user:) + organization.organizations_users + .find_by(user_id: user_id).organization_user_permissions + .find_by( + organization_permission: org_permission, + organizations_user: target_user + ) + .update(permitted: false) + end + + def enable_permission(user_id:, org_permission:, target_user:) + organization.organizations_users.find_by(user_id: user_id) + .organization_user_permissions + .find_or_create_by!( + organization_permission: org_permission, + organizations_user: target_user + ).update!(permitted: true) + end + + def org_user_permission_checker + @org_user_permission_checker ||= OrganizationUserPermissionChecker.new + end + def user_to_modify @user_to_modify ||= User.find(params.require(:id)) end @@ -100,4 +157,11 @@ def json_administered_users(users) params: { organization: organization } ) end + + def user_permission_params + params.permit(:userId, :permissionName) + user_id = params[:userId] + permission_name = params[:permissionName].strip + [user_id, permission_name] + end end diff --git a/app/controllers/team_management_controller.rb b/app/controllers/team_management_controller.rb index ca378ca0b06..05a88053bfb 100644 --- a/app/controllers/team_management_controller.rb +++ b/app/controllers/team_management_controller.rb @@ -114,13 +114,16 @@ def all_teams end def other_orgs + rejected_orgs = [ + JudgeTeam, + DvcTeam, + Representative, + VhaProgramOffice, + VhaRegionalOffice, + EducationRpo + ] Organization.order(:name).reject do |org| - org.is_a?(JudgeTeam) || - org.is_a?(DvcTeam) || - org.is_a?(Representative) || - org.is_a?(VhaProgramOffice) || - org.is_a?(VhaRegionalOffice) || - org.is_a?(EducationRpo) + rejected_orgs.any? { |excluded_org| org.is_a?(excluded_org) } end end diff --git a/app/controllers/test/correspondence_controller.rb b/app/controllers/test/correspondence_controller.rb new file mode 100644 index 00000000000..d91cd31f67b --- /dev/null +++ b/app/controllers/test/correspondence_controller.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require "rake" + +class Test::CorrespondenceController < ApplicationController + include RunAsyncable + + before_action :verify_access, only: [:index] + before_action :verify_feature_toggle, only: [:index] + + VALID_CE_API_VBMS_DOCUMENT_TYPE_IDS = [ + 163, 1186, 1320, 1333, 1348, 1458, 1608, 1643, 1754 + ].freeze + + def index + render_access_error unless verify_access && access_allowed? + end + + def generate_correspondence + nums = correspondence_params[:file_numbers].split(",").map(&:strip).reject(&:empty?) + result = classify_file_numbers(nums) + invalid_nums = result[:invalid] + valid_file_nums = result[:valid] + + begin + connect_corr_with_vet(valid_file_nums, correspondence_params[:count].to_i) + render json: { + invalid_file_numbers: invalid_nums, + valid_file_nums: valid_file_nums + }, status: :created + rescue StandardError => error + log_error(error) + end + end + + private + + def correspondence_params + params.permit(:file_numbers, :count) + end + + def verify_access + return true if current_user.admin? || current_user.inbound_ops_team_supervisor? || bva? + + redirect_to "/unauthorized" + end + + def bva? + Bva.singleton.user_has_access?(current_user) + end + + def access_allowed? + Rails.env.development? || + Rails.env.test? || + Rails.deploy_env?(:uat) || + Rails.deploy_env?(:demo) + end + + def render_access_error + render(Caseflow::Error::ActionForbiddenError.new( + message: COPY::ACCESS_DENIED_TITLE + ).serialize_response) + end + + def verify_feature_toggle + correspondence_queue = FeatureToggle.enabled?(:correspondence_queue) + correspondence_admin = FeatureToggle.enabled?(:correspondence_admin) + if !correspondence_queue && verify_access + redirect_to "/under_construction" + elsif !correspondence_queue || !verify_access || correspondence_admin + redirect_to "/unauthorized" + end + end + + def valid_veteran?(file_number) + if Rails.deploy_env?(:uat) + veteran = VeteranFinder.find_best_match(file_number) + veteran&.fetch_bgs_record.present? + else + veteran = Veteran.find_by(file_number: file_number) + veteran.present? + end + end + + def classify_file_numbers(file_number_arr) + valid_file_nums = [] + invalid_file_nums = [] + + file_number_arr.each do |file_number| + if valid_veteran?(file_number) + valid_file_nums << file_number + else + invalid_file_nums << file_number + end + end + + { valid: valid_file_nums, invalid: invalid_file_nums } + end + + def connect_corr_with_vet(valid_file_nums, count) + count.times do + valid_file_nums.each do |file| + veteran = Veteran.find_by_file_number(file) + ActiveRecord::Base.transaction do + correspondence = create_correspondence(veteran) + rand(1..5).times do + create_correspondence_document(veteran, correspondence) + end + if correspondence.nod? + correspondence.correspondence_documents.last.update!( + document_type: 1250, + vbms_document_type_id: 1250 + ) + end + end + end + end + + auto_assign_correspondence + end + + def create_correspondence(veteran) + Correspondence.create!( + uuid: SecureRandom.uuid, + correspondence_type_id: rand(1..8), + va_date_of_receipt: Faker::Date.between(from: 90.days.ago, to: Time.zone.yesterday), + notes: "This is a test note", + veteran: veteran, + nod: rand(2).zero? + ) + end + + def create_correspondence_document(veteran, correspondence) + doc_type = VALID_CE_API_VBMS_DOCUMENT_TYPE_IDS.sample + CorrespondenceDocument.find_or_create_by( + document_file_number: veteran.file_number, + uuid: SecureRandom.uuid, + correspondence_id: correspondence.id, + document_type: doc_type, + vbms_document_type_id: doc_type, + pages: rand(1..30) + ) + end + + def auto_assign_correspondence + batch = BatchAutoAssignmentAttempt.create!( + user: current_user, + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.started + ) + + job_args = { + current_user_id: current_user.id, + batch_auto_assignment_attempt_id: batch.id + } + + perform_later_or_now(AutoAssignCorrespondenceJob, job_args) + end +end diff --git a/app/controllers/test/users_controller.rb b/app/controllers/test/users_controller.rb index 4f28830ee7f..ebcbfb8419b 100644 --- a/app/controllers/test/users_controller.rb +++ b/app/controllers/test/users_controller.rb @@ -16,6 +16,7 @@ class Test::UsersController < ApplicationController links: { your_queue: "/queue", assignment_queue: "/queue/USER_CSS_ID/assign", # USER_CSS_ID is then updated in TestUsers file + correspondence_admin: "/test/correspondence", case_distribution_dashboard: "/acd-controls/test" } }, diff --git a/app/jobs/ama_notification_efolder_sync_job.rb b/app/jobs/ama_notification_efolder_sync_job.rb index bbf9b6db122..bb640e08332 100644 --- a/app/jobs/ama_notification_efolder_sync_job.rb +++ b/app/jobs/ama_notification_efolder_sync_job.rb @@ -40,7 +40,7 @@ def appeals_recently_outcoded Appeal .where(id: RootTask.where( appeal_type: "Appeal", - status: "completed", + status: Constants.TASK_STATUSES.completed, closed_at: 1.day.ago..Time.zone.now ) .pluck(:appeal_id) diff --git a/app/jobs/auto_assign_correspondence_job.rb b/app/jobs/auto_assign_correspondence_job.rb new file mode 100644 index 00000000000..e586351f75c --- /dev/null +++ b/app/jobs/auto_assign_correspondence_job.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AutoAssignCorrespondenceJob < CaseflowJob + queue_with_priority :low_priority + + def perform(current_user_id:, batch_auto_assignment_attempt_id:) + correspondence_auto_assigner.perform( + current_user_id: current_user_id, + batch_auto_assignment_attempt_id: batch_auto_assignment_attempt_id + ) + end + + private + + def correspondence_auto_assigner + @correspondence_auto_assigner ||= CorrespondenceAutoAssigner.new + end +end diff --git a/app/jobs/legacy_notification_efolder_sync_job.rb b/app/jobs/legacy_notification_efolder_sync_job.rb index 4107d2a9a45..6d810b552b0 100644 --- a/app/jobs/legacy_notification_efolder_sync_job.rb +++ b/app/jobs/legacy_notification_efolder_sync_job.rb @@ -40,7 +40,7 @@ def appeals_recently_outcoded LegacyAppeal .where(id: RootTask.where( appeal_type: "LegacyAppeal", - status: "completed", + status: Constants.TASK_STATUSES.completed, closed_at: 1.day.ago..Time.zone.now ) .pluck(:appeal_id) diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 4691f672563..866d21da3a6 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -26,6 +26,8 @@ class Appeal < DecisionReview has_many :email_recipients, class_name: "HearingEmailRecipient" has_many :available_hearing_locations, as: :appeal, class_name: "AvailableHearingLocations" has_many :vbms_uploaded_documents, as: :appeal + has_many :correspondence_appeals + has_many :correspondences, through: :correspondence_appeals has_many :notifications, as: :notifiable # decision_documents is effectively a has_one until post decisional motions are supported @@ -963,6 +965,10 @@ def can_redistribute_appeal? return true if relevant_tasks.all?(&:closed?) end + def open_cavc_task + CavcTask.open.where(appeal_id: self.id).any? + end + def is_legacy? false end diff --git a/app/models/auto_text.rb b/app/models/auto_text.rb new file mode 100644 index 00000000000..98472757963 --- /dev/null +++ b/app/models/auto_text.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# this is a lookup table +class AutoText < ApplicationRecord +end diff --git a/app/models/batch_auto_assignment_attempt.rb b/app/models/batch_auto_assignment_attempt.rb new file mode 100644 index 00000000000..4aa51a184df --- /dev/null +++ b/app/models/batch_auto_assignment_attempt.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class BatchAutoAssignmentAttempt < CaseflowRecord + include AutoAssignable + + belongs_to :user, optional: false + + has_many :individual_auto_assignment_attempts, dependent: :destroy +end diff --git a/app/models/cached_appeal.rb b/app/models/cached_appeal.rb index e17d150c976..e9b598f5690 100644 --- a/app/models/cached_appeal.rb +++ b/app/models/cached_appeal.rb @@ -4,6 +4,7 @@ class CachedAppeal < CaseflowRecord self.table_name = "cached_appeal_attributes" # For convenience when working in the Rails console + scope :correspondence, -> { where(appeal_type: "Correspondence") } scope :ama_appeal, -> { where(appeal_type: "Appeal") } scope :legacy_appeal, -> { where(appeal_type: "LegacyAppeal") } scope :docket, ->(docket) { where(docket_type: docket) } diff --git a/app/models/concerns/appealable_correspondence.rb b/app/models/concerns/appealable_correspondence.rb new file mode 100644 index 00000000000..aff419c811e --- /dev/null +++ b/app/models/concerns/appealable_correspondence.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +## +# Allows a Correspondence to respond to Appeal methods +# +# Ultimately, probably need to revisit the inheritance hierarchy here and give these models a common ancestor +# or something +module AppealableCorrespondence + extend ActiveSupport::Concern + + def active? + true + end + + def cavc? + true + end + + def open_cavc_task + true + end + + def root_task + Task.find_by(appeal_id: id, appeal_type: type, type: CorrespondenceRootTask.name) + end +end diff --git a/app/models/concerns/auto_assignable.rb b/app/models/concerns/auto_assignable.rb new file mode 100644 index 00000000000..f409a24ca1a --- /dev/null +++ b/app/models/concerns/auto_assignable.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module AutoAssignable + extend ActiveSupport::Concern + include ActiveModel::Validations + + VALID_STATUSES = [ + Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.started, + Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.completed, + Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.error + ].freeze + + included do + validates :status, inclusion: { in: VALID_STATUSES } + end +end diff --git a/app/models/concerns/issue_updater.rb b/app/models/concerns/issue_updater.rb index 3b5347083be..0115b4edacd 100644 --- a/app/models/concerns/issue_updater.rb +++ b/app/models/concerns/issue_updater.rb @@ -133,7 +133,7 @@ def create_issue_update_task(original_issue, decision_issue) # close out any tasks that might be open open_issue_task = Task.where( assigned_to: SpecialIssueEditTeam.singleton - ).where(status: "assigned").where(appeal: appeal) + ).where(status: Constants.TASK_STATUSES.assigned).where(appeal: appeal) open_issue_task[0].delete unless open_issue_task.empty? task = IssuesUpdateTask.create!( diff --git a/app/models/concerns/task_belongs_to_polymorphic_appeal_concern.rb b/app/models/concerns/task_belongs_to_polymorphic_appeal_concern.rb index 56a321d80e4..3a17d3154df 100644 --- a/app/models/concerns/task_belongs_to_polymorphic_appeal_concern.rb +++ b/app/models/concerns/task_belongs_to_polymorphic_appeal_concern.rb @@ -22,9 +22,14 @@ module TaskBelongsToPolymorphicAppealConcern -> { where(tasks: { appeal_type: "SupplementalClaim" }) }, class_name: "SupplementalClaim", foreign_key: "appeal_id", optional: true + belongs_to :correspondence, + -> { where(tasks: { appeal_type: "Correspondence" }) }, + class_name: "Correspondence", foreign_key: "appeal_id", optional: true + scope :ama, -> { where(appeal_type: "Appeal") } scope :legacy, -> { where(appeal_type: "LegacyAppeal") } scope :higher_level_review, -> { where(appeal_type: "HigherLevelReview") } scope :supplemental_claim, -> { where(appeal_type: "SupplementalClaim") } + scope :correspondence, -> { where(appeal_type: "Correspondence") } end end diff --git a/app/models/correspondence.rb b/app/models/correspondence.rb new file mode 100644 index 00000000000..5fe882ccd61 --- /dev/null +++ b/app/models/correspondence.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +# Correspondence is a top level object similar to Appeals. +# Serves as a collection of all data related to Correspondence workflow +class Correspondence < CaseflowRecord + validates :veteran_id, presence: true + + has_paper_trail + include PrintsTaskTree + include AppealableCorrespondence + + has_many :correspondence_documents, dependent: :destroy + has_many :correspondence_appeals, dependent: :destroy + has_many :appeals, through: :correspondence_appeals + has_many :correspondence_relations, dependent: :destroy + has_many :related_correspondences, through: :correspondence_relations, dependent: :destroy + has_many :correspondence_response_letters, dependent: :destroy + belongs_to :correspondence_type + belongs_to :veteran + + after_create :initialize_correspondence_tasks + + # root task manages checks for different child tasks + delegate :tasks_not_related_to_an_appeal, to: :root_task + delegate :closed_tasks_not_related_to_an_appeal, to: :root_task + delegate :correspondence_mail_tasks, to: :root_task + delegate :open_package_action_task, to: :root_task + delegate :review_package_task, to: :root_task + delegate :open_intake_task, to: :root_task + + def initialize_correspondence_tasks + CorrespondenceRootTaskFactory.new(self).create_root_and_sub_tasks! + end + + def status + root_task&.correspondence_status + end + + def type + "Correspondence" + end + + # Alias for cmp_packet_number + def docket_number + nil + end + + # Alias for package_document_type.name + def docket_name + nil + end + + # Cannot use has_many :tasks - Task model does not contain a correspondence_id column + def tasks + CorrespondenceTask.where(appeal_id: id, appeal_type: type) + end + + def root_task + CorrespondenceRootTask.find_by(appeal_id: id, appeal_type: type) + end + + def cancel_task_tree_for_appeal_intake + tasks.where(type: ReviewPackageTask.name).update_all( + instructions: "An appeal intake was started because this Correspondence is a 10182" + ) + tasks.update_all(status: Constants.TASK_STATUSES.cancelled) + end + + # Methods below are included to allow Correspondences to render in explain page + def veteran_full_name + veteran.name + end + + def self.prior_mail(veteran_id, uuid) + includes([:veteran, :correspondence_type]) + .where(veteran_id: veteran_id).where.not(uuid: uuid) + end +end diff --git a/app/models/correspondence_appeal.rb b/app/models/correspondence_appeal.rb new file mode 100644 index 00000000000..d4186377d06 --- /dev/null +++ b/app/models/correspondence_appeal.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class CorrespondenceAppeal < ApplicationRecord + belongs_to :correspondence + belongs_to :appeal + has_many :correspondences_appeals_tasks + has_many :tasks, through: :correspondences_appeals_tasks +end diff --git a/app/models/correspondence_auto_assignment_lever.rb b/app/models/correspondence_auto_assignment_lever.rb new file mode 100644 index 00000000000..1e1b9ec64b0 --- /dev/null +++ b/app/models/correspondence_auto_assignment_lever.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CorrespondenceAutoAssignmentLever < CaseflowRecord + has_paper_trail on: [:update, :destroy] + + class << self + def capacity_rule + find_by(name: "capacity") + end + + def max_capacity + CorrespondenceAutoAssignmentLever.capacity_rule&.value || + Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.max_assigned_tasks + end + end +end diff --git a/app/models/correspondence_config.rb b/app/models/correspondence_config.rb new file mode 100644 index 00000000000..6712a25caba --- /dev/null +++ b/app/models/correspondence_config.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +# :reek:RepeatedConditional + +class CorrespondenceConfig < QueueConfig + def to_hash + { + table_title: table_title, + active_tab: default_active_tab, + tasks_per_page: 15, + use_task_pages_api: true, + tabs: assignee.correspondence_queue_tabs.map { |tab| attach_tasks_to_tab(tab) } + } + end + + private + + # :reek:FeatureEnvy + def attach_tasks_to_tab(tab) + task_pager = CorrespondenceTaskPager.new( + assignee: assignee, + tab_name: tab.name, + sort_by: tab.default_sorting_column.name, + sort_order: tab.default_sorting_direction + ) + endpoint = "task_pages?#{Constants.QUEUE_CONFIG.TAB_NAME_REQUEST_PARAM}=#{tab.name}" + base_path = if assignee_is_org? + "organizations/#{assignee.id}/#{endpoint}" + else + "correspondence/users/#{assignee.id}/#{endpoint}" + end + + tab.to_hash.merge( + tasks: serialized_tasks_for_columns(task_pager.paged_tasks, tab.column_names), + task_page_count: task_pager.task_page_count, + total_task_count: task_pager.total_task_count, + task_page_endpoint_base_path: base_path + ) + end + + def serialized_tasks_for_columns(tasks, columns) + WorkQueue::CorrespondenceTaskColumnSerializer.new( + tasks, + is_collection: true, + params: { columns: columns } + ).serializable_hash[:data] + end + + def table_title + if assignee_is_org? + Constants.QUEUE_CONFIG.CORRESPONDENCE_ORG_TABLE_TITLE + else + Constants.QUEUE_CONFIG.CORRESPONDENCE_USER_TABLE_TITLE + end + end + + def default_active_tab + if assignee_is_org? + Constants.QUEUE_CONFIG.CORRESPONDENCE_UNASSIGNED_TASKS_TAB_NAME + else + Constants.QUEUE_CONFIG.CORRESPONDENCE_ASSIGNED_TASKS_TAB_NAME + end + end + + def default_sorting_column + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name + end +end diff --git a/app/models/correspondence_document.rb b/app/models/correspondence_document.rb new file mode 100644 index 00000000000..71e8417cf35 --- /dev/null +++ b/app/models/correspondence_document.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class CorrespondenceDocument < CaseflowRecord + belongs_to :correspondence + belongs_to :vbms_document_type + + # callbacks + after_update :update_correspondence_nod + + def pdf_name + "#{uuid}.pdf" + end + + # :reek:UtilityFunction + # :nocov: + def pdf_location + File.join(Rails.root, "lib", "pdfs", "KnockKnockJokes.pdf") + end + # :nocov: + + # contentName, providerData: {contentSource:, documentTypeId:, dateVaReceivedDocument:} are required fields + def claim_evidence_upload_hash + { + contentName: pdf_name, + providerData: { + contentSource: "VISTA", + claimantFirstName: correspondence.veteran.first_name, + claimantLastName: correspondence.veteran.last_name, + claimantParticipantId: correspondence.veteran.participant_id, + claimantSsn: correspondence.veteran.ssn, + documentTypeId: vbms_document_type_id, + dateVaReceivedDocument: correspondence.va_date_of_receipt.strftime("%Y-%m-%d"), + actionable: true + } + } + end + + def update_correspondence_nod + documents = correspondence.correspondence_documents + nod = documents.any? do |doc| + doc["vbms_document_type_id"] && Caseflow::DocumentTypes::TYPES[doc["vbms_document_type_id"]]&.include?("10182") + end + correspondence.update!( + nod: nod + ) + end +end diff --git a/app/models/correspondence_intake.rb b/app/models/correspondence_intake.rb new file mode 100644 index 00000000000..7e1ee50a6af --- /dev/null +++ b/app/models/correspondence_intake.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CorrespondenceIntake < ApplicationRecord + belongs_to :task + + validates :task_id, presence: true + validate :task_type_is_correct, on: :create + + def task_type_is_correct + errors.add(:task, "Must be CorrespondenceIntakeTask") unless task&.type == CorrespondenceIntakeTask.name + end +end diff --git a/app/models/correspondence_queue_column.rb b/app/models/correspondence_queue_column.rb new file mode 100644 index 00000000000..4f16fee0bed --- /dev/null +++ b/app/models/correspondence_queue_column.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class CorrespondenceQueueColumn < QueueColumn + include ActiveModel::Model + + attr_accessor :filterable, :name + + def initialize(args) + super + @filterable ||= false + end + + FILTER_OPTIONS = { + Constants.QUEUE_CONFIG.COLUMNS.TASK_TYPE.name => :task_type_options, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name => :va_dor_options, + Constants.QUEUE_CONFIG.COLUMNS.TASK_CLOSED_DATE.name => :date_completed_options, + Constants.QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name => :task_type_options, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name => :package_document_type_options + }.freeze + + private + + def package_document_type_options(tasks) + tasks.joins(:appeal).group(:nod).count.each_pair.map do |option, count| + label = if option + self.class.format_option_label(Constants.QUEUE_CONFIG.PACKAGE_DOC_TYPE_FILTER_OPTIONS.NOD, count) + else + self.class.format_option_label(Constants.QUEUE_CONFIG.PACKAGE_DOC_TYPE_FILTER_OPTIONS.NON_NOD, count) + end + self.class.filter_option_hash(option.to_s, label) + end + end + + # placeholder method because the function is required + def va_dor_options(dummy) + dummy + end + + def date_completed_options(dummy) + dummy + end +end diff --git a/app/models/correspondence_queue_tab.rb b/app/models/correspondence_queue_tab.rb new file mode 100644 index 00000000000..92bf21904de --- /dev/null +++ b/app/models/correspondence_queue_tab.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class CorrespondenceQueueTab < QueueTab + def columns + column_names.map { |column_name| CorrespondenceQueueColumn.from_name(column_name) } + end + + def task_includes + [ + { appeal: [:veteran] }, + :assigned_by, + :children, + :parent + ] + end + + # :reek:UtilityFunction + def default_sorting_column + CorrespondenceQueueColumn.from_name(Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name) + end + + # If you don't create your own tab name it will default to the tab defined in QueueTab + def self.from_name(tab_name) + tab = descendants.find { |subclass| subclass.tab_name == tab_name } + fail(Caseflow::Error::InvalidTaskTableTab, tab_name: tab_name) unless tab + + tab + end + + def column_names + self.class.column_names + end +end diff --git a/app/models/correspondence_relation.rb b/app/models/correspondence_relation.rb new file mode 100644 index 00000000000..a1fa33c9688 --- /dev/null +++ b/app/models/correspondence_relation.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CorrespondenceRelation < ApplicationRecord + belongs_to :correspondence + belongs_to :related_correspondence, class_name: "Correspondence" + + # Makes the relationship bi-directional - both Correspondences are aware of the relationship + after_create :create_inverse, unless: :inverse_exists? + after_destroy :destroy_inverses, if: :inverse_exists? + + validates :correspondence_id, presence: true + validates :related_correspondence_id, presence: true + validates :correspondence_id, numericality: true + validates :related_correspondence_id, numericality: true + + def create_inverse + self.class.create(inverse_match_options) + end + + def destroy_inverses + inverses.destroy_all + end + + def inverse_exists? + self.class.exists?(inverse_match_options) + end + + def inverses + self.class.where(inverse_match_options) + end + + def inverse_match_options + { related_correspondence_id: correspondence_id, correspondence_id: related_correspondence_id } + end +end diff --git a/app/models/correspondence_response_letter.rb b/app/models/correspondence_response_letter.rb new file mode 100644 index 00000000000..6d29100ff86 --- /dev/null +++ b/app/models/correspondence_response_letter.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CorrespondenceResponseLetter < CaseflowRecord + belongs_to :correspondence +end diff --git a/app/models/correspondence_task_pager.rb b/app/models/correspondence_task_pager.rb new file mode 100644 index 00000000000..53a6eb494c6 --- /dev/null +++ b/app/models/correspondence_task_pager.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CorrespondenceTaskPager < TaskPager + def filtered_tasks + CorrespondenceTaskFilter.new(filter_params: filters, tasks: tasks_for_tab).filtered_tasks + end + + def queue_tab + @queue_tab ||= CorrespondenceQueueTab.from_name(tab_name).new(assignee: assignee) + end + + def paged_tasks + @paged_tasks ||= begin + tasks = sorted_tasks(filtered_tasks) + pagination_enabled ? tasks.page(page).per(TASKS_PER_PAGE) : tasks + end + end +end diff --git a/app/models/correspondence_type.rb b/app/models/correspondence_type.rb new file mode 100644 index 00000000000..48512797729 --- /dev/null +++ b/app/models/correspondence_type.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CorrespondenceType < ApplicationRecord + has_many :correspondences +end diff --git a/app/models/correspondences_appeals_task.rb b/app/models/correspondences_appeals_task.rb new file mode 100644 index 00000000000..70b5c584739 --- /dev/null +++ b/app/models/correspondences_appeals_task.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class CorrespondencesAppealsTask < ApplicationRecord + belongs_to :correspondence_appeal + belongs_to :task +end diff --git a/app/models/individual_auto_assignment_attempt.rb b/app/models/individual_auto_assignment_attempt.rb new file mode 100644 index 00000000000..11dfb2752c1 --- /dev/null +++ b/app/models/individual_auto_assignment_attempt.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class IndividualAutoAssignmentAttempt < CaseflowRecord + include AutoAssignable + + belongs_to :user, optional: false + belongs_to :correspondence, optional: false + belongs_to :batch_auto_assignment_attempt, optional: false + + validates :nod, inclusion: [true, false] +end diff --git a/app/models/legacy_tasks/attorney_legacy_task.rb b/app/models/legacy_tasks/attorney_legacy_task.rb index 28276dbc28b..fe188382c1d 100644 --- a/app/models/legacy_tasks/attorney_legacy_task.rb +++ b/app/models/legacy_tasks/attorney_legacy_task.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class AttorneyLegacyTask < LegacyTask + # :reek:ControlParameter def available_actions(current_user, role) return [] if role != "attorney" || current_user != assigned_to @@ -9,11 +10,9 @@ def available_actions(current_user, role) # so we use the absence of this value to indicate that there is no case assignment and return no actions. return [] unless task_id - actions = [Constants.TASK_ACTIONS.REVIEW_LEGACY_DECISION.to_h, - Constants.TASK_ACTIONS.SUBMIT_OMO_REQUEST_FOR_REVIEW.to_h, - Constants.TASK_ACTIONS.ADD_ADMIN_ACTION.to_h] - - actions + [Constants.TASK_ACTIONS.REVIEW_LEGACY_DECISION.to_h, + Constants.TASK_ACTIONS.SUBMIT_OMO_REQUEST_FOR_REVIEW.to_h, + Constants.TASK_ACTIONS.ADD_ADMIN_ACTION.to_h] end def timeline_title diff --git a/app/models/organization.rb b/app/models/organization.rb index 20808069ad5..925b4986bb3 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -6,6 +6,7 @@ class Organization < CaseflowRecord has_many :organizations_users, dependent: :destroy has_many :users, through: :organizations_users has_many :membership_requests + has_many :organization_permissions has_many :non_admin_users, -> { non_admin }, class_name: "OrganizationsUser" require_dependency "dvc_team" @@ -142,6 +143,16 @@ def queue_tabs ] end + def correspondence_queue_tabs + [ + correspondence_unassigned_tasks_tab, + correspondence_action_required_tasks_tab, + correspondence_pending_tasks_tab, + correspondence_assigned_tasks_tab, + correspondence_completed_tasks_tab + ] + end + def unassigned_tasks_tab ::OrganizationUnassignedTasksTab.new( assignee: self, @@ -162,6 +173,26 @@ def completed_tasks_tab ::OrganizationCompletedTasksTab.new(assignee: self, show_regional_office_column: show_regional_office_in_queue?) end + def correspondence_unassigned_tasks_tab + ::OrganizationCorrespondenceUnassignedTasksTab.new(assignee: self) + end + + def correspondence_action_required_tasks_tab + ::OrganizationCorrespondenceActionRequiredTasksTab.new(assignee: self) + end + + def correspondence_assigned_tasks_tab + ::OrganizationCorrespondenceAssignedTasksTab.new(assignee: self) + end + + def correspondence_pending_tasks_tab + ::OrganizationCorrespondencePendingTasksTab.new(assignee: self) + end + + def correspondence_completed_tasks_tab + ::OrganizationCorrespondenceCompletedTasksTab.new(assignee: self) + end + def serialize { accepts_priority_pushed_cases: accepts_priority_pushed_cases, diff --git a/app/models/organization_permission.rb b/app/models/organization_permission.rb new file mode 100644 index 00000000000..de22aaf74f6 --- /dev/null +++ b/app/models/organization_permission.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class OrganizationPermission < CaseflowRecord + belongs_to :organization, optional: false + + belongs_to :parent_permission, class_name: "OrganizationPermission", optional: true + has_many :child_permissions, class_name: "OrganizationPermission", foreign_key: "parent_permission_id", + dependent: :destroy + + has_many :organization_user_permissions, dependent: :destroy + + validates :description, presence: true + validates :enabled, inclusion: [true, false] + + validate :valid_permission + + def valid_permission + errors.add(:permission, "Invalid permission") unless + self.class.valid_permission_names.include?(permission) + end + + class << self + def valid_permission_names + Constants.ORGANIZATION_PERMISSIONS.to_h.values + end + + def auto_assign(organization) + find_by(organization: organization, permission: Constants.ORGANIZATION_PERMISSIONS.auto_assign) + end + + def receive_nod_mail(organization) + find_by(organization: organization, permission: Constants.ORGANIZATION_PERMISSIONS.receive_nod_mail) + end + end +end diff --git a/app/models/organization_user_permission.rb b/app/models/organization_user_permission.rb new file mode 100644 index 00000000000..fefad5ea344 --- /dev/null +++ b/app/models/organization_user_permission.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class OrganizationUserPermission < CaseflowRecord + belongs_to :organizations_user, optional: false + belongs_to :organization_permission, optional: false + + validates :permitted, inclusion: [true, false] +end diff --git a/app/models/organizations/inbound_ops_team.rb b/app/models/organizations/inbound_ops_team.rb new file mode 100644 index 00000000000..4cc21b3b0f7 --- /dev/null +++ b/app/models/organizations/inbound_ops_team.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class InboundOpsTeam < Organization + class << self + def singleton + InboundOpsTeam.first || + InboundOpsTeam.create(name: "Inbound Ops Team", url: "inbound-ops-team") + end + + def super_users + super_users = [] + + OrganizationsUser.includes(:user).where(organization: InboundOpsTeam.singleton).find_each do |org_user| + user = org_user.user + + if user.inbound_ops_team_superuser? + super_users.push(user) + end + end + + super_users + end + end + + # :reek:UtilityFunction + def selectable_in_queue? + false + end + + def can_receive_task?(task) + task.can_be_received_by?(self) + end +end diff --git a/app/models/organizations_user.rb b/app/models/organizations_user.rb index 189d98de647..99d9d6eb38d 100644 --- a/app/models/organizations_user.rb +++ b/app/models/organizations_user.rb @@ -4,8 +4,9 @@ class OrganizationsUser < CaseflowRecord belongs_to :organization belongs_to :user - scope :non_admin, -> { where(admin: false) } + has_many :organization_user_permissions, dependent: :destroy + scope :non_admin, -> { where(admin: false) } scope :admin, -> { where(admin: true) } class << self diff --git a/app/models/queue_column.rb b/app/models/queue_column.rb index b848c345925..162d0dae631 100644 --- a/app/models/queue_column.rb +++ b/app/models/queue_column.rb @@ -41,7 +41,8 @@ def to_hash(tasks) }.freeze def filter_options(tasks) - filter_option_func = FILTER_OPTIONS[name] + filter_option_func = self.class::FILTER_OPTIONS[name] + if filter_option_func send(filter_option_func, tasks) else @@ -68,6 +69,14 @@ def self.filter_option_hash(value, label) private + def va_dor_options + nil + end + + def date_completed_options + nil + end + def case_type_options(tasks) options = tasks.with_cached_appeals.group(:case_type).count.each_pair.map do |option, count| label = self.class.format_option_label(option, count) diff --git a/app/models/queue_tabs/correspondence_assigned_tasks_tab.rb b/app/models/queue_tabs/correspondence_assigned_tasks_tab.rb new file mode 100644 index 00000000000..139c3206f1c --- /dev/null +++ b/app/models/queue_tabs/correspondence_assigned_tasks_tab.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CorrespondenceAssignedTasksTab < CorrespondenceQueueTab + validate :assignee_is_user + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_ASSIGNED_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_ASSIGNED_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_ASSIGNED_TASKS_DESCRIPTION + end + + def tasks + CorrespondenceTask.includes(*task_includes).user_assigned_tasks(assignee) + end + + # :reek:UtilityFunction + def self.column_names + [ + Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name, + Constants.QUEUE_CONFIG.COLUMNS.NOTES.name + ] + end +end diff --git a/app/models/queue_tabs/correspondence_completed_tasks_tab.rb b/app/models/queue_tabs/correspondence_completed_tasks_tab.rb new file mode 100644 index 00000000000..fd8df8c1e67 --- /dev/null +++ b/app/models/queue_tabs/correspondence_completed_tasks_tab.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class CorrespondenceCompletedTasksTab < CorrespondenceQueueTab + validate :assignee_is_user + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_COMPLETED_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_COMPLETED_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_COMPLETED_TASKS_DESCRIPTION + end + + def tasks + # root task ids for all the assignee's tasks + potential_root_tasks = assignee.tasks.select(:parent_id) + .where(status: Constants.TASK_STATUSES.completed).distinct.pluck(:parent_id) + + CorrespondenceTask.includes(*task_includes).where( + id: potential_root_tasks + ).completed_root_tasks + end + + # :reek:UtilityFunction + def self.column_names + [ + Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + Constants.QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name, + Constants.QUEUE_CONFIG.COLUMNS.NOTES.name + ] + end +end diff --git a/app/models/queue_tabs/correspondence_in_progress_tasks_tab.rb b/app/models/queue_tabs/correspondence_in_progress_tasks_tab.rb new file mode 100644 index 00000000000..07774224068 --- /dev/null +++ b/app/models/queue_tabs/correspondence_in_progress_tasks_tab.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CorrespondenceInProgressTasksTab < CorrespondenceQueueTab + validate :assignee_is_user + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_IN_PROGRESS_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_IN_PROGRESS_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_IN_PROGRESS_TASKS_DESCRIPTION + end + + def tasks + CorrespondenceTask.includes(*task_includes).user_in_progress_tasks(assignee) + end + + # :reek:UtilityFunction + def self.column_names + [ + Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + Constants.QUEUE_CONFIG.COLUMNS.TASK_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name, + Constants.QUEUE_CONFIG.COLUMNS.NOTES.name + ] + end +end diff --git a/app/models/queue_tabs/organization_completed_tasks_tab.rb b/app/models/queue_tabs/organization_completed_tasks_tab.rb index f189af5cada..385a3961f1c 100644 --- a/app/models/queue_tabs/organization_completed_tasks_tab.rb +++ b/app/models/queue_tabs/organization_completed_tasks_tab.rb @@ -3,6 +3,7 @@ class OrganizationCompletedTasksTab < QueueTab validate :assignee_is_organization + # :reek:UtilityFunction def label COPY::QUEUE_PAGE_COMPLETE_TAB_TITLE end @@ -11,6 +12,7 @@ def self.tab_name Constants.QUEUE_CONFIG.COMPLETED_TASKS_TAB_NAME end + # :reek:UtilityFunction def description COPY::QUEUE_PAGE_COMPLETE_LAST_SEVEN_DAYS_TASKS_DESCRIPTION end @@ -19,6 +21,7 @@ def tasks recently_completed_tasks end + # :reek:UtilityFunction # rubocop:disable Metrics/AbcSize def column_names [ diff --git a/app/models/queue_tabs/organization_correspondence_action_required_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_action_required_tasks_tab.rb new file mode 100644 index 00000000000..7404ae0ad80 --- /dev/null +++ b/app/models/queue_tabs/organization_correspondence_action_required_tasks_tab.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class OrganizationCorrespondenceActionRequiredTasksTab < CorrespondenceQueueTab + validate :assignee_is_organization + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_ACTION_REQUIRED_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_ACTION_REQUIRED_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_ACTION_REQUIRED_TASKS_DESCRIPTION + end + + def tasks + tasks = CorrespondenceTask.includes(*task_includes).action_required_tasks + + return tasks if RequestStore[:current_user].inbound_ops_team_supervisor? + + tasks.where.not(type: RemovePackageTask.name) + end + + # :reek:UtilityFunction + def self.column_names + [ + Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name, + Constants.QUEUE_CONFIG.COLUMNS.TASK_ASSIGNED_BY.name, + Constants.QUEUE_CONFIG.COLUMNS.ACTION_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.NOTES.name + ] + end +end diff --git a/app/models/queue_tabs/organization_correspondence_assigned_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_assigned_tasks_tab.rb new file mode 100644 index 00000000000..c6a55974a36 --- /dev/null +++ b/app/models/queue_tabs/organization_correspondence_assigned_tasks_tab.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class OrganizationCorrespondenceAssignedTasksTab < CorrespondenceQueueTab + validate :assignee_is_organization + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_TEAM_ASSIGNED_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_TEAM_ASSIGNED_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_TEAM_ASSIGNED_TASKS_DESCRIPTION + end + + def tasks + CorrespondenceTask.includes(*task_includes).assigned_tasks + end + + # :reek:UtilityFunction + def self.column_names + columns = Constants.QUEUE_CONFIG.COLUMNS + [ + columns.CHECKBOX_COLUMN.name, + columns.VETERAN_DETAILS.name, + columns.PACKAGE_DOCUMENT_TYPE.name, + columns.VA_DATE_OF_RECEIPT.name, + columns.DAYS_WAITING_CORRESPONDENCE.name, + columns.TASK_TYPE.name, + columns.TASK_ASSIGNEE.name, + columns.NOTES.name + ] + end +end diff --git a/app/models/queue_tabs/organization_correspondence_completed_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_completed_tasks_tab.rb new file mode 100644 index 00000000000..24b1c0a8935 --- /dev/null +++ b/app/models/queue_tabs/organization_correspondence_completed_tasks_tab.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class OrganizationCorrespondenceCompletedTasksTab < CorrespondenceQueueTab + validate :assignee_is_organization + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_TEAM_COMPLETED_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_TEAM_COMPLETED_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_TEAM_COMPLETED_TASKS_DESCRIPTION + end + + def tasks + CorrespondenceTask.includes(*task_includes).completed_root_tasks + end + + # :reek:UtilityFunction + def self.column_names + [ + Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + Constants.QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name, + Constants.QUEUE_CONFIG.COLUMNS.NOTES.name + ] + end +end diff --git a/app/models/queue_tabs/organization_correspondence_pending_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_pending_tasks_tab.rb new file mode 100644 index 00000000000..b82aa791396 --- /dev/null +++ b/app/models/queue_tabs/organization_correspondence_pending_tasks_tab.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class OrganizationCorrespondencePendingTasksTab < CorrespondenceQueueTab + validate :assignee_is_organization + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_PENDING_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_PENDING_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_PENDING_TASKS_DESCRIPTION + end + + # :reek:UtilityFunction + def tasks + CorrespondenceTask.includes(*task_includes).pending_tasks + end + + # :reek:UtilityFunction + def self.column_names + [ + Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name, + Constants.QUEUE_CONFIG.COLUMNS.TASK_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name + ] + end +end diff --git a/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb new file mode 100644 index 00000000000..c62bfe84d19 --- /dev/null +++ b/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class OrganizationCorrespondenceUnassignedTasksTab < CorrespondenceQueueTab + validate :assignee_is_organization + + # :reek:UtilityFunction + def label + Constants.QUEUE_CONFIG.CORRESPONDENCE_UNASSIGNED_TASKS_LABEL + end + + def self.tab_name + Constants.QUEUE_CONFIG.CORRESPONDENCE_UNASSIGNED_TASKS_TAB_NAME + end + + # :reek:UtilityFunction + def description + Constants.QUEUE_CONFIG.CORRESPONDENCE_UNASSIGNED_TASKS_DESCRIPTION + end + + def tasks + CorrespondenceTask.includes(*task_includes).unassigned_tasks + end + + # :reek:UtilityFunction + def self.column_names + user = RequestStore.store[:current_user] + columns = [ + Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name, + Constants.QUEUE_CONFIG.COLUMNS.NOTES.name + ] + columns.insert(0, Constants.QUEUE_CONFIG.COLUMNS.CHECKBOX_COLUMN.name) unless user.inbound_ops_team_superuser? + columns + end +end diff --git a/app/models/queues/attorney_queue.rb b/app/models/queues/attorney_queue.rb index aacf2d7203e..168f03e315c 100644 --- a/app/models/queues/attorney_queue.rb +++ b/app/models/queues/attorney_queue.rb @@ -18,8 +18,7 @@ def tasks record.status = Constants.TASK_STATUSES.on_hold end end - - caseflow_tasks = user.tasks.includes(*task_includes).incomplete_or_recently_completed + caseflow_tasks = user.tasks.not_correspondence.includes(*task_includes).incomplete_or_recently_completed (colocated_tasks_for_attorney_tasks + caseflow_tasks).flatten end diff --git a/app/models/queues/generic_queue.rb b/app/models/queues/generic_queue.rb index 42fae3704e7..2eb2d801c91 100644 --- a/app/models/queues/generic_queue.rb +++ b/app/models/queues/generic_queue.rb @@ -17,6 +17,7 @@ def tasks def relevant_tasks Task.incomplete_or_recently_completed.visible_in_queue_table_view .where(assigned_to: user) + .not_correspondence .includes(*task_includes) .order(created_at: :asc) .limit(limit) diff --git a/app/models/queues/judge_queue.rb b/app/models/queues/judge_queue.rb index 7e713bc5a0c..2aed3781ffb 100644 --- a/app/models/queues/judge_queue.rb +++ b/app/models/queues/judge_queue.rb @@ -2,6 +2,6 @@ class JudgeQueue < GenericQueue def tasks - super.active.where(type: JudgeAssignTask.name) + super.active.not_correspondence.where(type: JudgeAssignTask.name) end end diff --git a/app/models/request_issues_update.rb b/app/models/request_issues_update.rb index 70ba1f7eede..4fbf0d219c7 100644 --- a/app/models/request_issues_update.rb +++ b/app/models/request_issues_update.rb @@ -406,7 +406,7 @@ def create_issue_update_task(change_type, before_issue, after_issue = nil) # close out any tasks that might be open open_issue_task = Task.where( assigned_to: SpecialIssueEditTeam.singleton - ).where(status: "assigned").where(appeal: before_issue.decision_review) + ).where(status: Constants.TASK_STATUSES.assigned).where(appeal: before_issue.decision_review) open_issue_task[0].delete unless open_issue_task.empty? task = IssuesUpdateTask.create!( diff --git a/app/models/serializers/work_queue/administered_user_serializer.rb b/app/models/serializers/work_queue/administered_user_serializer.rb index 61b86097292..423078de3da 100644 --- a/app/models/serializers/work_queue/administered_user_serializer.rb +++ b/app/models/serializers/work_queue/administered_user_serializer.rb @@ -11,4 +11,13 @@ class WorkQueue::AdministeredUserSerializer < WorkQueue::UserSerializer params[:organization].dvc&.eql?(object) end end + attribute :user_permission do |object, params| + object&.organization_permissions(params[:organization]) + end + attribute :user_admin_permission do |object, params| + object&.organization_admin_permissions(params[:organization]) + end + attribute :description do |object, params| + object&.organization_admin_permissions(params[:organization]) + end end diff --git a/app/models/serializers/work_queue/appeal_search_serializer.rb b/app/models/serializers/work_queue/appeal_search_serializer.rb index 90be5a6f498..a1554d5686e 100644 --- a/app/models/serializers/work_queue/appeal_search_serializer.rb +++ b/app/models/serializers/work_queue/appeal_search_serializer.rb @@ -139,6 +139,16 @@ class WorkQueue::AppealSearchSerializer object.veteran ? object.veteran.id : nil end + attribute :docket_switch do |object| + if object.docket_switch + WorkQueue::DocketSwitchSerializer.new(object.docket_switch).serializable_hash[:data][:attributes] + end + end + + attribute :evidence_submission_task do |object| + object.tasks.find_by(type: "EvidenceSubmissionWindowTask", status: Constants.TASK_STATUSES.assigned) + end + attribute :readable_hearing_request_type, &:readable_current_hearing_request_type attribute :readable_original_hearing_request_type, &:readable_original_hearing_request_type diff --git a/app/models/serializers/work_queue/appeal_serializer.rb b/app/models/serializers/work_queue/appeal_serializer.rb index 1b6cdb0b63d..a8d611ceb5e 100644 --- a/app/models/serializers/work_queue/appeal_serializer.rb +++ b/app/models/serializers/work_queue/appeal_serializer.rb @@ -333,5 +333,9 @@ class WorkQueue::AppealSerializer ]).count end + attribute :evidence_submission_task do |object| + object.tasks.find_by(type: "EvidenceSubmissionWindowTask", status: Constants.TASK_STATUSES.assigned) + end + attribute :has_completed_sct_assign_task, &:completed_specialty_case_team_assign_task? end diff --git a/app/models/serializers/work_queue/correspondence_appeals_serializer.rb b/app/models/serializers/work_queue/correspondence_appeals_serializer.rb new file mode 100644 index 00000000000..5b72edb2657 --- /dev/null +++ b/app/models/serializers/work_queue/correspondence_appeals_serializer.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class WorkQueue::CorrespondenceAppealsSerializer + include FastJsonapi::ObjectSerializer + + set_key_transform :camel_lower + + attribute :id + attribute :correspondences_appeals_tasks + attribute :docket_number do |object| + object.appeal.docket_number + end + + attribute :veteran_name do |object| + object.appeal.veteran + end + + attribute :stream_type do |object| + object.appeal.stream_type + end + + attribute :appeal_uuid do |object| + object.appeal.uuid + end + + attribute :appeal_type do |object| + object.appeal.docket_type + end + + attribute :number_of_issues do |object| + object.appeal.issues.length + end + + attribute :task_added_data do |object| + tasks = [] + object.correspondences_appeals_tasks.each do |cor_app_task| + assigned_to = cor_app_task.task.assigned_to + assigned_to_text = assigned_to.is_a?(Organization) ? assigned_to.name : assigned_to.css_id + task_data = { + assigned_at: cor_app_task.task.assigned_at, + assigned_to: assigned_to_text, + assigned_to_type: cor_app_task.task.assigned_to_type, + instructions: cor_app_task.task.instructions, + type: cor_app_task.task.label + } + tasks << task_data + end + tasks + end + + attribute :status do |object| + object.correspondence.status + end + + attribute :assigned_to do |object| + object.tasks[0]&.assigned_to + end + + attribute :correspondence do |object| + object + end +end diff --git a/app/models/serializers/work_queue/correspondence_document_serializer.rb b/app/models/serializers/work_queue/correspondence_document_serializer.rb new file mode 100644 index 00000000000..98655a419ad --- /dev/null +++ b/app/models/serializers/work_queue/correspondence_document_serializer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class WorkQueue::CorrespondenceDocumentSerializer + include FastJsonapi::ObjectSerializer + + attribute :id + attribute :correspondence_id + attribute :document_file_number + attribute :pages + attribute :vbms_document_type_id + attribute :uuid + attribute :document_type + attribute :document_title do |object| + doc_id = object.attributes["vbms_document_type_id"] + Caseflow::DocumentTypes::TYPES[doc_id] + end +end diff --git a/app/models/serializers/work_queue/correspondence_response_letter_serializer.rb b/app/models/serializers/work_queue/correspondence_response_letter_serializer.rb new file mode 100644 index 00000000000..ecdc755b5d6 --- /dev/null +++ b/app/models/serializers/work_queue/correspondence_response_letter_serializer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class WorkQueue::CorrespondenceResponseLetterSerializer + include FastJsonapi::ObjectSerializer + + attribute :id + attribute :correspondence_id + attribute :letter_type + attribute :title + attribute :subcategory + attribute :reason + attribute :date_sent + attribute :response_window + attribute :user_id + + # Days left calculation as an attribute + attribute :days_left do |object| + date_sent = object.date_sent.to_date + response_window = object.response_window + + if response_window.nil? + "No response window required" + else + expiration_date = date_sent + response_window.days + days_remaining = (expiration_date - Time.zone.today).to_i + + if days_remaining > 0 + day_label = (days_remaining == 1) ? "day" : "days" + "#{expiration_date.strftime('%m/%d/%Y')} (#{days_remaining} #{day_label} left)" + else + "Expired on #{expiration_date.strftime('%m/%d/%Y')}" + end + end + end +end diff --git a/app/models/serializers/work_queue/correspondence_serializer.rb b/app/models/serializers/work_queue/correspondence_serializer.rb new file mode 100644 index 00000000000..269b8718351 --- /dev/null +++ b/app/models/serializers/work_queue/correspondence_serializer.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +class WorkQueue::CorrespondenceSerializer + include FastJsonapi::ObjectSerializer + + set_key_transform :camel_lower + + attribute :uuid + attribute :id + attribute :notes + attribute :va_date_of_receipt + attribute :nod + attribute :status + attribute :type + attribute :veteran_id + attribute :correspondence_documents do |object| + object.correspondence_documents.map do |document| + WorkQueue::CorrespondenceDocumentSerializer.new(document).serializable_hash[:data][:attributes] + end + end + + attribute :correspondence_type do |object| + object.correspondence_type&.name + end + + attribute :tasks_unrelated_to_appeal do |object| + filtered_tasks = object.tasks_not_related_to_an_appeal + + tasks = [] + + unless filtered_tasks.empty? + filtered_tasks.each do |task| + tasks << + { + label: task.label, + assignedOn: task.assigned_at.strftime("%m/%d/%Y"), + assignedTo: (task.assigned_to_type == "Organization") ? task.assigned_to.name : task.assigned_to.css_id, + type: task.assigned_to_type, + instructions: task.instructions, + availableActions: task.available_actions_unwrapper(RequestStore[:current_user]), + uniqueId: task.id, + status: task.status + } + end + end + tasks + end + + attribute :closed_tasks_unrelated_to_appeal do |object| + filtered_tasks = object.closed_tasks_not_related_to_an_appeal + tasks = [] + + unless filtered_tasks.empty? + filtered_tasks.each do |task| + tasks << + { + label: task.label, + assignedOn: task.assigned_at.strftime("%m/%d/%Y"), + assignedTo: (task.assigned_to_type == "Organization") ? task.assigned_to.name : task.assigned_to.css_id, + type: task.assigned_to_type, + instructions: task.instructions, + availableActions: task.available_actions_unwrapper(RequestStore[:current_user]), + uniqueId: task.id, + status: task.status + } + end + end + tasks + end + + attribute :correspondence_appeals do |object| + appeals = [] + object.correspondence_appeals.map do |appeal| + appeals << WorkQueue::CorrespondenceAppealsSerializer.new(appeal).serializable_hash[:data][:attributes] + end + appeals + end + + attribute :veteran_full_name do |object| + [object.veteran_full_name&.first_name, object.veteran_full_name&.last_name].join(" ") + end + + attribute :veteran_file_number do |object| + object.veteran&.file_number + end + + attribute :correspondence_appeal_ids do |object| + object.appeal_ids.map(&:to_s) + end + + attribute :correspondence_response_letters do |object| + object.correspondence_response_letters.map do |response_letter| + WorkQueue::CorrespondenceResponseLetterSerializer.new(response_letter).serializable_hash[:data][:attributes] + end + end + + attribute :related_correspondence_ids, &:related_correspondence_ids +end diff --git a/app/models/serializers/work_queue/correspondence_task_column_serializer.rb b/app/models/serializers/work_queue/correspondence_task_column_serializer.rb new file mode 100644 index 00000000000..19f8237c5a8 --- /dev/null +++ b/app/models/serializers/work_queue/correspondence_task_column_serializer.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +class WorkQueue::CorrespondenceTaskColumnSerializer + include FastJsonapi::ObjectSerializer + + def self.serialize_attribute?(params, columns) + (params[:columns] & columns).any? + end + + attribute :unique_id do |object| + object.id.to_s + end + + attribute :instructions + + attribute :nod + + attribute :veteran_details do |object| + vet = Veteran.find(object.correspondence.veteran_id) + "#{vet.first_name} #{vet.last_name} (#{vet.file_number})" + end + + attribute :notes do |object, params| + columns = [Constants.QUEUE_CONFIG.COLUMNS.NOTES.name] + if serialize_attribute?(params, columns) + object.correspondence.notes + end + end + + attribute :closed_at do |object, params| + columns = [Constants.QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name] + + if serialize_attribute?(params, columns) + object.completed_by_date + end + end + + attribute :days_waiting do |object, params| + columns = [Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name] + + if serialize_attribute?(params, columns) + object.days_waiting + end + end + + attribute :va_date_of_receipt do |object| + object.correspondence.va_date_of_receipt + end + + attribute :label do |object, params| + columns = [ + Constants.QUEUE_CONFIG.COLUMNS.TASK_TYPE.name, + Constants.QUEUE_CONFIG.COLUMNS.ACTION_TYPE.name + ] + + if serialize_attribute?(params, columns) + object.label + end + end + + attribute :assigned_at + + attribute :task_url + + attribute :parent_task_url do |object| + if object.is_a?(ReassignPackageTask) || object.is_a?(RemovePackageTask) + { parent_task_url: object.parent.task_url } + else + { parent_task_url: "" } + end + end + + attribute :assigned_to do |object, params| + columns = [ + Constants.QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name + ] + assignee = object.assigned_to + + if serialize_attribute?(params, columns) + { + name: assignee.is_a?(Organization) ? assignee.name : assignee.css_id + } + else + { + name: nil + } + end + end + + attribute :assigned_by do |object, params| + columns = [ + Constants.QUEUE_CONFIG.COLUMNS.TASK_ASSIGNED_BY.name + ] + + if serialize_attribute?(params, columns) + { + first_name: object.assigned_by_display_name.first, + last_name: object.assigned_by_display_name.last + } + else + { + first_name: nil, + last_name: nil + } + end + end +end diff --git a/app/models/serializers/work_queue/correspondence_task_serializer.rb b/app/models/serializers/work_queue/correspondence_task_serializer.rb new file mode 100644 index 00000000000..be65dfaeeb6 --- /dev/null +++ b/app/models/serializers/work_queue/correspondence_task_serializer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class WorkQueue::CorrespondenceTaskSerializer + include FastJsonapi::ObjectSerializer + + attribute :id + attribute :appeal_id + attribute :appeal_type + attribute :assigned_at + attribute :assigned_by_id + attribute :assigned_to_id + attribute :assigned_to_type + attribute :cancellation_reason + attribute :cancelled_by_id + attribute :closed_at + attribute :completed_by_id + attribute :created_at + attribute :instructions + attribute :parent_id + attribute :placed_on_hold_at + attribute :started_at + attribute :status + attribute :updated_at + attribute :type + # other location for task actions. Will be needed for correspondence related tasks + attribute :available_actions do |object, params| + object.available_actions_unwrapper(params[:user]) + 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 da5cc6bee81..a006371f454 100644 --- a/app/models/serializers/work_queue/task_column_serializer.rb +++ b/app/models/serializers/work_queue/task_column_serializer.rb @@ -174,8 +174,8 @@ def self.serialize_attribute?(params, columns) if serialize_attribute?(params, columns) { css_id: assignee.try(:css_id), - is_organization: assignee.is_a?(Organization), name: assignee.is_a?(Organization) ? assignee.name : assignee.css_id, + is_organization: assignee.is_a?(Organization), type: assignee.class.name, id: assignee.id } diff --git a/app/models/task.rb b/app/models/task.rb index f74de948c52..edac8061ba7 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -26,6 +26,8 @@ class Task < CaseflowRecord has_many :attorney_case_reviews, dependent: :destroy has_many :task_timers, dependent: :destroy + has_one :correspondences_appeals_task + has_one :correspondence_appeal, through: :correspondences_appeals_task has_one :cached_appeal, ->(task) { where(appeal_type: task.appeal_type) }, foreign_key: :appeal_id validates :assigned_to, :appeal, :type, :status, presence: true @@ -53,7 +55,8 @@ class Task < CaseflowRecord Constants.TASK_STATUSES.in_progress.to_sym => Constants.TASK_STATUSES.in_progress, Constants.TASK_STATUSES.on_hold.to_sym => Constants.TASK_STATUSES.on_hold, Constants.TASK_STATUSES.completed.to_sym => Constants.TASK_STATUSES.completed, - Constants.TASK_STATUSES.cancelled.to_sym => Constants.TASK_STATUSES.cancelled + Constants.TASK_STATUSES.cancelled.to_sym => Constants.TASK_STATUSES.cancelled, + Constants.TASK_STATUSES.unassigned.to_sym => Constants.TASK_STATUSES.unassigned } enum cancellation_reason: { @@ -113,6 +116,8 @@ class << self; undef_method :open; end scope :with_assignees, -> { joins(Task.joins_with_assignees_clause) } + scope :not_correspondence, -> { where.not(appeal_type: "Correspondence") } + scope :with_assigners, -> { joins(Task.joins_with_assigners_clause) } scope :with_cached_appeals, -> { joins(Task.joins_with_cached_appeals_clause) } @@ -155,7 +160,7 @@ def active_statuses end def open_statuses - active_statuses.concat([Constants.TASK_STATUSES.on_hold]) + active_statuses.concat([Constants.TASK_STATUSES.on_hold, Constants.TASK_STATUSES.unassigned]) end def create_many_from_params(params_array, current_user) @@ -187,13 +192,17 @@ def verify_user_can_create!(user, parent) end if !parent&.actions_allowable?(user) || !can_create - user_description = user ? "User #{user.id}" : "nil User" - parent_description = parent ? " from #{parent.class.name} #{parent.id}" : "" - message = "#{user_description} cannot assign #{name}#{parent_description}." - fail Caseflow::Error::ActionForbiddenError, message: message + fail Caseflow::Error::ActionForbiddenError, message: user_can_create_error_message(user, parent) end end + def user_can_create_error_message(user, parent) + user_description = user.present? ? "User #{user.id}" : "nil User" + parent_description = parent.present? ? "from #{parent.class.name} #{parent.id}" : "" + + "#{user_description} cannot assign #{name} #{parent_description}." + end + def child_task_assignee(_parent, params) Object.const_get(params[:assigned_to_type]).find(params[:assigned_to_id]) end @@ -621,6 +630,12 @@ def verify_user_can_update!(user) end end + def can_be_received_by?(team) + return false if assigned_to?(team) + + false if parent_assigned_to?(team) + end + # rubocop:disable Metrics/AbcSize def reassign(reassign_params, current_user) # We do not validate the number of tasks in this scenario because when a @@ -836,6 +851,14 @@ def create_and_auto_assign_child_task(options = {}) end end + def assigned_to?(team) + assigned_to == team + end + + def parent_assigned_to?(team) + assigned_to.is_a?(User) && parent && parent.assigned_to == team + end + def automatically_assign_org_task? assigned_to.is_a?(Organization) && assigned_to.automatically_assign_to_member? end @@ -933,7 +956,8 @@ def set_cancelled_by_id in_progress: :started_at, on_hold: :placed_on_hold_at, completed: :closed_at, - cancelled: :closed_at + cancelled: :closed_at, + unassigned: :assigned_at }.freeze def set_timestamp diff --git a/app/models/task_pager.rb b/app/models/task_pager.rb index dccce8a2d60..d5565bc993c 100644 --- a/app/models/task_pager.rb +++ b/app/models/task_pager.rb @@ -25,7 +25,7 @@ def initialize(args) def paged_tasks @paged_tasks ||= begin - tasks = sorted_tasks(filtered_tasks) + tasks = sorted_tasks(filtered_tasks).not_correspondence limit = queue_tab.custom_task_limit || TASKS_PER_PAGE pagination_enabled ? tasks.page(page).per(limit) : tasks end diff --git a/app/models/task_sorter.rb b/app/models/task_sorter.rb index 13c364866cd..6ae085350f8 100644 --- a/app/models/task_sorter.rb +++ b/app/models/task_sorter.rb @@ -21,13 +21,35 @@ def initialize(args) fail(Caseflow::Error::MissingRequiredProperty, message: errors.full_messages.join(", ")) unless valid? end + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity def sorted_tasks return tasks unless tasks.any? - # Always join to the CachedAppeal and users tables because we sometimes need it, joining does not slow down the - # application, and conditional logic to only join sometimes adds unnecessary complexity. - tasks.with_assignees.with_assigners.with_cached_appeals.order(order_clause) + # used for when the logic on the sorting needs to be reversed + reverse_sort_order = sort_order == "asc" ? "desc" : "asc" + + # The following cases are used for Correspondence Queue tables + case column.name + when Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name + tasks.joins(appeal: :veteran).order(last_name: sort_order.to_sym, first_name: sort_order.to_sym) + when Constants.QUEUE_CONFIG.COLUMNS.NOTES.name + tasks.joins(:appeal).order(notes: sort_order.to_sym) + when Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name + tasks.joins(:appeal).order(va_date_of_receipt: sort_order.to_sym) + when Constants.QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name + tasks.joins(:appeal).order(assigned_at: reverse_sort_order.to_sym) + when Constants.QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name + tasks.joins(:appeal).order(closed_at: sort_order.to_sym) + when Constants.QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name + # Reverse sort since nod is a bool column. However the UI text needs to be sorted by display value. + tasks.joins(:appeal).order(nod: reverse_sort_order.to_sym) + else + # Always join to the CachedAppeal and users tables because we sometimes need it, joining does not slow down the + # application, and conditional logic to only join sometimes adds unnecessary complexity. + tasks.with_assignees.with_assigners.with_cached_appeals.order(order_clause) + end end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity private diff --git a/app/models/tasks/address_change_correspondence_mail_task.rb b/app/models/tasks/address_change_correspondence_mail_task.rb new file mode 100644 index 00000000000..d331d742aee --- /dev/null +++ b/app/models/tasks/address_change_correspondence_mail_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddressChangeCorrespondenceMailTask < CorrespondenceMailTask + def self.label + COPY::ADDRESS_CHANGE_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/associated_with_claims_folder_mail_task.rb b/app/models/tasks/associated_with_claims_folder_mail_task.rb new file mode 100644 index 00000000000..42a4b973dba --- /dev/null +++ b/app/models/tasks/associated_with_claims_folder_mail_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AssociatedWithClaimsFolderMailTask < CorrespondenceMailTask + def self.label + COPY::ASSOCIATED_WITH_CLAIMS_FOLDER_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/cavc_correspondence_mail_task.rb b/app/models/tasks/cavc_correspondence_mail_task.rb index eed371eb471..cd56d7da7de 100644 --- a/app/models/tasks/cavc_correspondence_mail_task.rb +++ b/app/models/tasks/cavc_correspondence_mail_task.rb @@ -49,7 +49,7 @@ def appeal_at_cavc_lit_support end def open_cavc_task - CavcTask.open.where(appeal_id: appeal.id).any? + appeal.open_cavc_task end def organization_task_actions @@ -77,4 +77,12 @@ def user_task_assigned_to_cavc_lit_support def assigned_to_cavc_lit_team_member CavcLitigationSupport.singleton.users.include?(assigned_to) end + + def status_is_valid_on_create + unless [Constants.TASK_STATUSES.assigned, Constants.TASK_STATUSES.completed].include?(status) + fail Caseflow::Error::InvalidStatusOnTaskCreate, task_type: type + end + + true + end end diff --git a/app/models/tasks/congressional_interest_mail_task.rb b/app/models/tasks/congressional_interest_mail_task.rb index 63d72048f78..82982841298 100644 --- a/app/models/tasks/congressional_interest_mail_task.rb +++ b/app/models/tasks/congressional_interest_mail_task.rb @@ -12,4 +12,24 @@ def self.label def self.default_assignee(_parent) LitigationSupport.singleton end + + def self.available_actions(user) + return [] unless user + + options = [ + Constants.TASK_ACTIONS.CHANGE_CORR_TASK_TYPE.to_h, + Constants.TASK_ACTIONS.ASSIGN_CORR_TASK_TO_TEAM.to_h, + Constants.TASK_ACTIONS.MARK_TASK_COMPLETE.to_h, + Constants.TASK_ACTIONS.RETURN_TO_INBOUND_OPS.to_h, + Constants.TASK_ACTIONS.CANCEL_CORR_TASK.to_h + ] + + if user.assigned_to.name == User.name + options.insert(2, Constants.TASK_ACTIONS.ASSIGN_CORR_TASK_TO_PERSON.to_h) + else + options.insert(2, Constants.TASK_ACTIONS.REASSIGN_CORR_TASK_TO_PERSON.to_h) + end + + options + end end diff --git a/app/models/tasks/correspondence/reassign_package_task.rb b/app/models/tasks/correspondence/reassign_package_task.rb new file mode 100644 index 00000000000..d4d582727fb --- /dev/null +++ b/app/models/tasks/correspondence/reassign_package_task.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class ReassignPackageTask < CorrespondenceTask + before_create :verify_no_other_open_package_action_task_on_correspondence + + # :reek:UtilityFunction + def task_url + Constants.CORRESPONDENCE_TASK_URL.REASSIGN_PACKAGE_TASK_MODAL_URL + end + + def approve(current_user, new_assignee) + update!( + completed_by: current_user, + assigned_to_id: current_user, + assigned_to: current_user, + closed_at: Time.zone.now, + status: Constants.TASK_STATUSES.completed + ) + parent.update!( + status: Constants.TASK_STATUSES.completed, + closed_at: Time.zone.now, + completed_by: current_user + ) + ReviewPackageTask.create!( + assigned_to: new_assignee, + status: Constants.TASK_STATUSES.assigned, + appeal_id: appeal_id, + appeal_type: Correspondence.name + ) + end + + def reject(current_user, reason) + update!( + completed_by_id: current_user.id, + closed_at: Time.zone.now, + status: Constants.TASK_STATUSES.completed, + instructions: instructions.push(reason) + ) + parent.update!( + assigned_to_type: "User", + assigned_to: assigned_by, + status: Constants.TASK_STATUSES.in_progress + ) + end +end diff --git a/app/models/tasks/correspondence_intake_task.rb b/app/models/tasks/correspondence_intake_task.rb new file mode 100644 index 00000000000..43d471eeaa5 --- /dev/null +++ b/app/models/tasks/correspondence_intake_task.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CorrespondenceIntakeTask < CorrespondenceTask + class << self + def create_from_params(parent_task, user) + params = { + instructions: [], + assigned_to: user, + appeal_id: parent_task.appeal_id, + appeal_type: Correspondence.name, + status: Constants.TASK_STATUSES.in_progress, + type: name + } + # verify the user can create correspondences + verify_correspondence_access(user) + fail Caseflow::Error::ChildTaskAssignedToSameUser if parent_of_same_type_has_same_assignee(parent_task, params) + + current_params = modify_params_for_create(params) + CorrespondenceIntakeTask.create!( + appeal_type: Correspondence.name, + appeal_id: current_params[:appeal_id], + assigned_by_id: child_assigned_by_id(parent_task, user), + parent_id: parent_task.id, + assigned_to: current_params[:assigned_to], + instructions: current_params[:instructions], + status: current_params[:status] + ) + end + end + + def task_url + closed? ? "/under_construction" : Constants.CORRESPONDENCE_TASK_URL.INTAKE_TASK_URL.sub("uuid", correspondence.uuid) + end +end diff --git a/app/models/tasks/correspondence_root_task.rb b/app/models/tasks/correspondence_root_task.rb new file mode 100644 index 00000000000..8c0859a688f --- /dev/null +++ b/app/models/tasks/correspondence_root_task.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +class CorrespondenceRootTask < CorrespondenceTask + def correspondence_status + status = if action_required? + Constants.CORRESPONDENCE_STATUSES.action_required + elsif unassigned? + Constants.CORRESPONDENCE_STATUSES.unassigned + elsif assigned? + Constants.CORRESPONDENCE_STATUSES.assigned + elsif pending? + Constants.CORRESPONDENCE_STATUSES.pending + elsif completed? + Constants.CORRESPONDENCE_STATUSES.completed + else + "" + end + status + end + + def review_package_task + children.open.find_by(type: ReviewPackageTask.name) + end + + def open_intake_task + children.open.find_by(type: CorrespondenceIntakeTask.name) + end + + def open_package_action_task + CorrespondenceTask.action_required_tasks.find_by(appeal_id: appeal_id, appeal_type: appeal_type) + end + + def tasks_not_related_to_an_appeal + CorrespondenceTask.tasks_not_related_to_an_appeal.open.where(appeal_id: appeal_id, appeal_type: appeal_type) + end + + def closed_tasks_not_related_to_an_appeal + CorrespondenceTask.tasks_not_related_to_an_appeal.closed.where(appeal_id: appeal_id, appeal_type: appeal_type) + end + + # correspondence_mail_tasks are completed upon creation, so no open check + def correspondence_mail_tasks + CorrespondenceTask.correspondence_mail_tasks.where(appeal_id: appeal_id, appeal_type: appeal_type) + end + + # a correspondence root task is considered closed if it has a closed at + # date OR all children tasks are completed. + def completed_by_date + return closed_at unless closed_at.nil? + + if children&.all?(&:completed?) + children.maximum(:closed_at) + end + end + + private + + # logic for handling correspondence statuses + # unassigned if review package task is unassigned + def unassigned? + review_package_task&.status == Constants.TASK_STATUSES.unassigned + end + + # assigned if open (assigned or on hold status) review package task or intake task + def assigned? + !unassigned? && (!review_package_task.blank? || !open_intake_task.blank?) + end + + # action required if the correspondence has a package action task with a status of 'assigned' + def action_required? + !open_package_action_task.blank? + end + + def pending? + !tasks_not_related_to_an_appeal.blank? + end + + # completed if root task is closed or no open children tasks + def completed? + status == Constants.TASK_STATUSES.completed || children.open.blank? + end +end diff --git a/app/models/tasks/correspondence_task.rb b/app/models/tasks/correspondence_task.rb new file mode 100644 index 00000000000..861cc5b9891 --- /dev/null +++ b/app/models/tasks/correspondence_task.rb @@ -0,0 +1,204 @@ +# frozen_string_literal: true + +class CorrespondenceTask < Task + belongs_to :correspondence, foreign_type: "Correspondence", foreign_key: "appeal_id" + self.abstract_class = true + + before_create :verify_org_task_unique + belongs_to :appeal, class_name: "Correspondence" + validate :status_is_valid_on_create, on: :create + validate :assignee_status_is_valid_on_create, on: :create + + scope :package_action_tasks, -> { where(type: package_action_task_names) } + scope :tasks_not_related_to_an_appeal, -> { where(type: tasks_not_related_to_an_appeal_names) } + scope :correspondence_mail_tasks, -> { where(type: correspondence_mail_task_names) } + scope :efolder_parent_tasks, -> { where(id: where(type: EfolderUploadFailedTask.name).active.pluck(:parent_id)) } + + # scopes to handle task queue logic + # Correspondence Cases queries + scope :unassigned_tasks, -> { where(type: ReviewPackageTask.name, status: Constants.TASK_STATUSES.unassigned) } + # due to 'on_hold' tasks also getting action_required_tasks, join efolder parent task + scope :assigned_tasks, lambda { + where(type: active_task_names).active.or(efolder_parent_tasks) + } + scope :action_required_tasks, -> { where(assigned_to: InboundOpsTeam.singleton).package_action_tasks.active } + scope :pending_tasks, -> { tasks_not_related_to_an_appeal.open } + # a correspondence is completed if the root task is completed or there are no active child tasks + # since active child tasks set the root task status to 'on_hold', the assumption is if a root task isn't on hold or + # cancelled, the correspondence is completed. + # This assumption is used to lower the N+1 query checking all the child task statuses. + scope :completed_root_tasks, lambda { + where(type: CorrespondenceRootTask.name).where.not( + status: Constants.TASK_STATUSES.on_hold + ).where.not(status: Constants.TASK_STATUSES.cancelled) + } + + # Your Correspondence queries + scope :user_assigned_tasks, lambda { |assignee| + where(type: active_task_names).open.where("assigned_to_id=?", assignee&.id) + } + + scope :user_in_progress_tasks, lambda { |assignee| + where("assigned_to_id=?", assignee&.id) + .where.not(type: EfolderUploadFailedTask.name) + .where(status: [Constants.TASK_STATUSES.in_progress, Constants.TASK_STATUSES.on_hold]) + } + + delegate :nod, to: :correspondence + + class << self + def create_from_params(params, user) + # verify the user can create correspondence tasks + verify_correspondence_access(user) + + parent_task = Task.find(params[:parent_id]) + fail Caseflow::Error::ChildTaskAssignedToSameUser if parent_of_same_type_has_same_assignee(parent_task, params) + + verify_user_can_create!(user, parent_task) + + params = modify_params_for_create(params) + child = create_child_task(parent_task, user, params) + parent_task.update!(status: params[:status]) if params[:status] + child + end + + private + + # block users from creating correspondence tasks if they are not members of Inbound Ops Team + # ignore check if there is no current user on correspondence creation + def verify_correspondence_access(user) + fail Caseflow::Error::ActionForbiddenError, message: "User does not belong to Inbound Ops Team" unless + InboundOpsTeam.singleton.user_has_access?(user) || user&.system_user? + end + end + + def self.active_task_names + [ + CorrespondenceIntakeTask.name, + ReviewPackageTask.name + ].freeze + end + + def self.package_action_task_names + [ + ReassignPackageTask.name, + RemovePackageTask.name, + SplitPackageTask.name, + MergePackageTask.name + ].freeze + end + + def self.tasks_not_related_to_an_appeal_names + [ + CavcCorrespondenceCorrespondenceTask.name, + CongressionalInterestCorrespondenceTask.name, + DeathCertificateCorrespondenceTask.name, + FoiaRequestCorrespondenceTask.name, + OtherMotionCorrespondenceTask.name, + PowerOfAttorneyRelatedCorrespondenceTask.name, + PrivacyActRequestCorrespondenceTask.name, + PrivacyComplaintCorrespondenceTask.name, + StatusInquiryCorrespondenceTask.name + ].freeze + end + + def self.correspondence_mail_task_names + [ + AssociatedWithClaimsFolderMailTask.name, + AddressChangeCorrespondenceMailTask.name, + EvidenceOrArgumentCorrespondenceMailTask.name, + VacolsUpdatedMailTask.name + ].freeze + end + + def verify_org_task_unique + if Task.where( + appeal_id: appeal_id, + appeal_type: appeal_type, + type: type + ).open.any? + fail( + Caseflow::Error::DuplicateOrgTask, + task_type: self.class.name, + assignee_type: assigned_to.class.name + ) + end + end + + def verify_no_other_open_package_action_task_on_correspondence + return true unless package_action_task? + + if CorrespondenceTask.package_action_tasks.open.where(appeal_id: appeal_id).any? + fail Caseflow::Error::MultipleOpenTasksOfSameTypeError, task_type: "package action task" + end + end + + def remove_package + root_task = CorrespondenceRootTask.find_by!( + appeal_id: @correspondence.id, + assigned_to: InboundOpsTeam.singleton, + appeal_type: "Correspondence", + type: "CorrespondenceRootTask" + ) + root_task.cancel_task_and_child_subtasks + end + + def self.create_child_task(parent_task, current_user, params) + Task.create!( + type: params[:type], + appeal_type: "Correspondence", + appeal: parent_task.appeal, + assigned_by_id: child_assigned_by_id(parent_task, current_user), + parent_id: parent_task.id, + assigned_to: params[:assigned_to] || child_task_assignee(parent_task, params), + instructions: params[:instructions] + ) + end + + def correspondence + Correspondence.find(appeal_id) + end + + def completed_by_date + closed_at + end + + def task_url + # route to the Correspondence Details Page. + if !FeatureToggle.enabled?(:correspondence_queue) + "/under_construction" + else + Constants.CORRESPONDENCE_TASK_URL.CORRESPONDENCE_TASK_DETAIL_URL.sub("uuid", correspondence.uuid) + end + end + + private + + # rubocop:disable Metrics/CyclomaticComplexity + def status_is_valid_on_create + case type + when "ReviewPackageTask" + return Constants.TASK_STATUSES.on_hold if status != Constants.TASK_STATUSES.on_hold + when "CorrespondenceIntakeTask", "EfolderUploadFailedTask" + return Constants.TASK_STATUSES.in_progress if status != Constants.TASK_STATUSES.in_progress + when "CorrespondenceRootTask", "HearingPostponementRequestMailTask" + return Constants.TASK_STATUSES.completed if status != Constants.TASK_STATUSES.completed + else + fail Caseflow::Error::InvalidStatusOnTaskCreate, task_type: type unless status == Constants.TASK_STATUSES.assigned + end + true + end + # rubocop:enable Metrics/CyclomaticComplexity + + def assignee_status_is_valid_on_create + if parent&.child_must_have_active_assignee? && assigned_to.is_a?(User) && !assigned_to.active? + fail Caseflow::Error::InvalidAssigneeStatusOnTaskCreate, assignee: assigned_to + end + + true + end + + def package_action_task? + self.class.package_action_task_names.include?(self.class.name) + end +end diff --git a/app/models/tasks/correspondence_task_filter.rb b/app/models/tasks/correspondence_task_filter.rb new file mode 100644 index 00000000000..d99c4c2cd13 --- /dev/null +++ b/app/models/tasks/correspondence_task_filter.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +class CorrespondenceTaskFilter < TaskFilter + def filtered_tasks + result = tasks.all + + filter_params.each do |param| + case param + when /col=vaDor/ + value_hash = Rack::Utils.parse_nested_query(param).deep_symbolize_keys + result = result.merge(filter_by_va_dor(value_hash[:val])) + when /col=packageDocTypeColumn/ + value_hash = Rack::Utils.parse_nested_query(param).deep_symbolize_keys + result = result.merge(filter_by_nod(value_hash[:val])) + when /col=taskColumn/ + task_column_params = param.sub("col=taskColumn&val=", "").split("|") + result = result.merge(filter_by_task(task_column_params)) + when /col=correspondenceCompletedDateColumn/ + value_hash = Rack::Utils.parse_nested_query(param).deep_symbolize_keys + result = result.merge(filter_by_date_completed(value_hash[:val])) + end + end + + result + end + + private + + def filter_by_nod(nod_value) + # If both NOD and Non-NOD checkboxes are selected in the filter dropdown, + # nod_value will be "true|false" or "false|true", + # so we return all the tasks if the string contains pipe character. + return tasks if nod_value.include?("|") + + tasks.joins(:appeal).where("correspondences.nod = ?", nod_value) + end + + def filter_by_date(date_info) + date_type, first_date, second_date = date_info.split(",") + + case date_type + when "0" + tasks.where("closed_at > ? AND closed_at < ?", Time.zone.parse(first_date), Time.zone.parse(second_date)) + when "1" + tasks.where("closed_at < ?", Time.zone.parse(first_date)) + when "2" + tasks.where("closed_at > ?", Time.zone.parse(first_date)) + when "3" + tasks.where("DATE(closed_at) = (?)", Time.zone.parse(first_date)) + end + end + + def filter_by_va_dor(date_info) + date_type, first_date, second_date = date_info.split(",") + + case date_type + when "0" + filter_between_dates(first_date, second_date) + when "1" + filter_before_date(first_date) + when "2" + filter_after_date(first_date) + when "3" + filter_on_date(first_date) + end + end + + def filter_by_date_completed(date_info) + date_type, first_date, second_date = date_info.split(",") + + case date_type + when "0" + date_completed_filter_between_dates(first_date, second_date) + when "1" + date_completed_filter_before_date(first_date) + when "2" + date_completed_filter_after_date(first_date) + when "3" + date_completed_filter_on_date(first_date) + end + end + + def filter_by_task(task_types) + # used to store the results of each task query + collection = nil + task_types.each do |task_type| + task_type_match = tasks.where(type: task_type) + collection = collection.nil? ? tasks.merge(task_type_match) : collection.or(tasks.merge(task_type_match)) + end + collection + end + + def filter_between_dates(start_date, end_date) + tasks.joins(:appeal) + .where("correspondences.va_date_of_receipt > ? AND correspondences.va_date_of_receipt < ?", + Time.zone.parse(start_date), + Time.zone.parse(end_date)) + end + + def filter_before_date(date) + tasks.joins(:appeal).where("correspondences.va_date_of_receipt < ?", Time.zone.parse(date)) + end + + def filter_after_date(date) + tasks.joins(:appeal).where("correspondences.va_date_of_receipt > ?", Time.zone.parse(date)) + end + + def filter_on_date(date) + tasks.joins(:appeal).where("DATE(correspondences.va_date_of_receipt) = (?)", Time.zone.parse(date)) + end + + def date_completed_filter_between_dates(start_date, end_date) + tasks.where("closed_at > ? AND closed_at < ?", + Time.zone.parse(start_date), + Time.zone.parse(end_date)) + end + + def date_completed_filter_before_date(date) + tasks.where("closed_at < ?", Time.zone.parse(date)) + end + + def date_completed_filter_after_date(date) + tasks.where("closed_at > ?", Time.zone.parse(date)) + end + + def date_completed_filter_on_date(date) + tasks.where("DATE(closed_at) = (?)", Time.zone.parse(date)) + end +end diff --git a/app/models/tasks/efolder_upload_failed_task.rb b/app/models/tasks/efolder_upload_failed_task.rb new file mode 100644 index 00000000000..251b69364b5 --- /dev/null +++ b/app/models/tasks/efolder_upload_failed_task.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class EfolderUploadFailedTask < CorrespondenceTask + def task_url + if parent.type == ReviewPackageTask.name + Constants.CORRESPONDENCE_TASK_URL.REVIEW_PACKAGE_TASK_URL.sub("uuid", correspondence.uuid) + elsif parent.type == CorrespondenceIntakeTask.name + Constants.CORRESPONDENCE_TASK_URL.INTAKE_TASK_URL.sub("uuid", correspondence.uuid) + end + end +end diff --git a/app/models/tasks/evidence_or_argument_correspondence_mail_task.rb b/app/models/tasks/evidence_or_argument_correspondence_mail_task.rb new file mode 100644 index 00000000000..9cceaf565b6 --- /dev/null +++ b/app/models/tasks/evidence_or_argument_correspondence_mail_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class EvidenceOrArgumentCorrespondenceMailTask < CorrespondenceMailTask + def self.label + COPY::EVIDENCE_OR_ARGUMENT_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/mail_task.rb b/app/models/tasks/mail_task.rb index 88ef64e573c..89f0bf1fe22 100644 --- a/app/models/tasks/mail_task.rb +++ b/app/models/tasks/mail_task.rb @@ -60,6 +60,12 @@ def parent_if_blocking_task(parent_task) parent_task end + def verify_user_can_create!(user, parent) + return true if InboundOpsTeam.singleton.user_has_access?(user) + + super(user, parent) + end + def create_from_params(params, user) parent_task = Task.find(params[:parent_id]) diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/cavc_correspondence_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/cavc_correspondence_correspondence_task.rb new file mode 100644 index 00000000000..92359ff3532 --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/cavc_correspondence_correspondence_task.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CavcCorrespondenceCorrespondenceTask < CorrespondenceMailTask + def label + COPY::FOIA_REQUEST_MAIL_TASK_LABEL + end + + def task_url + Constants.CORRESPONDENCE_TASK_URL.CORRESPONDENCE_TASK_DETAIL_URL.sub("uuid", correspondence.uuid) + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/congressional_interest_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/congressional_interest_correspondence_task.rb new file mode 100644 index 00000000000..19680da8a3f --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/congressional_interest_correspondence_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class CongressionalInterestCorrespondenceTask < CorrespondenceMailTask + def label + COPY::CONGRESSIONAL_INTEREST_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/correspondence_mail_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/correspondence_mail_task.rb new file mode 100644 index 00000000000..2ba8521870a --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/correspondence_mail_task.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Abstract base class for "tasks not related to an appeal" added to a correspondence during Correspondence Intake. +class CorrespondenceMailTask < CorrespondenceTask + class << self + # allows inbound ops team users to create tasks in intake + def verify_user_can_create!(user, parent) + return true if InboundOpsTeam.singleton.user_has_access?(user) + + super(user, parent) + end + end + + self.abstract_class = true + + def self.create_child_task(parent, current_user, params) + Task.create!( + type: name, + appeal: parent.appeal, + appeal_type: Correspondence.name, + assigned_by_id: child_assigned_by_id(parent, current_user), + parent_id: parent.id, + assigned_to: params[:assigned_to] || child_task_assignee(parent, params), + instructions: params[:instructions] + ) + end + + # rubocop: disable Metrics/AbcSize + def self.available_actions(user) + return [] unless user + + options = [ + Constants.TASK_ACTIONS.CHANGE_CORR_TASK_TYPE.to_h, + Constants.TASK_ACTIONS.ASSIGN_CORR_TASK_TO_TEAM.to_h, + Constants.TASK_ACTIONS.MARK_TASK_COMPLETE.to_h, + Constants.TASK_ACTIONS.RETURN_TO_INBOUND_OPS.to_h, + Constants.TASK_ACTIONS.CANCEL_CORR_TASK.to_h, + Constants.TASK_ACTIONS.CANCEL_CORRESPONDENCE_TASK.to_h, + Constants.TASK_ACTIONS.COMPLETE_CORRESPONDENCE_TASK.to_h + ] + + if user.is_a? Organization + options.insert(2, Constants.TASK_ACTIONS.ASSIGN_CORR_TASK_TO_PERSON.to_h) + else + options.insert(2, Constants.TASK_ACTIONS.REASSIGN_CORR_TASK_TO_PERSON.to_h) + end + + options + end + # rubocop: enable Metrics/AbcSize +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/death_certificate_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/death_certificate_correspondence_task.rb new file mode 100644 index 00000000000..4ac65ba3292 --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/death_certificate_correspondence_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DeathCertificateCorrespondenceTask < CorrespondenceMailTask + def label + COPY::DEATH_CERTIFICATE_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/foia_request_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/foia_request_correspondence_task.rb new file mode 100644 index 00000000000..cac5199f8d5 --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/foia_request_correspondence_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class FoiaRequestCorrespondenceTask < CorrespondenceMailTask + def label + COPY::FOIA_REQUEST_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/other_motion_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/other_motion_correspondence_task.rb new file mode 100644 index 00000000000..f5f19bed7dd --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/other_motion_correspondence_task.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class OtherMotionCorrespondenceTask < CorrespondenceMailTask + def label + COPY::OTHER_MOTION_MAIL_TASK_LABEL + end + + # if you have a UNIQUE action for the specific task, put it here. + def available_actions(user) + return [] unless user + + options = [ + Constants.TASK_ACTIONS.CANCEL_CORRESPONDENCE_TASK.to_h, + Constants.TASK_ACTIONS.COMPLETE_CORRESPONDENCE_TASK.to_h, + Constants.TASK_ACTIONS.CHANGE_TASK_TYPE.to_h + ] + + options + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/power_of_attorney_related_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/power_of_attorney_related_correspondence_task.rb new file mode 100644 index 00000000000..c1710b9ae47 --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/power_of_attorney_related_correspondence_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class PowerOfAttorneyRelatedCorrespondenceTask < CorrespondenceMailTask + def label + COPY::POWER_OF_ATTORNEY_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/privacy_act_request_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/privacy_act_request_correspondence_task.rb new file mode 100644 index 00000000000..0472f93a1d0 --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/privacy_act_request_correspondence_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class PrivacyActRequestCorrespondenceTask < CorrespondenceMailTask + def label + COPY::PRIVACY_ACT_REQUEST_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/privacy_complaint_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/privacy_complaint_correspondence_task.rb new file mode 100644 index 00000000000..7c828442392 --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/privacy_complaint_correspondence_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class PrivacyComplaintCorrespondenceTask < CorrespondenceMailTask + def label + COPY::PRIVACY_COMPLAINT_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/mail_tasks_not_related_to_appeal/status_inquiry_correspondence_task.rb b/app/models/tasks/mail_tasks_not_related_to_appeal/status_inquiry_correspondence_task.rb new file mode 100644 index 00000000000..b8a6f905783 --- /dev/null +++ b/app/models/tasks/mail_tasks_not_related_to_appeal/status_inquiry_correspondence_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class StatusInquiryCorrespondenceTask < CorrespondenceMailTask + def label + COPY::STATUS_INQUIRY_MAIL_TASK_LABEL + end +end diff --git a/app/models/tasks/merge_package_task.rb b/app/models/tasks/merge_package_task.rb new file mode 100644 index 00000000000..ce74c538bba --- /dev/null +++ b/app/models/tasks/merge_package_task.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class MergePackageTask < CorrespondenceTask + before_create :verify_no_other_open_package_action_task_on_correspondence + + def task_url + "/under_construction" + end +end diff --git a/app/models/tasks/remove_package_task.rb b/app/models/tasks/remove_package_task.rb new file mode 100644 index 00000000000..f8845748e59 --- /dev/null +++ b/app/models/tasks/remove_package_task.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class RemovePackageTask < CorrespondenceTask + before_create :verify_no_other_open_package_action_task_on_correspondence + + # :reek:UtilityFunction + def task_url + Constants.CORRESPONDENCE_TASK_URL.REMOVE_PACKAGE_TASK_MODAL_URL + end + + def approve(user) + update!( + completed_by_id: user.id, + status: Constants.TASK_STATUSES.cancelled + ) + end + + def reject(user, reason) + update!( + completed_by_id: user.id, + closed_at: Time.zone.now, + status: Constants.TASK_STATUSES.completed, + instructions: instructions.push(reason) + ) + parent.update!( + status: Constants.TASK_STATUSES.in_progress + ) + end +end diff --git a/app/models/tasks/review_package_task.rb b/app/models/tasks/review_package_task.rb new file mode 100644 index 00000000000..1baf932e453 --- /dev/null +++ b/app/models/tasks/review_package_task.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class ReviewPackageTask < CorrespondenceTask + class << self + def create_from_params(params, user) + parent_task = ReviewPackageTask.find(params[:parent_id]) + # verify the user can create correspondence tasks + verify_correspondence_access(user) + fail Caseflow::Error::ChildTaskAssignedToSameUser if parent_of_same_type_has_same_assignee(parent_task, params) + + params = modify_params_for_create(params) + child = create_child_task(parent_task, user, params) + parent_task.update!(status: params[:status]) if params[:status] + child + end + end + + def when_child_task_created(_child_task) + true + end + + def task_url + if closed? + "/under_construction" + else + Constants.CORRESPONDENCE_TASK_URL.REVIEW_PACKAGE_TASK_URL.sub("uuid", correspondence.uuid) + end + end +end diff --git a/app/models/tasks/split_package_task.rb b/app/models/tasks/split_package_task.rb new file mode 100644 index 00000000000..f2487154ac7 --- /dev/null +++ b/app/models/tasks/split_package_task.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SplitPackageTask < CorrespondenceTask + before_create :verify_no_other_open_package_action_task_on_correspondence + + def task_url + "/under_construction" + end +end diff --git a/app/models/tasks/vacols_updated_mail_task.rb b/app/models/tasks/vacols_updated_mail_task.rb new file mode 100644 index 00000000000..cf0c6672d5f --- /dev/null +++ b/app/models/tasks/vacols_updated_mail_task.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class VacolsUpdatedMailTask < CorrespondenceMailTask + def self.label + COPY::VACOLS_UPDATED_MAIL_TASK_LABEL + end +end diff --git a/app/models/user.rb b/app/models/user.rb index c2e562a9b55..484d768d9ca 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# :reek:RepeatedConditional class User < CaseflowRecord # rubocop:disable Metrics/ClassLength include BgsService @@ -11,6 +12,7 @@ class User < CaseflowRecord # rubocop:disable Metrics/ClassLength has_many :annotations has_many :tasks, as: :assigned_to has_many :organizations_users, dependent: :destroy + has_many :organization_user_permissions, through: :organizations_users, dependent: :destroy has_many :organizations, through: :organizations_users has_many :membership_requests, foreign_key: :requestor_id has_many :decided_membership_requests, class_name: "MembershipRequest", foreign_key: :decider_id @@ -21,6 +23,7 @@ class User < CaseflowRecord # rubocop:disable Metrics/ClassLength # Alternative: where("roles @> ARRAY[?]::varchar[]", role) scope :with_role, ->(role) { where("? = ANY(roles)", role) } + scope :inbound_ops_team_users, -> { joins(:organizations).where(organizations: { type: InboundOpsTeam.name }) } BOARD_STATION_ID = "101" LAST_LOGIN_PRECISION = 5.minutes @@ -95,6 +98,63 @@ def hearings_user? can_any_of_these_roles?(["Build HearSched", "Edit HearSched", "RO ViewHearSched", "VSO", "Hearing Prep"]) end + def inbound_ops_team_superuser? + return false unless correspondence_queue_enabled? + + member_of_organization?(InboundOpsTeam.singleton) && + OrganizationUserPermissionChecker.new.can?( + permission_name: Constants.ORGANIZATION_PERMISSIONS.superuser, + organization: InboundOpsTeam.singleton, + user: self + ) + end + + # check for user that is not an admin of the inbound ops team + def inbound_ops_team_user? + return false unless correspondence_queue_enabled? + + organizations.include?(InboundOpsTeam.singleton) && + !inbound_ops_team_supervisor? + end + + def inbound_ops_team_supervisor? + return false unless correspondence_queue_enabled? + + administered_teams.include?(InboundOpsTeam.singleton) + end + + # :reek:UtilityFunction + def organization_permissions(org) + # get organization user from the org relationship + org_user = OrganizationsUser.find_by(organization_id: org.id) + # get user permission using the org_user + + # use org_user > org_user_permission > org_permission to get + # organization permissions assigned to the user. + OrganizationUserPermission.where(organizations_user: org_user) + .includes(:organization_permission, :organizations_user) + .where(organizations_user_id: org_user.id, permitted: true) + .pluck(:permission, :description).map do |permission, description| + { + permission: permission, + desciption: description + } + end + end + + def organization_admin_permissions(org) + return [] unless org.user_is_admin?(self) + + # if admin, directly grab admin permissions from the org_permission table + OrganizationPermission.where(organization: org, default_for_admin: true) + .pluck(:permission, :description).map do |permission, description| + { + permission: permission, + desciption: description + } + end + end + def can_assign_hearing_schedule? can_any_of_these_roles?(["Edit HearSched", "Build HearSched"]) end @@ -399,6 +459,9 @@ def selectable_organizations } end + # handle correspondence queue tables + handle_correspondence_queues(orgs) if member_of_organization?(InboundOpsTeam.singleton) + orgs end # rubocop:enable Metrics/MethodLength @@ -444,6 +507,14 @@ def queue_tabs end end + def correspondence_queue_tabs + [ + correspondence_assigned_tasks_tab, + correspondence_in_progress_tasks_tab, + correspondence_completed_tasks_tab + ] + end + def self.default_active_tab Constants.QUEUE_CONFIG.INDIVIDUALLY_ASSIGNED_TASKS_TAB_NAME end @@ -460,6 +531,18 @@ def completed_tasks_tab ::CompletedTasksTab.new(assignee: self, show_regional_office_column: show_regional_office_in_queue?) end + def correspondence_assigned_tasks_tab + ::CorrespondenceAssignedTasksTab.new(assignee: self) + end + + def correspondence_in_progress_tasks_tab + ::CorrespondenceInProgressTasksTab.new(assignee: self) + end + + def correspondence_completed_tasks_tab + ::CorrespondenceCompletedTasksTab.new(assignee: self) + end + def can_edit_unrecognized_poa? allowed_orgs = [LitigationSupport, ClerkOfTheBoard, BoardProductOwners, BvaIntake].map(&:singleton) colocated_in_vacols? || allowed_orgs.any? { |org| org.user_has_access?(self) } @@ -503,12 +586,37 @@ def show_reader_link_column? false end + def system_user? + self == User.system_user + end + private + def correspondence_queue_enabled? + FeatureToggle.enabled?(:correspondence_queue) + end + def inactive_judge_team JudgeTeam.unscoped.inactive.find_by(id: organizations_users.admin.pluck(:organization_id)) end + def handle_correspondence_queues(orgs) + if inbound_ops_team_superuser? || inbound_ops_team_user? + orgs << { + name: COPY::CASE_LIST_TABLE_QUEUE_DROPDOWN_OWN_CORRESPONDENCE_LABEL, + url: "/queue/correspondence" + } + end + + if inbound_ops_team_superuser? || inbound_ops_team_supervisor? + orgs << { + name: COPY::CASE_LIST_TABLE_QUEUE_DROPDOWN_CORRESPONDENCE_CASES, + url: "/queue/correspondence/team" + } + end + orgs + end + def user_reactivation # We do not automatically re-add organization membership for reactivated users inactive_judge_team&.active! diff --git a/app/models/vbms_document_type.rb b/app/models/vbms_document_type.rb new file mode 100644 index 00000000000..d2326567286 --- /dev/null +++ b/app/models/vbms_document_type.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class VbmsDocumentType < ApplicationRecord + has_many :correspondence_documents +end diff --git a/app/models/veteran.rb b/app/models/veteran.rb index a23c83dfb5b..b7bb1bda08b 100644 --- a/app/models/veteran.rb +++ b/app/models/veteran.rb @@ -8,6 +8,7 @@ class Veteran < CaseflowRecord include AssociatedBgsRecord + has_many :correspondences has_many :available_hearing_locations, foreign_key: :veteran_file_number, primary_key: :file_number, class_name: "AvailableHearingLocations" diff --git a/app/queries/appeals_updated_since_query.rb b/app/queries/appeals_updated_since_query.rb index 66dc9f410d5..cfa2e902542 100644 --- a/app/queries/appeals_updated_since_query.rb +++ b/app/queries/appeals_updated_since_query.rb @@ -28,6 +28,8 @@ def call request_decision_issues request_issues_updates vbms_uploaded_documents + correspondence_appeals + correspondences issue_modification_requests ].freeze diff --git a/app/repositories/task_action_repository.rb b/app/repositories/task_action_repository.rb index e5171aaf9f0..03d17665842 100644 --- a/app/repositories/task_action_repository.rb +++ b/app/repositories/task_action_repository.rb @@ -2,6 +2,34 @@ class TaskActionRepository # rubocop:disable Metrics/ClassLength class << self + def change_corr_task_type + # stubbed + end + + def assign_corr_task_to_team + # stubbed + end + + def assign_corr_task_to_person + # stubbed + end + + def reassign_corr_task_to_person + # stubbed + end + + def mark_corr_task_complete + # stubbed + end + + def return_to_inbound_ops + # stubbed + end + + def cancel_corr_task + # stubbed + end + def assign_to_organization_data(task, _user = nil) organizations = Organization.assignable(task).map do |organization| { @@ -52,6 +80,19 @@ def cancel_task_data(task, _user = nil) } end + # this is used to build the modal and handle redirect after modal is closed + def cancel_correspondence_task_data(task, _user = nil) + return_to_name = task_assigner_name(task) + + { + modal_title: COPY::CANCEL_TASK_MODAL_TITLE, + modal_body: format_cancel_body(task, COPY::CANCEL_TASK_MODAL_DETAIL, return_to_name), + message_title: format(COPY::CANCEL_TASK_CONFIRMATION, task.correspondence&.veteran_full_name), + message_detail: format(COPY::MARK_TASK_COMPLETE_CONFIRMATION_DETAIL, return_to_name), + redirect_after: "/queue/correspondence/:correspondence_uuid/" + } + end + def cancel_initial_letter_task_data(task, _user = nil) return_to_name = task.is_a?(AttorneyTask) ? task.parent.assigned_to.full_name : task_assigner_name(task) { @@ -376,6 +417,18 @@ def complete_data(task, _user = nil) params end + def complete_correspondence_task_data(task, _user = nil) + return_to_name = task_assigner_name(task) + + { + modal_title: COPY::MARK_TASK_COMPLETE_TITLE, + modal_body: COPY::CORRESPONDENCE_OTHER_MOTION_MODAL_DETAIL, + message_title: format(COPY::CORRESPONDENCE_COMPLETE_TASK_CONFIRMATION, task.correspondence&.veteran_full_name), + message_detail: format(COPY::CORRESPONDENCE_COMPLETE_TASK_CONFIRMATION_DETAIL, return_to_name), + redirect_after: "/queue/correspondence/:correspondence_uuid/" + } + end + def proceed_final_notification_letter_data(task, _user = nil) params = { modal_body: COMPLETE_TASK_MODAL_BODY_HASH[task.type.to_sym], diff --git a/app/serializers/attorney_serializer.rb b/app/serializers/attorney_serializer.rb index fea9e3dcd3e..d54abc8cd22 100644 --- a/app/serializers/attorney_serializer.rb +++ b/app/serializers/attorney_serializer.rb @@ -6,6 +6,6 @@ class AttorneySerializer attribute :css_id attribute :full_name attribute :active_task_count do |object| - object.tasks.active.size + QueueRepository.tasks_for_user(object.css_id).count + object.tasks.not_correspondence.active.size + QueueRepository.tasks_for_user(object.css_id).count end end diff --git a/app/services/appeal_active_task_cancellation.rb b/app/services/appeal_active_task_cancellation.rb index 7d9c46defc1..0a306e939d3 100644 --- a/app/services/appeal_active_task_cancellation.rb +++ b/app/services/appeal_active_task_cancellation.rb @@ -29,7 +29,7 @@ def cancel_active_tasks_except_those_needed_for_distribution end def assign_distribution_task - tasks.find_by(type: "DistributionTask").update!(status: "assigned") + tasks.find_by(type: "DistributionTask").update!(status: Constants.TASK_STATUSES.assigned) end def all_tasks_except_those_needed_for_distribution diff --git a/app/services/auto_assignable_user_finder.rb b/app/services/auto_assignable_user_finder.rb new file mode 100644 index 00000000000..a878b7b3012 --- /dev/null +++ b/app/services/auto_assignable_user_finder.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +# For correspondence auto assignment, determines whether any assignable users exist as well as returns the first +# assignable user for a given correspondence + +# :reek:FeatureEnvy +class AutoAssignableUserFinder + AssignableUser = Struct.new(:user_obj, :last_assigned_date, :num_assigned, :nod?, keyword_init: true) + + attr_reader :unassignable_reasons + + def initialize(current_user) + self.current_user = current_user + self.unassignable_reasons = [] + end + + def assignable_users_exist? + return false if FeatureToggle.enabled?(:auto_assign_banner_max_queue) + + assignable_users.count.positive? + end + + def get_first_assignable_user(correspondence:) + run_auto_assign_algorithm(correspondence, assignable_users) + end + + def can_user_work_this_correspondence?(user:, correspondence:) + return false if user_is_at_max_capacity?(user) + + assignable = user_to_assignable_user(user) + + run_auto_assign_algorithm(correspondence, [assignable]).present? + end + + private + + attr_accessor :current_user + attr_writer :unassignable_reasons + + def run_auto_assign_algorithm(correspondence, users) + nod_error = Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.NOD_ERROR + sensitivity_error = Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.SENSITIVITY_ERROR + users.each do |user| + if correspondence.nod && !user.nod? + unassignable_reasons << nod_error + next + end + + user_obj = user.user_obj + + if sensitivity_levels_compatible?(user: user_obj, veteran: correspondence.veteran) + return user_obj + else + unassignable_reasons << sensitivity_error + end + end + + nil + end + + def user_is_at_max_capacity?(user) + capacity_error = Constants.CORRESPONDENCE_AUTO_ASSIGN_ERROR.CAPACITY_ERROR + if num_assigned_user_tasks(user) >= CorrespondenceAutoAssignmentLever.max_capacity + unassignable_reasons << capacity_error + true + else + false + end + end + + def assignable_users + users = [] + + find_users.each do |user| + next if user_is_at_max_capacity?(user) + + assignable = user_to_assignable_user(user) + + users.push(assignable) + end + + sorted_assignable_users(users) + end + + def user_to_assignable_user(user) + nod_eligible = permission_checker.can?( + permission_name: Constants.ORGANIZATION_PERMISSIONS.receive_nod_mail, + organization: InboundOpsTeam.singleton, + user: user + ) + + AssignableUser.new( + user_obj: user, + last_assigned_date: user_review_package_tasks(user).maximum(:assigned_at), + num_assigned: num_assigned_user_tasks(user), + nod?: nod_eligible + ) + end + + # :reek:UncommunicativeVariableName + def sorted_assignable_users(users) + users.sort do |a, b| + if a.num_assigned == b.num_assigned + a.last_assigned_date <=> b.last_assigned_date + else + a.num_assigned <=> b.num_assigned + end + end + end + + def num_assigned_user_tasks(user) + count = user_review_package_tasks(user).count + + super_users = InboundOpsTeam.super_users + if super_users.include?(user) + count += ((MergePackageTask.count + ReassignPackageTask.count + SplitPackageTask.count) / super_users.count).round + end + + count + end + + def user_review_package_tasks(user) + user.tasks.where( + type: ReviewPackageTask.name, + status: [ + Constants.TASK_STATUSES.assigned, + Constants.TASK_STATUSES.in_progress, + Constants.TASK_STATUSES.on_hold + ] + ) + end + + def find_users + # Do NOT use manual caching here!!! + # Other processes may update data, so always use the DB as the source of truth and let Rails handle any caching + InboundOpsTeam.singleton.users.includes(:tasks, :organizations, :organization_user_permissions) + .where( + organization_user_permissions: { + organization_permission: OrganizationPermission.auto_assign(InboundOpsTeam.singleton), + permitted: true + } + ) + .references(:tasks, :organizations, :organization_user_permissions) + end + + def sensitivity_levels_compatible?(user:, veteran:) + begin + sensitivity_checker.sensitivity_level_for_user(user) >= + sensitivity_checker.sensitivity_level_for_veteran(veteran) + rescue StandardError => error + error_uuid = SecureRandom.uuid + Raven.capture_exception(error, extra: { error_uuid: error_uuid }) + + false + end + end + + def sensitivity_checker + return @sensitivity_checker if @sensitivity_checker.present? + + # Set for use by BGSService + RequestStore.store[:current_user] ||= current_user + + @sensitivity_checker = BGSService.new + end + + def permission_checker + @permission_checker ||= OrganizationUserPermissionChecker.new + end +end diff --git a/app/services/correspondence_auto_assign_logger.rb b/app/services/correspondence_auto_assign_logger.rb new file mode 100644 index 00000000000..a7be12b5d93 --- /dev/null +++ b/app/services/correspondence_auto_assign_logger.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +# For correspondence auto assignment, creates records of assignment attempts. +# A log for the entire auto assignment run is stored to BatchAutoAssignmentAttempt. +# Logs for reach assignment attempt are stroed to IndividualAutoAssignmentAttempt. + +# :reek:FeatureEnvy +class CorrespondenceAutoAssignLogger + def initialize(current_user, batch) + self.current_user = current_user + self.batch = batch + end + + class << self + def fail_run_validation(batch_auto_assignment_attempt_id:, msg:) + failed_batch = BatchAutoAssignmentAttempt.find(batch_auto_assignment_attempt_id) + + return if failed_batch.blank? + + failed_batch.update!( + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.error, + error_info: { message: msg }, + errored_at: Time.current + ) + end + end + + def begin + batch.update!( + started_at: Time.current, + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.started, + num_nod_packages_assigned: 0, + num_nod_packages_unassigned: 0, + num_packages_assigned: 0, + num_packages_unassigned: 0 + ) + end + + def end + batch.assign_attributes( + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.completed, + completed_at: Time.current + ) + + save_run_statistics + end + + def error(msg:) + batch.assign_attributes( + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.error, + error_info: { message: msg }, + errored_at: Time.current + ) + + save_run_statistics + end + + def assigned(task:, started_at:, assigned_to:) + correspondence = task.correspondence + + attempt = individual_auto_assignment_attempt + attempt.assign_attributes( + correspondence: correspondence, + completed_at: Time.current, + nod: correspondence.nod, + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.completed, + started_at: started_at + ) + + if correspondence.nod + batch.num_nod_packages_assigned += 1 + else + batch.num_packages_assigned += 1 + end + + save_attempt_statistics( + attempt: attempt, + task: task, + result: "Correspondence #{correspondence.id} assigned to User ID #{assigned_to.id}" + ) + end + + def no_eligible_assignees(task:, started_at:, unassignable_reason:) + correspondence = task.correspondence + + attempt = individual_auto_assignment_attempt + attempt.assign_attributes( + correspondence: correspondence, + errored_at: Time.current, + nod: correspondence.nod, + status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.error, + started_at: started_at + ) + + if correspondence.nod + batch.num_nod_packages_unassigned += 1 + else + batch.num_packages_unassigned += 1 + end + + save_attempt_statistics( + attempt: attempt, + task: task, + result: "No eligible assignees: #{unassignable_reason}" + ) + end + + private + + attr_accessor :batch, :current_user + + def individual_auto_assignment_attempt + IndividualAutoAssignmentAttempt.new( + batch_auto_assignment_attempt: batch, + user: current_user + ) + end + + def save_run_statistics + stats = { + seconds_elapsed: seconds_elapsed(record: batch, status: batch.status) + } + + batch.statistics = stats + batch.save! + end + + def save_attempt_statistics(attempt:, task:, result:) + stats = { + result: result, + seconds_elapsed: seconds_elapsed(record: attempt, status: attempt.status), + review_package_task_id: task.id + } + + attempt.statistics = stats + attempt.save! + end + + # :reek:ControlParameter + def seconds_elapsed(record:, status:) + if status == Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.completed + record.completed_at - record.started_at + else + record.errored_at - record.started_at + end + end +end diff --git a/app/services/correspondence_auto_assign_run_verifier.rb b/app/services/correspondence_auto_assign_run_verifier.rb new file mode 100644 index 00000000000..27d94a9426d --- /dev/null +++ b/app/services/correspondence_auto_assign_run_verifier.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# For correspondence auto assignment, determines whether or not an auto assign job can be run. +# In order to prevent race conditions and inconsistent data, only one correspondence auto assignment can be run +# at a time. + +class CorrespondenceAutoAssignRunVerifier + attr_accessor :verified_batch, :verified_user, :err_msg + + def can_run_auto_assign?(current_user_id:, batch_auto_assignment_attempt_id:) + verify_feature_toggles && + verify_id_params(current_user_id, batch_auto_assignment_attempt_id) && + verify_no_other_jobs_running + end + + private + + def verify_feature_toggles + if FeatureToggle.enabled?(:auto_assign_banner_failure) + self.err_msg = "#{COPY::BAAA_ERROR_MESSAGE} (failure triggered via feature toggle)" + return false + end + + true + end + + def verify_id_params(current_user_id, batch_auto_assignment_attempt_id) + self.verified_user = User.find_by(id: current_user_id) + if verified_user.blank? + self.err_msg = "User does not exist" + return false + end + + self.verified_batch = BatchAutoAssignmentAttempt.find_by(user: verified_user, id: batch_auto_assignment_attempt_id) + if verified_batch.blank? + self.err_msg = "BatchAutoAssignmentAttempt does not exist" + return false + end + + true + end + + def verify_no_other_jobs_running + if assignment_already_running? + self.err_msg = "Auto assignment already in progress" + return false + end + + true + end + + def assignment_already_running? + last_assignment = IndividualAutoAssignmentAttempt.last + + min_minutes_elapsed_individual_attempt = + Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.timing.min_minutes_elapsed_individual_attempt + + # Safe to move forward if we haven't seen any assignment attempts for the past X minutes + return true if last_assignment&.created_at.present? && + ((Time.current - last_assignment.created_at) / 60) < min_minutes_elapsed_individual_attempt + + min_minutes_elapsed_batch_attempt = + Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.timing.min_minutes_elapsed_batch_attempt + + # Safe to move forward if the last batch was started more than Y minutes ago + BatchAutoAssignmentAttempt + .where.not(id: verified_batch.id) + .where(status: Constants.CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.started) + .exists?(["created_at >= ?", min_minutes_elapsed_batch_attempt.minutes.ago]) + end +end diff --git a/app/services/correspondence_auto_assigner.rb b/app/services/correspondence_auto_assigner.rb new file mode 100644 index 00000000000..ff9f292aca3 --- /dev/null +++ b/app/services/correspondence_auto_assigner.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +# Assigns Correspondence ReviewPackageTasks to eligible InboundOpsTeam users. +# (Assigning the Correspondence's ReviewPackageTask is considered assigning the Correspondence.) + +class CorrespondenceAutoAssigner + # rubocop:disable Metrics/MethodLength + def perform(current_user_id:, batch_auto_assignment_attempt_id:) + # Don't catch these exceptions here so that if we're being called by a job + # the job will auto-retry with exponential back-off + validate_run!(current_user_id, batch_auto_assignment_attempt_id) + + logger.begin + + if !unassigned_review_package_tasks.count.positive? + logger.error(msg: COPY::BAAA_NO_UNASSIGNED_CORRESPONDENCE) + return + end + + if !assignable_user_finder.assignable_users_exist? + logger.error(msg: COPY::BAAA_USERS_MAX_QUEUE_REACHED) + return + end + + begin + unassigned_review_package_tasks.each do |task| + # validate a user is able to work the task + assignee = find_auto_assign_user(task) + + # guard clause if no assignee + break if assignee.blank? + + assign(task, assignee) + end + + logger.end + rescue StandardError => error + error_uuid = SecureRandom.uuid + Raven.capture_exception(error, extra: { error_uuid: error_uuid }) + logger.error(msg: "#{COPY::BAAA_ERROR_MESSAGE} (Error code: #{error_uuid})") + end + end + # rubocop:enable Metrics/MethodLength + + private + + attr_accessor :batch, :current_user + + def find_auto_assign_user(task) + started_at = Time.current + correspondence = task.correspondence + assignee = assignable_user_finder.get_first_assignable_user(correspondence: correspondence) + + # return last error message for the most recent auto assign error. + if assignee.blank? + logger.no_eligible_assignees( + task: task, + started_at: started_at, + unassignable_reason: assignable_user_finder.unassignable_reasons.last + ) + end + assignee + end + + def assign(task, assignee) + started_at = Time.current + assign_task_to_user(task, assignee) + logger.assigned(task: task, started_at: started_at, assigned_to: assignee) + end + + def assign_task_to_user(task, user) + task.update!( + assigned_to: user, + assigned_at: Time.current, + assigned_by: current_user, + assigned_to_type: User.name, + status: Constants.TASK_STATUSES.assigned + ) + end + + def unassigned_review_package_tasks + return [] if FeatureToggle.enabled?(:auto_assign_banner_no_rpt) + + ReviewPackageTask + .where(status: Constants.TASK_STATUSES.unassigned) + .preload(:appeal, :assigned_by, :assigned_to, :parent) + .includes(:correspondence, correspondence: :veteran) + .references(:correspondence, correspondence: :veteran) + .order(va_date_of_receipt: :asc) + end + + def validate_run!(current_user_id, batch_auto_assignment_attempt_id) + if run_verifier.can_run_auto_assign?( + current_user_id: current_user_id, + batch_auto_assignment_attempt_id: batch_auto_assignment_attempt_id + ) + self.batch = run_verifier.verified_batch + self.current_user = run_verifier.verified_user + else + CorrespondenceAutoAssignLogger.fail_run_validation( + batch_auto_assignment_attempt_id: batch_auto_assignment_attempt_id, + msg: run_verifier.err_msg + ) + fail run_verifier.err_msg + end + end + + def logger + @logger ||= CorrespondenceAutoAssignLogger.new(current_user, batch) + end + + def assignable_user_finder + @assignable_user_finder ||= AutoAssignableUserFinder.new(current_user) + end + + def run_verifier + @run_verifier ||= CorrespondenceAutoAssignRunVerifier.new + end +end diff --git a/app/services/correspondence_documents_efolder_uploader.rb b/app/services/correspondence_documents_efolder_uploader.rb new file mode 100644 index 00000000000..06fe411c267 --- /dev/null +++ b/app/services/correspondence_documents_efolder_uploader.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +class CorrespondenceDocumentsEfolderUploader + def upload_documents_to_claim_evidence(correspondence, current_user, parent_task) + if Rails.env.production? + do_upload(correspondence) + + true + elsif FeatureToggle.enabled?(:ce_api_demo_toggle) + if Rails.env.test? + do_upload(correspondence) + end + + true + else + fail "Mock failure for upload in non-prod env" + end + rescue StandardError => error + Rails.logger.error(error.full_message) + create_efolder_upload_failed_task(correspondence, current_user, parent_task) + + false + end + + private + + def corresondence_veteran(correspondence) + Veteran.find_by(id: correspondence.veteran_id) + end + + def create_efolder_upload_failed_task(correspondence, current_user, parent_task) + return if EfolderUploadFailedTask.where(appeal_id: correspondence.id).count > 0 + + euft = EfolderUploadFailedTask.find_or_create_by( + appeal_id: correspondence.id, + appeal_type: Correspondence.name, + type: EfolderUploadFailedTask.name, + assigned_to: current_user, + parent_id: parent_task.id + ) + + euft.update!(status: Constants.TASK_STATUSES.in_progress) + end + + # :reek:FeatureEnvy + def do_upload(correspondence) + correspondence.correspondence_documents.each do |doc| + ExternalApi::ClaimEvidenceService.upload_document( + doc.pdf_location, + corresondence_veteran(correspondence).file_number, + doc.claim_evidence_upload_hash + ) + end + end +end diff --git a/app/services/correspondence_intake_processor.rb b/app/services/correspondence_intake_processor.rb new file mode 100644 index 00000000000..1b0ad6ec393 --- /dev/null +++ b/app/services/correspondence_intake_processor.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +# :reek:DataClump +# :reek:FeatureEnvy +class CorrespondenceIntakeProcessor + def process_intake(intake_params, current_user) + correspondence = Correspondence.find_by(uuid: intake_params[:correspondence_uuid]) + + fail "Correspondence not found" if correspondence.blank? + + parent_task = CorrespondenceIntakeTask.find_by(appeal_id: correspondence.id) + + return false if !correspondence_documents_efolder_uploader.upload_documents_to_claim_evidence( + correspondence, + current_user, + parent_task + ) + + do_upload_success_actions(parent_task, intake_params, correspondence, current_user) + end + + private + + # :reek:LongParameterList + def do_upload_success_actions(parent_task, intake_params, correspondence, current_user) + ActiveRecord::Base.transaction do + parent_task.update!(status: Constants.TASK_STATUSES.completed) + + create_correspondence_relations(intake_params, correspondence.id) + create_response_letter(intake_params, correspondence.id) + add_tasks_to_related_appeals(intake_params, current_user, correspondence.id) + complete_waived_evidence_submission_tasks(intake_params) + create_tasks_not_related_to_appeals(intake_params, correspondence, current_user) + create_mail_tasks(intake_params, correspondence, current_user) + end + + true + rescue StandardError => error + Rails.logger.error(error.full_message) + + false + end + + def create_correspondence_relations(intake_params, correspondence_id) + intake_params[:related_correspondence_uuids]&.map do |uuid| + CorrespondenceRelation.create!( + correspondence_id: correspondence_id, + related_correspondence_id: Correspondence.find_by(uuid: uuid)&.id + ) + end + end + + def create_response_letter(intake_params, correspondence_id) + current_user = RequestStore.store[:current_user] ||= User.system_user + + intake_params[:response_letters]&.map do |data| + current_value = nil + if data[:responseWindows] == "Custom" + current_value = data[:customValue] + end + + if data[:responseWindows] == "65 days" + current_value = 65 + end + + CorrespondenceResponseLetter.create!( + correspondence_id: correspondence_id, + date_sent: data[:date], + title: data[:title], + subcategory: data[:subType], + reason: data[:reason], + response_window: current_value, + letter_type: data[:type], + user_id: current_user.id + ) + end + end + + def link_appeals_to_correspondence(intake_params, correspondence_id) + intake_params[:related_appeal_ids]&.map do |appeal_id| + CorrespondenceAppeal.find_or_create_by(correspondence_id: correspondence_id, appeal_id: appeal_id) + end + end + + def create_appeal_related_tasks(data, current_user, correspondence_id) + appeal = Appeal.find(data[:appeal_id]) + # find the CorrespondenceAppeal created in link_appeals_to_correspondence + cor_appeal = CorrespondenceAppeal.find_by( + correspondence_id: correspondence_id, + appeal_id: appeal.id + ) + task = task_class_for_task_related(data).create_from_params( + { + appeal: appeal, + parent_id: appeal.root_task&.id, + assigned_to: class_for_assigned_to(data[:assigned_to]).singleton, + instructions: data[:content] + }, current_user + ) + # create join table to CorrespondenceAppealTask for tracking + CorrespondencesAppealsTask.find_or_create_by( + correspondence_appeal: cor_appeal, + task: task + ) + end + + def add_tasks_to_related_appeals(intake_params, current_user, correspondence_id) + link_appeals_to_correspondence(intake_params, correspondence_id) + intake_params[:tasks_related_to_appeal]&.map do |data| + create_appeal_related_tasks(data, current_user, correspondence_id) + end + end + + def complete_waived_evidence_submission_tasks(intake_params) + intake_params[:waived_evidence_submission_window_tasks]&.map do |task| + evidence_submission_window_task = EvidenceSubmissionWindowTask.find(task[:task_id]) + instructions = evidence_submission_window_task.instructions + evidence_submission_window_task.when_timer_ends + evidence_submission_window_task.update!(instructions: (instructions << task[:waive_reason])) + end + end + + def create_tasks_not_related_to_appeals(intake_params, correspondence, current_user) + unrelated_task_data = intake_params[:tasks_not_related_to_appeal] + + return if unrelated_task_data.blank? || !unrelated_task_data.length + + unrelated_task_data.map do |data| + task_class_for_task_unrelated(data).create_from_params( + { + parent_id: correspondence.root_task.id, + assigned_to: class_for_assigned_to(data[:assigned_to]).singleton, + instructions: data[:content] + }, current_user + ) + end + end + + def create_mail_tasks(intake_params, correspondence, current_user) + mail_task_data = intake_params[:mail_tasks] + + return if mail_task_data.blank? || !mail_task_data.length + + mail_task_data.map do |mail_task_type| + task = mail_task_class_for_type(mail_task_type).create_from_params( + { + parent_id: correspondence.root_task.id, + assigned_to_id: current_user.id, + assigned_to_type: User.name + }, current_user + ) + + task.update!(status: Constants.TASK_STATUSES.completed) + end + end + + def task_class_for_task_related(data) + task_type = data[:klass] + TASKS_RELATED_TO_APPEAL_TASK_TYPES[task_type]&.constantize + end + + def task_class_for_task_unrelated(data) + task_type = data[:klass] + CorrespondenceTask.tasks_not_related_to_an_appeal_names + .find { |name| name == task_type }&.constantize + end + + def correspondence_documents_efolder_uploader + @correspondence_documents_efolder_uploader ||= CorrespondenceDocumentsEfolderUploader.new + end + + def mail_task_class_for_type(task_type) + mail_task_types = { + AssociatedWithClaimsFolderMailTask.label => AssociatedWithClaimsFolderMailTask.name, + AddressChangeCorrespondenceMailTask.label => AddressChangeCorrespondenceMailTask.name, + EvidenceOrArgumentCorrespondenceMailTask.label => EvidenceOrArgumentCorrespondenceMailTask.name, + VacolsUpdatedMailTask.label => VacolsUpdatedMailTask.name + }.with_indifferent_access + + mail_task_types[task_type]&.constantize + end + + TASKS_RELATED_TO_APPEAL_TASK_TYPES = { + CavcCorrespondenceMailTask.name => CavcCorrespondenceMailTask.name, + ClearAndUnmistakeableErrorMailTask.name => ClearAndUnmistakeableErrorMailTask.name, + AddressChangeMailTask.name => AddressChangeMailTask.name, + CongressionalInterestMailTask.name => CongressionalInterestMailTask.name, + ControlledCorrespondenceMailTask.name => ControlledCorrespondenceMailTask.name, + DeathCertificateMailTask.name => DeathCertificateMailTask.name, + DocketSwitchMailTask.name => DocketSwitchMailTask.name, + EvidenceOrArgumentMailTask.name => EvidenceOrArgumentMailTask.name, + ExtensionRequestMailTask.name => ExtensionRequestMailTask.name, + FoiaRequestMailTask.name => FoiaRequestMailTask.name, + HearingPostponementRequestMailTask.name => HearingPostponementRequestMailTask.name, + HearingRelatedMailTask.name => HearingRelatedMailTask.name, + HearingWithdrawalRequestMailTask.name => HearingWithdrawalRequestMailTask.name, + ReconsiderationMotionMailTask.name => ReconsiderationMotionMailTask.name, + AodMotionMailTask.name => AodMotionMailTask.name, + OtherMotionMailTask.name => OtherMotionMailTask.name, + PowerOfAttorneyRelatedMailTask.name => PowerOfAttorneyRelatedMailTask.name, + PrivacyActRequestMailTask.name => PrivacyActRequestMailTask.name, + PrivacyComplaintMailTask.name => PrivacyComplaintMailTask.name, + ReturnedUndeliverableCorrespondenceMailTask.name => ReturnedUndeliverableCorrespondenceMailTask.name, + StatusInquiryMailTask.name => StatusInquiryMailTask.name, + AppealWithdrawalMailTask.name => AppealWithdrawalMailTask.name + }.with_indifferent_access + + def class_for_assigned_to(assigned_to) + available_assignees = { + AodTeam.name => AodTeam.name, + BvaDispatch.name => BvaDispatch.name, + CaseReview.name => CaseReview.name, + CavcLitigationSupport.name => CavcLitigationSupport.name, + ClerkOfTheBoard.name => ClerkOfTheBoard.name, + Colocated.name => Colocated.name, + HearingAdmin.name => HearingAdmin.name, + LitigationSupport.name => LitigationSupport.name, + PrivacyTeam.name => PrivacyTeam.name + }.with_indifferent_access + + available_assignees[assigned_to]&.constantize + end +end diff --git a/app/services/external_api/bgs_service.rb b/app/services/external_api/bgs_service.rb index 9ece1d0f259..d07ac013f93 100644 --- a/app/services/external_api/bgs_service.rb +++ b/app/services/external_api/bgs_service.rb @@ -24,6 +24,54 @@ def initialize(client: init_client) @people_by_ssn = {} end + def sensitivity_level_for_user(user) + fail "Invalid user" if !user.instance_of?(User) + + participant_id = get_participant_id_for_user(user) + + Rails.cache.fetch("sensitivity_level_for_user_id_#{user.id}", expires_in: 1.hour) do + DBService.release_db_connections + + MetricsService.record( + "BGS: sensitivity level for user #{user.id}", + service: :bgs, + name: "security.find_person_scrty_log_by_ptcpnt_id" + ) do + response = client.security.find_person_scrty_log_by_ptcpnt_id(participant_id) + + response.key?(:scrty_level_type_cd) ? Integer(response[:scrty_level_type_cd]) : 0 + rescue BGS::ShareError + 0 + end + end + end + + # :reek:FeatureEnvy + def sensitivity_level_for_veteran(veteran) + fail "Invalid veteran" if !veteran.instance_of?(Veteran) + + participant_id = veteran.participant_id + + Rails.cache.fetch("sensitivity_level_for_veteran_id_#{veteran.id}", expires_in: 1.hour) do + DBService.release_db_connections + + MetricsService.record( + "BGS: sensitivity level for veteran #{veteran.id}", + service: :bgs, + name: "security.find_sensitivity_level_by_participant_id" + ) do + response = client.security.find_sensitivity_level_by_participant_id(participant_id) + + # guard clause for no response + return 0 if response.blank? + + response&.key?(:scrty_level_type_cd) ? Integer(response[:scrty_level_type_cd]) : 0 + rescue BGS::ShareError + 0 + end + end + end + # :nocov: def get_end_products(vbms_id) diff --git a/app/services/organization_user_permission_checker.rb b/app/services/organization_user_permission_checker.rb new file mode 100644 index 00000000000..e041396adf5 --- /dev/null +++ b/app/services/organization_user_permission_checker.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class OrganizationUserPermissionChecker + def can_do_all?(permissions:, organization:, user:) + permissions.each do |permission| + return false if !can?( + permission_name: permission, + organization: organization, + user: user + ) + end + + true + end + + # Checks if a user possesses the permission and has the permitted flag enabled + # permission_name - a string pulled off a OrganizationPermission.permission attribute + # organization - an organization object + # user - an OrganizationUser object + def can?(permission_name:, organization:, user:) + org_permission = organization_permission(organization, permission_name) + return false if org_permission.blank? + + org_user = organization_user(organization, user) + return false if org_user.blank? + + OrganizationUserPermission.find_by( + organization_permission: org_permission, + organizations_user: org_user, + permitted: true + ).present? + end + + private + + def organization_permission(organization, permission_name) + OrganizationPermission.find_by( + organization: organization, + permission: permission_name, + enabled: true + ) + end + + def organization_user(organization, user) + OrganizationsUser.find_by( + organization: organization, + user: user + ) + end +end diff --git a/app/views/appeals/edit.html.erb b/app/views/appeals/edit.html.erb index 917a6cf17c5..5b0e54f2e38 100644 --- a/app/views/appeals/edit.html.erb +++ b/app/views/appeals/edit.html.erb @@ -26,6 +26,7 @@ correctClaimReviews: FeatureToggle.enabled?(:correct_claim_reviews, user: current_user), covidTimelinessExemption: FeatureToggle.enabled?(:covid_timeliness_exemption, user: current_user), split_appeal_workflow: FeatureToggle.enabled?(:split_appeal_workflow, user: current_user), + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user), cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), specialtyCaseTeamDistribution: FeatureToggle.enabled?(:specialty_case_team_distribution, user: current_user), mstIdentification: FeatureToggle.enabled?(:mst_identification, user: current_user), diff --git a/app/views/application/under_construction.html.erb b/app/views/application/under_construction.html.erb new file mode 100644 index 00000000000..5407297a3fb --- /dev/null +++ b/app/views/application/under_construction.html.erb @@ -0,0 +1,11 @@ +<% content_for :page_title do certification_header("Under Construction") end %> + +<% content_for :full_page_content do %> + <%= react_component("UnderConstruction", props: { + userDisplayName: current_user ? current_user.display_name : "", + dropdownUrls: dropdown_urls, + feedbackUrl: feedback_url, + buildDate: build_date, + dependenciesFaked: ApplicationController.dependencies_faked?, + }) %> +<% end %> diff --git a/app/views/correspondence/correspondence_cases.html.erb b/app/views/correspondence/correspondence_cases.html.erb new file mode 100644 index 00000000000..97f89d0387e --- /dev/null +++ b/app/views/correspondence/correspondence_cases.html.erb @@ -0,0 +1,24 @@ +<% content_for :full_page_content do %> + <%= react_component("Queue", props: { + userDisplayName: current_user.display_name, + userId: current_user.id, + userRole: (current_user.vacols_roles.first || "").capitalize, + userCssId: current_user.css_id, + isInboundOpsTeamUsers: current_user.inbound_ops_team_user?, + isInboundOpsSupervisor: current_user.inbound_ops_team_supervisor?, + isInboundOpsSuperuser: current_user.inbound_ops_team_superuser?, + organizations: current_user.selectable_organizations.map {|o| o.slice(:name, :url)}, + caseSearchHomePage: case_search_home_page, + dropdownUrls: dropdown_urls, + applicationUrls: application_urls, + feedbackUrl: feedback_url, + responseType: @response_type, + responseHeader: @response_header, + responseMessage: @response_message, + configUrl: '/queue/correspondence?json', + flash: flash, + featureToggles: { + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user) + } + }) %> +<% end %> diff --git a/app/views/correspondence/correspondence_details.html.erb b/app/views/correspondence/correspondence_details.html.erb new file mode 100644 index 00000000000..d8ae2e2587a --- /dev/null +++ b/app/views/correspondence/correspondence_details.html.erb @@ -0,0 +1,28 @@ +<% content_for :full_page_content do %> + <%= react_component("Queue", props: { + userDisplayName: current_user.display_name, + userId: current_user.id, + userRole: (current_user.vacols_roles.first || "").capitalize, + userCssId: current_user.css_id, + organizations: current_user.selectable_organizations.map {|o| o.slice(:name, :url)}, + caseSearchHomePage: case_search_home_page, + dropdownUrls: dropdown_urls, + applicationUrls: application_urls, + feedbackUrl: feedback_url, + correspondence: @correspondence_details[:correspondence], + flash: flash, + isInboundOpsUser: current_user.inbound_ops_team_user?, + isInboundOpsSupervisor: current_user.inbound_ops_team_supervisor?, + isInboundOpsSuperuser: current_user.inbound_ops_team_superuser?, + inboundOpsTeamUsers: @correspondence_details[:inbound_ops_team_users], + correspondenceAppeals: @correspondence_appeals, + responseType: @response_type, + responseHeader: @response_header, + responseMessage: @response_message, + organizations: @correspondence_details[:organizations], + correspondenceResponseLetters: @correspondence_response_letters, + featureToggles: { + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user) + } + }) %> +<% end %> diff --git a/app/views/correspondence/correspondence_team.html.erb b/app/views/correspondence/correspondence_team.html.erb new file mode 100644 index 00000000000..24cf161a375 --- /dev/null +++ b/app/views/correspondence/correspondence_team.html.erb @@ -0,0 +1,26 @@ +<% content_for :full_page_content do %> + <%= react_component("Queue", props: { + userDisplayName: current_user.display_name, + userId: current_user.id, + userRole: (current_user.vacols_roles.first || "").capitalize, + userCssId: current_user.css_id, + organizations: current_user.selectable_organizations.map {|o| o.slice(:name, :url)}, + caseSearchHomePage: case_search_home_page, + dropdownUrls: dropdown_urls, + applicationUrls: application_urls, + feedbackUrl: feedback_url, + configUrl: '/queue/correspondence/team?json', + flash: flash, + isInboundOpsUser: current_user.inbound_ops_team_user?, + isInboundOpsSupervisor: current_user.inbound_ops_team_supervisor?, + isInboundOpsSuperuser: current_user.inbound_ops_team_superuser?, + inboundOpsTeamUsers: @inbound_ops_team_users, + inboundOpsTeamNonAdmin: @inbound_ops_team_non_admin, + responseType: @response_type, + responseHeader: @response_header, + responseMessage: @response_message, + featureToggles: { + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user) + } + }) %> +<% end %> diff --git a/app/views/correspondence/intake.html.erb b/app/views/correspondence/intake.html.erb new file mode 100644 index 00000000000..0bb9ee5b0ad --- /dev/null +++ b/app/views/correspondence/intake.html.erb @@ -0,0 +1,20 @@ +<% content_for :full_page_content do %> + <%= react_component("Queue", props: { + userDisplayName: current_user.display_name, + userId: current_user.id, + userRole: (current_user.vacols_roles.first || "").capitalize, + userCssId: current_user.css_id, + dropdownUrls: dropdown_urls, + applicationUrls: application_urls, + feedbackUrl: feedback_url, + organizations: current_user.selectable_organizations.map {|o| o.slice(:name, :url)}, + featureToggles: { + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user) + }, + priorMail: @prior_mail, + correspondence: @correspondence, + isInboundOpsSupervisor: current_user.inbound_ops_team_supervisor?, + reduxStore: @redux_store, + autoTexts: @auto_texts, + }) %> +<% end %> diff --git a/app/views/correspondence/review_package.html.erb b/app/views/correspondence/review_package.html.erb new file mode 100644 index 00000000000..f928607ba51 --- /dev/null +++ b/app/views/correspondence/review_package.html.erb @@ -0,0 +1,20 @@ +<% content_for :full_page_content do %> + <%= react_component("Queue", props: { + userDisplayName: current_user.display_name, + userId: current_user.id, + userRole: (current_user.vacols_roles.first || "").capitalize, + userCssId: current_user.css_id, + isInboundOpsSuperuser: current_user.inbound_ops_team_superuser?, + userIsInboundOpsSupervisor: current_user.inbound_ops_team_supervisor?, + organizations: current_user.selectable_organizations.map {|o| o.slice(:name, :url)}, + caseSearchHomePage: case_search_home_page, + dropdownUrls: dropdown_urls, + inboundOpsTeamUsers: @inbound_ops_team_users, + correspondence: @correspondence, + correspondenceTypes: @correspondence_types, + hasEfolderFailedTask: @has_efolder_failed_task, + featureToggles: { + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user) + } + }) %> +<% end %> diff --git a/app/views/explain/show.html.erb b/app/views/explain/show.html.erb index 6c14341c578..8bb65bcf1d2 100644 --- a/app/views/explain/show.html.erb +++ b/app/views/explain/show.html.erb @@ -82,6 +82,188 @@ +<% elsif appeal.class.name == "Correspondence" %> +
+ + + + + + + + + + + +
+

Correspondence <%=appeal["uuid"]%>

+ +
+
+ show_pii = <%= show_pii_query_param %>. +
To toggle PII, click + <%= link_to('toggle show_pii', {action: 'show', show_pii: !show_pii_query_param, + fields: fields_query_param.to_s, + sections: sections_query_param.to_s}) %>. +
+
+
+ + <% + sections1 = %w[task_tree task_versions] + sections2 = [] + + enabledSections = (enabled_sections.first&.downcase == 'all') ? sections1 + sections2 : enabled_sections + enabledSections << "task_tree" unless fields_query_param.nil?; + %> + +
+
+ <% [sections1, sections2].each do |sections| %> + <% sections.each do |sectionId| %> + + <% sectionEnabled = enabledSections.include?(sectionId) %> + + onclick="toggleSection(this, 'main_panel', '<%=sectionId%>')"> + + (go there) + + <% end %> + <% end %> + + + + + +
+
+ +
+
+ + <%= render "menubar", menubarId: "section_checkboxes", contentId: "main_panel", autoshowId: "autoshow_chkbox" %> + <%= render "side_panel" %> + +
+
+ <% (sections1 + sections2).each do |sectionId| %> + <% sectionEnabled = enabledSections.include?(sectionId) %> +
+ <%= render sectionId %> +
+
+ <% end %> +
+
+ + +
+ <% else %>
@@ -264,7 +446,7 @@ } const setDetailsPaneMessage = (htmlContent)=>{ - document.getElementById("side_panel_message").innerHTML = htmlContent; + document.getElementById("side_panel_message").textContent = htmlContent; }; const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; diff --git a/app/views/queue/index.html.erb b/app/views/queue/index.html.erb index 60ded4506a7..2c721b368f7 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -5,6 +5,8 @@ userRole: (current_user.vacols_roles.first || "").capitalize, userCssId: current_user.css_id, organizations: current_user.selectable_organizations.map {|o| o.slice(:name, :url)}, + organizationPermissions: @permissions, + userPermissions: @user_permissions, userIsVsoEmployee: current_user.vso_employee?, userIsCamoEmployee: current_user.camo_employee?, userIsSCTCoordinator: current_user.specialty_case_team_coordinator?, @@ -21,6 +23,9 @@ userCanViewHearingSchedule: current_user.can_view_hearing_schedule?, userCanViewOvertimeStatus: current_user.can_view_overtime_status?, userCanViewEditNodDate: current_user.can_view_edit_nod_date?, + isInboundOpsTeamUser: current_user.inbound_ops_team_user?, + isInboundOpsSupervisor: current_user.inbound_ops_team_supervisor?, + isInboundOpsSuperuser: current_user.inbound_ops_team_superuser?, canEditCavcRemands: current_user.can_edit_cavc_remands?, canEditCavcDashboards: current_user.can_edit_cavc_dashboards?, canViewCavcDashboards: current_user.can_view_cavc_dashboards?, @@ -51,6 +56,8 @@ das_case_timeliness: FeatureToggle.enabled?(:das_case_timeliness, user: current_user), das_case_timeline: FeatureToggle.enabled?(:das_case_timeline, user: current_user), split_appeal_workflow: FeatureToggle.enabled?(:split_appeal_workflow, user: current_user), + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user), + ce_api_demo_toggle: FeatureToggle.enabled?(:ce_api_demo_toggle, user: current_user), cavc_remand_granted_substitute_appellant: FeatureToggle.enabled?(:cavc_remand_granted_substitute_appellant, user: current_user), cavc_dashboard_workflow: FeatureToggle.enabled?(:cavc_dashboard_workflow, user: current_user), mstIdentification: FeatureToggle.enabled?(:mst_identification, user: current_user), diff --git a/app/views/test/correspondence/index.html.erb b/app/views/test/correspondence/index.html.erb new file mode 100644 index 00000000000..8d5dff048d2 --- /dev/null +++ b/app/views/test/correspondence/index.html.erb @@ -0,0 +1,12 @@ +<% content_for :page_title do %>  >  Test Correspondence<% end %> + +<% content_for :full_page_content do %> + <%= react_component("TestCorrespondence", props: { + userDisplayName: current_user.display_name, + dropdownUrls: dropdown_urls, + applicationUrls: application_urls, + featureToggles: { + + } + }) %> +<% end %> diff --git a/app/workflows/correspondence_root_task_factory.rb b/app/workflows/correspondence_root_task_factory.rb new file mode 100644 index 00000000000..00e150898d0 --- /dev/null +++ b/app/workflows/correspondence_root_task_factory.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class CorrespondenceRootTaskFactory + include TasksFactoryConcern + + def initialize(correspondence) + @correspondence = correspondence + end + + def create_root_and_sub_tasks! + ActiveRecord::Base.transaction do + create_root! + create_subtasks! + end + end + + private + + def create_root! + @root_task = CorrespondenceRootTask.find_or_create_by!( + appeal_id: @correspondence.id, + assigned_to: InboundOpsTeam.singleton, + appeal_type: Correspondence.name + ) + + @root_task.update(status: Constants.TASK_STATUSES.on_hold) + end + + def create_subtasks! + @review_package_task = ReviewPackageTask.find_or_create_by!( + appeal_id: @correspondence.id, + assigned_to: InboundOpsTeam.singleton, + appeal_type: Correspondence.name, + parent_id: @root_task.id, + type: ReviewPackageTask.name + ) + + @review_package_task.update!(status: Constants.TASK_STATUSES.unassigned) + end +end diff --git a/client/COPY.json b/client/COPY.json index bbc5dc80ad9..5d01542d219 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -52,6 +52,13 @@ "CASE_LIST_TABLE_QUEUE_DROPDOWN_LABEL": "Switch views", "CASE_LIST_TABLE_QUEUE_DROPDOWN_OWN_CASES_LABEL": "Your cases", "CASE_LIST_TABLE_QUEUE_DROPDOWN_TEAM_CASES_LABEL": "%s team cases", + "CASE_LIST_TABLE_QUEUE_DROPDOWN_CORRESPONDENCE_CASES": "Correspondence Cases", + "CASE_LIST_TABLE_QUEUE_DROPDOWN_OWN_CORRESPONDENCE_LABEL": "Your Correspondence", + "CASE_LIST_TABLE_PACKAGE_DOCUMENT_TYPE_COLUMN_TITLE": "Package Document Type", + "TITLE_MODAL_EDIT_DOCUMENT_TYPE_CORRESPONDENCE": "Edit document type", + "TEXT_MODAL_EDIT_DOCUMENT_TYPE_CORRESPONDENCE": "Change the document type of the selected document.", + "ORIGINAL_DOC_EDIT_DOCUMENT_TYPE_CORRESPONDENCE": "Original document type", + "NEW_DOC_EDIT_DOCUMENT_TYPE_CORRESPONDENCE": "New document type", "CASE_LIST_TABLE_EMPTY_TEXT": "This Veteran has no appeals at this time.", "CASE_LIST_TABLE_TOTAL_DAYS_COLUMN_TITLE": "Total Days", "CASE_LIST_TABLE_BOARD_INTAKE": "Board Intake", @@ -80,6 +87,44 @@ "OTHER_REVIEWS_TABLE_ESTABLISHING": "Establishing...", "OTHER_REVIEWS_TABLE_SYNCING_DECISIONS": "Syncing decisions...", "OTHER_REVIEWS_TABLE_SYNCING_DECISIONS_ERROR": "Decisions sync failed. Support notified.", + "CORRESPONDENCE_REVIEW_PACKAGE_TITLE": "Review Package", + "CORRESPONDENCE_REVIEW_PACKAGE_SUB_TITLE": "Review the mail package details below. If the document type is NOD, select the appropriate action needed for the form.", + "CORRESPONDENCE_REVIEW_CMP_INFO_TITLE": "CMP Information", + "CORRESPONDENCE_INTAKE_FORM_SUBMIT_MODAL_TITLE": "Confirm submission", + "CORRESPONDENCE_INTAKE_FORM_SUBMIT_MODAL_BODY": "Submitting the correspondence record will upload the mail package to the eFolder. Make sure that all information is correct before submitting.", + "CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TITLE": "The correspondence's documents have failed to upload to the eFolder", + "CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TEXT": "You can upload it from this page to the veteran's eFolder at a later time.", + "CORRESPONDENCE_QUEUE_PACKAGE_ACTION_SUCCESS": { + "REASSIGN": { + "APPROVE": { + "TITLE": "You have successfully reassigned a mail record for %s", + "MESSAGE": "Please go to your individual queue to see any self assigned correspondence." + }, + "REJECT": { + "TITLE": "You have successfully rejected a package request for %s", + "MESSAGE": "The package will be re-assigned to the user that sent the request." + } + }, + "REMOVE": { + "APPROVE": { + "TITLE": "You have successfully removed a mail package for %s", + "MESSAGE": "The package has been removed from Caseflow and must be manually uploaded again from the Centralized Mail Portal, if it needs to be processed." + }, + "REJECT": { + "TITLE": "You have successfully rejected a package request for %s", + "MESSAGE": "The package will be re-assigned to the user that sent the request." + } + } + }, + "CORRESPONDENCE_QUEUE_PACKAGE_ACTION_DECISION_OPTIONS": { + "APPROVE": "approve", + "REJECT": "reject" + }, + "CORRESPONDENCE_OTHER_MOTION_TASK_COMPLETE": "%s task has been marked complete. If you have made a mistake, please email %s for any changes.", + "CORRESPONDENCE_OTHER_MOTION_MODAL_DETAIL": "Provide context and instructions for the action", + "CORRESPONDENCE_COMPLETE_TASK_CONFIRMATION": "Task for %s's case has been completed", + "CORRESPONDENCE_COMPLETE_TASK_CONFIRMATION_DETAIL": "Task for %s's case has been completed", + "DOCUMENT_PREVIEW": "Document Preview", "TASK_SNAPSHOT_ABOUT_BOX_TITLE": "About the case", "TASK_SNAPSHOT_ABOUT_BOX_HEARING_REQUEST_TYPE_LABEL": "Hearing Type", "TASK_SNAPSHOT_ABOUT_BOX_TYPE_LABEL": "Appeal Stream Type", @@ -775,7 +820,7 @@ "USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_TEXT": "User Name or CSS ID to add", "USER_MANAGEMENT_EDIT_USER_IN_ORG_LABEL": "Edit current team members", "USER_MANAGEMENT_ADMIN_RIGHTS_HEADING": "Add / Remove admin rights: ", - "USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION": "Determines which users have access to the team queue and the ability to manage the team membership.", + "USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION": "Grants the ability to manage team membership and perform superuser actions.", "USER_MANAGEMENT_REMOVE_USER_HEADING": "Remove from team: ", "USER_MANAGEMENT_JUDGE_TEAM_REMOVE_USER_DESCRIPTION": "Remove an attorney from this judge team. Judges may not be removed from their own judge team.", "USER_MANAGEMENT_REMOVE_USER_DESCRIPTION": "Remove a user from this team.", @@ -1428,6 +1473,50 @@ "POA_SUCCESSFULLY_REFRESH_MESSAGE": "Successfully refreshed. No power of attorney information was found at this time.", "POA_UPDATED_SUCCESSFULLY": "POA Updated Successfully", "EMPLOYER_IDENTIFICATION_NUMBER": "Employer Identification Number", + "PACKAGE_ACTION_MERGE_DESCRIPTION": "By confirming, you will send a request for the supervisor to take action on the following package:", + "PACKAGE_ACTION_MERGE_TITLE": "Request merge", + "PACKAGE_ACTION_MERGE_TEXTAREA_LABEL": "Reason for merge", + "PACKAGE_ACTION_MERGE_RADIO_LABEL": "Select a reason for merging this package.", + "PACKAGE_ACTION_MODAL_DESCRIPTION": "By confirming, you will send a request for the supervisor to take action on the following package:", + "PACKAGE_ACTION_REMOVAL_TITLE": "Request package removal", + "PACKAGE_ACTION_REMOVAL_TEXTAREA_LABEL": "Provide a reason for removal", + "PACKAGE_ACTION_REASSIGN_DESCRIPTION": "By confirming, you will send a request for the supervisor to take action on the following package:", + "PACKAGE_ACTION_REASSIGN_TITLE":"Request package reassignment", + "PACKAGE_ACTION_REASSIGN_TEXTAREA_LABEL":"Provide a reason for reassignment", + "PACKAGE_ACTION_SPLIT_TEXTAREA_LABEL":"Reason for split", + "PACKAGE_ACTION_SPLIT_TITLE": "Request split package", + "CORRESPONDENCE_DOC_UPLOAD_FAILED_HEADER": "The correspondence's documents have failed to upload to the eFolder", + "CORRESPONDENCE_DOC_UPLOAD_FAILED_MESSAGE": "You can upload it from this page to the veteran's eFolder at a later time.", + "CORRESPONDENCE_HEADER_REMOVE_PACKAGE": "Review request", + "CORRESPONDENCE_TITLE_REMOVE_PACKAGE": "Reason for removal:", + "CORRESPONDENCE_TITLE_REASSIGN_PACKAGE": "Reason for reassign:", + "CORRRESPONDENCE_LABEL_OPTION_REMOVE_PACKAGE": " Choose whether to approve the request for removal or reject it.", + "CORRRESPONDENCE_LABEL_OPTION_REASSIGN_PACKAGE": " Choose whether to approve the request for reassign or reject it.", + "CORRRESPONDENCE_TEXT_REMOVE_PACKAGE": "\nThis is the reason for removal provided by the general user.", + "CORRRESPONDENCE_SECOND_TEXT_REMOVE_PACKAGE": "\nThis is the reason for removal provided by the user that submitted the request.", + "CORRESPONDENCE_TITLE_REMOVE_PACKAGE_REASON_REJECT": "Provide a reason for rejection", + "CORRESPONDENCE_TITLE_REMOVE_PACKAGE_BANNER": "You have successfully removed a mail package for %s", + "CORRESPONDENCE_TITLE_REMOVE_PACKAGE_MESSAGE": "You have successfully submitted removal request for %s", + "CORRESPONDENCE_TITLE_SPLIT_PACKAGE_MESSAGE": "You have successfully submitted split request for %s", + "CORRESPONDENCE_TITLE_REASSIGNMENT_PACKAGE_MESSAGE": "You have successfully submitted reassignment request for %s", + "CORRESPONDENCE_TITLE_MERGE_PACKAGE_MESSAGE": "You have successfully submitted merge request for %s", + "CORRESPONDENCE_MESSAGE_REMOVE_PACKAGE_BANNER": "The package has been removed from Caseflow and must be manually uploaded again from the Centralized Mail Portal, if it needs to be processed.", + "CORRESPONDENCE_PACKAGE_ACTION_DESCRIPTION": "A supervisor will review your request and split if warranted.", + "UNDER_CONSTRUCTION_MESSAGE": "This page is under construction.", + "CORRESPONDENCE_READONLY_BANNER_HEADER": "This package has a pending request", + "CORRESPONDENCE_READONLY_BANNER_MESSAGE": "Any further actions cannot be made on this package until a supervisor or superuser reviews the request.", + "CORRESPONDENCE_READONLY_SUPERVISOR_BANNER_MESSAGE": "Any further actions cannot be made on this package until a supervisor reviews the request.", + "ASSOCIATED_WITH_CLAIMS_FOLDER_MAIL_TASK_LABEL": "Associated with Claims Folder", + "CORRESPONDENCE_CASES_REMOVE_PACKAGE_MODAL_TITLE": "Review request", + "CORRESPONDENCE_CASES_REASSIGN_PACKAGE_MODAL_TITLE": "Review request", + "VACOLS_UPDATED_MAIL_TASK_LABEL": "VACOLS updated", + "AUTO_ASSIGN_CORRESPONDENCES_BUTTON": "Auto assign correspondences", + "BAAA_NO_UNASSIGNED_CORRESPONDENCE": "There are no correspondences available for assignment", + "BAAA_USERS_MAX_QUEUE_REACHED": "Queues have reached maximum capacity for all team members", + "BAAA_SUCCESS_MESSAGE": "Team members were assigned correspondences based on auto-assignment rules", + "BAAA_FAILED_TITLE": "Auto-assignment for correspondence has failed", + "BAAA_UNSUCCESSFUL_TITLE": "Auto-assignment for correspondence could not be completed", + "BAAA_ERROR_MESSAGE": "Please try again at a later time or contact the Help Desk", "MOVE_TO_SCT_MODAL_TITLE": "Move appeal to SCT queue", "MOVE_TO_SCT_MODAL_BODY": "One or more Specialty Case Team (SCT) issues have been added and the appeal will now be routed to the SCT queue.", "MOVE_TO_DISTRIBUTION_MODAL_TITLE": "Move appeal to regular distribution", @@ -1488,6 +1577,21 @@ "CASE_DISTRIBUTION_LEVER_HISTORY_PREV_VALUE": "Previous Value", "CASE_DISTRIBUTION_LEVER_HISTORY_UPDATED_VALUE": "Updated Value", "CASE_DISTRIBUTION_STATIC_LEVERS_VALUES": "Values", + "CORRESPONDENCE_ADMIN": { + "HEADER": "Correspondence admin", + "SUB_HEADER": "Correspondence generation process", + "DESCRIPTION": "All generated correspondence will be assigned out using the auto assign algorithm. If any correspondence were not assigned due to users not meeting the rules/permissions, they will be placed in the unassigned tab", + "COUNT_LABEL": "Enter the number of correspondence to be generated (Value must be between 1 and 40):", + "SUBMIT_BUTTON_TEXT": "Generate correspondence", + "INVALID_ERROR": { + "TITLE": "Invalid file numbers", + "MESSAGE": "The following Veteran file number(s) were invalid. Please enter valid file number(s) and try again: " + }, + "SUCCESS": { + "TITLE": "You have successfully generated correspondence", + "MESSAGE": " correspondence have been created for the following Veteran file number(s): " + } + }, "TEST_SEEDS_ALERT_MESSAGE": " in progress", "TEST_SEEDS_RUN_SEEDS": "Scenario Seeds", "TEST_SEEDS_CUSTOM_SEEDS": "Custom Seeds", @@ -1516,5 +1620,10 @@ } }, "VHA_BANNER_DISPOSITIONS_CANNOT_BE_UPDATED_NON_ADMIN": "Requests for issue modifications have been submitted for this case. Dispositions cannot be made until a VHA admin completes review of the requested changes.", - "VHA_BANNER_DISPOSITIONS_CANNOT_BE_UPDATED_ADMIN": "Requests for issue modifications have been submitted for this case. Dispositions cannot be made until a VHA admin completes review of the requested changes. Click the \"Edit issues\" button above to review the issue modification requests." + "VHA_BANNER_DISPOSITIONS_CANNOT_BE_UPDATED_ADMIN": "Requests for issue modifications have been submitted for this case. Dispositions cannot be made until a VHA admin completes review of the requested changes. Click the \"Edit issues\" button above to review the issue modification requests.", + "CORRESPONDENCE_DETAILS": { + "COMPLETED_MAIL_TASKS": "Completed Mail Tasks", + "NO_COMPLETED_MAIL_TASKS": "No previously completed mail tasks prior to intake." + }, + "ALL_CORRESPONDENCES": "All Correspondence" } diff --git a/client/app/components/Dropdown.jsx b/client/app/components/Dropdown.jsx index ba141a66c81..dd098968d14 100644 --- a/client/app/components/Dropdown.jsx +++ b/client/app/components/Dropdown.jsx @@ -36,7 +36,7 @@ export default class Dropdown extends React.Component { {label || name} {required && Required} {errorMessage && {errorMessage}} - { defaultText && } {options.map((option, index) =>
+
+ {paginationButtons} +
+ + ); + } +} + +CorrespondencePagination.propTypes = { + pageSize: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + currentItems: PropTypes.number.isRequired, + totalPages: PropTypes.number, + totalItems: PropTypes.number, + updatePage: PropTypes.func.isRequired, + columns: PropTypes.func, + summary: PropTypes.string, + className: PropTypes.string, + rowObjects: PropTypes.arrayOf(object).isRequired, + headerClassName: PropTypes.string, + bodyClassName: PropTypes.string, + tbodyId: PropTypes.string, + getKeyForRow: PropTypes.func +}; + +export default React.memo(CorrespondencePagination); diff --git a/client/app/components/Pagination/Pagination.jsx b/client/app/components/Pagination/Pagination.jsx index a9b06122680..abf002945bb 100644 --- a/client/app/components/Pagination/Pagination.jsx +++ b/client/app/components/Pagination/Pagination.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Table from 'app/components/Table'; import PaginationButton from './PaginationButton'; @@ -36,7 +37,7 @@ class Pagination extends React.PureComponent { }; generateBlankButton = (key) => { - return ; + return ; }; render() { @@ -121,10 +122,15 @@ class Pagination extends React.PureComponent { } return ( +
+ { this.props.enableTopPagination && (
+ {paginationButtons} +
) }
{paginationSummary}
+ {this.props.table}
{paginationButtons}
@@ -139,7 +145,9 @@ Pagination.propTypes = { currentCases: PropTypes.number.isRequired, totalPages: PropTypes.number, totalCases: PropTypes.number, - updatePage: PropTypes.func.isRequired + updatePage: PropTypes.func.isRequired, + table: PropTypes.oneOfType([PropTypes.instanceOf(Table), PropTypes.object]), + enableTopPagination: PropTypes.bool }; export default Pagination; diff --git a/client/app/components/RadioField.jsx b/client/app/components/RadioField.jsx index 2775bda6282..71f3cc7a0e7 100644 --- a/client/app/components/RadioField.jsx +++ b/client/app/components/RadioField.jsx @@ -4,9 +4,10 @@ import classNames from 'classnames'; import RequiredIndicator from './RequiredIndicator'; import StringUtil from '../util/StringUtil'; -import Tooltip from './Tooltip'; +import MaybeAddTooltip from './TooltipHelper'; -import ACD_LEVERS from '../../constants/ACD_LEVERS'; +import RadioInput from './RadioInput'; +import { extractFieldProps } from './fieldUtils'; import { helpText } from './RadioField.module.scss'; @@ -29,11 +30,9 @@ RadioFieldHelpText.propTypes = { */ export const RadioField = (props) => { + + const { id, className, label, inputRef } = extractFieldProps(props); const { - id, - className, - label, - inputRef, inputProps, name, options, @@ -68,25 +67,6 @@ export const RadioField = (props) => { ); - const maybeAddTooltip = (option, radioField) => { - if (option.tooltipText) { - const keyId = `tooltip-${option.value}`; - - return - {radioField} - ; - } - - return radioField; - }; - const isDisabled = (option) => Boolean(option.disabled); const handleChange = (event) => onChange?.(event.target.value); @@ -109,17 +89,15 @@ export const RadioField = (props) => { className="cf-form-radio-option" key={`${idPart}-${option.value}-${i}`} > -
); - return maybeAddTooltip(option, radioField); + return ( + + {radioField} + + ); })} diff --git a/client/app/components/RadioFieldWithChildren.jsx b/client/app/components/RadioFieldWithChildren.jsx new file mode 100644 index 00000000000..2799f4fd097 --- /dev/null +++ b/client/app/components/RadioFieldWithChildren.jsx @@ -0,0 +1,181 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import RequiredIndicator from './RequiredIndicator'; +import StringUtil from '../util/StringUtil'; +import MaybeAddTooltip from './TooltipHelper'; + +import RadioInput from './RadioInput'; +import { extractFieldProps } from './fieldUtils'; + +import { helpText } from './RadioField.module.scss'; +const RadioFieldHelpText = ({ help, className }) => { + const helpClasses = classNames('cf-form-radio-help', helpText, className); + + return
{help}
; +}; + +RadioFieldHelpText.propTypes = { + help: PropTypes.string.isRequired, + className: PropTypes.string, +}; + +/** + * Clone of RadioField button component that allows children objects . + * See StyleGuideRadioField.jsx for usage examples. + */ + +// Correspondence: Refactor Candidate +// CodeClimate: Identical blocks of code with RadioField +/* eslint-disable */ + +export const RadioFieldWithChildren = (props) => { + + const { id, className, label, inputRef } = extractFieldProps(props); + + const { + inputProps, + name, + options, + value, + onChange, + required, + errorMessage, + strongLabel, + hideLabel, + styling, + vertical, + optionsStyling + } = props; + + const isVertical = useMemo(() => props.vertical || props.options.length > 2, [ + vertical, + options, + ]); + + const radioClass = className. + concat(isVertical ? 'cf-form-radio' : 'cf-form-radio-inline'). + concat(errorMessage ? 'usa-input-error' : ''); + + const labelClass = hideLabel ? 'usa-sr-only' : ''; + + // Since HTML5 IDs should not contain spaces... + const idPart = StringUtil.html5CompliantId(id || name); + + const labelContents = ( + + {label || name} {required && } + + ); + + const isDisabled = (option) => Boolean(option.disabled); + + const handleChange = (event) => onChange?.(event.target.value); + const controlled = useMemo(() => typeof value !== 'undefined', [value]); + + return ( +
+ + {strongLabel ? {labelContents} : labelContents} + + + {errorMessage && ( + {errorMessage} + )} + +
+ {options.map((option, i) => { + const optionDisabled = isDisabled(option); + const radioField = (
+ + + {option.displayElement && option.element} + {option.help && } +
+ ); + + return ( + + {radioField} + + ); + })} +
+
+ ); +}; + +RadioFieldWithChildren.defaultProps = { + required: false, + displayElement: false, + className: ['usa-fieldset-inputs'], +}; + +RadioFieldWithChildren.propTypes = { + id: PropTypes.string, + className: PropTypes.arrayOf(PropTypes.string), + required: PropTypes.bool, + // Pass a ref to the `input` element + inputRef: PropTypes.oneOfType([ + // Either a function + PropTypes.func, + // Or the instance of a DOM native element (see the note about SSR) + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), + // Props to be applied to the `input` element + inputProps: PropTypes.object, + // Text to display in a `legend` element for the radio group fieldset + label: PropTypes.node, + // String to be applied to the `name` attribute of all the `input` elements + name: PropTypes.string.isRequired, + + /** + * Callback fired when value is changed + * @param {string} value The current value of the component + */ + onChange: PropTypes.func, + // an array of options used to define individual radio inputs + options: PropTypes.arrayOf( + PropTypes.shape({ + // Text to be used as label for individual radio input + displayText: PropTypes.node, + // The `value` attribute for the radio input + value: PropTypes.string, + // Help text to be displayed below the label + help: PropTypes.string, + // The child element to display under the radiofield option + element: PropTypes.element, + // Used to control visibility of child element + displayElement: PropTypes.bool + }) + ), + // The value of the named `input` element(s); required for a controlled component + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + // Stack `input` elements vertically (automatic for more than two options) + vertical: PropTypes.bool, + errorMessage: PropTypes.string, + strongLabel: PropTypes.bool, + hideLabel: PropTypes.bool, + styling: PropTypes.object, + optionsStyling: PropTypes.object +}; + +export default RadioFieldWithChildren; diff --git a/client/app/components/RadioInput.jsx b/client/app/components/RadioInput.jsx new file mode 100644 index 00000000000..343f5b2c2ea --- /dev/null +++ b/client/app/components/RadioInput.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ACD_LEVERS from '../../constants/ACD_LEVERS'; + +const RadioInput = ({ handleChange, name, idPart, option, controlled, value, inputRef, inputProps }) => { + const isChecked = controlled ? value === option.value : option.checked; + + return ( + + ); +}; + +RadioInput.propTypes = { + handleChange: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + idPart: PropTypes.string.isRequired, + option: PropTypes.object.isRequired, + controlled: PropTypes.bool.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired, + inputRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), + inputProps: PropTypes.object, +}; + +export default RadioInput; diff --git a/client/app/components/ReactSelectDropdown.jsx b/client/app/components/ReactSelectDropdown.jsx index fc26daadd7d..88528d315da 100644 --- a/client/app/components/ReactSelectDropdown.jsx +++ b/client/app/components/ReactSelectDropdown.jsx @@ -58,17 +58,22 @@ const selectContainerStyles = css({ }); const ReactSelectDropdown = (props) => { + const isDisabled = props.disabled || false; + return ( -
+
+ this.props.paginate ? +
+ + } + enableTopPagination = {this.props.enableTopPagination || false} + /> +
: +
); }; } CaseListTable.propTypes = { appeals: PropTypes.arrayOf(PropTypes.object).isRequired, + taskRelatedAppealIds: PropTypes.array, + showCheckboxes: PropTypes.bool, + paginate: PropTypes.bool, + linkOpensInNewTab: PropTypes.bool, + checkboxOnChange: PropTypes.func, styling: PropTypes.object, clearCaseListSearch: PropTypes.func, userRole: PropTypes.string, - userCssId: PropTypes.string + userCssId: PropTypes.string, + currentPage: PropTypes.number, + updatePageHandlerCallback: PropTypes.func, + disabled: PropTypes.bool, + enableTopPagination: PropTypes.bool +}; + +CaseListTable.defaultProps = { + showCheckboxes: false, + paginate: false, + currentPage: 1 }; const mapStateToProps = (state) => ({ diff --git a/client/app/queue/CaseListView.jsx b/client/app/queue/CaseListView.jsx index 0f951cdbdc2..882a410221c 100644 --- a/client/app/queue/CaseListView.jsx +++ b/client/app/queue/CaseListView.jsx @@ -154,6 +154,7 @@ const mapStateToProps = (state, ownProps) => { const appeals = caseflowVeteranIds.flatMap( (id) => appealsByCaseflowVeteranId(state, { caseflowVeteranId: id }) ); + const claimReviews = caseflowVeteranIds.flatMap( (id) => claimReviewsByCaseflowVeteranId(state, { caseflowVeteranId: id }) ); diff --git a/client/app/queue/OrganizationPermissions.jsx b/client/app/queue/OrganizationPermissions.jsx new file mode 100644 index 00000000000..800f0ceff3c --- /dev/null +++ b/client/app/queue/OrganizationPermissions.jsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react'; +import Checkbox from '../components/Checkbox'; +import PropTypes from 'prop-types'; +import ApiUtil from '../util/ApiUtil'; +import INBOUND_OPS_PERMISSIONS from '../../../client/constants/ORGANIZATION_PERMISSIONS'; + +const OrganizationPermissions = (props) => { + const [toggledCheckboxes, setToggledCheckboxes] = useState([]); + const organization = props.organization; + + const updateToggledCheckBoxes = (userId, permissionName, checked) => { + const newData = { userId, permissionName, checked }; + const stateCopy = toggledCheckboxes; + + // check if the id and permission already exist in the state. Returns undefined if it didn't find a match. + const existsInState = toggledCheckboxes.findIndex((checkboxData) => + checkboxData.userId === newData.userId && checkboxData.permissionName === newData.permissionName); + + // add the item to state if it didn't exist, update it otherwise. + if (existsInState > -1) { + stateCopy[existsInState].checked = !stateCopy[existsInState].checked; + setToggledCheckboxes([...stateCopy]); + } else { + setToggledCheckboxes([...[newData], ...stateCopy]); + } + }; + + const modifyUserPermission = (userId, permissionName) => () => { + const payload = { data: { userId, permissionName } }; + + ApiUtil.patch(`/organizations/${props.organization}/update_permissions`, payload). + then((response) => { + updateToggledCheckBoxes(userId, permissionName, response.body.checked); + }, (error) => { + // eslint-disable-next-line no-console + console.log(error); + }); + }; + + const userPermissions = (user, permission) => { + if (user.attributes.user_permission === null || + typeof user.attributes.user_permission === 'undefined' || + user.attributes.user_permission.length === 0) { + return false; + } + + if (user.attributes.user_permission.flat().find((userPer) => userPer.permission === permission)) { + return true; + } + }; + + const checkAdminPermission = (user, permission) => { + if (user.attributes.user_admin_permission === null || + typeof user.attributes.user_admin_permission === 'undefined') { + return false; + } + + if (user.attributes.user_admin_permission.find((adminPer) => adminPer.permission === permission)) { + return true; + } + }; + + const parentPermissionChecked = (userId, parentId) => { + if (typeof parentId !== 'number') { + return true; + } + + let result = false; + const parentPermission = props.permissions.find((permission) => permission.id === parentId); + const orgUserPermissions = props.orgnizationUserPermissions.find((x) => + x.user_id === Number(userId)).organization_user_permissions; + + const checkboxInState = toggledCheckboxes.find((permission) => + permission.userId === userId && + permission.permissionName === parentPermission.permission); + + if (typeof checkboxInState !== 'undefined' && checkboxInState.checked) { + return true; + } + + orgUserPermissions.forEach((permission) => { + if (permission.organization_permission.permission === parentPermission.permission && + permission.permitted && + typeof checkboxInState === 'undefined') { + result = true; + } + }); + + return result; + }; + + // Correspondence: Refactor Candidate + // CodeClimate: Avoid too many return statements within this function. + const getCheckboxEnabled = (user, orgUserData, permission) => { + let isEnabled = false; + + // uses the local state over what comes in over props + const stateValue = toggledCheckboxes.find( + (storedCheckbox) => + storedCheckbox.userId === user.id && + storedCheckbox.permissionName === permission.permission + ); + + if ( + toggledCheckboxes.find( + (checkboxInState) => + checkboxInState.userId === user.id && + checkboxInState.permissionName === permission.permission && + checkboxInState.checked + ) + ) { + isEnabled = true; + } + + // check if user is marked as admin to auto check the checkbox. + if (permission.default_for_admin && user.attributes.admin) { + isEnabled = true; + } + + if (typeof stateValue !== 'undefined') { + isEnabled = stateValue.checked; + } + + // default state that came in when page loads, used as final fallback. + const relevantPermissions = props.orgnizationUserPermissions.find( + (oup) => oup.user_id === Number(user.id) + ).organization_user_permissions; + + if ( + relevantPermissions.find( + (perm) => + perm.organization_permission.permission === permission.permission && + perm.permitted + ) + ) { + isEnabled = true; + } + + return isEnabled; + }; + + const permissionAdminCheck = (user, permission) => { + if (user.attributes.admin && permission.default_for_admin) { + return true; + } + if (user.attributes.admin && !permission.default_for_admin) { + return false; + } + + return true; + }; + + const orderInboundOpsCheckboxes = (permissions) => { + const orderedPermissions = []; + + // sort through permissions and order based on constants file + Object.keys(INBOUND_OPS_PERMISSIONS).map((orgPermission) => { + return orderedPermissions.push(permissions.find((permission) => permission.permission === orgPermission)); + }); + + return orderedPermissions; + }; + + // switch statement to order permissions for the organization + const orderCheckboxes = (permissions) => { + switch (organization) { + case 'inbound-ops-team': + return orderInboundOpsCheckboxes(permissions); + default: + return permissions; + } + }; + + const generateCheckboxes = (permissions, user) => { + return orderCheckboxes(permissions).map((permission) => { + const marginL = permission.parent_permission_id ? '25px' : '0px'; + + const checkboxStyle = { + style: { + marginTop: '0', + marginLeft: marginL, + marginBottom: '10px' + } + }; + + return (parentPermissionChecked(user.id, permission.parent_permission_id) && + permissionAdminCheck(user, permission) && + ); + }); + }; + + return (<> +

User permissions:

+ {generateCheckboxes(props.permissions, props.user)} + ); + +}; + +export default OrganizationPermissions; + +OrganizationPermissions.propTypes = { + permissions: PropTypes.array, + user: PropTypes.object, + organization: PropTypes.string, + orgUserData: PropTypes.object, + orgnizationUserPermissions: PropTypes.array + +}; diff --git a/client/app/queue/OrganizationQueue.jsx b/client/app/queue/OrganizationQueue.jsx index a8db8bc118f..79ee4a78b73 100644 --- a/client/app/queue/OrganizationQueue.jsx +++ b/client/app/queue/OrganizationQueue.jsx @@ -23,12 +23,12 @@ class OrganizationQueue extends React.PureComponent { } render = () => { - const { success } = this.props; + const { success, featureToggles } = this.props; return {success && } - + ; }; @@ -36,7 +36,8 @@ class OrganizationQueue extends React.PureComponent { OrganizationQueue.propTypes = { clearCaseSelectSearch: PropTypes.func, - success: PropTypes.object + success: PropTypes.object, + featureToggles: PropTypes.object }; const mapStateToProps = (state) => ({ success: state.ui.messages.success }); diff --git a/client/app/queue/OrganizationUsers.jsx b/client/app/queue/OrganizationUsers.jsx index 886850ccba6..e8bafffbc43 100644 --- a/client/app/queue/OrganizationUsers.jsx +++ b/client/app/queue/OrganizationUsers.jsx @@ -1,5 +1,7 @@ +/* eslint-disable max-lines */ /* eslint-disable no-nested-ternary */ /* eslint-disable max-len */ + import React from 'react'; import PropTypes from 'prop-types'; import { css } from 'glamor'; @@ -16,30 +18,8 @@ import { LOGO_COLORS } from '../constants/AppConstants'; import COPY from '../../COPY'; import LoadingDataDisplay from '../components/LoadingDataDisplay'; import MembershipRequestTable from './MembershipRequestTable'; +import OrganizationPermissions from './OrganizationPermissions'; -const userStyle = css({ - margin: '.5rem 0 .5rem', - padding: '.5rem 0 .5rem', - listStyle: 'none' -}); -const topUserStyle = css({ - borderTop: '.1rem solid gray', - margin: '.5rem 0 .5rem', - padding: '1rem 0 .5rem', - listStyle: 'none' -}); -const topUserBorder = css({ - borderBottom: '.1rem solid gray', -}); -const buttonStyle = css({ - paddingRight: '1rem', - display: 'inline-block' -}); -const buttonContainerStyle = css({ - borderBottom: '1rem solid gray', - borderWidth: '1px', - padding: '.5rem 0 2rem', -}); const listStyle = css({ listStyle: 'none' }); @@ -195,6 +175,7 @@ export default class OrganizationUsers extends React.PureComponent { } modifyAdminRights = (user, adminFlag) => () => { + const flagName = 'changingAdminRights'; this.modifyUser(user, flagName); @@ -207,7 +188,6 @@ export default class OrganizationUsers extends React.PureComponent { this.modifyUserError(COPY.USER_MANAGEMENT_ADMIN_RIGHTS_CHANGE_ERROR_TITLE, error.message, user, flagName); }); } - asyncLoadUser = (inputValue) => { // don't search till we have min length input if (inputValue.length < 2) { @@ -226,7 +206,7 @@ export default class OrganizationUsers extends React.PureComponent { } adminButton = (user, admin) => -
removeUserButton = (user) => -
@@ -1462,6 +1600,13 @@ QueueApp.propTypes = { setUserCssId: PropTypes.func, setUserId: PropTypes.func, setOrganizations: PropTypes.func, + setMailTeamUser: PropTypes.func, + setMailSupervisor: PropTypes.func, + setInboundOpsSuperUser: PropTypes.func, + isInboundOpsTeamUser: PropTypes.bool, + isInboundOpsSupervisor: PropTypes.bool, + isInboundOpsSuperuser: PropTypes.bool, + inboundOpsTeamUsers: PropTypes.array, organizations: PropTypes.array, setUserIsVsoEmployee: PropTypes.func, userIsVsoEmployee: PropTypes.bool, @@ -1480,10 +1625,21 @@ QueueApp.propTypes = { userCanViewOvertimeStatus: PropTypes.bool, userCanViewEditNodDate: PropTypes.bool, userCanAssignHearingSchedule: PropTypes.bool, + userIsInboundOpsSupervisor: PropTypes.bool, canEditCavcRemands: PropTypes.bool, canEditCavcDashboards: PropTypes.bool, canViewCavcDashboards: PropTypes.bool, userIsCobAdmin: PropTypes.bool, + correspondence: PropTypes.object, + correspondenceTypes: PropTypes.array, + hasEfolderFailedTask: PropTypes.bool, + priorMail: PropTypes.array, + autoTexts: PropTypes.array, + reduxStore: PropTypes.object, + organizationPermissions: PropTypes.array, + userPermissions: PropTypes.array, + configUrl: PropTypes.string, + correspondenceResponseLetters: PropTypes.array, }; const mapStateToProps = (state) => ({ @@ -1509,6 +1665,9 @@ const mapDispatchToProps = (dispatch) => setUserIsSCTCoordinator, setFeedbackUrl, setOrganizations, + setMailTeamUser, + setMailSupervisor, + setInboundOpsSuperUser, }, dispatch ); diff --git a/client/app/queue/QueueDropdownFilter.jsx b/client/app/queue/QueueDropdownFilter.jsx index 8444a439381..ba2037816ad 100644 --- a/client/app/queue/QueueDropdownFilter.jsx +++ b/client/app/queue/QueueDropdownFilter.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import { COLORS } from '@department-of-veterans-affairs/caseflow-frontend-toolkit/util/StyleConstants'; import { css } from 'glamor'; import { startCase } from 'lodash'; +import ReceiptDatePicker from '../components/ReceiptDatePicker'; +import TaskCompletedDatePicker from '../components/TaskCompletedDatePicker'; const dropdownFilterViewListStyle = css({ margin: 0 @@ -18,14 +20,191 @@ const dropdownFilterViewListItemStyle = css( } ); +const receiptDateFilterStates = { + UNINITIALIZED: '', + BETWEEN: 0, + BEFORE: 1, + AFTER: 2, + ON: 3 +}; + +const taskCompletedDateFilterStates = { + UNINITIALIZED: '', + BETWEEN: 0, + BEFORE: 1, + AFTER: 2, + ON: 3 +}; + +const convertStringToDate = (stringDate) => { + const date = new Date(); + const splitVals = stringDate.split('-'); + + date.setFullYear(Number(splitVals[0])); + // the datepicker component returns months from 1-12. Javascript dates count months from 0-11 + // this offsets it so they match. + date.setMonth(Number(splitVals[1] - 1)); + date.setDate(Number(splitVals[2])); + + return date; +}; + class QueueDropdownFilter extends React.PureComponent { constructor() { super(); this.state = { - rootElemWidth: null + rootElemWidth: null, + receiptDateState: -1, + receiptDatePrimaryValue: '', + receiptDateSecondaryValue: '', + taskCompletedDateState: -1, + taskCompletedDatePrimaryValue: '', + taskCompletedDateSecondaryValue: '', + dateErrorsFrom: [], + dateErrorsTo: [], }; } + setreceiptDateState = (value) => { + this.setState({ receiptDateState: value.value }); + }; + + validateDate = (date, type, filterType) => { + let foundErrors = []; + let primaryDate = ''; + let secondaryDate = ''; + let currentSelector = ''; + let messageText = ''; + + if (filterType === 'VADOR') { + currentSelector = this.state.receiptDateState; + primaryDate = this.state.receiptDatePrimaryValue; + secondaryDate = this.state.receiptDateSecondaryValue; + messageText = 'Receipt date'; + } else { + currentSelector = this.state.taskCompletedDateState; + primaryDate = this.state.taskCompletedDatePrimaryValue; + secondaryDate = this.state.taskCompletedDateSecondaryValue; + messageText = 'Date completed'; + } + + if (currentSelector === 0) { + if (type === 'fromDate') { + if (secondaryDate !== '' && date > secondaryDate) { + foundErrors = [...foundErrors, { key: type, message: 'From date cannot occur after to date.' }]; + } + } else if (date < primaryDate) { + foundErrors = [...foundErrors, { key: type, message: 'To date cannot occur before from date.' }]; + } + } + + // Prevent the date from being picked past the current day. + if (convertStringToDate(date) > new Date()) { + foundErrors = [...foundErrors, { key: type, message: `${messageText} cannot occur in the future.` }]; + } + + if (type === 'fromDate') { + this.setState({ dateErrorsFrom: foundErrors }); + if (secondaryDate !== '' && + date <= secondaryDate && + convertStringToDate(secondaryDate) <= new Date()) { + this.setState({ dateErrorsTo: foundErrors }); + } + } else { + this.setState({ dateErrorsTo: foundErrors }); + if (primaryDate !== '' && + date >= primaryDate && + convertStringToDate(primaryDate) <= new Date()) { + this.setState({ dateErrorsFrom: [] }); + } + } + + return foundErrors; + }; + + setTaskCompletedDateState = (value) => { + this.setState({ taskCompletedDateState: value.value }); + }; + + handleDateChange = (value) => { + this.validateDate(value, 'fromDate', 'VADOR'); + this.setState({ receiptDatePrimaryValue: value }); + } + + handleTaskCompletedDateChange = (value) => { + this.validateDate(value, 'fromDate', 'FDATE'); + this.setState({ taskCompletedDatePrimaryValue: value }); + } + + // Used when the between dates option is selected to store the second date. + handleSecondaryDateChange = (value) => { + this.validateDate(value, 'toDate', 'VADOR'); + this.setState({ receiptDateSecondaryValue: value }); + } + + handleTaskCompletedSecondaryDateChange = (value) => { + this.validateDate(value, 'toDate', 'FDATE'); + this.setState({ taskCompletedDateSecondaryValue: value }); + } + + handleApplyFilter = () => { + if (this.state.receiptDateState === 0) { + this.props.setSelectedValue( + [ + this.state.receiptDateState, + this.state.receiptDatePrimaryValue, + this.state.receiptDateSecondaryValue + ], 'vaDor'); + } else { + this.props.setSelectedValue([this.state.receiptDateState, this.state.receiptDatePrimaryValue], 'vaDor'); + } + } + + isApplyButtonEnabled = () => { + + if (this.state.dateErrorsFrom.length > 0 || this.state.dateErrorsTo.length > 0) { + return true; + } + if (this.state.receiptDateState >= 1 && this.state.receiptDatePrimaryValue !== '') { + return false; + } + + if (this.state.receiptDateState === 0 && + this.state.receiptDatePrimaryValue.length > 0 && + this.state.receiptDateSecondaryValue.length > 0) { + return false; + } + + return true; + } + + clearAllFilters = () => { + this.setreceiptDateState(-1); + this.setState({ receiptDatePrimaryValue: '' }); + this.setState({ receiptDateSecondaryValue: '' }); + this.setTaskCompletedDateState(-1); + this.setState({ taskCompletedDatePrimaryValue: '' }); + this.setState({ taskCompletedDateSecondaryValue: '' }); + this.setState({ dateErrorsFrom: [] }); + this.setState({ dateErrorsTo: [] }); + this.props.clearFilters(); + + } + + handleTaskCompletedApplyFilter = () => { + if (this.state.taskCompletedDateState === 0) { + this.props.setSelectedValue( + [ + this.state.taskCompletedDateState, + this.state.taskCompletedDatePrimaryValue, + this.state.taskCompletedDateSecondaryValue + ], 'completedDateColumn'); + } else { + this.props.setSelectedValue([this.state.taskCompletedDateState, this.state.taskCompletedDatePrimaryValue], + 'completedDateColumn'); + } + } + render() { const { children, name } = this.props; @@ -42,8 +221,8 @@ class QueueDropdownFilter extends React.PureComponent { this.rootElem = rootElem; }}> {this.props.addClearFiltersRow && -
-
} - {React.cloneElement(React.Children.only(children), { - dropdownFilterViewListStyle, - dropdownFilterViewListItemStyle - })} + {this.props.isReceiptDateFilter && } + {this.props.isTaskCompletedDateFilter && + + } + {!(this.props.isReceiptDateFilter || this.props.isTaskCompletedDateFilter) && + React.cloneElement(React.Children.only(children), { + dropdownFilterViewListStyle, + dropdownFilterViewListItemStyle + })} ; } @@ -87,6 +294,9 @@ QueueDropdownFilter.propTypes = { handleClose: PropTypes.func, addClearFiltersRow: PropTypes.bool, name: PropTypes.string, + setSelectedValue: PropTypes.func.isRequired, + isReceiptDateFilter: PropTypes.bool, + isTaskCompletedDateFilter: PropTypes.bool, }; export default QueueDropdownFilter; diff --git a/client/app/queue/QueueTable.jsx b/client/app/queue/QueueTable.jsx index 42d4098bf55..0217d421c08 100644 --- a/client/app/queue/QueueTable.jsx +++ b/client/app/queue/QueueTable.jsx @@ -13,7 +13,7 @@ import Pagination from '../components/Pagination/Pagination'; import { COLORS, LOGO_COLORS } from '../constants/AppConstants'; import ApiUtil from '../util/ApiUtil'; import LoadingScreen from '../components/LoadingScreen'; -import { tasksWithAppealsFromRawTasks } from './utils'; +import { tasksWithAppealsFromRawTasks, tasksWithCorrespondenceFromRawTasks } from './utils'; import QUEUE_CONFIG from '../../constants/QUEUE_CONFIG'; import COPY from '../../COPY'; @@ -171,6 +171,8 @@ export const HeaderRow = (props) => { filterOptionsFromApi={props.useTaskPagesApi && column.filterOptions} updateFilters={(newFilters) => props.updateFilteredByList(newFilters)} filteredByList={props.filteredByList} + isReceiptDateFilter={column.name === QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name} + isTaskCompletedDateFilter={column.name === QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name} /> ); } @@ -347,7 +349,7 @@ export default class QueueTable extends React.PureComponent { const firstResponse = { task_page_count: this.props.numberOfPages, tasks_per_page: this.props.casesPerPage, - total_task_count: this.props.rowObjects.length, + total_task_count: this.props.totalTaskCount, tasks: this.props.rowObjects }; @@ -670,7 +672,9 @@ export default class QueueTable extends React.PureComponent { tasks: { data: tasks } } = response.body; - const preparedTasks = tasksWithAppealsFromRawTasks(tasks); + const preparedTasks = this.props.isCorrespondenceTable ? + tasksWithCorrespondenceFromRawTasks(tasks) : + tasksWithAppealsFromRawTasks(tasks); const preparedResponse = Object.assign(response.body, { tasks: preparedTasks }); @@ -695,6 +699,10 @@ export default class QueueTable extends React.PureComponent { catch(() => this.setState({ loadingComponent: null })); }; + filterTasksFromSearchbar = (tasks, searchValue) => { + return tasks.filter((task) => this.props.taskMatchesSearch(task, searchValue)); + }; + render() { const { columns, @@ -712,6 +720,7 @@ export default class QueueTable extends React.PureComponent { bodyStyling, enablePagination, useTaskPagesApi, + searchValue, reduxCache, useReduxCache } = this.props; @@ -818,7 +827,7 @@ export default class QueueTable extends React.PureComponent { tbodyRef={tbodyRef} columns={columns} getKeyForRow={keyGetter} - rowObjects={rowObjects} + rowObjects={searchValue ? this.filterTasksFromSearchbar(rowObjects, searchValue) : rowObjects} bodyClassName={bodyClassName ?? ''} rowClassNames={rowClassNames} bodyStyling={bodyStyling} @@ -887,6 +896,9 @@ HeaderRow.propTypes = FooterRow.propTypes = Row.propTypes = BodyRows.propTypes = }), onHistoryUpdate: PropTypes.func, preserveFilter: PropTypes.bool, + isCorrespondenceTable: PropTypes.bool, + searchValue: PropTypes.string, + taskMatchesSearch: PropTypes.func, useReduxCache: PropTypes.bool, reduxCache: PropTypes.object, updateReduxCache: PropTypes.func diff --git a/client/app/queue/QueueTableBuilder.jsx b/client/app/queue/QueueTableBuilder.jsx index 15d1077b12d..16496bf9e1c 100644 --- a/client/app/queue/QueueTableBuilder.jsx +++ b/client/app/queue/QueueTableBuilder.jsx @@ -138,7 +138,9 @@ const QueueTableBuilder = (props) => { return

{config.table_title}

- + { organizations: state.ui.organizations, isVhaOrg: isActiveOrganizationVHA(state), userCanBulkAssign: state.ui.activeOrganization.userCanBulkAssign, + activeOrganization: state.ui.activeOrganization }; }; @@ -166,6 +169,14 @@ QueueTableBuilder.propTypes = { requireDasRecord: PropTypes.bool, userCanBulkAssign: PropTypes.bool, isVhaOrg: PropTypes.bool, + featureToggles: PropTypes.object, + activeOrganization: PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + isVso: PropTypes.bool, + userCanBulkAssign: PropTypes.bool, + type: PropTypes.string + }) }; export default connect(mapStateToProps)(QueueTableBuilder); diff --git a/client/app/queue/cavc/editCavcRemandTasks/editCavcRemandTasksView.jsx b/client/app/queue/cavc/editCavcRemandTasks/editCavcRemandTasksView.jsx index 3fba256ded7..37fa658043b 100644 --- a/client/app/queue/cavc/editCavcRemandTasks/editCavcRemandTasksView.jsx +++ b/client/app/queue/cavc/editCavcRemandTasks/editCavcRemandTasksView.jsx @@ -50,7 +50,7 @@ export const EditCavcRemandTasksView = () => { const cancelTaskIds = activeTasks.filter((task) => task.disabled && task.type !== 'MdrTask').map( (disTask) => disTask.id); - const getReActivateTaksIds = () => { + const getReActivateTaskIds = () => { if (closedSendCavcRemandProcessedLetterTask) { return [closedSendCavcRemandProcessedLetterTask.id]; } else if (openSendCavcRemandProcessedLetterTask) { @@ -60,7 +60,7 @@ export const EditCavcRemandTasksView = () => { return []; }; - const reActivateTaskIds = getReActivateTaksIds(); + const reActivateTaskIds = getReActivateTaskIds(); const [selectedCancelTaskIds, setSelectedCancelTaskIds] = useState(existingValues?.cancelTaskIds || []); const [selectedReActivateTaskIds, setSelectedReActivateTaskIds] = useState(reActivateTaskIds); diff --git a/client/app/queue/components/ActionsDropdown.jsx b/client/app/queue/components/ActionsDropdown.jsx index 0de2b360e98..8d46a4bd748 100644 --- a/client/app/queue/components/ActionsDropdown.jsx +++ b/client/app/queue/components/ActionsDropdown.jsx @@ -23,7 +23,8 @@ class ActionsDropdown extends React.PureComponent { const { appealId, task, - history + history, + type } = this.props; if (!option) { @@ -40,7 +41,13 @@ class ActionsDropdown extends React.PureComponent { this.props.stageAppeal(appealId); this.props.resetDecisionOptions(); - history.push(`/queue/appeals/${appealId}/tasks/${task.uniqueId}/${option.value}`); + // routing for correspondence + if (type === 'Correspondence') { + history.push(`/queue/correspondence/${appealId}/tasks/${task.uniqueId}/${option.value}`); + } else { + history.push(`/queue/appeals/${appealId}/tasks/${task.uniqueId}/${option.value}`); + } + }; render = () => { @@ -61,6 +68,7 @@ class ActionsDropdown extends React.PureComponent { ActionsDropdown.propTypes = { appealId: PropTypes.string, + type: PropTypes.string, task: PropTypes.object, history: PropTypes.object, resetDecisionOptions: PropTypes.func, diff --git a/client/app/queue/components/CorrespondenceCancelTaskModal.jsx b/client/app/queue/components/CorrespondenceCancelTaskModal.jsx new file mode 100644 index 00000000000..86bd8a4b5f8 --- /dev/null +++ b/client/app/queue/components/CorrespondenceCancelTaskModal.jsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { get } from 'lodash'; + +import { taskById } from '../selectors'; +import { requestPatch } from '../uiReducer/uiActions'; +import { taskActionData } from '../utils'; +import TextareaField from '../../components/TextareaField'; +import COPY from '../../../COPY'; +import TASK_STATUSES from '../../../constants/TASK_STATUSES'; +import QueueFlowModal from './QueueFlowModal'; +import { + setTaskNotRelatedToAppealBanner, + cancelTaskNotRelatedToAppeal, +} from '../correspondence/correspondenceDetailsReducer/correspondenceDetailsActions'; + +/* eslint-disable camelcase */ +const CorrespondenceCancelTaskModal = (props) => { + const { task } = props; + const taskData = taskActionData(props); + + // Show task instructions by default + const shouldShowTaskInstructions = get(taskData, 'show_instructions', true); + + const [instructions, setInstructions] = useState(''); + const [instructionsAdded, setInstructionsAdded] = useState(true); + + useEffect(() => { + // Handle document search position + if (instructions.length > 0) { + setInstructionsAdded(false); + } else { + setInstructionsAdded(true); + } + }, [instructions]); + + const validateForm = () => { + if (!shouldShowTaskInstructions) { + return true; + } + + return instructions.length > 0; + }; + + const submit = () => { + + const payload = { + data: { + task: { + status: TASK_STATUSES.cancelled, + instructions, + ...(taskData?.business_payloads && { business_payloads: taskData?.business_payloads }) + } + } + }; + + const filteredTasks = props.correspondenceInfo.tasksUnrelatedToAppeal.filter((filterdTask) => + parseInt(filterdTask.uniqueId, 10) !== parseInt(props.task_id, 10)); + + const tempCor = props.correspondenceInfo; + + tempCor.tasksUnrelatedToAppeal = filteredTasks; + + return props.cancelTaskNotRelatedToAppeal(props.task_id, tempCor, payload); + + }; + + // Additional properties - should be removed later once generic submit buttons are styled the same across all modals + const modalProps = {}; + + if ([ + 'AssessDocumentationTask', + 'EducationAssessDocumentationTask', + 'HearingPostponementRequestMailTask' + ].includes(task?.type) || task?.appeal.hasCompletedSctAssignTask) { + modalProps.submitButtonClassNames = ['usa-button']; + modalProps.submitDisabled = !validateForm(); + } + + return ( + + {shouldShowTaskInstructions && + + } + + ); + +}; +/* eslint-enable camelcase */ + +CorrespondenceCancelTaskModal.propTypes = { + requestPatch: PropTypes.func, + cancelTaskNotRelatedToAppeal: PropTypes.func, + correspondenceInfo: PropTypes.object, + task: PropTypes.shape({ + uniqueId: PropTypes.number, + appeal: PropTypes.shape({ + hasCompletedSctAssignTask: PropTypes.bool + }), + assignedTo: PropTypes.shape({ + type: PropTypes.string + }), + taskId: PropTypes.string, + type: PropTypes.string, + onHoldDuration: PropTypes.number + }), + task_id: PropTypes.string, + correspondence_uuid: PropTypes.number, +}; + +const mapStateToProps = (state, ownProps) => ({ + task: taskById(state, { taskId: ownProps.taskId }), + taskNotRelatedToAppealBanner: state.correspondenceDetails.bannerAlert, + correspondenceInfo: state.correspondenceDetails.correspondenceInfo, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + requestPatch, + setTaskNotRelatedToAppealBanner, + cancelTaskNotRelatedToAppeal, +}, dispatch); + +export default (withRouter( + connect(mapStateToProps, mapDispatchToProps)( + CorrespondenceCancelTaskModal + ) +)); diff --git a/client/app/queue/components/CorrespondenceChangeTaskTypeModal.jsx b/client/app/queue/components/CorrespondenceChangeTaskTypeModal.jsx new file mode 100644 index 00000000000..78e2812cb6a --- /dev/null +++ b/client/app/queue/components/CorrespondenceChangeTaskTypeModal.jsx @@ -0,0 +1,140 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { withRouter } from 'react-router-dom'; +import { requestPatch } from '../uiReducer/uiActions'; +import { marginTop } from '../constants'; +import { taskActionData } from '../utils'; +import QueueFlowModal from '../components/QueueFlowModal'; +import TextareaField from 'app/components/TextareaField'; +import SearchableDropdown from 'app/components/SearchableDropdown'; +import Alert from 'app/components/Alert'; +import COPY from '../../../COPY'; +import { + changeTaskTypeNotRelatedToAppeal, + setTaskNotRelatedToAppealBanner +} from 'app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsActions'; + +const CorrespondenceChangeTaskTypeModal = (props) => { + const { error } = props; + const taskData = taskActionData(props); + const [typeOption, setTypeOption] = useState(null); + const [instructions, setInstructions] = useState(''); + + const validateForm = () => Boolean(typeOption) && Boolean(instructions); + + const submit = () => { + const payload = { + data: { + task: { + type: typeOption.value, + instructions + } + } + }; + + const typeNames = { + oldType: props.task.label, + newType: typeOption.label + }; + + const updatedTasks = props.correspondenceInfo.tasksUnrelatedToAppeal.map((filteredTask) => { + if (parseInt(filteredTask.uniqueId, 10) === parseInt(props.task_id, 10)) { + filteredTask.label = typeOption.label; + filteredTask.instructions.push(instructions); + } + + return filteredTask; + }); + + const tempCor = props.correspondenceInfo; + + tempCor.tasksUnrelatedToAppeal = updatedTasks; + + return props.changeTaskTypeNotRelatedToAppeal(props.task_id, payload, typeNames, tempCor); + }; + + const actionForm = () => ( + <> +
+
+ option && setTypeOption(option)} + value={typeOption?.value} + /> +
+
+ +
+
+ + ); + + return ( + + {error && ( + + {error.detail} + + )} + {actionForm()} + + ); +}; + +CorrespondenceChangeTaskTypeModal.propTypes = { + correspondence_uuid: PropTypes.string, + error: PropTypes.shape({ + title: PropTypes.string, + detail: PropTypes.string + }), + correspondenceInfo: PropTypes.object, + requestPatch: PropTypes.func, + changeTaskTypeNotRelatedToAppeal: PropTypes.func, + task: PropTypes.shape({ + appeal: PropTypes.shape({ + hasCompletedSctAssignTask: PropTypes.bool + }), + assignedTo: PropTypes.shape({ + type: PropTypes.string + }), + taskId: PropTypes.string, + type: PropTypes.string, + label: PropTypes.string, + onHoldDuration: PropTypes.number + }), + task_id: PropTypes.string, +}; + +const mapStateToProps = (state, ownProps) => ({ + error: state.ui.messages.error, + task: state.correspondenceDetails. + correspondenceInfo.tasksUnrelatedToAppeal.find((tsk) => tsk.uniqueId.toString() === ownProps.task_id), + taskNotRelatedToAppealBanner: state.correspondenceDetails.bannerAlert, + correspondenceInfo: state.correspondenceDetails.correspondenceInfo, + showActionsDropdown: state.correspondenceDetails.showActionsDropdown, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + requestPatch, + setTaskNotRelatedToAppealBanner, + changeTaskTypeNotRelatedToAppeal +}, dispatch); + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CorrespondenceChangeTaskTypeModal)); diff --git a/client/app/queue/components/CorrespondenceCompleteTaskModal.jsx b/client/app/queue/components/CorrespondenceCompleteTaskModal.jsx new file mode 100644 index 00000000000..6c3248bfaa8 --- /dev/null +++ b/client/app/queue/components/CorrespondenceCompleteTaskModal.jsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import { taskById } from '../selectors'; +import { requestPatch } from '../uiReducer/uiActions'; +import { taskActionData } from '../utils'; +import TextareaField from '../../components/TextareaField'; +import COPY from '../../../COPY'; +import TASK_STATUSES from '../../../constants/TASK_STATUSES'; +import QueueFlowModal from './QueueFlowModal'; +import { + setTaskNotRelatedToAppealBanner, + completeTaskNotRelatedToAppeal } from '../correspondence/correspondenceDetailsReducer/correspondenceDetailsActions'; + +/* eslint-disable camelcase */ +const CorrespondenceCompleteTaskModal = (props) => { + const taskData = taskActionData(props); + + const [instructions, setInstructions] = useState(''); + + const submit = () => { + + const correspondence = props.correspondenceInfo; + + // eslint-disable-next-line no-shadow + const updatedTask = correspondence.tasksUnrelatedToAppeal.find((task) => + parseInt(props.task_id, 10) === parseInt(task.uniqueId, 10)); + + const payload = { + data: { + task: { + status: TASK_STATUSES.completed, + instructions, + ...(taskData?.business_payloads && { business_payloads: taskData?.business_payloads }) + } + } + }; + + const frontendParams = { + taskId: props.task_id, + taskName: updatedTask.label, + teamName: updatedTask.assignedTo + }; + + // eslint-disable-next-line no-shadow + const filteredTasks = props.correspondenceInfo.tasksUnrelatedToAppeal.filter((task) => + parseInt(task.uniqueId, 10) !== parseInt(props.task_id, 10)); + + correspondence.tasksUnrelatedToAppeal = filteredTasks; + + updatedTask.status = TASK_STATUSES.completed; + + correspondence.closedTasksUnrelatedToAppeal.push(updatedTask); + + return props.completeTaskNotRelatedToAppeal(payload, frontendParams, correspondence); + + }; + + // Additional properties - should be removed later once generic submit buttons are styled the same across all modals + const modalProps = {}; + + return ( + + {taskData?.modal_body && + +
+
+ + } + + + ); + +}; +/* eslint-enable camelcase */ + +CorrespondenceCompleteTaskModal.propTypes = { + requestPatch: PropTypes.func, + completeTaskNotRelatedToAppeal: PropTypes.func, + task: PropTypes.shape({ + appeal: PropTypes.shape({ + hasCompletedSctAssignTask: PropTypes.bool + }), + assignedTo: PropTypes.shape({ + type: PropTypes.string + }), + taskId: PropTypes.string, + uniqueId: PropTypes.string, + type: PropTypes.string, + label: PropTypes.string, + onHoldDuration: PropTypes.number + }), + task_id: PropTypes.string, + correspondence_uuid: PropTypes.string, + correspondenceInfo: PropTypes.func, + team: PropTypes.string +}; + +const mapStateToProps = (state, ownProps) => ({ + task: taskById(state, { taskId: ownProps.taskId }), + taskNotRelatedToAppealBanner: state.correspondenceDetails.bannerAlert, + correspondenceInfo: state.correspondenceDetails.correspondenceInfo, + showActionsDropdown: state.correspondenceDetails.showActionsDropdown, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + requestPatch, + setTaskNotRelatedToAppealBanner, + completeTaskNotRelatedToAppeal +}, dispatch); + +export default (withRouter( + connect(mapStateToProps, mapDispatchToProps)( + CorrespondenceCompleteTaskModal + ) +)); diff --git a/client/app/queue/components/QueueOrganizationDropdown.jsx b/client/app/queue/components/QueueOrganizationDropdown.jsx index f5f4756e25c..1081e5b6906 100644 --- a/client/app/queue/components/QueueOrganizationDropdown.jsx +++ b/client/app/queue/components/QueueOrganizationDropdown.jsx @@ -22,14 +22,22 @@ export default class QueueOrganizationDropdown extends React.Component { label: COPY.CASE_LIST_TABLE_QUEUE_DROPDOWN_OWN_CASES_LABEL }; + const correspondenceTable = (org) => { + return org.name === COPY.CASE_LIST_TABLE_QUEUE_DROPDOWN_OWN_CORRESPONDENCE_LABEL || + org.name === COPY.CASE_LIST_TABLE_QUEUE_DROPDOWN_CORRESPONDENCE_CASES; + }; + const organizationItems = organizations.map((org, index) => { // If the url is a specified path, use it over the organization route const orgHref = org.url.includes('/') ? org.url : `/organizations/${org.url}`; + const label = correspondenceTable(org) ? + org.name : sprintf(COPY.CASE_LIST_TABLE_QUEUE_DROPDOWN_TEAM_CASES_LABEL, org.name); + return { key: (index + 1).toString(), href: (location === org.url) ? '#' : orgHref, - label: sprintf(COPY.CASE_LIST_TABLE_QUEUE_DROPDOWN_TEAM_CASES_LABEL, org.name) + label }; }); diff --git a/client/app/queue/components/TaskTableColumns.jsx b/client/app/queue/components/TaskTableColumns.jsx index 85ca86f9d5f..a120b9639fb 100644 --- a/client/app/queue/components/TaskTableColumns.jsx +++ b/client/app/queue/components/TaskTableColumns.jsx @@ -13,6 +13,14 @@ import ContinuousProgressBar from 'app/components/ContinuousProgressBar'; import OnHoldLabel, { numDaysOnHold } from './OnHoldLabel'; import IhpDaysWaitingTooltip from './IhpDaysWaitingTooltip'; import TranscriptionTaskTooltip from './TranscriptionTaskTooltip'; +import Checkbox from '../../components/Checkbox'; +import { useDispatch, useSelector } from 'react-redux'; +import { + setShowReassignPackageModal, + setShowRemovePackageModal, + setSelectedTasks, + setSelectedVeteranDetails +} from 'app/queue/correspondence/correspondenceReducer/correspondenceActions'; import { taskHasCompletedHold, hasDASRecord, collapseColumn, regionalOfficeCity, renderAppealType } from '../utils'; import { DateString, daysSinceAssigned, daysSincePlacedOnHold } from '../../util/DateUtil'; @@ -222,6 +230,180 @@ export const assignedByColumn = () => { }; }; +export const veteranDetails = () => { + const dispatch = useDispatch(); + + const handleRemoveClick = (task) => { + dispatch(setSelectedVeteranDetails(task)); + dispatch(setShowRemovePackageModal(true)); + }; + + const handleReassignClick = (task) => { + dispatch(setSelectedVeteranDetails(task)); + dispatch(setShowReassignPackageModal(true)); + }; + + // Developer Function for in-progress and future work + const handleExplainPageInConSoleLog = (task) => { + // eslint-disable-next-line + console.log("Correspondence Explain Page for Under Construction Link:") + // eslint-disable-next-line + console.log(window.location.origin + task.taskUrl); + }; + + return { + header: 'Veteran Details', + label: 'Veteran Details', + name: QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, + backendCanSort: true, + getSortValue: (task) => task.veteranDetails, + valueFunction: (task) => { + if (task.taskUrl === '/modal/reassign_package') { + return handleReassignClick(task)} + id="task-link" + > + {task.veteranDetails} + ; + } else if (task.taskUrl === '/modal/remove_package') { + return handleRemoveClick(task)} + id="task-link" + > + {task.veteranDetails} + ; + } else if (task.taskUrl.includes('explain')) { + return handleExplainPageInConSoleLog(task)} + id="task-link" + > + {task.veteranDetails} + ; + } + + return + {task.veteranDetails} + ; + }, + }; +}; + +export const vaDor = () => { + return { + header: 'VA DOR', + filterOptions: [], + columnName: 'Receipt Date', + backendCanSort: true, + enableFilter: true, + getSortValue: (task) => task.vaDor, + name: QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, + label: 'Filter by VA Date of Receipt', + valueFunction: (task) => { + return moment(task.vaDor).format('MM/DD/YYYY'); + } + }; +}; + +export const packageDocumentType = (filterOptions) => { + return { + header: COPY.CASE_LIST_TABLE_PACKAGE_DOCUMENT_TYPE_COLUMN_TITLE, + filterOptions, + columnName: COPY.CASE_LIST_TABLE_PACKAGE_DOCUMENT_TYPE_COLUMN_TITLE, + valueName: COPY.CASE_LIST_TABLE_PACKAGE_DOCUMENT_TYPE_COLUMN_TITLE, + backendCanSort: true, + enableFilter: true, + anyFiltersAreSet: true, + getSortValue: (task) => task.nod, + name: QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name, + label: 'Filter by Package Document Type', + valueFunction: (task) => { + return task.nod ? 'NOD' : 'Non-NOD'; + } + }; +}; + +export const notes = () => { + return { + header: 'Notes', + name: QUEUE_CONFIG.COLUMNS.NOTES.name, + valueFunction: (task) => task.notes, + backendCanSort: true, + getSortValue: (task) => task.notes + }; +}; +export const checkboxColumn = (handleCheckboxChange) => { + const dispatch = useDispatch(); + + const currentlySelectedTasks = useSelector((state) => state.intakeCorrespondence.selectedTasks); + + const toggleChange = (value) => { + const indexOfTask = currentlySelectedTasks.indexOf(value); + + const newSelectedTasks = currentlySelectedTasks; + + if (indexOfTask === -1) { + newSelectedTasks.push(value); + } else { + newSelectedTasks.splice(indexOfTask, 1); + } + + dispatch(setSelectedTasks(newSelectedTasks)); + + handleCheckboxChange(newSelectedTasks.length > 0); + + }; + + return { + header: 'Select', + checked: true, + name: QUEUE_CONFIG.COLUMNS.CHECKBOX_COLUMN, + valueFunction: (task) => { + + if (task) { + return toggleChange(task.uniqueId)} + />; + } + + return null; + // or any other valid JSX element + + } + + }; +}; + +export const actionType = () => { + return { + header: 'Action Type', + name: QUEUE_CONFIG.COLUMNS.ACTION_TYPE.name, + backendCanSort: true, + getSortValue: (task) => task.label, + valueFunction: (task) => task.label.split(' ')[0] + }; +}; + +export const daysWaitingCorrespondence = () => { + return { + header: 'Days Waiting', + name: QUEUE_CONFIG.COLUMNS.DAYS_WAITING.name, + enableFilter: true, + backendCanSort: true, + getSortValue: (task) => task.daysWaiting, + valueFunction: (task) => task.daysWaiting + }; +}; + export const regionalOfficeColumn = (tasks, filterOptions) => { return { header: COPY.CASE_LIST_TABLE_REGIONAL_OFFICE_COLUMN_TITLE, @@ -462,3 +644,19 @@ export const taskCompletedDateColumn = () => { getSortValue: (task) => task.closedAt ? new Date(task.closedAt) : null }; }; + +export const correspondenceCompletedDateColumn = () => { + return { + header: COPY.CASE_LIST_TABLE_COMPLETED_ON_DATE_COLUMN_TITLE, + name: QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name, + filterOptions: [], + label: 'Filter by date completed', + columnName: 'Date Completed', + backendCanSort: true, + enableFilter: true, + valueFunction: (task) => { + return moment(task.closedAt).format('MM/DD/YYYY'); + }, + getSortValue: (task) => new Date(task.closedAt) + }; +}; diff --git a/client/app/queue/constants.js b/client/app/queue/constants.js index c7afb3183e9..552ec956277 100644 --- a/client/app/queue/constants.js +++ b/client/app/queue/constants.js @@ -11,6 +11,8 @@ import { COLORS as COMMON_COLORS } from '@department-of-veterans-affairs/caseflo import COPY from '../../COPY'; import VACOLS_COLUMN_MAX_LENGTHS from '../../constants/VACOLS_COLUMN_MAX_LENGTHS'; import LEGACY_APPEAL_TYPES_BY_ID from '../../constants/LEGACY_APPEAL_TYPES_BY_ID'; +import QUEUE_INTAKE_FORM_TASK_TYPES from '../../constants/QUEUE_INTAKE_FORM_TASK_TYPES'; +import CORRESPONDENCE_LETTER_SELECTIONS from '../../constants/CORRESPONDENCE_LETTER_SELECTIONS'; import { DEFAULT_SORTING_COLUMN_KEY, DEFAULT_SORTING_DIRECTION_KEY, COLUMNS } from '../../constants/QUEUE_CONFIG'; export const COLORS = { @@ -144,6 +146,8 @@ const formatRemandReasons = (reasons) => Object.assign({}, })) ); +export const ADD_CORRESPONDENCE_LETTER_SELECTIONS = CORRESPONDENCE_LETTER_SELECTIONS; + export const LEGACY_REMAND_REASONS = formatRemandReasons(LEGACY_REMAND_REASONS_BY_ID); export const REMAND_REASONS = formatRemandReasons(REMAND_REASONS_BY_ID); @@ -165,6 +169,8 @@ export const LEGACY_APPEAL_TYPES = _.fromPairs(_.zip( _.values(LEGACY_APPEAL_TYPES_BY_ID) )); +export const INTAKE_FORM_TASK_TYPES = QUEUE_INTAKE_FORM_TASK_TYPES; + export const ISSUE_DESCRIPTION_MAX_LENGTH = VACOLS_COLUMN_MAX_LENGTHS.ISSUES.ISSDESC; export const ATTORNEY_COMMENTS_MAX_LENGTH = VACOLS_COLUMN_MAX_LENGTHS.DECASS.DEATCOM; export const DOCUMENT_ID_MAX_LENGTH = VACOLS_COLUMN_MAX_LENGTHS.DECASS.DEDOCID; @@ -213,7 +219,11 @@ export const PAGE_TITLES = { CONVERT_HEARING_TO_VIDEO: 'Change Hearing Request Type to Video', CONVERT_HEARING_TO_CENTRAL: 'Change Hearing Request Type to Central', COMPLETE_HEARING_POSTPONEMENT_REQUEST: 'Complete Hearing Postponement Request', - COMPLETE_HEARING_WITHDRAWAL_REQUEST: 'Complete Hearing Withdrawal Request' + COMPLETE_HEARING_WITHDRAWAL_REQUEST: 'Complete Hearing Withdrawal Request', + REVIEW_PACKAGE: 'Review Package', + CORRESPONDENCE_CASES_LIST: 'Correspondence Cases', + CORRESPONDENCE_INTAKE: 'Correspondence Intake', + CORRESPONDENCE_DETAILS: 'Correspondence Details' }; export const CUSTOM_HOLD_DURATION_TEXT = 'Custom'; diff --git a/client/app/queue/correspondence/CorrespondenceCaseTimeline.jsx b/client/app/queue/correspondence/CorrespondenceCaseTimeline.jsx new file mode 100644 index 00000000000..805face2be4 --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceCaseTimeline.jsx @@ -0,0 +1,77 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import CorrespondenceTaskRows from './CorrespondenceTaskRows'; +import Alert from '../../components/Alert'; +import { + setTaskNotRelatedToAppealBanner, + setTasksUnrelatedToAppealEmpty } from './correspondenceDetailsReducer/correspondenceDetailsActions'; + +const CorrespondenceCaseTimeline = (props) => { + + const { taskNotRelatedToAppealBanner, correspondenceInfo } = props; + + useEffect(() => { + + if (correspondenceInfo.tasksUnrelatedToAppeal.length === 0) { + props.setTasksUnrelatedToAppealEmpty(true); + } + + }, []); + + return ( + + { (Object.keys(taskNotRelatedToAppealBanner).length > 0) && ( +
+ + {taskNotRelatedToAppealBanner.message} + +
+ + )} +
+ + + +
+ + ); +}; + +CorrespondenceCaseTimeline.propTypes = { + loadCorrespondence: PropTypes.func, + setTasksUnrelatedToAppealEmpty: PropTypes.func, + correspondence: PropTypes.object, + correspondenceInfo: PropTypes.object, + taskNotRelatedToAppealBanner: PropTypes.object, + organizations: PropTypes.array, + tasksToDisplay: PropTypes.array, + userCssId: PropTypes.string +}; + +const mapStateToProps = (state) => ({ + correspondences: state.intakeCorrespondence.correspondences, + taskNotRelatedToAppealBanner: state.correspondenceDetails.bannerAlert, + correspondenceInfo: state.correspondenceDetails.correspondenceInfo, + tasksUnrelatedToAppeal: state.correspondenceDetails.tasksUnrelatedToAppeal, + tasksUnrelatedToAppealEmpty: state.correspondenceDetails.tasksUnrelatedToAppealEmpty, +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + setTaskNotRelatedToAppealBanner, + setTasksUnrelatedToAppealEmpty + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CorrespondenceCaseTimeline); diff --git a/client/app/queue/correspondence/CorrespondenceCases.jsx b/client/app/queue/correspondence/CorrespondenceCases.jsx new file mode 100644 index 00000000000..07ee9285511 --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceCases.jsx @@ -0,0 +1,336 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + loadCorrespondenceConfig, + setShowReassignPackageModal, + setShowRemovePackageModal +} from './correspondenceReducer/correspondenceActions'; +import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; +import PropTypes, { string } from 'prop-types'; +import COPY from '../../../COPY'; +import ApiUtil from '../../util/ApiUtil'; +import { sprintf } from 'sprintf-js'; +import CorrespondenceTableBuilder from './CorrespondenceTableBuilder'; +import Alert from '../../components/Alert'; +import Modal from 'app/components/Modal'; +import RadioFieldWithChildren from '../../components/RadioFieldWithChildren'; +import ReactSelectDropdown from '../../components/ReactSelectDropdown'; +import TextareaField from '../../components/TextareaField'; +import AutoAssignAlertBanner from '../correspondence/component/AutoAssignAlertBanner'; +import { css } from 'glamor'; +import WindowUtil from '../../util/WindowUtil'; + +const CorrespondenceCases = (props) => { + const dispatch = useDispatch(); + const configUrl = props.configUrl || '/queue/correspondence?json'; + + const currentAction = useSelector((state) => state.reviewPackage.lastAction); + + const vetName = useSelector( + (state) => state.reviewPackage.correspondence.veteranFullName + ); + + const currentSelectedVeteran = useSelector((state) => state.intakeCorrespondence.selectedVeteranDetails); + const reassignModalVisible = useSelector((state) => state.intakeCorrespondence.showReassignPackageModal); + + const [selectedMailTeamUser, setSelectedMailTeamUser] = useState(''); + const [selectedRequestChoice, setSelectedRequestChoice] = useState(''); + const [decisionReason, setDecisionReason] = useState(''); + + const buildMailUserData = (data) => { + + if (typeof data === 'undefined') { + return []; + } + + return data.map((user) => { + return { + value: user, + label: user + }; + }); + }; + + const handleDecisionReasonInput = (value) => { + setDecisionReason(value); + }; + + const handleViewPackage = () => { + let url = window.location.href; + const index = url.indexOf('/team'); + + url = url.slice(0, index); + const parentUrlArray = (currentSelectedVeteran.parentTaskUrl.split('/')); + + window.location.href = (`${url }/${parentUrlArray[3]}/${parentUrlArray[4]}`); + }; + + const resetState = () => { + setSelectedMailTeamUser(''); + setSelectedRequestChoice(''); + setDecisionReason(''); + }; + + const handleReassignClose = () => { + resetState(); + dispatch(setShowReassignPackageModal(false)); + }; + + const handleRemoveClose = () => { + resetState(); + dispatch(setShowRemovePackageModal(false)); + }; + + const confirmButtonDisabled = () => { + if (selectedRequestChoice === 'approve' && selectedMailTeamUser === '' && reassignModalVisible) { + return true; + } + + if (selectedRequestChoice === 'reject' && decisionReason === '') { + return true; + } + + if (selectedRequestChoice === '') { + return true; + } + + return false; + }; + + const styles = { + optSelect: css({ + '.reassign': { + }, + '& .css-yk16xz-control, .css-1pahdxg-control': { + borderRadius: '0px', + fontSize: '17px' + } + }) + }; + + const packageActionMessage = () => { + switch (currentAction.action_type) { + case 'removePackage': + return sprintf(COPY.CORRESPONDENCE_TITLE_REMOVE_PACKAGE_MESSAGE, vetName); + case 'splitPackage': + return sprintf(COPY.CORRESPONDENCE_TITLE_SPLIT_PACKAGE_MESSAGE, vetName); + case 'mergePackage': + return sprintf(COPY.CORRESPONDENCE_TITLE_MERGE_PACKAGE_MESSAGE, vetName); + case 'reassignPackage': + return sprintf(COPY.CORRESPONDENCE_TITLE_REASSIGNMENT_PACKAGE_MESSAGE, vetName); + default: + } + }; + + const approveElement = ( +
+ setSelectedMailTeamUser(val.value)} + options={buildMailUserData(props.inboundOpsTeamNonAdmin)} + /> +
); + + const textAreaElement = ( +
+ +
); + + useEffect(() => { + dispatch(loadCorrespondenceConfig(configUrl)); + }, []); + + const config = useSelector((state) => state.intakeCorrespondence.correspondenceConfig); + const showReassignPackageModal = useSelector((state) => state.intakeCorrespondence.showReassignPackageModal); + const showRemovePackageModal = useSelector((state) => state.intakeCorrespondence.showRemovePackageModal); + + const reassignOptions = [ + { displayText: 'Approve request', + value: 'approve', + element: approveElement, + displayElement: selectedRequestChoice === 'approve' + }, + { displayText: 'Reject request', + value: 'reject', + element: textAreaElement, + displayElement: selectedRequestChoice === 'reject' + } + ]; + + const removeOptions = [ + { displayText: 'Approve request', + value: 'approve', + displayElement: selectedRequestChoice === 'approve' + }, + { displayText: 'Reject request', + value: 'reject', + element: textAreaElement, + displayElement: selectedRequestChoice === 'reject' + } + ]; + const handleConfirmReassignRemoveClick = (actionType) => { + try { + const data = { + action_type: actionType, + new_assignee: selectedMailTeamUser, + decision: selectedRequestChoice, + decision_reason: decisionReason, + }; + + ApiUtil.patch(`/queue/correspondence/tasks/${currentSelectedVeteran.uniqueId}/update`, { data }). + then(() => { + WindowUtil.reloadPage(); + }); + + } catch (error) { + console.error(error); + } + }; + + const reassignModalButtons = [ + { + classNames: ['cf-modal-link', 'cf-btn-link'], + name: 'Cancel', + onClick: handleReassignClose, + disabled: false + }, + { + id: '#confirm-button', + classNames: ['usa-button', 'usa-button-primary', 'cf-margin-left-2rem'], + name: 'Confirm', + onClick: () => handleConfirmReassignRemoveClick('reassign'), + disabled: confirmButtonDisabled() + }, + { + id: '#view-package-button', + classNames: ['usa-button', 'usa-button-secondary'], + name: 'View package', + onClick: handleViewPackage, + disabled: false + } + ]; + + const removeModalButtons = [ + { + classNames: ['cf-modal-link', 'cf-btn-link'], + name: 'Cancel', + onClick: handleRemoveClose, + disabled: false + }, + { + id: '#confirm-button', + classNames: ['usa-button', 'usa-button-primary', 'cf-margin-left-2rem'], + name: 'Confirm', + onClick: () => handleConfirmReassignRemoveClick('remove'), + disabled: confirmButtonDisabled() + }, + { + id: '#view-package-button', + classNames: ['usa-button', 'usa-button-secondary'], + name: 'View package', + onClick: handleViewPackage, + disabled: false + } + ]; + + return ( + <> + {props.responseType && ( + + )} + + {props.featureToggles.correspondence_queue && } + {(vetName) && + currentAction.action_type === 'DeleteReviewPackage' && ( + + )} + {['splitPackage', 'removePackage', 'reassignPackage', 'mergePackage'].includes(currentAction.action_type) && ( + + )} + {config && + } + {showReassignPackageModal && + + Reason for reassignment: +

{currentSelectedVeteran.instructions}

+
+ setSelectedRequestChoice(val)} + value={selectedRequestChoice} + /> +
+
} + {showRemovePackageModal && + + Reason for removal: +

{currentSelectedVeteran.instructions}

+ setSelectedRequestChoice(val)} + value={selectedRequestChoice} + /> +
} +
+ + ); +}; + +CorrespondenceCases.propTypes = { + organizations: PropTypes.array, + loadCorrespondenceConfig: PropTypes.func, + correspondenceConfig: PropTypes.object, + currentAction: PropTypes.object, + configUrl: PropTypes.string, + inboundOpsTeamUsers: PropTypes.arrayOf(string), + inboundOpsTeamNonAdmin: PropTypes.arrayOf(string), + responseType: PropTypes.string, + responseHeader: PropTypes.string, + responseMessage: PropTypes.string, + taskIds: PropTypes.array, + isInboundOpsTeamUser: PropTypes.bool, + isInboundOpsSuperuser: PropTypes.bool, + isInboundOpsSupervisor: PropTypes.bool, + featureToggles: PropTypes.object +}; + +export default CorrespondenceCases; diff --git a/client/app/queue/correspondence/CorrespondencePaginationWrapper.jsx b/client/app/queue/correspondence/CorrespondencePaginationWrapper.jsx new file mode 100644 index 00000000000..2fbd7c204c1 --- /dev/null +++ b/client/app/queue/correspondence/CorrespondencePaginationWrapper.jsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import PropTypes, { object } from 'prop-types'; +import CorrespondencePagination from '../../components/Pagination/CorrespondencePagination'; +const CorrespondencePaginationWrapper = (props) => { + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(props.rowObjects.length / props.columnsToDisplay); + const startIndex = (currentPage * props.columnsToDisplay) - 15; + const endIndex = (currentPage * props.columnsToDisplay); + + return + { + setCurrentPage(incoming + 1); + }} + /> + ; +}; + +CorrespondencePaginationWrapper.propTypes = { + children: PropTypes.node, + columnsToDisplay: PropTypes.number, + rowObjects: PropTypes.arrayOf(object), + columns: PropTypes.func, + summary: PropTypes.string, + className: PropTypes.string, + headerClassName: PropTypes.string, + bodyClassName: PropTypes.string, + tbodyId: PropTypes.string, + getKeyForRow: PropTypes.func +}; + +export default React.memo(CorrespondencePaginationWrapper); diff --git a/client/app/queue/correspondence/CorrespondenceTable.jsx b/client/app/queue/correspondence/CorrespondenceTable.jsx new file mode 100644 index 00000000000..6bec750926b --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceTable.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import QueueTable from '../QueueTable'; +import PropTypes from 'prop-types'; +import Link from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/Link'; + +class CorrespondenceTable extends React.Component { + + render() { + + const columns = [ + { + name: 'veteranDetails', + header: 'Veteran Details', + align: 'left', + valueName: 'veteranDetails', + getSortValue: (row) => row.firstName, + backendCanSort: true, + valueFunction: (row) => ( + + {`${row.firstName} ${row.lastName} (${row.fileNumber})`} + + ) + }, + { + name: 'cmPacketNumber', + header: 'CM Packet Number ', + align: 'left', + valueName: 'cmPacketNumber', + getSortValue: (row) => row.cmPacketNumber, + backendCanSort: true + } + ]; + const tabPaginationOptions = { + onPageLoaded: this.onPageLoaded + }; + + return ( + + ); + } +} + +CorrespondenceTable.propTypes = { + hearingScheduleColumns: PropTypes.array, + hearingScheduleRows: PropTypes.array, + onApply: PropTypes.func, + loadCorrespondenceConfig: PropTypes.func, + correspondenceConfig: PropTypes.array, + history: PropTypes.object, + user: PropTypes.shape({ + userCanBuildHearingSchedule: PropTypes.bool + }) +}; + +export default CorrespondenceTable; diff --git a/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx b/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx new file mode 100644 index 00000000000..0e9b4cc4088 --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx @@ -0,0 +1,366 @@ +import React, { useState, useEffect } from 'react'; +import { useSelector, connect } from 'react-redux'; +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import { sprintf } from 'sprintf-js'; +import querystring from 'querystring'; +import Button from '../../components/Button'; +import SearchableDropdown from '../../components/SearchableDropdown'; +import moment from 'moment'; +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 SearchBar from '../../components/SearchBar'; +import BatchAutoAssignButton from './component/BatchAutoAssignButton'; +import { + actionType, + assignedToColumn, + assignedByColumn, + checkboxColumn, + daysWaitingCorrespondence, + notes, + taskColumn, + correspondenceCompletedDateColumn, + vaDor, + veteranDetails, + packageDocumentType +} from '../components/TaskTableColumns'; + +import { tasksWithCorrespondenceFromRawTasks } from '../utils'; + +import COPY from '../../../COPY'; +import QUEUE_CONFIG from '../../../constants/QUEUE_CONFIG'; +import { isActiveOrganizationVHA } from '../selectors'; + +/** + * A component to create a queue table's tabs and columns from a queue config or the assignee's tasks + * The props are: + * - @assignedTasks {array[object]} array of task objects to appear in the assigned tab + **/ + +const buildMailUserData = (data) => { + return data.map((user) => { + return { + value: user, + label: user + }; + }); +}; + +const CorrespondenceTableBuilder = (props) => { + const [selectedMailTeamUser, setSelectedMailTeamUser] = useState(null); + const [isAnyCheckboxSelected, setIsAnyCheckboxSelected] = useState(false); + const [isDropdownItemSelected, setIsDropdownItemSelected] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const selectedTasks = useSelector((state) => state.intakeCorrespondence.selectedTasks); + + const paginationOptions = () => querystring.parse(window.location.search.slice(1)); + const [storedPaginationOptions, setStoredPaginationOptions] = useState( + querystring.parse(window.location.search.slice(1)) + ); + + // Causes one additional rerender of the QueueTables/tabs but prevents saved pagination behavior + // e.g. clearing filter in a tab, then swapping tabs, then swapping back and the filter will still be applied + useEffect(() => { + setStoredPaginationOptions({}); + }, []); + + const handleMailTeamUserChange = (selectedUser) => { + setSelectedMailTeamUser(selectedUser); + setIsDropdownItemSelected(Boolean(selectedUser)); + }; + + const handleCheckboxChange = (isChecked) => { + setIsAnyCheckboxSelected(isChecked); + }; + + const handleAssignButtonClick = () => { + // Logic to handle assigning tasks to the selected mail team user + // candidate for refactor using PATCH request + if (selectedMailTeamUser && isDropdownItemSelected && isAnyCheckboxSelected) { + const mailTeamUser = selectedMailTeamUser.value; + const taskIds = selectedTasks.map((task) => task); + let newUrl = window.location.href; + + newUrl += newUrl.includes('?') ? `&user=${mailTeamUser}&task_ids=${taskIds}` : + `?user=${mailTeamUser}&task_ids=${taskIds}`; + window.location.href = newUrl; + } + }; + + const calcActiveTabIndex = (config) => { + const tabNames = config.tabs.map((tab) => { + return tab.name; + }); + + const activeTab = paginationOptions().tab || config.active_tab; + const index = _.indexOf(tabNames, activeTab); + + return index === -1 ? 0 : index; + }; + + const handleSearchChange = (value) => { + setSearchValue(value); + }; + + const handleClearSearch = () => { + setSearchValue(''); + }; + + const taskMatchesSearch = (task) => { + const taskNotes = task.notes || ''; + const daysWaiting = task.daysWaiting ? task.daysWaiting.toString() : ''; + const assignedByfirstName = (task.assignedBy && task.assignedBy.firstName) || ''; + const assignedBylastName = (task.assignedBy && task.assignedBy.lastName) || ''; + const assignedToName = (task.assignedTo && task.assignedTo.name) || ''; + const taskVeteranDetails = task.veteranDetails || ''; + const taskLabel = task.label || ''; + const taskVaDor = task.vaDor || ''; + const closedAt = task.closedAt || ''; + const packageDocType = task.nod ? 'NOD' : 'Non-NOD'; + const searchValueTrimmed = searchValue.trim(); + const isNumericSearchValue = !isNaN(parseFloat(searchValueTrimmed)) && isFinite(searchValueTrimmed); + + return ( + taskVeteranDetails.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + taskNotes.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + moment(taskVaDor).format('MM/DD/YYYY'). + includes(searchValueTrimmed) || + assignedByfirstName.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + assignedBylastName.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + assignedToName.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + packageDocType.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + taskLabel.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + (isNumericSearchValue && daysWaiting.trim() === searchValueTrimmed) || + moment(closedAt).format('MM/DD/YYYY'). + includes(searchValue) + ); + }; + + const queueConfig = () => { + const { config } = props; + + config.active_tab_index = calcActiveTabIndex(config); + + return config; + }; + + const filterValuesForColumn = (column) => + column && column.filterable && column.filter_options; + + const createColumnObject = (column, config, tasks) => { + + const filterOptions = filterValuesForColumn(column); + const functionForColumn = { + [QUEUE_CONFIG.COLUMNS.DAYS_WAITING_CORRESPONDENCE.name]: daysWaitingCorrespondence(), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name]: assignedToColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_ASSIGNED_BY.name]: assignedByColumn(), + [QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name]: correspondenceCompletedDateColumn(), + [QUEUE_CONFIG.COLUMNS.TASK_TYPE.name]: taskColumn(tasks, filterOptions), + [QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name]: veteranDetails(), + [QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name]: vaDor(tasks, filterOptions), + [QUEUE_CONFIG.COLUMNS.NOTES.name]: notes(), + [QUEUE_CONFIG.COLUMNS.CHECKBOX_COLUMN.name]: checkboxColumn(handleCheckboxChange), + [QUEUE_CONFIG.COLUMNS.ACTION_TYPE.name]: actionType(), + [QUEUE_CONFIG.COLUMNS.PACKAGE_DOCUMENT_TYPE.name]: packageDocumentType(filterOptions) + }; + + 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 = tasksWithCorrespondenceFromRawTasks(tabConfig.tasks); + let totalTaskCount = tabConfig.total_task_count; + let noCasesMessage; + + const { isVhaOrg } = props; + + if (tabConfig.contains_legacy_tasks) { + tasks.unshift(...props.assignedTasks); + totalTaskCount = tasks.length; + + noCasesMessage = totalTaskCount === 0 && ( +

+ {COPY.NO_CASES_IN_QUEUE_MESSAGE} + + {COPY.NO_CASES_IN_QUEUE_LINK_TEXT} + + . +

+ ); + } + + // Setup default sorting. + const defaultSort = {}; + + const getBulkAssignArea = () => { + return (<> +

Assign to Inbound Ops Team user

+
+
+ +
+ {tabConfig.name === QUEUE_CONFIG.CORRESPONDENCE_UNASSIGNED_TASKS_TAB_NAME && + <> + +
+
+ ); + + }; + + // If there is no sort by column in the pagination options, then use the tab config default sort + // eslint-disable-next-line camelcase + if (!savedPaginationOptions?.sort_by) { + Object.assign(defaultSort, tabConfig.defaultSort); + } + + return { + label: sprintf(tabConfig.label, totalTaskCount), + page: ( + <> + {/* this setup should prevent a double render of the bulk assign area if a + user is a superuser and also a supervisor */} + {(props.isInboundOpsSupervisor || (props.isInboundOpsSupervisor && props.isInboundOpsSuperuser)) && + (tabConfig.name === QUEUE_CONFIG.CORRESPONDENCE_UNASSIGNED_TASKS_TAB_NAME || + tabConfig.name === QUEUE_CONFIG.CORRESPONDENCE_TEAM_ASSIGNED_TASKS_TAB_NAME) && + <> + {getBulkAssignArea()} + + } + { + (props.isInboundOpsSuperuser && !props.isInboundOpsSupervisor && + tabConfig.name === QUEUE_CONFIG.CORRESPONDENCE_TEAM_ASSIGNED_TASKS_TAB_NAME) && + <> + {getBulkAssignArea()} + + + } +
+

+ {noCasesMessage || tabConfig.description} +

+
+ handleSearchChange(value)} + onClearSearch={handleClearSearch} + value={searchValue} + /> +
+
+ +
+ task.uniqueId} + casesPerPage={config.tasks_per_page} + numberOfPages={tabConfig.task_page_count} + totalTaskCount={totalTaskCount} + taskPagesApiEndpoint={tabConfig.task_page_endpoint_base_path} + tabPaginationOptions={ + savedPaginationOptions.tab === tabConfig.name ? savedPaginationOptions : {} + } + // Limit filter preservation/retention to only VHA orgs for now. + {...(isVhaOrg ? { preserveFilter: true } : {})} + defaultSort={defaultSort} + useTaskPagesApi={ + config.use_task_pages_api && !tabConfig.contains_legacy_tasks + } + enablePagination + isCorrespondenceTable + searchValue={searchValue} + taskMatchesSearch={taskMatchesSearch} + /> +
+ + ), + }; + }; + + const tabsFromConfig = (config) => { + return (config.tabs || []).map((tabConfig) => + taskTableTabFactory(tabConfig, config) + ); + }; + + const config = queueConfig(); + + return
+

{config.table_title}

+ + +
; +}; + +const mapStateToProps = (state) => { + return { + config: state.intakeCorrespondence.correspondenceConfig, + organizations: state.ui.organizations, + isVhaOrg: isActiveOrganizationVHA(state), + userCanBulkAssign: state.ui.activeOrganization.userCanBulkAssign, + }; +}; + +CorrespondenceTableBuilder.propTypes = { + organizations: PropTypes.array, + assignedTasks: PropTypes.array, + config: PropTypes.shape({ + table_title: PropTypes.string, + active_tab_index: PropTypes.number, + }), + userCanBulkAssign: PropTypes.bool, + isVhaOrg: PropTypes.bool, + featureToggles: PropTypes.object, + inboundOpsTeamNonAdmin: PropTypes.array, + selectedTasks: PropTypes.array, + isInboundOpsTeamUser: PropTypes.bool, + isInboundOpsSuperuser: PropTypes.bool, + isInboundOpsSupervisor: PropTypes.bool +}; + +export default connect(mapStateToProps)(CorrespondenceTableBuilder); diff --git a/client/app/queue/correspondence/CorrespondenceTaskRows.jsx b/client/app/queue/correspondence/CorrespondenceTaskRows.jsx new file mode 100644 index 00000000000..58428168ce5 --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceTaskRows.jsx @@ -0,0 +1,331 @@ +import { css } from 'glamor'; +import React from 'react'; +import { connect } from 'react-redux'; +import moment from 'moment'; +import PropTypes from 'prop-types'; +import Button from '../../components/Button'; +import COPY from '../../../COPY'; +import { CancelIcon } from '../../components/icons/CancelIcon'; +import { GrayDotIcon } from '../../components/icons/GrayDotIcon'; +import { GreenCheckmarkIcon } from '../../components/icons/GreenCheckmarkIcon'; +import { COLORS } from '../../constants/AppConstants'; +import { sortCaseTimelineEvents } from '../utils'; +import CaseDetailsDescriptionList from '../components/CaseDetailsDescriptionList'; +import ActionsDropdown from '../components/ActionsDropdown'; +import TASK_STATUSES from '../../../constants/TASK_STATUSES'; + +export const grayLineStyling = css({ + width: '5px', + background: COLORS.GREY_LIGHT, + margin: 'auto', + position: 'absolute', + top: '30px', + left: '45.5%', + bottom: 0 +}); + +export const grayLineTimelineStyling = css(grayLineStyling, { left: '9%', + marginLeft: '12px', + top: '39px' }); + +const greyDotAndlineStyling = css({ top: '25px' }); + +const closedAtIcon = (task, timeline) => { + return (task.closedAt && timeline ? : ); +}; + +const taskContainerStyling = css({ + border: 'none', + verticalAlign: 'top', + padding: '3px', + paddingBottom: '3rem' +}); + +const taskInfoWithIconContainer = css({ + textAlign: 'center', + border: 'none', + padding: '0 0 0 0', + position: 'relative', + verticalAlign: 'top', + width: '15px' +}); + +const taskTimeContainerStyling = css(taskContainerStyling, { width: '20%' }); +const taskInformationContainerStyling = css(taskContainerStyling, { width: '25%' }); +const taskTimeTimelineContainerStyling = css(taskContainerStyling, { width: '40%' }); +const taskInfoWithIconTimelineContainer = + css(taskInfoWithIconContainer, { textAlign: 'left', + marginLeft: '5px', + width: '10%', + paddingLeft: '0px' }); + +const isCancelled = (task) => { + return task.status === TASK_STATUSES.cancelled; +}; + +const establishmentTaskCorrespondence = (task) => { + return task.type === 'EstablishmentTask'; +}; + +const tdClassNamesforCorrespondence = (timeline, task) => { + const closedAtClass = task.closedAt ? null : ; + const containerClass = timeline ? taskInfoWithIconTimelineContainer : ''; + + return [containerClass, closedAtClass].filter((val) => val).join(' '); +}; + +const cancelGrayTimeLineStyle = (timeline) => { + return timeline ? grayLineTimelineStyling : ''; +}; + +class CorrespondenceTaskRows extends React.PureComponent { + + constructor(props) { + super(props); + + this.state = { + taskInstructionsIsVisible: [], + showEditNodDateModal: false, + activeTasks: [props.taskList], + }; + } + + toggleTaskInstructionsVisibility = (taskKey) => { + if (this.state.taskInstructionsIsVisible.includes(taskKey)) { + const state = this.state.taskInstructionsIsVisible; + + const index = this.state.taskInstructionsIsVisible.indexOf(taskKey); + + state.splice(index, 1); + this.setState({ taskInstructionsIsVisible: [...state] }); + } else { + const state = this.state.taskInstructionsIsVisible; + + state.push(taskKey); + this.setState({ taskInstructionsIsVisible: [...state] }); + } + }; + + assignedOnListItem = (task) => { + + return task.assignedOn ? ( +
+
{COPY.TASK_SNAPSHOT_TASK_ASSIGNMENT_DATE_LABEL}
+
{moment(task.assignedOn).format('MM/DD/YYYY')}
+
+ ) : null; + }; + + assignedToListItem = (task) => { + + return ( +
+
{COPY.TASK_SNAPSHOT_TASK_ASSIGNEE_LABEL}
+
{task.assignedTo}
+
+ ); + }; + + taskLabelListItem = (task) => { + if (task.closedAt) { + return null; + } + + return task.label ? ( +
+
{COPY.TASK_SNAPSHOT_TASK_TYPE_LABEL}
+
{task.label}
+
+ ) : null; + }; + + taskInstructionsWithLineBreaks = (task) => { + if (!task.instructions || !task.instructions.length) { + return
; + } + + // We specify the same 2.4rem margin-bottom as paragraphs to each set of instructions + // to ensure a consistent margin between instruction content and the "Hide" button + const divStyles = { marginBottom: '2.4rem', marginTop: '1em' }; + + return ( + + {task.instructions.map((text) => ( + +
+

{text}

+
+
+ ))} +
+ ); + }; + + taskInstructionsListItem = (task) => { + if (!task.instructions || !task.instructions.length > 0) { + return null; + } + + const taskInstructionsVisible = this.state.taskInstructionsIsVisible.includes(task.label); + + return ( +
+ {taskInstructionsVisible && ( + + {!establishmentTaskCorrespondence(task) && +
+ {COPY.TASK_SNAPSHOT_TASK_INSTRUCTIONS_LABEL} +
+ } +
+ {this.taskInstructionsWithLineBreaks(task)} +
+
+ )} +
+ ); + }; + showActionsListItem = (task, correspondence) => { + if (task.availableActions.length <= 0) { + return null; + } + + return this.showActionsSection(task) ? ( +
+

{COPY.TASK_SNAPSHOT_ACTION_BOX_TITLE}

+ +
+ ) : null; + }; + + showActionsSection = (task) => task && !this.props.hideDropdown; + + showTimelineDescriptionItems = (task) => { + + return ( + + {task.type !== 'IssuesUpdateTask' && this.assignedToListItem(task)} + {this.taskLabelListItem(task)} + {this.taskInstructionsListItem(task)} + + ); + }; + + taskTemplate = (templateConfig) => { + const { + task, + sortedTimelineEvents, + index, + timeline, + correspondence, + } = templateConfig; + + const timelineTitle = isCancelled(task) ? + `${task.type} cancelled` : + task.timelineTitle; + + return ( + + + + {this.assignedOnListItem(task)} + + + + + {isCancelled(task) ? : closedAtIcon(task, timeline)} + + {((index < sortedTimelineEvents.length && timeline) || + (index < this.state.activeTasks.length - 1 && !timeline)) && ( +
+ )} + + + + {timeline && timelineTitle} + {this.showTimelineDescriptionItems(task)} + + + + {!timeline && ( + + {this.showActionsListItem(task, correspondence)}{' '} + + )} + + ); + }; + + render = () => { + const { correspondence, taskList } = this.props; + // Non-tasks are only relevant for the main Case Timeline + const sortedTimelineEvents = sortCaseTimelineEvents( + taskList, + ); + + return ( + + {sortedTimelineEvents.map((timelineEvent, index) => { + const templateConfig = { + task: timelineEvent, + index, + sortedTimelineEvents, + correspondence, + }; + + return this.taskTemplate(templateConfig); + })} + + ); + }; +} + +CorrespondenceTaskRows.propTypes = { + correspondence: PropTypes.object, + hideDropdown: PropTypes.bool, + taskList: PropTypes.array, + timeline: PropTypes.bool, + showActionsDropdown: PropTypes.bool, +}; + +const mapStateToProps = (state) => ({ + showActionsDropdown: state.correspondenceDetails.showActionsDropdown, +}); + +export default connect( + mapStateToProps, +)(CorrespondenceTaskRows); diff --git a/client/app/queue/correspondence/CorrespondenceTasksAdded.jsx b/client/app/queue/correspondence/CorrespondenceTasksAdded.jsx new file mode 100644 index 00000000000..b0a3429cdc6 --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceTasksAdded.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import CaseDetailsLink from '../CaseDetailsLink'; +import DocketTypeBadge from '../../components/DocketTypeBadge'; +import CorrespondenceCaseTimeline from './CorrespondenceCaseTimeline'; + +const CorrespondenceTasksAdded = (props) => { + const veteranFullName = props.correspondence.veteranFullName; + + return ( + <> +
+
+
+

DOCKET NUMBER

+ + + props.task_added.docketNumber} + task={props.task_added} + + linkOpensInNewTab + /> + + +
+
+

APPELLANT NAME

+

{veteranFullName}

+
+
+

APPEAL STREAM TYPE

+

{props.task_added.streamType}

+
+
+

NUMBER OF ISSUES

+

{props.task_added.numberOfIssues}

+
+
+

STATUS

+

{props.task_added.status}

+
+
+

ASSIGNED TO

+

{props.task_added.assignedTo ? props.task_added.assignedTo.name : ''}

+
+ +
+
+ Tasks added to appeal +
+ +
+
+
+ + ); +}; + +CorrespondenceTasksAdded.propTypes = { + correspondence: PropTypes.object, + task_added: PropTypes.object, + organizations: PropTypes.array, + userCssId: PropTypes.string, +}; + +export default CorrespondenceTasksAdded; diff --git a/client/app/queue/correspondence/ReviewPackage/CmpDocuments.jsx b/client/app/queue/correspondence/ReviewPackage/CmpDocuments.jsx new file mode 100644 index 00000000000..7bb2a7062ea --- /dev/null +++ b/client/app/queue/correspondence/ReviewPackage/CmpDocuments.jsx @@ -0,0 +1,97 @@ +/* eslint-disable camelcase */ + +import PropTypes from 'prop-types'; +import React, { useState } from 'react'; +import COPY from 'app/../COPY'; +import Button from 'app/components/Button'; +import EditDocumentTypeModal from '../component/EditDocumentTypeModal'; +import CorrespondencePdfUI from '../pdfPreview/CorrespondencePdfUI'; + +export const CmpDocuments = (props) => { + const { documents, isReadOnly } = props; + + const [selectedId, setSelectedId] = useState(0); + + const paginationText = `Viewing 1-${documents.length} out of ${documents.length} total documents`; + + const setCurrentDocument = (index) => { + setSelectedId(index); + }; + + const [modalState, setModalState] = useState(false); + + const openModal = () => { + setModalState(true); + }; + const closeModal = () => { + setModalState(false); + }; + + const tableStyle = (index, document) => { + if (selectedId === index) { + return setCurrentDocument(index)}> {document?.document_title} + ; + } + + return setCurrentDocument(index)}> {document?.document_title} + ; + }; + + return ( +
+

{COPY.DOCUMENT_PREVIEW}

+
+
{paginationText}
+ + + + + + + + {modalState && + + } + { documents?.map((document, index) => { + return ( + + + {tableStyle(index, document)} + + + + ); + })} +
Document Type Action
+ +
+
+ +
+ ); +}; + +CmpDocuments.propTypes = { + documents: PropTypes.array, + setSelectedId: PropTypes.func, + selectedId: PropTypes.number, + isReadOnly: PropTypes.bool +}; + +export default CmpDocuments; diff --git a/client/app/queue/correspondence/ReviewPackage/CorrespondenceReviewPackage.jsx b/client/app/queue/correspondence/ReviewPackage/CorrespondenceReviewPackage.jsx new file mode 100644 index 00000000000..1773ac1e67a --- /dev/null +++ b/client/app/queue/correspondence/ReviewPackage/CorrespondenceReviewPackage.jsx @@ -0,0 +1,297 @@ +import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; +import React, { useEffect, useState } from 'react'; +import { useHistory } from 'react-router'; +import ReviewPackageCaseTitle from './ReviewPackageCaseTitle'; +import Button from '../../../components/Button'; +import ReviewForm from './ReviewForm'; +import { CmpDocuments } from './CmpDocuments'; +import ApiUtil from '../../../util/ApiUtil'; +import PropTypes from 'prop-types'; +import { setFileNumberSearch, doFileNumberSearch } from '../../../intake/actions/intake'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import PackageActionModal from '../modals/PackageActionModal'; +import ReviewPackageNotificationBanner from './ReviewPackageNotificationBanner'; +import moment from 'moment'; +import { + CORRESPONDENCE_READONLY_BANNER_HEADER, + CORRESPONDENCE_READONLY_BANNER_MESSAGE, + CORRESPONDENCE_READONLY_SUPERVISOR_BANNER_MESSAGE, + CORRESPONDENCE_DOC_UPLOAD_FAILED_HEADER, + CORRESPONDENCE_DOC_UPLOAD_FAILED_MESSAGE } + from '../../../../COPY'; + +export const CorrespondenceReviewPackage = (props) => { + const history = useHistory(); + + // state variables for editable portions of the form that can be passed to child components + const [notes, setNotes] = useState(props.correspondence.notes); + const [veteranFileNumber, setVeteranFileNumber] = useState(props.correspondence.veteranFileNumber); + const [correspondenceTypeId, setCorrespondenceTypeId] = useState( + props.correspondence.correspondence_type_id + ); + const [vaDor, setVaDor] = useState(moment.utc((props.correspondence.vaDateOfReceipt)).format('YYYY-MM-DD')); + const [disableButton, setDisableButton] = useState(false); + const [disableSaveButton, setDisableSaveButton] = useState(true); + const [isReturnToQueue, setIsReturnToQueue] = useState(false); + const [showModal, setShowModal] = useState(false); + const [packageActionModal, setPackageActionModal] = useState(null); + const [errorMessage, setErrorMessage] = useState(''); + const [selectedId, setSelectedId] = useState(0); + const [isReadOnly, setIsReadOnly] = useState(false); + const [isReassignPackage, setIsReassignPackage] = useState(false); + const [corrTypeSelected, setCorrTypeSelected] = useState(true); + const [blockingTaskId, setBlockingTaskId] = useState({ + veteranName: '', + taskId: [], + }); + + // Banner Information takes in the following object: + // { title: , message: , bannerType: } + const [bannerInformation, setBannerInformation] = useState(null); + + // When a remove package task is active and pending review, the page is read-only + const isPageReadOnly = (tasks) => { + const assignedRemoveTask = tasks.find((task) => task.status === 'assigned' && task.type === 'RemovePackageTask'); + + if (assignedRemoveTask) { + setBlockingTaskId(assignedRemoveTask.id); + } + + // Return true if a removePackageTask that is currently assigned is found, else false + return (typeof assignedRemoveTask !== 'undefined'); + }; + + // When a reassign package task is active and pending review, the page is read-only + const hasAssignedReassignPackageTask = (tasks) => { + const assignedReassignTask = tasks.find((task) => task.status === 'assigned' && + task.type === 'ReassignPackageTask'); + + if (assignedReassignTask) { + setBlockingTaskId(assignedReassignTask.id); + } + + // Return true if a reassignPackageTask that is currently assigned is found, else false + return ( + (typeof assignedReassignTask !== 'undefined') + ); + }; + + useEffect(() => { + // Check for eFolder upload failure + if (props.hasEfolderFailedTask) { + setBannerInformation({ + title: CORRESPONDENCE_DOC_UPLOAD_FAILED_HEADER, + message: CORRESPONDENCE_DOC_UPLOAD_FAILED_MESSAGE, + bannerType: 'error' + }); + } + + if (isPageReadOnly(props.correspondence.correspondence_tasks)) { + setBannerInformation({ + title: CORRESPONDENCE_READONLY_BANNER_HEADER, + message: CORRESPONDENCE_READONLY_SUPERVISOR_BANNER_MESSAGE, + bannerType: 'info' + }); + setIsReadOnly(true); + } + + if (hasAssignedReassignPackageTask(props.correspondence.correspondence_tasks)) { + setBannerInformation({ + title: CORRESPONDENCE_READONLY_BANNER_HEADER, + message: CORRESPONDENCE_READONLY_BANNER_MESSAGE, + bannerType: 'info' + }); + setIsReadOnly(true); + setIsReassignPackage(true); + } + }, [props.hasEfolderFailedTask]); + + const handleModalClose = () => { + if (isReturnToQueue) { + setShowModal(!showModal); + } else { + history.goBack(); + } + }; + + const handlePackageActionModal = (value) => { + setPackageActionModal(value); + }; + + const handleReview = () => { + history.push('/queue/correspondence'); + }; + + // used to validate there are no non-null values (notes can be null) + const nullValuesPresent = () => { + return !veteranFileNumber || !correspondenceTypeId || !vaDor; + }; + + const intakeAppeal = async () => { + props.setFileNumberSearch(veteranFileNumber); + try { + await props.doFileNumberSearch('appeal', veteranFileNumber, true); + await ApiUtil.patch(`/queue/correspondence/${props.correspondence_uuid}/intake_update`); + window.location.href = '/intake/review_request'; + } catch (error) { + console.error(error); + setBannerInformation({ + title: CORRESPONDENCE_DOC_UPLOAD_FAILED_HEADER, + message: CORRESPONDENCE_DOC_UPLOAD_FAILED_MESSAGE, + bannerType: 'error' + }); + } + }; + + const intakeLink = async () => { + const data = { + id: props.correspondence.id + }; + + try { + await ApiUtil.post(`/queue/correspondence/${props.correspondence_uuid}/correspondence_intake_task`, { data }); + window.location.href = `/queue/correspondence/${props.correspondence_uuid}/intake`; + } catch (error) { + console.error(error); + } + }; + + // check for in-flight changes to disable the button + useEffect(() => { + // disable create record button if save button is enabled or null values exist + setDisableButton(!disableSaveButton || nullValuesPresent()); + setErrorMessage(''); + }, [disableSaveButton]); + + return ( +
+ { bannerInformation && ( + + )} + + + + + {packageActionModal && + + } + + + + +
+
+
+
+ { (props.correspondence.nod && !isReadOnly) && ( + +
+
+
+
+ ); +}; + +CorrespondenceReviewPackage.propTypes = { + correspondence_uuid: PropTypes.string, + inboundOpsTeamUsers: PropTypes.array, + correspondence: PropTypes.object, + correspondenceTypes: PropTypes.array, + hasEfolderFailedTask: PropTypes.bool, + packageDocumentType: PropTypes.object, + setFileNumberSearch: PropTypes.func, + doFileNumberSearch: PropTypes.func, + userIsInboundOpsSupervisor: PropTypes.bool, + isInboundOpsSuperuser: PropTypes.bool, + createRecordIsReadOnly: PropTypes.string, +}; + +const mapStateToProps = (state) => ({ + correspondence: state.reviewPackage.correspondence, + packageDocumentType: state.reviewPackage.packageDocumentType, + createRecordIsReadOnly: state.reviewPackage.createRecordIsReadOnly, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + setFileNumberSearch, + doFileNumberSearch +}, dispatch); + +export default +connect( + mapStateToProps, + mapDispatchToProps, +)(CorrespondenceReviewPackage); diff --git a/client/app/queue/correspondence/ReviewPackage/ReviewForm.jsx b/client/app/queue/correspondence/ReviewPackage/ReviewForm.jsx new file mode 100644 index 00000000000..f2f981dba08 --- /dev/null +++ b/client/app/queue/correspondence/ReviewPackage/ReviewForm.jsx @@ -0,0 +1,332 @@ +import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; +import React, { useState, useEffect } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import TextField from '../../../components/TextField'; +import SearchableDropdown from '../../../components/SearchableDropdown'; +import TextareaField from '../../../components/TextareaField'; +import Button from '../../../components/Button'; +import ApiUtil from '../../../util/ApiUtil'; +import PropTypes from 'prop-types'; +import Modal from '../../../components/Modal'; +import DateSelector from '../../../components/DateSelector'; +import { + updateCmpInformation, + setCreateRecordIsReadOnly, + setCorrespondence +} from '../correspondenceReducer/reviewPackageActions'; +import { validateDateNotInFuture } from '../../../intake/util/issues'; + +export const ReviewForm = (props) => { + const { correspondenceTypeId, notes, vaDor, veteranFileNumber } = props; + + const correspondenceTypes = props.correspondenceTypes; + const [dateError, setDateError] = useState(false); + + const handleCorrespondenceTypeEmpty = () => { + if (correspondenceTypeId === null) { + return 'Select...'; + } + + const type = correspondenceTypes.find((value) => value.id === correspondenceTypeId); + + return type.name; + }; + + // enable save button and enable return to queue + const enableSaveButton = () => { + props.setDisableSaveButton(false); + props.setIsReturnToQueue(true); + }; + + const handleFileNumber = (value) => { + // use reg expression to check the value only contains 9 numbers + const isNumeric = ((/^\d{0,9}$/).test(value)); + + // only attempt to update value if valid file number + if (isNumeric) { + props.setVeteranFileNumber(value); + enableSaveButton(); + } + }; + + const handleChangeNotes = (value) => { + props.setNotes(value); + enableSaveButton(); + }; + + const generateOptions = (options) => + options.map((option) => ({ + value: option.id, + label: option.name, + id: option.id, + })); + + const handleSelectCorrespondenceType = (val) => { + // update the correspondence type id and update the correspondence type + // in the dropdown with placeholder + props.setCorrespondenceTypeId(val.id); + enableSaveButton(); + }; + + const vaDORReadOnly = () => { + if (props.userIsInboundOpsSupervisor) { + return false; + } + + return true; + + }; + + const handleSelectVADOR = (val) => { + // check for future issue + const error = validateDateNotInFuture(val) ? false : 'Receipt date cannot be in the future'; + + props.setVaDor(val); + setDateError(error); + + // if no errors, enable the save button + if (!error) { + enableSaveButton(); + } + }; + + const handleSubmit = async () => { + // disable the save button on submit + props.setDisableSaveButton(true); + + props.setCreateRecordIsReadOnly(''); + const correspondence = props; + const payloadData = { + data: { + correspondence: { + notes, + correspondence_type_id: correspondenceTypeId, + va_date_of_receipt: vaDor + }, + veteran: { + file_number: veteranFileNumber + } + }, + }; + + try { + const response = await ApiUtil.patch( + `/queue/correspondence/${correspondence.correspondence_uuid}/review_package`, + payloadData + ); + + const { body } = response; + + // console.log(`response: ${JSON.stringify(response.body.correspondence, 1, 1)}`); + // console.log(`body status: ${response.body.status}`); + + props.setIsReturnToQueue(false); + if (body.status === 'ok') { + // set error message to false and update redux stored correspondence + props.setErrorMessage(''); + props.setCorrespondence(response.body.correspondence); + + } + } catch (error) { + const { body } = error.response; + + props.setErrorMessage(body.error); + } + }; + + // enable save button if changes happen to form (with no errors) + useEffect(() => { + + // error validation + if (dateError || props.errorMessage) { + props.setDisableSaveButton(true); + } + }, [handleChangeNotes, handleFileNumber, handleSelectCorrespondenceType, handleSelectVADOR]); + + const veteranFileNumStyle = () => { + if (props.errorMessage) { + return
+ +
; + } + + return
+ +
; + }; + + const vaDORReadOnlyStyling = () => { + if (vaDORReadOnly() || props.isReadOnly) { + return ; + } + + return ; + }; + + return ( + +
+

General Information

+
+ +
+
+
+ {veteranFileNumStyle()} +
+ +
+
+
+ +
+ +
+ + {vaDORReadOnlyStyling()} +
+
+
+
+ +
+
+ +
+
+ {props.showModal && ( + + + All unsaved changes made to this mail package will be lost
upon returning to your queue. +
+
+ )} +
+
+
+ ); +}; + +ReviewForm.propTypes = { + correspondenceTypeId: PropTypes.number, + setCorrespondenceTypeId: PropTypes.func, + notes: PropTypes.string, + vaDor: PropTypes.string, + veteranFileNumber: PropTypes.string, + disableSaveButton: PropTypes.bool, + setDisableSaveButton: PropTypes.func, + setIsReturnToQueue: PropTypes.bool, + setCreateRecordIsReadOnly: PropTypes.func, + setCorrespondence: PropTypes.func, + setErrorMessage: PropTypes.func, + setVeteranFileNumber: PropTypes.func, + setNotes: PropTypes.func, + setVaDor: PropTypes.func, + showModal: PropTypes.bool, + handleModalClose: PropTypes.func, + handleReview: PropTypes.func, + errorMessage: PropTypes.any, + isReadOnly: PropTypes.bool, + userIsInboundOpsSupervisor: PropTypes.bool, + correspondence: PropTypes.object, + setCorrTypeSelected: PropTypes.bool, + correspondenceTypes: PropTypes.array +}; + +const mapStateToProps = (state) => ({ + correspondence: state.reviewPackage.correspondence, + packageDocumentType: state.reviewPackage.packageDocumentType +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + updateCmpInformation, + setCreateRecordIsReadOnly, + setCorrespondence +}, dispatch); + +export default +connect( + mapStateToProps, + mapDispatchToProps, +)(ReviewForm); diff --git a/client/app/queue/correspondence/ReviewPackage/ReviewPackageCaseTitle.jsx b/client/app/queue/correspondence/ReviewPackage/ReviewPackageCaseTitle.jsx new file mode 100644 index 00000000000..6b8325075e4 --- /dev/null +++ b/client/app/queue/correspondence/ReviewPackage/ReviewPackageCaseTitle.jsx @@ -0,0 +1,150 @@ + +import PropTypes from 'prop-types'; +import React, { useState } from 'react'; +import COPY from 'app/../COPY'; +import Button from '../../../components/Button'; +import SearchableDropdown from '../../../components/SearchableDropdown'; +import RemovePackageModal from '../component/RemovePackageModal'; +import ReassignPackageModal from '../component/ReassignPackageModal'; + +const ReviewPackageCaseTitle = (props) => { + + return ( +
+ + +
+ ); +}; + +const CaseTitleScaffolding = (props) => { + + const [modalRemoveState, setRemoveModalState] = useState(false); + const [modalReassignState, setReassignModalState] = useState(false); + + const openRemoveModal = () => { + setRemoveModalState(true); + }; + const closeRemoveModal = () => { + setRemoveModalState(false); + }; + const openReassignModal = () => { + setReassignModalState(true); + }; + const closeReassignModal = () => { + setReassignModalState(false); + }; + + return ( +
+

+ {COPY.CORRESPONDENCE_REVIEW_PACKAGE_TITLE} +

+ + + { ( + props.isReadOnly && !props.isReassignPackage && + props.userIsInboundOpsSupervisor) && +
+ ); + +}; + +const CaseSubTitleScaffolding = (props) => ( +
+
+ {COPY.CORRESPONDENCE_REVIEW_PACKAGE_SUB_TITLE} +
+
+ { (!props.isReadOnly && !props.efolder) && + props.handlePackageActionModal(option.value)} + placeholder="Request package action" + label="Request package action dropdown" + hideLabel + name="" + value={props.packageActionModal} + /> } +
+
+); + +ReviewPackageCaseTitle.propTypes = { + handlePackageActionModal: PropTypes.func, + inboundOpsTeamUsers: PropTypes.array, + correspondence: PropTypes.object, + isReadOnly: PropTypes.bool, + isReassignPackage: PropTypes.bool, + userIsInboundOpsSupervisor: PropTypes.bool, + isInboundOpsSuperuser: PropTypes.bool +}; + +CaseSubTitleScaffolding.propTypes = { + handlePackageActionModal: PropTypes.func, + inboundOpsTeamUsers: PropTypes.array, + packageActionModal: PropTypes.string, + isReadOnly: PropTypes.bool, + efolder: PropTypes.bool, + userIsInboundOpsSupervisor: PropTypes.bool, + isInboundOpsSuperuser: PropTypes.bool +}; + +CaseTitleScaffolding.propTypes = { + correspondence_id: PropTypes.number, + inboundOpsTeamUsers: PropTypes.array, + isReadOnly: PropTypes.bool, + isReassignPackage: PropTypes.bool, + userIsInboundOpsSupervisor: PropTypes.bool, + isInboundOpsSuperuser: PropTypes.bool, + blockingTaskId: PropTypes.number +}; + +export default ReviewPackageCaseTitle; diff --git a/client/app/queue/correspondence/ReviewPackage/ReviewPackageLoadingScreen.jsx b/client/app/queue/correspondence/ReviewPackage/ReviewPackageLoadingScreen.jsx new file mode 100644 index 00000000000..342c6bf778d --- /dev/null +++ b/client/app/queue/correspondence/ReviewPackage/ReviewPackageLoadingScreen.jsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import LoadingDataDisplay from '../../../components/LoadingDataDisplay'; +import { LOGO_COLORS } from '../../../constants/AppConstants'; +import ApiUtil from '../../../util/ApiUtil'; + +import { + setCorrespondence, + setTaskInstructions +} from '../correspondenceReducer/reviewPackageActions'; +import WindowUtil from '../../../util/WindowUtil'; + +class ReviewPackageLoadingScreen extends React.PureComponent { + + createLoadPromise = async () => { + return await ApiUtil.get( + `/queue/correspondence/${this.props.correspondence_uuid}/review_package`).then( + (response) => { + /* eslint-disable no-unused-vars, camelcase */ + const { + correspondence, + general_information, + taskInstructions + } = response.body; + + this.props.setCorrespondence(correspondence); + this.props.setTaskInstructions(taskInstructions); + } + ); + } + + render = () => { + const failStatusMessageChildren =
+ It looks like Caseflow was unable to load this correspondence
+ Please refresh the page and try again. +
; + + const loadingDataDisplay = + {this.props.children} + ; + + return
+ {loadingDataDisplay} +
; + }; +} + +ReviewPackageLoadingScreen.propTypes = { + correspondence_uuid: PropTypes.string, + children: PropTypes.node, + setCorrespondence: PropTypes.func, + setTaskInstructions: PropTypes.func +}; + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + setCorrespondence, + setTaskInstructions +}, dispatch); + +export default (connect(null, mapDispatchToProps)(ReviewPackageLoadingScreen)); diff --git a/client/app/queue/correspondence/ReviewPackage/ReviewPackageNotificationBanner.jsx b/client/app/queue/correspondence/ReviewPackage/ReviewPackageNotificationBanner.jsx new file mode 100644 index 00000000000..f07efc5d43c --- /dev/null +++ b/client/app/queue/correspondence/ReviewPackage/ReviewPackageNotificationBanner.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Alert from '../../../components/Alert'; +import PropTypes from 'prop-types'; + +const ReviewPackageNotificationBanner = (props) => { + return ( +
+ +
+
+ ); +}; + +ReviewPackageNotificationBanner.propTypes = { + message: PropTypes.string, + title: PropTypes.string, + type: PropTypes.string +}; + +export default ReviewPackageNotificationBanner; diff --git a/client/app/queue/correspondence/ReviewPackage/utils.js b/client/app/queue/correspondence/ReviewPackage/utils.js new file mode 100644 index 00000000000..d7a5ee7179f --- /dev/null +++ b/client/app/queue/correspondence/ReviewPackage/utils.js @@ -0,0 +1,122 @@ +import React from 'react'; +import moment from 'moment'; +import { + PACKAGE_ACTION_MERGE_DESCRIPTION, + PACKAGE_ACTION_MERGE_TITLE, + PACKAGE_ACTION_MERGE_TEXTAREA_LABEL, + PACKAGE_ACTION_MERGE_RADIO_LABEL, + PACKAGE_ACTION_MODAL_DESCRIPTION, + PACKAGE_ACTION_REMOVAL_TITLE, + PACKAGE_ACTION_REMOVAL_TEXTAREA_LABEL, + PACKAGE_ACTION_REASSIGN_DESCRIPTION, + PACKAGE_ACTION_REASSIGN_TITLE, + PACKAGE_ACTION_REASSIGN_TEXTAREA_LABEL, + PACKAGE_ACTION_SPLIT_TITLE, + PACKAGE_ACTION_SPLIT_TEXTAREA_LABEL +} from '../../../../COPY'; + +export const getPackageActionColumns = (dropdownType) => { + const baseColumns = [ + { + cellClass: 'package-document-type-column', + header: ( + + Package Document Type + + ), + valueFunction: (row) => ( + +

{row.correspondence.nod ? 'NOD' : 'Non-NOD'}

+
+ ) + }, + ]; + + if (dropdownType === 'removePackage' || dropdownType === 'reassignPackage' || dropdownType === 'splitPackage') { + baseColumns.push( + { + cellClass: 'veteran-details-column', + header: ( + + Veteran Details + + ), + valueFunction: (row) => { + const vetName = row.correspondence.veteranFullName; + const fileNumber = row.correspondence.veteranFileNumber; + + return ( + +

+ {`${vetName}\n(${fileNumber})`} +

+
+ ); + } + } + ); + } + + if (dropdownType === 'mergePackage') { + baseColumns.push( + { + cellClass: 'vador-details-column', + header: ( + + VA DOR + + ), + valueFunction: (row) => { + const vaDate = moment.utc(row.correspondence.va_date_of_receipt).format('MM/DD/YYYY'); + + return ( + +

+ {`${vaDate}`} +

+
+ ); + } + } + ); + } + + return baseColumns; +}; + +export const getModalInformation = (dropdownType) => { + switch (dropdownType) { + case 'mergePackage': + return { + title: PACKAGE_ACTION_MERGE_TITLE, + description: PACKAGE_ACTION_MERGE_DESCRIPTION, + label: PACKAGE_ACTION_MERGE_TEXTAREA_LABEL, + radioLabel: PACKAGE_ACTION_MERGE_RADIO_LABEL, + }; + case 'removePackage': + return { + title: PACKAGE_ACTION_REMOVAL_TITLE, + description: PACKAGE_ACTION_MODAL_DESCRIPTION, + label: PACKAGE_ACTION_REMOVAL_TEXTAREA_LABEL + }; + case 'reassignPackage': + return { + title: PACKAGE_ACTION_REASSIGN_TITLE, + description: PACKAGE_ACTION_REASSIGN_DESCRIPTION, + label: PACKAGE_ACTION_REASSIGN_TEXTAREA_LABEL + }; + case 'splitPackage': + return { + title: PACKAGE_ACTION_SPLIT_TITLE, + description: PACKAGE_ACTION_MODAL_DESCRIPTION, + label: PACKAGE_ACTION_SPLIT_TEXTAREA_LABEL, + placeholder: 'This is a reason for split', + }; + default: + return { + title: '', + description: '', + label: '' + }; + } +}; diff --git a/client/app/queue/correspondence/component/AutoAssignAlertBanner.jsx b/client/app/queue/correspondence/component/AutoAssignAlertBanner.jsx new file mode 100644 index 00000000000..7f4aa3ada23 --- /dev/null +++ b/client/app/queue/correspondence/component/AutoAssignAlertBanner.jsx @@ -0,0 +1,129 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import COPY from '../../../../COPY'; +import Alert from '../../../components/Alert'; +import { + setAutoAssignmentAlertBanner, + setAutoAssignButtonDisabled +} from '../correspondenceReducer/reviewPackageActions'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import ApiUtil from '../../../util/ApiUtil'; +import CORRESPONDENCE_AUTO_ASSIGNMENT from '../../../../../client/constants/CORRESPONDENCE_AUTO_ASSIGNMENT'; + +const AutoAssignAlertBanner = (props) => { + const { + batchId, + bannerAlert, + } = { ...props }; + + const AUTO_ASSIGN_POLLING_INTERVAL = 60000; + const intervalIdRef = useRef(); + + const clearIntervalRef = () => { + clearInterval(intervalIdRef.current); + intervalIdRef.current = null; + }; + + const handleBatchAutoAssignmentBanner = async (response) => { + if (response.status === CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.error) { + const bannerPayload = { + message: response.error_message.message + }; + + if (response.error_message.message.includes(COPY.BAAA_ERROR_MESSAGE)) { + bannerPayload.type = 'error'; + bannerPayload.title = COPY.BAAA_FAILED_TITLE; + } else { + bannerPayload.type = 'warning'; + bannerPayload.title = COPY.BAAA_UNSUCCESSFUL_TITLE; + } + + props.setAutoAssignmentAlertBanner(bannerPayload); + } else if (response.status === CORRESPONDENCE_AUTO_ASSIGNMENT.statuses.completed) { + props.setAutoAssignmentAlertBanner({ + title: `You have successfully assigned ${response.number_assigned} correspondences`, + message: COPY.BAAA_SUCCESS_MESSAGE, + type: 'success' + }); + } else { + props.setAutoAssignmentAlertBanner({ + type: 'pending' + }); + } + }; + + const fetchAutoAssignBannerInfo = async () => { + if (batchId) { + try { + const response = await ApiUtil.get(`/queue/correspondence/${batchId}/auto_assign_status`); + const data = await response.body; + + handleBatchAutoAssignmentBanner(data); + } catch (error) { + clearIntervalRef(); + console.error('Failed to fetch auto assign banner info', error); + } + } + }; + + // UseEffect to trigger an immediate fetch after button click + useEffect(() => { + fetchAutoAssignBannerInfo(); + + // setInterval in useRef to clearInterval() outside local scope of useEffect + // and removes an extra async call from being added to the call stack + if (!intervalIdRef.current) { + intervalIdRef.current = setInterval(() => { + fetchAutoAssignBannerInfo(); + }, AUTO_ASSIGN_POLLING_INTERVAL); + } + + return () => { + clearIntervalRef(); + }; + }, [batchId]); + + useEffect(() => { + if (bannerAlert.type !== 'pending' && bannerAlert.message) { + clearIntervalRef(); + props.setAutoAssignButtonDisabled(false); + } + }, [bannerAlert]); + + return ( + <> + {batchId && bannerAlert.message && + + } + + ); +}; + +AutoAssignAlertBanner.propTypes = { + setAutoAssignmentAlertBanner: PropTypes.func, + setAutoAssignButtonDisabled: PropTypes.func +}; + +const mapStateToProps = (state) => ({ + batchId: state.reviewPackage.autoAssign.batchId, + bannerAlert: state.reviewPackage.autoAssign.bannerAlert +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + setAutoAssignmentAlertBanner, + setAutoAssignButtonDisabled + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AutoAssignAlertBanner); diff --git a/client/app/queue/correspondence/component/BatchAutoAssignButton.jsx b/client/app/queue/correspondence/component/BatchAutoAssignButton.jsx new file mode 100644 index 00000000000..02baa853b05 --- /dev/null +++ b/client/app/queue/correspondence/component/BatchAutoAssignButton.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import Button from '../../../components/Button'; +import ApiUtil from '../../../util/ApiUtil'; +import COPY from '../../../../COPY'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { + setBatchAutoAssignmentAttemptId, + setAutoAssignButtonDisabled +} from '../correspondenceReducer/reviewPackageActions'; + +const BatchAutoAssignButton = (props) => { + const handleAutoAssign = async () => { + try { + props.setAutoAssignButtonDisabled(true); + const response = await ApiUtil.get('/queue/correspondence/auto_assign_correspondences'); + const data = await response.body; + + props.setBatchAutoAssignmentAttemptId(data.batch_auto_assignment_attempt_id); + } catch (error) { + console.error(error); + } + }; + + return ( +
+ +
+ ); +}; + +BatchAutoAssignButton.propTypes = { + disabled: PropTypes.bool, + setBatchAutoAssignmentAttemptId: PropTypes.func, + setAutoAssignButtonDisabled: PropTypes.func +}; + +const mapStateToProps = (state) => ({ + disabled: state.reviewPackage.autoAssign.isButtonDisabled +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + setBatchAutoAssignmentAttemptId, + setAutoAssignButtonDisabled + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BatchAutoAssignButton); diff --git a/client/app/queue/correspondence/component/EditDocumentTypeModal.jsx b/client/app/queue/correspondence/component/EditDocumentTypeModal.jsx new file mode 100644 index 00000000000..3f9c43bd440 --- /dev/null +++ b/client/app/queue/correspondence/component/EditDocumentTypeModal.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ApiUtil from '../../../util/ApiUtil'; +import { sprintf } from 'sprintf-js'; +import SearchableDropdown from 'app/components/SearchableDropdown'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { updateDocumentTypeName, setCorrespondence } from '../correspondenceReducer/reviewPackageActions'; +import COPY from '../../../../COPY'; +import Modal from '../../../components/Modal'; +import Button from '../../../components/Button'; + +class EditDocumentTypeModal extends React.Component { + + constructor(props) { + super(props); + + this.state = { + packageDocument: null, + disabledSaveButton: true, + packageOptions: {}, + }; + } + + componentDidMount() { + setTimeout(this.getPackages, 0); + } + + getPackages = () => { + ApiUtil.get('/queue/correspondence/edit_document_type_correspondence').then((resp) => { + const documents = resp.body.data.map((doc) => ({ + label: doc.name, + value: doc.id + })); + + this.setState({ packageOptions: documents }); + }); + } + + packageDocumentOnChange = (value) => { + this.setState({ + packageDocument: value, + disabledSaveButton: false + }); + }; + + updateDocumentType = async () => { + try { + ApiUtil.patch(`/queue/correspondence/${this.props.document.id}/update_document`, { + data: { + vbms_document_type_id: this.state.packageDocument.value + } + }).then((resp) => { + this.props.setCorrespondence(resp.body.correspondence); + }); + this.props.updateDocumentTypeName(this.state.packageDocument, this.props.indexDoc); + this.props.setModalState(false); + } catch (error) { + console.error(error); + } + } + + render() { + const { onCancel, document } = this.props; + const { packageDocument } = this.state; + + const submit = () => { + this.updateDocumentType(); + }; + const originalDocumentTitle = document.document_title; + + return ( + Save} + cancelButton={} + > +

{sprintf(COPY.TEXT_MODAL_EDIT_DOCUMENT_TYPE_CORRESPONDENCE)}

+
+ {sprintf(COPY.ORIGINAL_DOC_EDIT_DOCUMENT_TYPE_CORRESPONDENCE)} +
+

{originalDocumentTitle}

+
+ +
+
+ ); + } +} + +EditDocumentTypeModal.propTypes = { + modalState: PropTypes.bool, + onCancel: PropTypes.func, + document: PropTypes.object, + onSaveValue: PropTypes.func, + updateDocumentTypeName: PropTypes.func, + setCorrespondence: PropTypes.func, + setModalState: PropTypes.func, + indexDoc: PropTypes.number +}; + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + updateDocumentTypeName, + setCorrespondence +}, dispatch); + +export default connect( + null, + mapDispatchToProps, +)(EditDocumentTypeModal); diff --git a/client/app/queue/correspondence/component/ReassignPackageModal.jsx b/client/app/queue/correspondence/component/ReassignPackageModal.jsx new file mode 100644 index 00000000000..114a747e53e --- /dev/null +++ b/client/app/queue/correspondence/component/ReassignPackageModal.jsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import ApiUtil from '../../../util/ApiUtil'; +import { sprintf } from 'sprintf-js'; +import { useSelector } from 'react-redux'; +import TextareaField from '../../../components/TextareaField'; +import ReactSelectDropdown from '../../../components/ReactSelectDropdown'; +import COPY from '../../../../COPY'; +import Modal from '../../../components/Modal'; +import Button from '../../../components/Button'; +import RadioFieldWithChildren from '../../../components/RadioFieldWithChildren'; + +const ReassignPackageModal = (props) => { + const [selectedRequestChoice, setSelectedRequestChoice] = useState(''); + const [selectedMailTeamUser, setSelectedMailTeamUser] = useState(''); + const [decisionReason, setDecisionReason] = useState(''); + const taskInstructions = useSelector((state) => state.reviewPackage.taskInstructions); + + const { onCancel } = props; + const submit = () => { + try { + const data = { + action_type: 'reassign', + new_assignee: selectedMailTeamUser, + decision: selectedRequestChoice, + decision_reason: decisionReason, + }; + + ApiUtil.patch(`/queue/correspondence/tasks/${props.blockingTaskId}/update`, { data }). + then(() => { + window.location.href = '/queue/correspondence/team'; + }); + + } catch (error) { + console.error(error); + } + }; + + const confirmButtonDisabled = () => { + if (selectedRequestChoice === 'approve' && selectedMailTeamUser === '') { + return true; + } + + if (selectedRequestChoice === 'reject' && decisionReason === '') { + return true; + } + + if (selectedRequestChoice === '') { + return true; + } + + return false; + }; + + const buildMailUserData = (data) => { + + if (typeof data === 'undefined') { + return []; + } + + return data.map((user) => { + return { + value: user, + label: user + }; + }); + }; + + const approveElement = ( +
+ setSelectedMailTeamUser(val.value)} + options={buildMailUserData(props.inboundOpsTeamUsers)} + /> +
+ ); + + const textAreaElement = ( +
+ +
+ ); + + const reassignOptions = [ + { displayText: 'Approve request', + value: 'approve', + element: approveElement, + displayElement: selectedRequestChoice === 'approve' + }, + { displayText: 'Reject request', + value: 'reject', + element: textAreaElement, + displayElement: selectedRequestChoice === 'reject' + } + ]; + + return ( + (submit())}>Confirm} + cancelButton={} + > +

+ {sprintf(COPY.CORRESPONDENCE_TITLE_REASSIGN_PACKAGE)}
+ {taskInstructions[0]} +

+ + setSelectedRequestChoice(val)} + /> +
+ ); +}; + +ReassignPackageModal.propTypes = { + modalState: PropTypes.bool, + onCancel: PropTypes.func, + inboundOpsTeamUsers: PropTypes.array, + setModalState: PropTypes.func, + correspondence_id: PropTypes.number, + taskInstructions: PropTypes.array, + blockingTaskId: PropTypes.number, + updateLastAction: PropTypes.func, +}; + +export default ReassignPackageModal; diff --git a/client/app/queue/correspondence/component/RemovePackageModal.jsx b/client/app/queue/correspondence/component/RemovePackageModal.jsx new file mode 100644 index 00000000000..bac5f637beb --- /dev/null +++ b/client/app/queue/correspondence/component/RemovePackageModal.jsx @@ -0,0 +1,151 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ApiUtil from '../../../util/ApiUtil'; +import { sprintf } from 'sprintf-js'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { updateLastAction } from '../correspondenceReducer/reviewPackageActions'; +import TextareaField from '../../../components/TextareaField'; +import RadioField from '../../../components/RadioField'; +import COPY from '../../../../COPY'; +import Modal from '../../../components/Modal'; +import Button from '../../../components/Button'; +import { Redirect } from 'react-router-dom'; + +class RemovePackageModal extends React.Component { + + constructor(props) { + super(props); + + this.state = { + reasonForRemove: null, + disabledSaveButton: true, + reasonReject: '', + updateCancelSuccess: false + }; + } + + handleSelect(reasonForRemove) { + if (reasonForRemove === 'Approve request') { + this.setState({ reasonForRemove, + disabledSaveButton: false + }); + } else { + this.setState({ reasonForRemove, + disabledSaveButton: true }); + } + } + + reasonChange = (value) => { + if (value.trim().length > 0) { + this.setState({ + reasonReject: value, + disabledSaveButton: false + }); + } else { + this.setState({ + reasonReject: '', + disabledSaveButton: true + }); + } + } + + render() { + const { onCancel } = this.props; + const submit = () => { + let selectedRequestChoice; + + if (this.state.reasonForRemove === 'Approve request') { + selectedRequestChoice = 'approve'; + } else { + selectedRequestChoice = 'reject'; + } + + try { + const data = { + action_type: 'remove', + decision: selectedRequestChoice, + decision_reason: this.state.reasonReject, + }; + + ApiUtil.patch(`/queue/correspondence/tasks/${this.props.blockingTaskId}/update`, { data }). + then(() => { + window.location.href = '/queue/correspondence/team'; + }); + + } catch (error) { + console.error(error); + } + }; + + const removeReasonOptions = [ + { displayText: 'Approve request', + value: 'Approve request' }, + { displayText: 'Reject request', + value: 'Reject request' } + ]; + + if (this.state.updateCancelSuccess) { + return ; + } + + return ( + Confirm} + cancelButton={} + > +

+ {sprintf(COPY.CORRESPONDENCE_TITLE_REMOVE_PACKAGE)}
+ {this.props.taskInstructions[0]} +

+ + this.handleSelect(val)} + /> + + {this.state.reasonForRemove === 'Reject request' && + + } + +
+ ); + + } +} + +const mapStateToProps = (state) => { + return { vetInfo: state.reviewPackage.lastAction, + taskInstructions: state.reviewPackage.taskInstructions }; +}; + +RemovePackageModal.propTypes = { + blockingTaskId: PropTypes.number, + modalState: PropTypes.bool, + onCancel: PropTypes.func, + setModalState: PropTypes.func, + correspondence_id: PropTypes.number, + vetInfo: PropTypes.object, + taskInstructions: PropTypes.array, + updateLastAction: PropTypes.func, +}; + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + updateLastAction +}, dispatch); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(RemovePackageModal); diff --git a/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsActions.js b/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsActions.js new file mode 100644 index 00000000000..f3db3362501 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsActions.js @@ -0,0 +1,140 @@ +import { ACTIONS } from './correspondenceDetailsConstants'; +import ApiUtil from '../../../util/ApiUtil'; +import { sprintf } from 'sprintf-js'; +// eslint-disable-next-line import/extensions +import CORRESPONDENCE_DETAILS_BANNERS from '../../../../constants/CORRESPONDENCE_DETAILS_BANNERS.json'; + +export const setTaskNotRelatedToAppealBanner = (bannerDetails) => (dispatch) => { + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER, + payload: { + bannerAlert: { + title: bannerDetails.title, + message: bannerDetails.message, + type: bannerDetails.type + } + } + }); +}; + +export const cancelTaskNotRelatedToAppeal = (taskID, correspondence, payload) => (dispatch) => { + + return ApiUtil.patch(`/queue/correspondence/tasks/${taskID}/cancel`, payload). + then(() => { + + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER, + payload: { + bannerAlert: CORRESPONDENCE_DETAILS_BANNERS.successBanner + } + }); + + dispatch({ + type: ACTIONS.CORRESPONDENCE_INFO, + payload: { + correspondence + } + }); + + }). + catch((error) => { + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER, + payload: { + bannerAlert: CORRESPONDENCE_DETAILS_BANNERS.failBanner + } + }); + console.error(error); + }); +}; +export const changeTaskTypeNotRelatedToAppeal = (taskID, payload, taskNames, correspondence) => (dispatch) => { + + return ApiUtil.patch(`/queue/correspondence/tasks/${taskID}/change_task_type`, payload). + then(() => { + + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER, + payload: { + bannerAlert: { + title: 'Success', + // eslint-disable-next-line max-len + message: `You have changed the task type from ${taskNames.oldType} to ${taskNames.newType}. These changes are now reflected in the tasks section below.`, + type: 'success' + } + } + }); + + dispatch({ + type: ACTIONS.CORRESPONDENCE_INFO, + payload: { + correspondence + } + }); + }). + catch((error) => { + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER, + payload: { + bannerAlert: { + title: 'Warning', + message: error, + type: 'warning' + } + } + }); + }); +}; + +export const completeTaskNotRelatedToAppeal = (payload, frontendParams, correspondence) => (dispatch) => { + + return ApiUtil.patch(`/queue/correspondence/tasks/${frontendParams.taskId}/complete`, payload). + then(() => { + + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER, + payload: { + bannerAlert: { + title: CORRESPONDENCE_DETAILS_BANNERS.completeBanner.title, + message: sprintf(CORRESPONDENCE_DETAILS_BANNERS.completeBanner.message, + frontendParams.taskName, + frontendParams.teamName), + type: CORRESPONDENCE_DETAILS_BANNERS.completeBanner.type + } + } + }); + + dispatch({ + type: ACTIONS.CORRESPONDENCE_INFO, + payload: { + correspondence + } + }); + + }). + catch((error) => { + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER, + payload: { + bannerAlert: CORRESPONDENCE_DETAILS_BANNERS.completeFailBanner + } + }); + console.error(error); + }); +}; + +export const correspondenceInfo = (correspondence) => (dispatch) => { + dispatch({ + type: ACTIONS.CORRESPONDENCE_INFO, + payload: { + correspondence + } }); +}; + +export const setTasksUnrelatedToAppealEmpty = (tasksUnrelatedToAppealEmpty) => (dispatch) => { + dispatch({ + type: ACTIONS.TASKS_UNRELATED_TO_APPEAL_EMPTY, + payload: { + tasksUnrelatedToAppealEmpty + } + }); +}; diff --git a/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsConstants.js b/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsConstants.js new file mode 100644 index 00000000000..bb90fca2634 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsConstants.js @@ -0,0 +1,8 @@ +export const ACTIONS = { + SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER: 'SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER', + CORRESPONDENCE_INFO: 'CORRESPONDENCE_INFO', + SHOW_ACTIONS_DROP_DOWN: 'SHOW_ACTIONS_DROP_DOWN', + CANCEL_TASK_NOT_RELATED_TO_APPEAL: 'CANCEL_TASK_NOT_RELATED_TO_APPEAL', + CORRESPONDENCE_INFO: 'CORRESPONDENCE_INFO', + TASKS_UNRELATED_TO_APPEAL_EMPTY: 'TASKS_UNRELATED_TO_APPEAL_EMPTY' +}; diff --git a/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsReducer.js b/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsReducer.js new file mode 100644 index 00000000000..5899af3fa3f --- /dev/null +++ b/client/app/queue/correspondence/correspondenceDetailsReducer/correspondenceDetailsReducer.js @@ -0,0 +1,41 @@ +import { update } from '../../../util/ReducerUtil'; +import { ACTIONS } from './correspondenceDetailsConstants'; + +export const initialState = { + + bannerAlert: {}, + correspondenceInfo: { + tasksUnrelatedToAppeal: {} + }, + tasksUnrelatedToAppealEmpty: false, + +}; + +export const correspondenceDetailsReducer = (state = initialState, action = {}) => { + switch (action.type) { + + case ACTIONS.SET_CORRESPONDENCE_TASK_NOT_RELATED_TO_APPEAL_BANNER: + return update(state, { + bannerAlert: { + $set: action.payload.bannerAlert + } + }); + case ACTIONS.CORRESPONDENCE_INFO: + return update(state, { + correspondenceInfo: { + $set: action.payload.correspondence + } + }); + case ACTIONS.TASKS_UNRELATED_TO_APPEAL_EMPTY: + return update(state, { + tasksUnrelatedToAppealEmpty: { + $set: action.payload.tasksUnrelatedToAppealEmpty + } + }); + + default: + return state; + } +}; + +export default correspondenceDetailsReducer; diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js new file mode 100644 index 00000000000..8390d63ffe9 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js @@ -0,0 +1,224 @@ +import { ACTIONS } from './correspondenceConstants'; +import ApiUtil from '../../../util/ApiUtil'; + +export const loadSavedIntake = (savedStore) => + (dispatch) => { + dispatch({ + type: ACTIONS.LOAD_SAVED_INTAKE, + payload: { + savedStore + } + }); + }; + +export const saveCurrentIntake = (currentIntake, data, onSave) => (dispatch) => { + ApiUtil.post(`/queue/correspondence/${data.correspondence_uuid}/current_step`, { data }). + then((response) => { + if (!response.ok) { + console.error(response); + } + + dispatch({ + type: ACTIONS.SAVE_CURRENT_INTAKE, + payload: { + currentIntake + } + }); + if (onSave) { + onSave(); + } + }). + catch((err) => { + console.error(new Error(`Problem with GET ${currentIntake} ${err}`)); + }); +}; + +export const loadVetCorrespondence = (vetCorrespondences) => + (dispatch) => { + dispatch({ + type: ACTIONS.LOAD_VET_CORRESPONDENCE, + payload: { + vetCorrespondences + } + }); + }; + +export const loadCorrespondence = (correspondence) => + (dispatch) => { + dispatch({ + type: ACTIONS.LOAD_CORRESPONDENCE, + payload: { + correspondence + } + }); + }; + +export const loadCorrespondenceConfig = (configUrl) => + (dispatch) => { + ApiUtil.get(configUrl).then( + (response) => { + const returnedObject = response.body; + const correspondenceConfig = returnedObject.correspondence_config; + + dispatch( + { + type: ACTIONS.LOAD_CORRESPONDENCE_CONFIG, + payload: { + correspondenceConfig + } + }); + + }). + catch( + (err) => { + console.error(new Error(`Problem with GET ${configUrl} ${err}`)); + }); + }; + +export const updateRadioValue = (value) => + (dispatch) => { + dispatch({ + type: ACTIONS.UPDATE_RADIO_VALUE, + payload: value + }); + }; + +export const saveCheckboxState = (correspondence, isChecked) => + (dispatch) => { + dispatch({ + type: ACTIONS.SAVE_CHECKBOX_STATE, + payload: { + correspondence, isChecked + } + }); + }; + +export const clearCheckboxState = () => + (dispatch) => { + dispatch({ + type: ACTIONS.CLEAR_CHECKBOX_STATE, + }); + }; + +export const setSelectedTasks = (values) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_SELECTED_TASKS, + payload: { values } + }); + }; + +export const setTaskRelatedAppealIds = (appealIds) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_TASK_RELATED_APPEAL_IDS, + payload: { + appealIds + } + }); + }; + +export const setUnrelatedTasks = (tasks) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_UNRELATED_TASKS, + payload: { + tasks + } + }); + }; + +export const setFetchedAppeals = (appeals) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_FETCHED_APPEALS, + payload: { + appeals + } + }); + }; + +export const saveMailTaskState = (name) => + (dispatch) => { + dispatch({ + type: ACTIONS.SAVE_MAIL_TASK_STATE, + payload: { + name + } + }); + }; + +export const setNewAppealRelatedTasks = (newAppealRelatedTasks) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_NEW_APPEAL_RELATED_TASKS, + payload: { + newAppealRelatedTasks + } + }); + }; + +export const setWaivedEvidenceTasks = (task) => (dispatch) => { + dispatch({ + type: ACTIONS.SET_WAIVED_EVIDENCE_TASKS, + payload: { + task + } + }); +}; + +export const setResponseLetters = (responseLetters) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_RESPONSE_LETTERS, + payload: { + responseLetters + } + }); + }; + +export const removeResponseLetters = (index) => + (dispatch) => { + dispatch({ + type: ACTIONS.REMOVE_RESPONSE_LETTERS, + payload: { + index + } + }); + }; + +export const setShowReassignPackageModal = (isVisible) => (dispatch) => { + dispatch({ + type: ACTIONS.SET_SHOW_REASSIGN_PACKAGE_MODAL, + payload: { + isVisible + } + }); +}; + +export const setShowRemovePackageModal = (isVisible) => (dispatch) => { + dispatch({ + type: ACTIONS.SET_SHOW_REMOVE_PACKAGE_MODAL, + payload: { + isVisible + } + }); +}; + +export const setSelectedVeteranDetails = (selectedVeteranDetails) => (dispatch) => { + dispatch({ + type: ACTIONS.SET_SELECTED_VETERAN_DETAILS, + payload: { + selectedVeteranDetails + } + }); +}; + +export const setErrorBanner = (isVisible) => (dispatch) => { + dispatch({ + type: ACTIONS.SET_SHOW_CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER, + payload: { + isVisible + } + }); +}; diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js new file mode 100644 index 00000000000..2ad2584db8f --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js @@ -0,0 +1,25 @@ +export const ACTIONS = { + LOAD_SAVED_INTAKE: 'LOAD_SAVED_INTAKE', + LOAD_VET_CORRESPONDENCE: 'LOAD_VET_CORRESPONDENCE', + LOAD_CORRESPONDENCE: 'LOAD_CORRESPONDENCE', + LOAD_CORRESPONDENCE_CONFIG: 'LOAD_CORRESPONDENCE_CONFIG', + LOAD_INBOUND_OPS_TEAM_USERS: 'LOAD_INBOUND_OPS_TEAM_USERS', + UPDATE_RADIO_VALUE: 'UPDATE_RADIO_VALUE', + SAVE_CHECKBOX_STATE: 'SAVE_CHECKBOX_STATE', + CLEAR_CHECKBOX_STATE: 'CLEAR_CHECKBOX_STATE', + SAVE_MAIL_TASK_STATE: 'SAVE_MAIL_TASK_STATE', + SAVE_CURRENT_INTAKE: 'SAVE_CURRENT_INTAKE', + SET_TASK_RELATED_APPEAL_IDS: 'SET_TASK_RELATED_APPEAL_IDS', + SET_UNRELATED_TASKS: 'SET_UNRELATED_TASKS', + SET_FETCHED_APPEALS: 'SET_FETCHED_APPEALS', + SET_NEW_APPEAL_RELATED_TASKS: 'SET_NEW_APPEAL_RELATED_TASKS', + SET_WAIVED_EVIDENCE_TASKS: 'SET_WAIVED_EVIDENCE_TASKS', + SET_RESPONSE_LETTERS: 'SET_RESPONSE_LETTERS', + REMOVE_RESPONSE_LETTERS: 'REMOVE_RESPONSE_LETTERS', + LOAD_CORRESPONDENCE_INFORMATION: 'LOAD_CORRESPONDENCE_INFORMATION', + SET_SHOW_REASSIGN_PACKAGE_MODAL: 'SET_SHOW_REASSIGN_PACKAGE_MODAL', + SET_SHOW_REMOVE_PACKAGE_MODAL: 'SET_SHOW_REMOVE_PACKAGE_MODAL', + SET_SELECTED_TASKS: 'SET_SELECTED_TASKS', + SET_SELECTED_VETERAN_DETAILS: 'SET_SELECTED_VETERAN_DETAILS', + SET_SHOW_CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER: 'SET_SHOW_CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER' +}; diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js new file mode 100644 index 00000000000..9f4e7824cac --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js @@ -0,0 +1,188 @@ +import { update } from '../../../util/ReducerUtil'; +import { ACTIONS } from './correspondenceConstants'; + +export const initialState = { + taskRelatedAppealIds: [], + newAppealRelatedTasks: [], + fetchedAppeals: [], + radioValue: '0', + relatedCorrespondences: [], + selectedTasks: [], + mailTasks: [], + unrelatedTasks: [], + waivedEvidenceTasks: [], + responseLetters: {}, + correspondenceInformation: {}, + selectedVeteranDetails: {}, + showReassignPackageModal: false, + showRemovePackageModal: false, + showErrorBanner: false +}; + +export const intakeCorrespondenceReducer = (state = initialState, action = {}) => { + switch (action.type) { + case ACTIONS.LOAD_SAVED_INTAKE: + return action.payload.savedStore; + + case ACTIONS.SAVE_CURRENT_INTAKE: + return action.payload.currentIntake; + + case ACTIONS.LOAD_VET_CORRESPONDENCE: + return update(state, { + vetCorrespondences: { + $set: action.payload.vetCorrespondences + } + }); + + case ACTIONS.LOAD_CORRESPONDENCE: + return update(state, { + correspondence: { + $set: action.payload.correspondence + } + }); + + case ACTIONS.LOAD_CORRESPONDENCE_CONFIG: + return update(state, { + correspondenceConfig: { + $set: action.payload.correspondenceConfig + } + }); + + case ACTIONS.LOAD_INBOUND_OPS_TEAM_USERS: + return update(state, { + inboundOpsTeamUsers: { + $set: action.payload.inboundOpsTeamUsers + } + }); + + case ACTIONS.UPDATE_RADIO_VALUE: + return update(state, { + radioValue: { + $set: action.payload.radioValue + } + }); + + case ACTIONS.SAVE_CHECKBOX_STATE: + if (action.payload.isChecked) { + return update(state, { + relatedCorrespondences: { + $push: [action.payload.correspondence] + } + }); + } + + return update(state, { + relatedCorrespondences: { + $set: state.relatedCorrespondences.filter((corr) => corr.uuid !== action.payload.correspondence.uuid) + } + }); + + case ACTIONS.CLEAR_CHECKBOX_STATE: + return update(state, { + relatedCorrespondences: { + $set: [] + } + }); + + // fix this to use the actual value for set + case ACTIONS.SET_SELECTED_TASKS: + return update(state, { + selectedTasks: { + $set: [...action.payload.values] + } + }); + + case ACTIONS.SET_UNRELATED_TASKS: + return update(state, { + unrelatedTasks: { + $set: [...action.payload.tasks] + } + }); + + case ACTIONS.SET_FETCHED_APPEALS: + return update(state, { + fetchedAppeals: { + $set: [...action.payload.appeals] + } + }); + + case ACTIONS.SAVE_MAIL_TASK_STATE: + return update(state, { + mailTasks: { + $set: [...action.payload.name] + } + }); + + case ACTIONS.SET_TASK_RELATED_APPEAL_IDS: + return update(state, { + taskRelatedAppealIds: { + $set: [...action.payload.appealIds] + } + }); + + case ACTIONS.SET_NEW_APPEAL_RELATED_TASKS: + return update(state, { + newAppealRelatedTasks: { + $set: [...action.payload.newAppealRelatedTasks] + } + }); + + case ACTIONS.SET_WAIVED_EVIDENCE_TASKS: + return update(state, { + waivedEvidenceTasks: { + $set: [...action.payload.task] + } + }); + + case ACTIONS.SET_RESPONSE_LETTERS: + return update(state, { + responseLetters: { + $merge: action.payload.responseLetters + } + }); + + case ACTIONS.REMOVE_RESPONSE_LETTERS: + const newResponseLetters = state.responseLetters; + + delete newResponseLetters[action.payload.index]; + + return update(state, { + responseLetters: { + $set: newResponseLetters + } + }); + + case ACTIONS.SET_SHOW_REASSIGN_PACKAGE_MODAL: + return update(state, { + showReassignPackageModal: { + $set: action.payload.isVisible + } + }); + + case ACTIONS.SET_SHOW_REMOVE_PACKAGE_MODAL: + return update(state, { + showRemovePackageModal: { + $set: action.payload.isVisible + } + }); + + case ACTIONS.SET_SELECTED_VETERAN_DETAILS: + return update(state, { + selectedVeteranDetails: { + $set: action.payload.selectedVeteranDetails + } + }); + + case ACTIONS.SET_SHOW_CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER: + return update(state, { + showErrorBanner: { + $set: action.payload.isVisible + } + }); + + default: + return state; + } +}; + +export default intakeCorrespondenceReducer; diff --git a/client/app/queue/correspondence/correspondenceReducer/reviewPackageActions.js b/client/app/queue/correspondence/correspondenceReducer/reviewPackageActions.js new file mode 100644 index 00000000000..8385a66c274 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/reviewPackageActions.js @@ -0,0 +1,105 @@ +import { ACTIONS } from './reviewPackageConstants'; + +export const setCorrespondence = (correspondence) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE, + payload: { + correspondence + } + }); + }; + +export const setCreateRecordIsReadOnly = (createRecordIsReadOnly) => + (dispatch) => { + dispatch({ + type: ACTIONS.CREATE_RECORD_IS_READ_ONLY, + payload: { + createRecordIsReadOnly + } + }); + }; + +export const updateCmpInformation = (packageDocumentType, date) => + (dispatch) => { + dispatch({ + type: ACTIONS.UPDATE_CMP_INFORMATION, + payload: { + packageDocumentType, + date + } + }); + }; + +export const updateDocumentTypeName = (newName, index) => + (dispatch) => { + dispatch({ + type: ACTIONS.UPDATE_DOCUMENT_TYPE_NAME, + payload: { + newName, + index + } + }); + }; + +export const updateLastAction = (currentAction) => + (dispatch) => { + dispatch({ + type: ACTIONS.REMOVE_PACKAGE_ACTION, + payload: { + currentAction + } + }); + }; + +export const setTaskInstructions = (taskInstructions) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_TASK_INSTRUCTIONS, + payload: { + taskInstructions + } + }); + }; + +export const setBatchAutoAssignmentAttemptId = (batchId) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_BATCH_AUTO_ASSIGN_ATTEMPT_ID, + payload: { + batchId + } + }); + }; + +export const updateLastReassignAction = (currentAction) => + (dispatch) => { + dispatch({ + type: ACTIONS.REASSIGN_PACKAGE_ACTION, + payload: { + currentAction + } + }); + }; + +export const setAutoAssignmentAlertBanner = (bannerDetails) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_AUTO_ASSIGN_BANNER, + payload: { + title: bannerDetails.title, + message: bannerDetails.message, + type: bannerDetails.type + } + }); + }; + +export const setAutoAssignButtonDisabled = (isButtonDisabled) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_AUTO_ASSIGN_BUTTON_DISABLED, + payload: { + isButtonDisabled + } + }); + }; diff --git a/client/app/queue/correspondence/correspondenceReducer/reviewPackageConstants.js b/client/app/queue/correspondence/correspondenceReducer/reviewPackageConstants.js new file mode 100644 index 00000000000..2f88efd2782 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/reviewPackageConstants.js @@ -0,0 +1,14 @@ +export const ACTIONS = { + SET_CORRESPONDENCE: 'SET_CORRESPONDENCE', + SET_PACKAGE_DOCUMENT_TYPE: 'SET_PACKAGE_DOCUMENT_TYPE', + UPDATE_CMP_INFORMATION: 'UPDATE_CMP_INFORMATION', + UPDATE_DOCUMENT_TYPE_NAME: 'UPDATE_DOCUMENT_TYPE_NAME', + REMOVE_PACKAGE_ACTION: 'REMOVE_PACKAGE_ACTION', + SET_TASK_INSTRUCTIONS: 'SET_TASK_INSTRUCTIONS', + SET_REASON_REMOVE_PACKAGE: 'SET_REASON_REMOVE_PACKAGE', + SET_AUTO_ASSIGN_BANNER: 'SET_AUTO_ASSIGN_BANNER', + SET_BATCH_AUTO_ASSIGN_ATTEMPT_ID: 'SET_BATCH_AUTO_ASSIGN_ATTEMPT_ID', + SET_AUTO_ASSIGN_BUTTON_DISABLED: 'AUTO_ASSIGN_BUTTON_DISABLED', + REASSIGN_PACKAGE_ACTION: 'REASSIGN_PACKAGE_ACTION', + CREATE_RECORD_IS_READ_ONLY: 'CREATE_RECORD_IS_READ_ONLY', +}; diff --git a/client/app/queue/correspondence/correspondenceReducer/reviewPackageReducer.js b/client/app/queue/correspondence/correspondenceReducer/reviewPackageReducer.js new file mode 100644 index 00000000000..846f9773599 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/reviewPackageReducer.js @@ -0,0 +1,137 @@ +import { update } from '../../../util/ReducerUtil'; +import { ACTIONS } from './reviewPackageConstants'; + +export const initialState = { + correspondence: {}, + correspondenceDocuments: [], + packageDocumentType: {}, + lastAction: {}, + taskInstructions: [], + reasonForRemovePackage: {}, + createRecordIsReadOnly: 'Select...', + autoAssign: { + isButtonDisabled: false, + batchId: null, + bannerAlert: {}, + }, + reasonForReassignPackage: {} +}; + +export const reviewPackageReducer = (state = initialState, action = {}) => { + switch (action.type) { + case ACTIONS.SET_CORRESPONDENCE: + return update(state, { + correspondence: { + $set: action.payload.correspondence + } + }); + + case ACTIONS.SET_PACKAGE_DOCUMENT_TYPE: + return update(state, { + packageDocumentType: { + $set: action.payload.packageDocumentType + } + }); + + case ACTIONS.CREATE_RECORD_IS_READ_ONLY: + + return update(state, { + + createRecordIsReadOnly: { + $set: action.payload.createRecordIsReadOnly + } + }); + + case ACTIONS.UPDATE_CMP_INFORMATION: + return update(state, { + correspondence: { + va_date_of_receipt: { + $set: action.payload.date + } + }, + packageDocumentType: { + id: { + $set: action.payload.packageDocumentType.value + }, + name: { + $set: action.payload.packageDocumentType.label + } + } + }); + + case ACTIONS.UPDATE_DOCUMENT_TYPE_NAME: + return update(state, { + correspondence: { + correspondenceDocuments: { + [action.payload.index]: { + vbms_document_type_id: { + $set: action.payload.newName.value + }, + document_title: { + $set: action.payload.newName.label + } + } + } + } + }); + + case ACTIONS.REMOVE_PACKAGE_ACTION: + return update(state, { + lastAction: { + action_type: { + $set: action.payload.currentAction + } + } + }); + + case ACTIONS.SET_TASK_INSTRUCTIONS: + return update(state, { + taskInstructions: { + $set: action.payload.taskInstructions + } + }); + + case ACTIONS.SET_BATCH_AUTO_ASSIGN_ATTEMPT_ID: + return update(state, { + autoAssign: { + batchId: { + $set: action.payload.batchId + } + } + }); + + case ACTIONS.REASSIGN_PACKAGE_ACTION: + return update(state, { + lastAction: { + action_type: { + $set: action.payload.currentAction + } + } + }); + + case ACTIONS.SET_AUTO_ASSIGN_BANNER: + return update(state, { + autoAssign: { + bannerAlert: { + title: { $set: action.payload.title }, + message: { $set: action.payload.message }, + type: { $set: action.payload.type } + } + } + }); + + case ACTIONS.SET_AUTO_ASSIGN_BUTTON_DISABLED: + return update(state, { + autoAssign: { + isButtonDisabled: { + $set: action.payload.isButtonDisabled + } + } + }); + + default: + return state; + } +}; + +export default reviewPackageReducer; diff --git a/client/app/queue/correspondence/details/CorrespondenceDetails.jsx b/client/app/queue/correspondence/details/CorrespondenceDetails.jsx new file mode 100644 index 00000000000..a800f50c8b6 --- /dev/null +++ b/client/app/queue/correspondence/details/CorrespondenceDetails.jsx @@ -0,0 +1,512 @@ +/* eslint-disable max-lines */ +import React, { useState, useEffect } from 'react'; +import { useDispatch, connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; +import PropTypes from 'prop-types'; +import TabWindow from '../../../components/TabWindow'; +import CopyTextButton from '../../../components/CopyTextButton'; +import { loadCorrespondence } from '../correspondenceReducer/correspondenceActions'; +import CorrespondenceCaseTimeline from '../CorrespondenceCaseTimeline'; +import { correspondenceInfo } from './../correspondenceDetailsReducer/correspondenceDetailsActions'; +import CorrespondenceResponseLetters from './CorrespondenceResponseLetters'; +import COPY from '../../../../COPY'; +import CaseListTable from 'app/queue/CaseListTable'; +// import TaskSnapshot from '../../TaskSnapshot'; +import { prepareAppealForSearchStore } from 'app/queue/utils'; +import CorrespondenceTasksAdded from '../CorrespondenceTasksAdded'; +import moment from 'moment'; +import Pagination from 'app/components/Pagination/Pagination'; +import Table from 'app/components/Table'; +import { ExternalLinkIcon } from 'app/components/icons/ExternalLinkIcon'; +import { COLORS } from 'app/constants/AppConstants'; +import Checkbox from 'app/components/Checkbox'; +import CorrespondencePaginationWrapper from 'app/queue/correspondence/CorrespondencePaginationWrapper'; + +const CorrespondenceDetails = (props) => { + const dispatch = useDispatch(); + const correspondence = props.correspondence; + const mailTasks = props.correspondence.mailTasks; + + const allCorrespondences = props.correspondence.all_correspondences; + const [viewAllCorrespondence, setViewAllCorrespondence] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const totalPages = Math.ceil(allCorrespondences.length / 15); + const startIndex = (currentPage * 15) - 15; + const endIndex = (currentPage * 15); + const priorMail = correspondence.prior_mail; + const relatedCorrespondenceIds = props.correspondence.relatedCorrespondenceIds; + + priorMail.sort((first, second) => { + const firstInRelated = relatedCorrespondenceIds.includes(first.id); + const secondInRelated = relatedCorrespondenceIds.includes(second.id); + + if (firstInRelated && secondInRelated) { + return new Date(second.vaDateOfReceipt) - new Date(first.vaDateOfReceipt); + } else if (firstInRelated) { + return -1; + } else if (secondInRelated) { + return -1; + } + + return 1; + }); + + const updatePageHandler = (idx) => { + const newCurrentPage = idx + 1; + + setCurrentPage(newCurrentPage); + }; + + const getKeyForRow = (rowNumber, object) => object.id; + + const getColumns = () => { + const columns = []; + + columns.push( + { + header: 'Package Document Type', + valueFunction: (correspondenceObj) => ( + +

+ + {correspondenceObj.nod ? 'NOD' : 'Non-NOD'} + + + + +

+
+ ) + }, + { + header: 'VA DOR', + valueFunction: (correspondenceObj) => { + const date = new Date(correspondenceObj.vaDateOfReceipt); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const formattedDate = `${month}/${day}/${year}`; + + return formattedDate; + } + }, + { + header: 'Notes', + valueFunction: (correspondenceObj) => correspondenceObj.notes + }, + { + header: 'Status', + valueFunction: (correspondenceObj) => correspondenceObj.status + } + ); + + return columns; + }; + + const handleViewAllCorrespondence = () => { + setViewAllCorrespondence(!viewAllCorrespondence); + }; + + const viewDisplayText = () => { + return viewAllCorrespondence ? 'Hide all correspondence' : 'View all correspondence'; + }; + + const allCorrespondencesList = () => { + return viewAllCorrespondence && ( +
+

{COPY.ALL_CORRESPONDENCES}

+ + + } + enableTopPagination = {false} + /> + +
+ ); + }; + + const appealsResult = props.correspondence.appeals_information; + const appeals = []; + let filteredAppeals = []; + let unfilteredAppeals = []; + + appealsResult.appeals.map((appeal) => { + if (correspondence.correspondenceAppealIds?.includes(appeal.id)) { + return filteredAppeals.push(appeal); + } + + return unfilteredAppeals.push(appeal); + }); + + filteredAppeals = filteredAppeals.sort((leftAppeal, rightAppeal) => leftAppeal.id - rightAppeal.id); + unfilteredAppeals = unfilteredAppeals.sort((leftAppeal, rightAppeal) => leftAppeal.id - rightAppeal.id); + const sortedAppeals = filteredAppeals.concat(unfilteredAppeals); + + const searchStoreAppeal = prepareAppealForSearchStore(sortedAppeals); + const appeall = searchStoreAppeal.appeals; + const appealldetail = searchStoreAppeal.appealDetails; + const hashKeys = Object.keys(appeall); + + hashKeys.map((key) => { + const combinedHash = { ...appeall[key], ...appealldetail[key] }; + + appeals.push(combinedHash); + + return appeals; + }); + + useEffect(() => { + dispatch(loadCorrespondence(correspondence)); + dispatch(correspondenceInfo(correspondence)); + }, []); + + const isTasksUnrelatedToAppealEmpty = () => { + if (props.tasksUnrelatedToAppealEmpty === true) { + return 'Completed'; + } + + return props.correspondence.status; + }; + + const correspondenceTasks = () => { + return ( + +
+

Completed Mail Tasks

+ +
    2 ? 'grid-list' : ''}`} + aria-label={COPY.CORRESPONDENCE_DETAILS.COMPLETED_MAIL_TASKS} role="list" aria-live="polite" + > + { + mailTasks.length > 0 ? + mailTasks.map((item, index) => ( +
  • {item}
  • + )) : +
  • + {COPY.CORRESPONDENCE_DETAILS.NO_COMPLETED_MAIL_TASKS} +
  • + } +
+
+
+
+

Existing Appeals

+ + + + View veteran documents +
+ +
+
+
+ + +
+ {(props.correspondence.correspondenceAppeals.map((taskAdded) => + + taskAdded.correspondencesAppealsTasks?.length > 0 && + ) + )} +
+
+ ); + }; + const correspondenceAndAppealTaskComponents = <> + {correspondenceTasks()} + +
Tasks not related to an appeal
+
+ +
+ ; + + const correspondencePackageDetails = () => { + return ( + <> +
+

General Information

+ + + + + + + + + + + + + + + + + + + + + +
Veteran DetailsCorrespondence TypePackage Document TypeVA DOR
+ {props.correspondence.veteranFullName} ({props.correspondence.veteranFileNumber}) + {props.correspondence.correspondenceType}{props.correspondence.nod ? 'NOD' : 'Non-NOD'} + {moment(props.correspondence.vaDateOfReceipt).format('MM/DD/YYYY')} +
+ Notes
+ {props.correspondence.notes}
+
+ + ); + }; + + const correspondenceResponseLetters = () => { + return ( + <> +
+ +
+ + ); + }; + + const getDocumentColumns = (correspondenceRow) => { + return [ + { + cellClass: 'checkbox-column', + valueFunction: () => ( +
+ el === correspondenceRow.id)} + disabled + /> +
+ ) + }, + { + cellClass: 'va-dor-column', + ariaLabel: 'va-dor-header-label', + header: ( +
+ + VA DOR + +
+ ), + valueFunction: () => { + const date = new Date(correspondenceRow.vaDateOfReceipt); + + return ( + +

{date.toLocaleDateString('en-US')}

+
+ ); + } + }, + { + cellClass: 'package-document-type-column', + ariaLabel: 'package-document-type-header-label', + header: ( +
+ + Package Document Type + +
+ ), + valueFunction: () => ( + +

+ + {correspondenceRow?.nod ? 'NOD' : 'Non-NOD'} + + + + +

+
+ ) + }, + { + cellClass: 'correspondence-type-column', + ariaLabel: 'correspondence-type-header-label', + header: ( +
+ + Correspondence Type + +
+ ), + valueFunction: () => ( + +

{correspondenceRow.correspondenceType}

+
+ ) + }, + { + cellClass: 'notes-column', + ariaLabel: 'notes-header-label', + header: ( +
+ + Notes + +
+ ), + valueFunction: () => ( + +

{correspondenceRow.notes}

+
+ ) + } + ]; + }; + + const associatedPriorMail = () => { + return ( + <> +
+ +

Please select prior mail to link to this correspondence

+
+ +
+
+
+ + ); + }; + + const tabList = [ + { + disable: false, + label: 'Correspondence and Appeal Tasks', + page: correspondenceAndAppealTaskComponents + }, + { + disable: false, + label: 'Package Details', + page: correspondencePackageDetails() + }, + { + disable: false, + label: 'Response Letters', + page: correspondenceResponseLetters() + }, + { + disable: false, + label: 'Associated Prior Mail', + page: associatedPriorMail() + } + ]; + + return ( + <> + +
+

{props.correspondence.veteranFullName}

+
+

Veteran ID:

+ +
+

{viewDisplayText()}

+
+

Record status: {isTasksUnrelatedToAppealEmpty()}

+
+
+ { allCorrespondencesList() } +
+ + +
+ + ); +}; + +CorrespondenceDetails.propTypes = { + loadCorrespondence: PropTypes.func, + correspondence: PropTypes.object, + organizations: PropTypes.array, + userCssId: PropTypes.string, + enableTopPagination: PropTypes.bool, + correspondence_appeal_ids: PropTypes.bool, + tasksUnrelatedToAppealEmpty: PropTypes.bool, + correspondenceResponseLetters: PropTypes.array +}; + +const mapStateToProps = (state) => ({ + correspondenceInfo: state.correspondenceDetails.correspondenceInfo, + tasksUnrelatedToAppealEmpty: state.correspondenceDetails.tasksUnrelatedToAppealEmpty, +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + correspondenceInfo + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CorrespondenceDetails); + +/* eslint-enable max-lines */ diff --git a/client/app/queue/correspondence/details/CorrespondenceResponseLetters.jsx b/client/app/queue/correspondence/details/CorrespondenceResponseLetters.jsx new file mode 100644 index 00000000000..fb2893cbae6 --- /dev/null +++ b/client/app/queue/correspondence/details/CorrespondenceResponseLetters.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; + +const CorrespondenceResponseLetters = (props) => { + const { letters } = props; + + return ( +
+

+ Response Letter +

+ {letters.map((letter, index) => ( +
+ + + + + + + + + + + + + + + + + + + + + + +   + +
+ Letter response expiration: + + {letter.days_left} + +
+ Date response letter sent + + Letter type + + Letter title + + Letter subcategory + + Letter subcategory reasons +
+
+
+ {moment(letter.date_sent).format('MM/DD/YYYY')} + + {letter.letter_type} + + {letter.title} + + {letter.subcategory} + + {letter.reason} +
+
+ ))} +
+ ); +}; + +CorrespondenceResponseLetters.propTypes = { + letters: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + correspondence_id: PropTypes.number, + letter_type: PropTypes.string, + title: PropTypes.string, + subcategory: PropTypes.string, + reason: PropTypes.string, + date_sent: PropTypes.string, + response_window: PropTypes.number, + user_id: PropTypes.number, + days_left: PropTypes.string, + expired: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool + ]), + }) + ).isRequired +}; + +export default CorrespondenceResponseLetters; diff --git a/client/app/queue/correspondence/intake/components/AddCorrespondence/AddCorrespondenceView.jsx b/client/app/queue/correspondence/intake/components/AddCorrespondence/AddCorrespondenceView.jsx new file mode 100644 index 00000000000..d4fa3e9248c --- /dev/null +++ b/client/app/queue/correspondence/intake/components/AddCorrespondence/AddCorrespondenceView.jsx @@ -0,0 +1,251 @@ +/* eslint-disable max-lines */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import Checkbox from '../../../../../components/Checkbox'; +import RadioField from '../../../../../components/RadioField'; +import { COLORS } from '../../../../../constants/AppConstants'; +import { ExternalLinkIcon } from 'app/components/icons/ExternalLinkIcon'; +import CorrespondencePaginationWrapper from '../../../CorrespondencePaginationWrapper'; +import { AddLetter } from '../AddCorrespondence/AddLetter'; +import { + updateRadioValue, + saveCheckboxState, + clearCheckboxState, + setResponseLetters +} from '../../../correspondenceReducer/correspondenceActions'; + +const RELATED_NO = '0'; +const RELATED_YES = '1'; + +class AddCorrespondenceView extends React.Component { + constructor(props) { + super(props); + this.state = { + veteranId: '', + vaDateOfReceipt: '', + packageDocumentType: '', + correspondenceType: '', + notes: '', + selectedCheckboxes: [], + ifContinueDisabled: null + }; + } + + onChange = (value) => { + this.props.updateRadioValue({ radioValue: value }); + + if (value === RELATED_YES) { + this.setState({ ifContinueDisabled: this.props.isContinueEnabled }); + } + + const valueToUpdate = this.state.ifContinueDisabled && value === RELATED_NO; + + this.props.onContinueStatusChange(valueToUpdate); + this.props.clearCheckboxState(); + } + + onChangeCheckbox = (correspondence, isChecked) => { + this.props.saveCheckboxState(correspondence, isChecked); + let selectedCheckboxes = [...this.props.checkboxes]; + + if (isChecked) { + selectedCheckboxes.push(correspondence); + } else { + selectedCheckboxes = selectedCheckboxes.filter((checkbox) => checkbox.id !== correspondence.id); + } + const isAnyCheckboxSelected = selectedCheckboxes.length > 0; + + this.props.onCheckboxChange(isAnyCheckboxSelected); + } + + getKeyForRow = (index, { id }) => { + return `${id}`; + }; + + // eslint-disable-next-line max-statements + getDocumentColumns = (correspondence) => { + return [ + { + cellClass: 'checkbox-column', + valueFunction: () => ( +
+ el.id === correspondence.id)} + onChange={(checked) => this.onChangeCheckbox(correspondence, checked)} + /> +
+ ), + }, + { + cellClass: 'va-dor-column', + ariaLabel: 'va-dor-header-label', + header: ( +
+ + VA DOR + +
+ ), + valueFunction: () => { + const date = new Date(correspondence.vaDateOfReceipt); + + return ( + +

{date.toLocaleDateString('en-US')}

+
+ ); + } + }, + { + cellClass: 'package-document-type-column', + ariaLabel: 'package-document-type-header-label', + header: ( +
+ + Package Document Type + +
+ ), + valueFunction: () => ( + +

+ + {correspondence?.nod ? 'NOD' : 'Non-NOD'} + + + + +

+
+ ) + }, + { + cellClass: 'correspondence-type-column', + ariaLabel: 'correspondence-type-header-label', + header: ( +
+ + Correspondence Type + +
+ ), + valueFunction: () => ( + +

{correspondence.correspondenceType}

+
+ ) + }, + { + cellClass: 'notes-column', + ariaLabel: 'notes-header-label', + header: ( +
+ + Notes + +
+ ), + valueFunction: () => ( + +

{correspondence.notes}

+
+ ) + }, + ]; + }; + + render() { + const priorMailAnswer = [ + { displayText: 'Yes', + value: RELATED_YES }, + { displayText: 'No', + value: RELATED_NO } + ]; + + return ( +
+

Add Related Correspondence

+

Add any related correspondence to the mail package that is in progress.

+

Response Letter

+ {/* add letter here */} + +
+

Associate with prior Mail

+

Is this correspondence related to prior mail?

+ + {this.props.radioValue === RELATED_YES && ( +
+

Please select the prior mail to link to this correspondence

+
+ + +
+
+ )} +
+ ); + } +} + +AddCorrespondenceView.propTypes = { + correspondence: PropTypes.object, + priorMail: PropTypes.arrayOf(PropTypes.object), + featureToggles: PropTypes.object, + correspondenceUuid: PropTypes.string, + updateRadioValue: PropTypes.func, + radioValue: PropTypes.string, + saveCheckboxState: PropTypes.func, + onContinueStatusChange: PropTypes.func, + onCheckboxChange: PropTypes.func.isRequired, + clearCheckboxState: PropTypes.func.isRequired, + checkboxes: PropTypes.array, + setResponseLetters: PropTypes.func, + currentLetters: PropTypes.number, + isContinueEnabled: PropTypes.func +}; + +const mapStateToProps = (state) => ({ + radioValue: state.intakeCorrespondence.radioValue, + checkboxes: state.intakeCorrespondence.relatedCorrespondences, + currentLetters: state.intakeCorrespondence.responseLetters, +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + updateRadioValue, + saveCheckboxState, + clearCheckboxState, + setResponseLetters + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AddCorrespondenceView); diff --git a/client/app/queue/correspondence/intake/components/AddCorrespondence/AddLetter.jsx b/client/app/queue/correspondence/intake/components/AddCorrespondence/AddLetter.jsx new file mode 100644 index 00000000000..fbdb242d7bb --- /dev/null +++ b/client/app/queue/correspondence/intake/components/AddCorrespondence/AddLetter.jsx @@ -0,0 +1,123 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import Button from '../../../../../components/Button'; +import NewLetter from './NewLetter'; +import { useSelector } from 'react-redux'; + +export const AddLetter = (props) => { + const onContinueStatusChange = props.onContinueStatusChange; + + const responseLetters = useSelector((state) => state.intakeCorrespondence.responseLetters); + + const [letters, setLetters] = useState(Object.keys(responseLetters)); + + const [dataLetter, setDataLetter] = useState([]); + + const addLetter = (index) => { + setLetters([...letters, index]); + }; + + const [unrelatedTasksCanContinue, setUnrelatedTasksCanContinue] = useState(true); + + const canContinue = (currentLetters) => { + const output = []; + const opts = ['65 days', 'No response window']; + + for (const [, value] of Object.entries(currentLetters)) { + if ((value !== null) && (value !== '')) { + output.push(value); + } + } + + if ((output.length === 7) && (opts.includes(output[6]))) { + return true; + } else if (output.length === 8) { + return true; + } + + return false; + }; + + const taskUpdatedCallback = (updatedTask) => { + setDataLetter((prevDataLetter) => [...prevDataLetter.filter((cdl) => cdl.id !== updatedTask.id), updatedTask]); + }; + + const removeLetter = (index) => { + const restLetters = letters.filter((letter) => letter !== index); + const dls = dataLetter.filter((dl) => dl.id !== index); + + setLetters(restLetters); + setDataLetter(dls); + }; + + useEffect(() => { + onContinueStatusChange(unrelatedTasksCanContinue); + }, [unrelatedTasksCanContinue]); + + useEffect(() => { + if (letters.length > 0) { + setUnrelatedTasksCanContinue(false); + } else { + setUnrelatedTasksCanContinue(true); + } + }, [letters]); + + useEffect(() => { + if ((dataLetter.length > 0) && letters.length === dataLetter.length) { + for (let i = 0; i < dataLetter.length; i++) { + if (canContinue(dataLetter[i])) { + setUnrelatedTasksCanContinue(true); + } else { + setUnrelatedTasksCanContinue(false); + } + } + } else if (letters.length === 0) { + setUnrelatedTasksCanContinue(true); + } else { + setUnrelatedTasksCanContinue(false); + } + + }, [dataLetter]); + + return ( + <> +
+ { letters.map((letter) => ( +
+ +
+ )) } +
+ +
+ +
+ + ); +}; + +AddLetter.propTypes = { + removeLetter: PropTypes.func, + index: PropTypes.number, + setUnrelatedTasksCanContinue: PropTypes.func, + onContinueStatusChange: PropTypes.func, +}; diff --git a/client/app/queue/correspondence/intake/components/AddCorrespondence/NewLetter.jsx b/client/app/queue/correspondence/intake/components/AddCorrespondence/NewLetter.jsx new file mode 100644 index 00000000000..b3e28882658 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/AddCorrespondence/NewLetter.jsx @@ -0,0 +1,382 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import TextField from '../../../../../components/TextField'; +import Button from '../../../../../components/Button'; +import SearchableDropdown from 'app/components/SearchableDropdown'; +import DateSelector from 'app/components/DateSelector'; +import RadioField from '../../../../../components/RadioField'; +import { ADD_CORRESPONDENCE_LETTER_SELECTIONS } from '../../../../constants'; +import moment from 'moment'; +import { connect, useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { + setResponseLetters, removeResponseLetters +} from '../../../correspondenceReducer/correspondenceActions'; + +export const NewLetter = (props) => { + const index = props.index; + const currentDate = moment(new Date()).format('YYYY-MM-DD'); + const currentLetter = props.currentLetter; + const letterHash = {}; + const setUnrelatedTasksCanContinue = props.setUnrelatedTasksCanContinue; + const displayLetter = (typeof currentLetter !== 'undefined'); + const [letterCard, setLetterCard] = useState({ + id: index, + date: displayLetter ? currentLetter.date : currentDate, + type: displayLetter ? currentLetter.type : '', + title: displayLetter ? currentLetter.title : '', + subType: displayLetter ? currentLetter.subType : '', + reason: displayLetter ? currentLetter.reason : '', + responseWindows: displayLetter ? currentLetter.responseWindows : '', + customValue: displayLetter ? currentLetter.customValue : '' + }); + const [letterTitleSelector, setLetterTitleSelector] = useState(''); + const [letterSubSelector, setLetterSubSelector] = useState(''); + const [letterSubReason, setLetterSubReason] = useState(''); + const customResponseVal = Boolean(displayLetter && currentLetter?.customValue > 0); + const [customResponseWindowState, setCustomResponseWindowState] = useState(customResponseVal); + + const [stateOptions, setStateOptions] = useState(true); + + const [responseWindows, setResponseWindows] = useState(displayLetter ? currentLetter.responseWindows : ''); + const naValue = 'N/A'; + const dispatch = useDispatch(); + + const radioOptions = [ + { displayText: '65 days', + value: '65 days', + disabled: stateOptions }, + { displayText: 'No response window', + value: 'No response window', + disabled: stateOptions }, + { displayText: 'Custom', + value: 'Custom', + disabled: stateOptions } + ]; + + const [valueOptions, setValueOptions] = useState(radioOptions); + + const handleDays = (value) => { + const currentNumber = parseInt(value.trim(), 10); + + if ((currentNumber >= 1) && (currentNumber <= 64)) { + setLetterCard({ ...letterCard, + customValue: currentNumber }); + } else { + setLetterCard({ ...letterCard, + customValue: null }); + } + }; + + const handleCustomWindowState = (currentOpt) => { + if (currentOpt === radioOptions[2].value) { + setResponseWindows(radioOptions[2].value); + setCustomResponseWindowState(true); + } else { + setResponseWindows(currentOpt); + setCustomResponseWindowState(false); + setLetterCard({ ...letterCard, + customValue: null }); + } + }; + + const letterTypesData = ADD_CORRESPONDENCE_LETTER_SELECTIONS.map((option) => ({ label: (option.letter_type), + value: option.letter_type })); + + const selectResponseWindows = (option, aux) => { + if (displayLetter) { + const responseWindowsValue = currentLetter.responseWindows; + + setLetterCard({ ...letterCard, + responseWindows: responseWindowsValue }); + setResponseWindows(responseWindowsValue); + } else if (option.response_window_option_default) { + const responseWindowsValue = option.response_window_option_default; + + setLetterCard({ ...letterCard, + responseWindows: responseWindowsValue }); + setResponseWindows(responseWindowsValue); + } else if (option.letter_titles[aux].letter_title === letterCard.title) { + setLetterCard({ ...letterCard, + responseWindows: option.letter_titles[aux].response_window_option_default }); + setResponseWindows(option.letter_titles[aux].response_window_option_default); + } + }; + + const canContinue = () => { + const output = []; + const opts = ['65 days', 'No response window']; + + for (const [, value] of Object.entries(letterCard)) { + if ((value !== null) && (value !== '')) { + output.push(value); + } + } + + if ((output.length === 7) && (opts.includes(output[6]))) { + return true; + } else if (output.length === 8) { + return true; + } + + return false; + }; + + const findSub = (option, aux) => { + const subCate = []; + const listReason = []; + + selectResponseWindows(option, aux); + + for (let aux1 = 0; aux1 < option.letter_titles[aux].letter_subcategories.length; aux1++) { + subCate.push({ label: option.letter_titles[aux].letter_subcategories[aux1].subcategory, + value: option.letter_titles[aux].letter_subcategories[aux1].subcategory }); + } + + if (subCate.length === 0) { + setLetterSubSelector([{ label: naValue, value: naValue }]); + } else { + setLetterSubSelector(subCate); + } + + for (let aux1 = 0; aux1 < option.letter_titles[aux].letter_subcategories.length; aux1++) { + if (letterCard.subType === option.letter_titles[aux].letter_subcategories[aux1].subcategory) { + option.letter_titles[aux].letter_subcategories[aux1].reasons.map((currentReason) => + listReason.push({ label: currentReason, value: currentReason })); + } + } + + if (listReason.length === 0) { + setLetterSubReason([{ label: naValue, value: naValue }]); + } else { + setLetterSubReason(listReason); + } + }; + + const findSubCategoryReason = (option) => { + for (let aux = 0; aux < option.letter_titles.length; aux++) { + if (option.letter_titles[aux].letter_title === letterCard.title) { + findSub(option, aux); + } else { + selectResponseWindows(option, aux); + } + } + }; + + const activateWindowsOption = () => { + if (responseWindows.trim().length > 0) { + for (let i = 0; i < valueOptions.length; i++) { + const option = valueOptions[i]; + + setStateOptions(false); + option.disabled = false; + } + setValueOptions(valueOptions); + } else { + for (let i = 0; i < valueOptions.length; i++) { + const option = valueOptions[i]; + + setStateOptions(true); + option.disabled = true; + } + setValueOptions(valueOptions); + } + }; + + const letterTitlesData = () => { + for (let i = 0; i < ADD_CORRESPONDENCE_LETTER_SELECTIONS.length; i++) { + const option = ADD_CORRESPONDENCE_LETTER_SELECTIONS[i]; + + if (option.letter_type === letterCard.type) { + setLetterTitleSelector(option.letter_titles.map((current) => ({ + label: current.letter_title, value: current.letter_title + }))); + + if (letterCard.type.length > 0) { + findSubCategoryReason(option); + } + } + } + }; + + useEffect(() => { + if (canContinue()) { + letterHash[index] = letterCard; + dispatch(setResponseLetters(letterHash)); + setUnrelatedTasksCanContinue(true); + props.taskUpdatedCallback(letterCard); + } else { + setUnrelatedTasksCanContinue(false); + } + }, [letterCard]); + + useEffect(() => { + activateWindowsOption(); + setLetterCard({ ...letterCard, + responseWindows }); + }, [responseWindows]); + + useEffect(() => { + if (letterCard.type.length > 0) { + letterTitlesData(); + } + }, [letterCard.type]); + + useEffect(() => { + if (letterCard.subType.length > 0) { + letterTitlesData(); + } + }, [letterCard.subType]); + + const changeLetterType = (val) => { + setLetterCard({ ...letterCard, + type: val, + title: '', + subType: '', + reason: '', + responseWindows: '' + }); + }; + + useEffect(() => { + if (letterCard.title.length > 0) { + letterTitlesData(); + if (responseWindows.length > 0) { + activateWindowsOption(); + } + } + }, [letterCard.title]); + + const changeLetterTitle = (val) => { + setLetterCard({ ...letterCard, + title: val + }); + }; + + const changeLetterSubTitle = (val) => { + setLetterCard({ ...letterCard, + subType: val + }); + }; + + const changeSubReason = (val) => { + setLetterCard({ ...letterCard, + reason: val + }); + }; + + const changeDate = (val) => { + setLetterCard({ ...letterCard, + date: val + }); + }; + + const removeLetter = () => { + dispatch(removeResponseLetters(index)); + props.removeLetter(index); + }; + + return ( +
+
+ changeDate(val)} + type="date" + /> +
+
+ changeLetterType(val.value)} + /> +
+ changeLetterTitle(val.value)} + /> +
+ changeLetterSubTitle(val.value)} + /> +
+ changeSubReason(val.value)} + /> +
+ handleCustomWindowState(val)} + /> + { customResponseWindowState && + + } +
+ +
+ ); +}; + +NewLetter.propTypes = { + removeLetter: PropTypes.func.isRequired, + index: PropTypes.number.isRequired, + setLetterType: PropTypes.func, + letterType: PropTypes.string, + letterTitle: PropTypes.string, + setLetterTitle: PropTypes.func, + setResponseLetters: PropTypes.func, + removeResponseLetters: PropTypes.func, + setUnrelatedTasksCanContinue: PropTypes.func, + currentLetter: PropTypes.func, + taskUpdatedCallback: PropTypes.func, + onContinueStatusChange: PropTypes.func +}; + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + setResponseLetters + }, dispatch) +); + +export default connect( + mapDispatchToProps +)(NewLetter); + diff --git a/client/app/queue/correspondence/intake/components/CheckboxModal.jsx b/client/app/queue/correspondence/intake/components/CheckboxModal.jsx new file mode 100644 index 00000000000..68fb129c241 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/CheckboxModal.jsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import PropTypes, { string } from 'prop-types'; +import Modal from '../../../../components/Modal'; +import Checkbox from '../../../../components/Checkbox'; + +const CheckboxModal = (props) => { + + const [toggledCheckBoxes, setToggledCheckboxes] = useState([]); + + const handleToggleCheckbox = (checkboxId) => { + const index = toggledCheckBoxes.indexOf(checkboxId); + const checkboxes = [...toggledCheckBoxes]; + + // remove it if checkboxes contains it, append it if it doesn't + if (index === -1) { + checkboxes.push(checkboxId); + } else { + checkboxes.splice(index, 1); + } + setToggledCheckboxes(checkboxes); + }; + + const handleClear = () => { + setToggledCheckboxes([]); + }; + + return ( + props.handleAccept(toggledCheckBoxes), + disabled: toggledCheckBoxes.length === 0, + }, + { + id: 'clear-checkboxes-button', + classNames: ['usa-button', 'usa-button-secondary', 'cf-margin-left-2rem'], + name: 'Clear all', + onClick: handleClear, + disabled: false, + } + ]}> +
+ {props.checkboxData.map((checkboxText, index) => ( + handleToggleCheckbox(index)} + value={toggledCheckBoxes.indexOf(index) > -1} + />)) + } +
+
+ ); +}; + +CheckboxModal.propTypes = { + // the method which the modal executes when the ok button is pressed. + handleAccept: PropTypes.func, + + // responsible for closing the modal. Occurs on both the close button and the X in the top right. + closeHandler: PropTypes.func, + + // method to be called when the clear button is pressed. + handleClear: PropTypes.func, + + // the values that will be used as names for the checkboxes. + checkboxData: PropTypes.arrayOf(string) + +}; + +export default CheckboxModal; diff --git a/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmCorrespondenceView.jsx b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmCorrespondenceView.jsx new file mode 100644 index 00000000000..c919beecd98 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmCorrespondenceView.jsx @@ -0,0 +1,260 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { PencilIcon } from '../../../../../components/icons/PencilIcon'; +import Button from '../../../../../components/Button'; +import { useSelector } from 'react-redux'; +import CorrespondenceDetailsTable from './CorrespondenceDetailsTable'; +import ConfirmTasksNotRelatedToAnAppeal from './ConfirmTasksNotRelatedToAnAppeal'; +import Table from '../../../../../components/Table'; +import ConfirmTasksRelatedToAnAppeal from './ConfirmTasksRelatedToAnAppeal'; +import { formatDateStr } from 'app/util/DateUtil'; + +export const ConfirmCorrespondenceView = (props) => { + + const checkedMailTasks = props.mailTasks; + const intakeCorrespondence = useSelector((state) => state.intakeCorrespondence); + const relatedCorrespondences = intakeCorrespondence.relatedCorrespondences; + const responseLetters = intakeCorrespondence.responseLetters; + + let correspondenceTable = null; + let mailTaskTable = null; + + // eslint-disable-next-line max-statements + const getDocumentColumns = (correspondence) => { + + return [ + { + cellClass: 'va-dor-column', + ariaLabel: 'va-dor-header-label', + header: ( +
+ + VA DOR + +
+ ), + valueFunction: () => { + const date = new Date(correspondence.vaDateOfReceipt); + + return ( + +

{date.toLocaleDateString('en-US')}

+
+ ); + } + }, + { + cellClass: 'source-type-column', + ariaLabel: 'source-type-header-label', + header: ( +
+ + Source Type + +
+ ), + valueFunction: () => ( + +

{correspondence.sourceType}

+
+ ) + }, + { + cellClass: 'package-document-type-column', + ariaLabel: 'package-document-type-header-label', + header: ( +
+ + Package Document Type + +
+ ), + valueFunction: () => ( + +

{correspondence.packageDocumentType}

+
+ ) + }, + { + cellClass: 'correspondence-type-column', + ariaLabel: 'correspondence-type-header-label', + header: ( +
+ + Correspondence Type + +
+ ), + valueFunction: () => ( + +

{correspondence.correspondenceType}

+
+ ) + }, + { + cellClass: 'notes-column', + ariaLabel: 'notes-header-label', + header: ( +
+ + Notes + +
+ ), + valueFunction: () => ( + +

{correspondence.notes}

+
+ ) + }, + ]; + }; + + if (relatedCorrespondences.length === 0) { + correspondenceTable = +
Correspondence is not related to prior mail
; + } else { + + correspondenceTable = ; + } + + if (checkedMailTasks.length === 0) { + mailTaskTable =
; + } else { + mailTaskTable = checkedMailTasks.map((name, index) => ( +
    + +
  • {name}
  • + +
+ )); + } + + return ( +
+

Review and Confirm Correspondence

+

+ Review the details below to make sure the information is correct before submitting. + If you need to make changes, please go back to the associated section. +

+ +
+
+

Response Letters

+
+ +
+
+
+
+
+ + + + + + + + + + + { Object.keys(responseLetters)?.map((indexValue) => { + const responseLetter = responseLetters[indexValue]; + const responseDate = new Date(responseLetter?.date).toISOString(); + + return ( + + + + + + + + + + + ); + })} +
Date Sent Letter Type Letter Title Letter Subcategory Letter Subcategory Reasons Response Window
{formatDateStr(responseDate)} {responseLetter?.type} {responseLetter?.title} {responseLetter?.subType} {responseLetter?.reason} + { + (responseLetter?.customValue === '' || responseLetter?.customValue === null) ? + responseLetter?.responseWindows : + `${responseLetter?.customValue } days` + } +
+
+
+
+
+
+

Associated Prior Mail

+
+ +
+
+
+
+ {correspondenceTable} +
+
+
+
+
+

Completed Mail Tasks

+
+ +
+
+
+
+ Completed Mail Tasks +
+ {mailTaskTable} +
+
+
+
+

Linked Appeals & New Tasks

+
+ +
+
+ + +
+

Tasks not related to an Appeal

+
+ +
+
+ +
+ + ); +}; + +ConfirmCorrespondenceView.propTypes = { + goToStep: PropTypes.func, + correspondence: PropTypes.object, + mailTasks: PropTypes.arrayOf(PropTypes.string) +}; +export default ConfirmCorrespondenceView; diff --git a/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksNotRelatedToAnAppeal.jsx b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksNotRelatedToAnAppeal.jsx new file mode 100644 index 00000000000..4392bc18972 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksNotRelatedToAnAppeal.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +const ConfirmTasksNotRelatedToAnAppeal = () => { + const tasks = useSelector((state) => state.intakeCorrespondence.unrelatedTasks); + + const rowObjects = tasks.map((task) => { + return ( + + + {task.label} + + + {task.content} + + + ); + }); + + const renderNonRelatedTask = () => { + if (tasks.length === 0) { + const rendererOfNonRelatedTask =
; + + return rendererOfNonRelatedTask; + } + const rendererOfNonRelatedTask = rowObjects; + + return rendererOfNonRelatedTask; + + }; + + return ( +
+
+
+
+ + + + + + + + + {renderNonRelatedTask()} + +
TasksTask Instructions or Context
+
+
+ ); +}; + +export default ConfirmTasksNotRelatedToAnAppeal; diff --git a/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksRelatedToAnAppeal.jsx b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksRelatedToAnAppeal.jsx new file mode 100644 index 00000000000..99774f60148 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksRelatedToAnAppeal.jsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { COLORS } from 'app/constants/AppConstants'; +import DocketTypeBadge from '../../../../../components/DocketTypeBadge'; +import { ExternalLinkIcon } from '../../../../../components/icons/ExternalLinkIcon'; +import PropTypes from 'prop-types'; + +const borderlessTd = { + borderTop: 'none', + borderBottom: 'none', + backgroundColor: COLORS.GREY_BACKGROUND, +}; + +const ConfirmTasksRelatedToAnAppeal = () => { + const tasks = useSelector((state) => state.intakeCorrespondence.newAppealRelatedTasks); + const taskIds = useSelector((state) => state.intakeCorrespondence.taskRelatedAppealIds). + sort((first, second) => first - second); + const fetchedAppeals = useSelector((state) => state.intakeCorrespondence.fetchedAppeals); + const waivedEvidenceTasks = useSelector((state) => state.intakeCorrespondence.waivedEvidenceTasks); + + const rowObjects = taskIds.map((task, index) => { + const evidenceSubmission = (fetchedAppeals.find((appeal) => appeal.id === task).evidenceSubmissionTask); + const waivedEvidenceTask = (waivedEvidenceTasks.find((waivedEvTask) => waivedEvTask.id === evidenceSubmission.id)); + + const formatDocketName = () => { + const currentAppeal = fetchedAppeals.find((appeal) => appeal.id === task); + + if (currentAppeal && evidenceSubmission) { + let unformattedText = currentAppeal.docketName; + + unformattedText = unformattedText.replace('_', ' '); + + // Capitalize the first character of each string + const formattedText = unformattedText.split(' ').map((text) => + text.charAt(0).toUpperCase() + text.slice(1)). + join(' '). + trim(); + + return formattedText; + } + + return ''; + }; + + // Handles what to display for EvidenceW Window Waived? column + const getYesOrNo = () => { + if (waivedEvidenceTask) { + return `Yes - ${waivedEvidenceTask.waiveReason}`; + } + + if (evidenceSubmission) { + return 'No'; + } + + return ''; + }; + + // Gives less space to the first appeal, and more to all after. + const paddingAmount = index === 0 ? 'first-style-for-appeals-number-tasks' : 'style-for-appeals-number-tasks'; + + return ( + <> + + +

Appeal {index + 1} Tasks

+ + + + + Linked Appeal + + + {evidenceSubmission && 'Currently Active Task'} + + + {evidenceSubmission && 'Evidence Window Waived?'} + + + {evidenceSubmission && 'Assigned To'} + + + + +
+ appeal.id === task).externalId}`} + target="_blank" + rel="noopener noreferrer"> + appeal.id === task).docketName)} /> + {fetchedAppeals.find((appeal) => appeal.id === task).docketNumber} + + +
+ + {formatDocketName()} + {getYesOrNo()} + {evidenceSubmission ? evidenceSubmission.assigned_to_type : ''} + + + + Additional Tasks + + + Task Instructions or Context + + + {tasks.filter((taskById) => taskById.appealId === task).map((taskById) => + + + {taskById.label} + + + {taskById.content} + + )} + + + ); + }); + + const renderingTask = () => { + + if (taskIds.length === 0) { + const taskRenderer =
Correspondence is not related to an existing appeal
; + + return taskRenderer; + } + const taskRenderer = + + {rowObjects} + +
; + + return taskRenderer; + + }; + + return ( + <> +
+
+ {renderingTask()} +
+
+ ); +}; + +ConfirmTasksRelatedToAnAppeal.propTypes = { + bottonStyling: PropTypes.object +}; + +export default ConfirmTasksRelatedToAnAppeal; diff --git a/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/CorrespondenceDetailsTable.jsx b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/CorrespondenceDetailsTable.jsx new file mode 100644 index 00000000000..b9d0cdb4dc0 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/CorrespondenceDetailsTable.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import moment from 'moment'; +import PropTypes from 'prop-types'; + +export const CorrespondenceDetailsTable = (props) => { + + return ( +
+

About the Correspondence

+ + + + + + + + + + + + + + + + + + + + + +
Package Document TypeVA DORVeteranCorrespondence Type
+ {props.correspondence.nod ? 'NOD' : 'Non-NOD'} + {moment(props.correspondence.vaDateOfReceipt).format('MM/DD/YYYY')}{props.correspondence.veteranFullName} ({props.correspondence.veteranFileNumber}){props.correspondence.correspondenceType}
+ Notes
+ {props.correspondence.notes}
+
+ ); +}; + +CorrespondenceDetailsTable.propTypes = { + correspondence: PropTypes.object +}; + +export default CorrespondenceDetailsTable; + diff --git a/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/SubmitCorrespondenceModal.jsx b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/SubmitCorrespondenceModal.jsx new file mode 100644 index 00000000000..133ef72fa10 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/SubmitCorrespondenceModal.jsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'app/components/Modal'; +import { useSelector } from 'react-redux'; +import ApiUtil from 'app/util/ApiUtil'; +import { + CORRESPONDENCE_INTAKE_FORM_SUBMIT_MODAL_TITLE, + CORRESPONDENCE_INTAKE_FORM_SUBMIT_MODAL_BODY, +} from 'app/../COPY'; + +export const SubmitCorrespondenceModal = ({ + setSubmitCorrespondenceModalVisible, + setErrorBannerVisible, + correspondence +}) => { + + const relatedCorrespondences = useSelector((state) => state.intakeCorrespondence.relatedCorrespondences); + const waivedEvidenceTasks = useSelector((state) => state.intakeCorrespondence.waivedEvidenceTasks); + const relatedAppealIds = useSelector((state) => state.intakeCorrespondence.taskRelatedAppealIds); + const tasksRelatedToAppeal = useSelector((state) => state.intakeCorrespondence.newAppealRelatedTasks); + const tasksNotRelatedToAppeal = useSelector((state) => state.intakeCorrespondence.unrelatedTasks); + const mailTasks = useSelector((state) => state.intakeCorrespondence.mailTasks); + const responseLettersHash = useSelector((state) => state.intakeCorrespondence.responseLetters); + let responseLetters = []; + + if (responseLettersHash && Object.values(responseLettersHash).length > 0) { + responseLetters = Object.values(responseLettersHash); + } + + const [loading, setLoading] = useState(false); + + const onCancel = () => { + setSubmitCorrespondenceModalVisible(false); + }; + + const handleRouting = (status) => { + if (status === 201) { + window.location.href = '/queue/correspondence'; + } else { + setErrorBannerVisible(true); + onCancel(); + } + }; + + const onSubmit = async() => { + const relatedUuids = relatedCorrespondences.map((corr) => corr.uuid); + const serializedWaivedEvidenceTasks = waivedEvidenceTasks.map((task) => ( + { task_id: task.id, waive_reason: task.waiveReason } + )); + + const serializedTasksRelatedToAppeal = tasksRelatedToAppeal.map((task) => ({ + appeal_id: task.appealId, + klass: task.type.klass, + assigned_to: task.type.assigned_to, + content: task.content + })); + + const serializedTasksNotRelatedToAppeal = tasksNotRelatedToAppeal.map((task) => ({ + klass: task.type.klass, + assigned_to: task.type.assigned_to, + content: task.content + })); + + const submitData = { + related_correspondence_uuids: relatedUuids, + tasks_related_to_appeal: serializedTasksRelatedToAppeal, + waived_evidence_submission_window_tasks: serializedWaivedEvidenceTasks, + related_appeal_ids: relatedAppealIds, + tasks_not_related_to_appeal: serializedTasksNotRelatedToAppeal, + mail_tasks: mailTasks, + response_letters: responseLetters + }; + + setLoading(true); + // Where data goes to be submitted before redirecting back to correspondence queue + let status; + + await ApiUtil.post(`/queue/correspondence/${correspondence.uuid}/intake`, { data: submitData }). + then((response) => { + status = response.status; + }). + // eslint-disable-next-line no-console + catch((error) => console.log(error.message)); + + setLoading(false); + handleRouting(status); + }; + + const buttons = [ + { + classNames: ['cf-modal-link', 'cf-btn-link'], + name: 'Cancel', + onClick: onCancel, + }, + { + classNames: ['usa-button', 'usa-button-primary'], + name: 'Confirm', + loading, + onClick: onSubmit + }, + ]; + + /* eslint-disable camelcase */ + return ( + + {CORRESPONDENCE_INTAKE_FORM_SUBMIT_MODAL_BODY} + + ); + /* eslint-enable camelcase */ +}; + +SubmitCorrespondenceModal.propTypes = { + correspondence: PropTypes.object, + onCancel: PropTypes.func, + onSubmit: PropTypes.func, + loading: PropTypes.bool, + setSubmitCorrespondenceModalVisible: PropTypes.func, + setErrorBannerVisible: PropTypes.func, +}; diff --git a/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx b/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx new file mode 100644 index 00000000000..1187476d453 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx @@ -0,0 +1,271 @@ +import React, { useState, useEffect } from 'react'; +import ProgressBar from 'app/components/ProgressBar'; +import Button from '../../../../components/Button'; +import PropTypes from 'prop-types'; +import AddCorrespondenceView from './AddCorrespondence/AddCorrespondenceView'; +import { AddTasksAppealsView } from './TasksAppeals/AddTasksAppealsView'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { + loadSavedIntake, + setUnrelatedTasks, + saveCurrentIntake, + setErrorBanner +} from '../../correspondenceReducer/correspondenceActions'; +import { useHistory } from 'react-router-dom'; +import { ConfirmCorrespondenceView } from './ConfirmCorrespondence/ConfirmCorrespondenceView'; +import { SubmitCorrespondenceModal } from './ConfirmCorrespondence/SubmitCorrespondenceModal'; +import Alert from 'app/components/Alert'; +import { + CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TITLE, + CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TEXT +} from '../../../../../COPY'; +import ReturnToQueueModal from './ReturnToQueueModal'; +import ApiUtil from '../../../../util/ApiUtil'; + +const progressBarSections = [ + { + title: '1. Add Related Correspondence', + step: 1 + }, + { + title: '2. Review Tasks & Appeals', + step: 2 + }, + { + title: '3. Confirm', + step: 3 + }, +]; + +export const CorrespondenceIntake = (props) => { + const dispatch = useDispatch(); + const intakeCorrespondence = useSelector((state) => state.intakeCorrespondence); + const showErrorBanner = useSelector((state) => state.intakeCorrespondence.showErrorBanner); + const [currentStep, setCurrentStep] = useState(1); + const [isContinueEnabled, setContinueEnabled] = useState(true); + const [addTasksVisible, setAddTasksVisible] = useState(false); + const [returnToQueueModal, setReturnToQueueModal] = useState(false); + const [submitCorrespondenceModalVisible, setSubmitCorrespondenceModalVisible] = useState(false); + const history = useHistory(); + + const handleBannerState = (bannerState) => { + dispatch(setErrorBanner(bannerState)); + }; + + const exportStoredata = { + correspondence_uuid: props.correspondence_uuid, + current_step: currentStep, + redux_store: intakeCorrespondence + }; + + const handleContinueStatusChange = (isEnabled) => { + setContinueEnabled(isEnabled); + }; + + const handleCheckboxChange = (isSelected) => { + setContinueEnabled(isSelected); + }; + + const nextStep = () => { + if (currentStep < 3) { + setCurrentStep(currentStep + 1); + window.scrollTo(0, 0); + history.replace({ hash: '' }); + } + }; + + const handleContinueAfterBack = () => { + setContinueEnabled(true); + }; + + const redirectToPage = (userAction) => { + const newUrl = new URL(window.location.href); + const searchParams = new URLSearchParams(newUrl.search); + + // Encode and set the query parameters + searchParams.set('correspondence_uuid', encodeURIComponent(exportStoredata.correspondence_uuid)); + searchParams.set('userAction', encodeURIComponent(userAction)); + searchParams.set('tab', encodeURIComponent('correspondence_unassigned')); + searchParams.set('page', encodeURIComponent('1')); + + // Construct the new URL with encoded query parameters + newUrl.search = searchParams.toString(); + newUrl.pathname = props.isInboundOpsSupervisor ? '/queue/correspondence/team' : '/queue/correspondence'; + window.location.href = newUrl.href; + }; + + const handleContinueIntakeLater = () => { + props.saveCurrentIntake(intakeCorrespondence, exportStoredata, () => { + redirectToPage('continue_later'); + }); + + }; + + const handleCancelIntake = () => { + ApiUtil.post(`/queue/correspondence/${exportStoredata.correspondence_uuid}/cancel_intake`, { exportStoredata }). + then((response) => { + if (!response.ok) { + console.error(response); + } + redirectToPage('cancel_intake'); + + }). + catch((err) => { + console.error(new Error(`Problem with GET ${intakeCorrespondence} ${err}`)); + }); + }; + + const prevStep = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + handleContinueAfterBack(); + window.scrollTo(0, 0); + history.replace({ hash: '' }); + } + }; + + const sections = progressBarSections.map(({ title, step }) => ({ + title, + current: (step === currentStep) + }), + ); + + useEffect(() => { + if (currentStep !== 1) { + props.saveCurrentIntake(intakeCorrespondence, exportStoredata); + } + }, [currentStep]); + + useEffect(() => { + // load previous correspondence intake from database (if any) + if (props.reduxStore !== null) { + setCurrentStep(3); + props.loadSavedIntake(props.reduxStore); + } + }, []); + + return
+ { showErrorBanner && + + {CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TEXT} + + } + + {currentStep === 1 && + + } + {currentStep === 2 && + + } + {currentStep === 3 && +
+ + props.toggledCorrespondences.indexOf(String(correspondence.uuid)) !== -1)} + /> +
+ } +
+ {returnToQueueModal && + setReturnToQueueModal(false)} + handleContinueIntakeLater={handleContinueIntakeLater} + handleCancelIntake={handleCancelIntake} + /> + } + } + {currentStep === 3 && + } + {currentStep > 1 && + } + {currentStep === 3 && submitCorrespondenceModalVisible && + + } +
+
; +}; + +CorrespondenceIntake.propTypes = { + correspondence_uuid: PropTypes.string, + correspondence: PropTypes.object, + toggledCorrespondences: PropTypes.array, + priorMail: PropTypes.array, + unrelatedTasks: PropTypes.arrayOf(Object), + setUnrelatedTasks: PropTypes.func, + mailTasks: PropTypes.arrayOf(PropTypes.string), + autoTexts: PropTypes.arrayOf(PropTypes.string), + reduxStore: PropTypes.object, + isInboundOpsSupervisor: PropTypes.bool, + loadSavedIntake: PropTypes.func, + saveCurrentIntake: PropTypes.func +}; + +const mapStateToProps = (state) => ({ + unrelatedTasks: state.intakeCorrespondence.unrelatedTasks, + mailTasks: state.intakeCorrespondence.mailTasks, + toggledCorrespondences: state.intakeCorrespondence.relatedCorrespondences +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + setUnrelatedTasks, + loadSavedIntake, + saveCurrentIntake + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CorrespondenceIntake); diff --git a/client/app/queue/correspondence/intake/components/ReturnToQueueModal.jsx b/client/app/queue/correspondence/intake/components/ReturnToQueueModal.jsx new file mode 100644 index 00000000000..acdb57f10c7 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ReturnToQueueModal.jsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import Modal from '../../../../components/Modal'; +import PropTypes from 'prop-types'; +import RadioField from '../../../../components/RadioField'; +import Alert from '../../../../components/Alert'; + +const ReturnToQueueModal = (props) => { + const [selectedRadio, setSelectedRadio] = useState(''); + + const radioOptions = [ + { displayText: 'Cancel intake', + value: 'cancel_intake' }, + { displayText: 'Continue intake at a later date', + value: 'continue_later' } + ]; + + const onRadioChange = (value) => { + setSelectedRadio(value); + }; + + const handleConfirm = () => { + if (selectedRadio === 'continue_later') { + props.handleContinueIntakeLater(); + } else if (selectedRadio === 'cancel_intake') { + props.handleCancelIntake(); + } + }; + + return ( + + + {selectedRadio === 'continue_later' && } + + ); +}; + +ReturnToQueueModal.propTypes = { + onCancel: PropTypes.func, + handleContinueIntakeLater: PropTypes.func, + handleCancelIntake: PropTypes.func +}; + +export default ReturnToQueueModal; diff --git a/client/app/queue/correspondence/intake/components/TasksAppeals/AddAppealRelatedTaskView.jsx b/client/app/queue/correspondence/intake/components/TasksAppeals/AddAppealRelatedTaskView.jsx new file mode 100644 index 00000000000..9765e85c7a1 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/TasksAppeals/AddAppealRelatedTaskView.jsx @@ -0,0 +1,216 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { LOGO_COLORS } from '../../../../../constants/AppConstants'; +import CaseListTable from '../../../../CaseListTable'; +import ApiUtil from '../../../../../util/ApiUtil'; +import { prepareAppealForSearchStore } from '../../../../utils'; +import LoadingContainer from '../../../../../components/LoadingContainer'; +import RadioField from '../../../../../components/RadioField'; +import ExistingAppealTasksView from './ExistingAppealTasksView'; +import { + setFetchedAppeals, + setNewAppealRelatedTasks, + setTaskRelatedAppealIds, + setWaivedEvidenceTasks +} from '../../../correspondenceReducer/correspondenceActions'; + +const RELATED_NO = '0'; +const RELATED_YES = '1'; + +const existingAppealAnswer = [ + { displayText: 'Yes', + value: RELATED_YES }, + { displayText: 'No', + value: RELATED_NO } +]; + +export const AddAppealRelatedTaskView = (props) => { + const appeals = useSelector((state) => state.intakeCorrespondence.fetchedAppeals); + const [taskRelatedAppeals, setTaskRelatedAppeals] = + useState(useSelector((state) => state.intakeCorrespondence.taskRelatedAppealIds)); + const [newTasks, setNewTasks] = useState(useSelector((state) => state.intakeCorrespondence.newAppealRelatedTasks)); + const [waivedTasks, setWaivedTasks] = + useState(useSelector((state) => state.intakeCorrespondence.waivedEvidenceTasks)); + const [existingAppealRadio, setExistingAppealRadio] = + useState(taskRelatedAppeals.length ? RELATED_YES : RELATED_NO); + const [loading, setLoading] = useState(false); + const [nextTaskId, setNextTaskId] = useState(newTasks.length); + const [currentAppealPage, setCurrentAppealPage] = useState(1); + const [tableUpdateTrigger, setTableUpdateTrigger] = useState(1); + + const dispatch = useDispatch(); + + const appealById = (appealId) => { + return appeals.find((el) => el.id === appealId); + }; + + const appealsPageUpdateHandler = (newCurrentPage) => { + setCurrentAppealPage(newCurrentPage); + setTableUpdateTrigger((prev) => prev + 1); + }; + + useEffect(() => { + dispatch(setTaskRelatedAppealIds(taskRelatedAppeals)); + }, [taskRelatedAppeals]); + + useEffect(() => { + // Creates an array of Task IDs then sorts them so that the highest ID is the last in the array. + const existingIds = [...newTasks.map((task) => task.id)].sort((task1, task2) => task1 - task2); + + // Set the value to 0 if there are no IDs. Otherwise use the highest value ID + 1 + setNextTaskId(existingIds.length === 0 ? 0 : existingIds[existingIds.length - 1] + 1); + + dispatch(setNewAppealRelatedTasks(newTasks)); + }, [newTasks]); + + useEffect(() => { + dispatch(setWaivedEvidenceTasks(waivedTasks)); + }, [waivedTasks]); + + const appealCheckboxOnChange = (appealId, isChecked) => { + if (isChecked) { + if (!taskRelatedAppeals.includes(appealId)) { + setTaskRelatedAppeals([...taskRelatedAppeals, appealId]); + } + } else { + const selectedAppeals = taskRelatedAppeals.filter((checkedId) => checkedId !== appealId); + const filteredNewTasks = newTasks.filter((task) => task.appealId !== appealId); + const waivedEvidenceTasks = filteredNewTasks.filter((taskEvidence) => taskEvidence.isWaived); + + setTaskRelatedAppeals(selectedAppeals); + setNewTasks(filteredNewTasks); + setTableUpdateTrigger((prev) => prev + 1); + setWaivedTasks(waivedEvidenceTasks); + } + }; + + useEffect(() => { + // Clear the selected appeals and any tasks when the user toggles the radio button + if (existingAppealRadio === RELATED_NO) { + setTaskRelatedAppeals([]); + setNewTasks([]); + setWaivedTasks([]); + } + }, [existingAppealRadio]); + + useEffect(() => { + let canContinue = true; + + // Check if radio button is selected + if (existingAppealRadio !== '0') { + // Check if at least one checkbox is selected + if (taskRelatedAppeals.length === 0) { + canContinue = false; + } else { + // Check the conditions for each task and waived task + canContinue = newTasks.every((task) => task.content !== '' && task.type !== '') && + waivedTasks.every((task) => task.isWaived ? task.waiveReason !== '' : true); + } + } + + props.setRelatedTasksCanContinue(canContinue); + }, [existingAppealRadio, newTasks, waivedTasks, taskRelatedAppeals]); + + const veteranFileNumber = props.correspondence.veteranFileNumber; + + useEffect(() => { + // Don't refetch (use cache) + if (appeals.length) { + return; + } + + if (veteranFileNumber) { + // Visually indicate that we are fetching data + setLoading(true); + + ApiUtil.get('/appeals', { headers: { 'case-search': veteranFileNumber } }). + then((appealResponse) => { + const appealsForStore = prepareAppealForSearchStore(appealResponse.body.appeals); + + const appealArr = Object.values(appealsForStore.appeals).sort((first, second) => first.id - second.id); + + dispatch(setFetchedAppeals(appealArr)); + setLoading(false); + }); + } + }, [veteranFileNumber]); + + return ( +
+ setExistingAppealRadio(val)} + /> + {existingAppealRadio === RELATED_YES && loading && + +
+
+
+ } + {existingAppealRadio === RELATED_YES && !loading && +
+
+
+
+ Existing Appeals +
+
    + Please select prior appeal(s) to link to this correspondence +
+
    +
    + +
    +
+
+
+ {taskRelatedAppeals.toSorted().map((appealId, index) => { + return ( + + ); + })} +
+
+
+ } +
+ ); +}; + +AddAppealRelatedTaskView.propTypes = { + correspondence: PropTypes.object.isRequired, + setRelatedTasksCanContinue: PropTypes.func.isRequired, + filterUnavailableTaskTypeOptions: PropTypes.func.isRequired, + allTaskTypeOptions: PropTypes.array.isRequired, + autoTexts: PropTypes.arrayOf(PropTypes.string).isRequired, +}; + +export default AddAppealRelatedTaskView; diff --git a/client/app/queue/correspondence/intake/components/TasksAppeals/AddEvidenceSubmissionTaskView.jsx b/client/app/queue/correspondence/intake/components/TasksAppeals/AddEvidenceSubmissionTaskView.jsx new file mode 100644 index 00000000000..12fad543eea --- /dev/null +++ b/client/app/queue/correspondence/intake/components/TasksAppeals/AddEvidenceSubmissionTaskView.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import TextareaField from '../../../../../components/TextareaField'; +import ReactSelectDropdown from '../../../../../components/ReactSelectDropdown'; +import Checkbox from '../../../../../components/Checkbox'; +import PropTypes from 'prop-types'; + +const AddEvidenceSubmissionTaskView = (props) => { + const task = props.task; + + const handleIsWaivedChange = (newIsWaved) => { + const newTask = { id: task.id, isWaived: newIsWaved, waiveReason: task.waiveReason }; + + // Parent will add/remove task from list based on isWaived + props.taskUpdatedCallback(newTask); + }; + + const handleReasonChange = (newReason) => { + const newTask = { id: task.id, isWaived: task.isWaived, waiveReason: newReason }; + + props.taskUpdatedCallback(newTask); + }; + + const dropdownOptions = [ + { value: 'evidence_submission', label: 'Evidence Window Submission Task', isDisabled: true }, + ]; + + return ( +
+
+
+
+ +
+
+ + handleIsWaivedChange(checked)} + /> + {task.isWaived && ( + + )} +
+
+
+ ); +}; + +AddEvidenceSubmissionTaskView.propTypes = { + task: PropTypes.object.isRequired, + taskUpdatedCallback: PropTypes.func.isRequired +}; + +export default AddEvidenceSubmissionTaskView; diff --git a/client/app/queue/correspondence/intake/components/TasksAppeals/AddTaskView.jsx b/client/app/queue/correspondence/intake/components/TasksAppeals/AddTaskView.jsx new file mode 100644 index 00000000000..f25dd7af6bf --- /dev/null +++ b/client/app/queue/correspondence/intake/components/TasksAppeals/AddTaskView.jsx @@ -0,0 +1,191 @@ +import React, { useState } from 'react'; +import TextareaField from '../../../../../components/TextareaField'; +import CheckboxModal from '../CheckboxModal'; +import Button from '../../../../../components/Button'; +import Select from 'react-select'; +import PropTypes from 'prop-types'; + +const customSelectStyless = { + dropdownIndicator: () => ({ + width: '80%' + }), + + control: (styles) => { + return { + ...styles, + alignContent: 'center', + borderRadius: 0, + border: '1px solid black' + }; + }, + + menu: () => ({ + boxShadow: '0 0 0 1px hsla(0,0%,0%,0.1), 0 4px 11px hsla(0,0%,0%,0.1)', + marginTop: '8px' + }), + + valueContainer: (styles) => ({ + + ...styles, + lineHeight: 'normal', + // this is a hack to fix a problem with changing the height of the dropdown component. + // Changing the height causes problems with text shifting. + marginTop: '-10%', + marginBottom: '-10%', + paddingTop: '-10%', + minHeight: '140px', + borderRadius: 50 + + }), + singleValue: (styles) => { + return { + ...styles, + alignContent: 'center', + }; + }, + + placeholder: (styles) => ({ + ...styles, + color: 'black', + }), + + option: (styles, { isFocused }) => ({ + color: 'black', + fontSize: '17px', + padding: '8px 12px', + backgroundColor: isFocused ? 'white' : 'null', + ':hover': { + ...styles[':hover'], + backgroundColor: '#5b616b', + color: 'white', + } + }) +}; + +const AddTaskView = (props) => { + const task = props.task; + const [modalVisible, setModalVisible] = useState(false); + + const objectForSelectedTaskType = () => { + return props.allTaskTypeOptions.find((option) => { + return option.value.assigned_to === task.type.assigned_to; + }); + }; + + const updateTaskContent = (newContent) => { + const newTask = { id: task.id, appealId: task.appealId, type: task.type, label: task.label, content: newContent }; + + props.taskUpdatedCallback(newTask); + }; + + const updateTaskType = (newType) => { + const newTask = + { id: task.id, appealId: task.appealId, type: newType.value, label: newType.label, content: task.content }; + + props.taskUpdatedCallback(newTask); + }; + + const handleModalToggle = () => { + setModalVisible(!modalVisible); + }; + + const handleAutotext = (autoTextValues) => { + let autoTextOutput = ''; + + if (task.content) { + autoTextOutput = task.content; + } + + if (autoTextValues.length > 0) { + autoTextValues.forEach((id) => { + autoTextOutput += `${props.autoTexts[id] }\n`; + }); + } + updateTaskContent(autoTextOutput); + handleModalToggle(); + }; + + return ( +
+
+ {modalVisible && + + } + +
+
+
+
+ + - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - -
+ + + + +
+
@@ -5984,7 +6059,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -6499,7 +6574,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -6543,7 +6618,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -19622,7 +19697,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -20518,7 +20593,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -20562,7 +20637,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -35986,7 +36061,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -36882,7 +36957,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -36926,7 +37001,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -42625,66 +42700,144 @@ exports[`Details Displays HearingConversion when converting from video 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - -
+ + + + +
+ @@ -48133,7 +48286,7 @@ exports[`Details Displays HearingConversion when converting from video 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="6:00 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -48648,7 +48801,7 @@ exports[`Details Displays HearingConversion when converting from video 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="6:00 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -48692,7 +48845,7 @@ exports[`Details Displays HearingConversion when converting from video 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="6:00 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -61743,7 +61896,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -62639,7 +62792,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -62683,7 +62836,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -78094,7 +78247,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -78990,7 +79143,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -79034,7 +79187,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -85079,7 +85232,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -85209,7 +85362,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -85253,7 +85406,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -86121,63 +86274,138 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -91626,7 +91854,7 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="6:00 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -92141,7 +92369,7 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="6:00 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -92185,7 +92413,7 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="6:00 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -105236,7 +105464,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -106132,7 +106360,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -106176,7 +106404,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -121587,7 +121815,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -122483,7 +122711,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -122527,7 +122755,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -128572,7 +128800,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -128702,7 +128930,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -128746,7 +128974,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -131614,7 +131842,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -131744,7 +131972,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -131788,7 +132016,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -133125,7 +133353,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -133255,7 +133483,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -133299,7 +133527,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -136043,7 +136271,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="2 (1W200B)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -136290,7 +136518,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="2 (1W200B)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -136334,7 +136562,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="2 (1W200B)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -138470,7 +138698,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -138605,7 +138833,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -138649,7 +138877,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -150750,7 +150978,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -151646,7 +151874,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -151690,7 +151918,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -166948,7 +167176,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -167844,7 +168072,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -167888,7 +168116,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -174664,7 +174892,6 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -174808,7 +175035,6 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -174852,7 +175078,6 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -176716,7 +176941,6 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -176864,7 +177088,6 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -176908,7 +177131,6 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -177659,82 +177881,182 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - -
+ + + +
+
+ - - - + + + + + + @@ -180328,7 +180650,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -180458,7 +180780,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -180502,7 +180824,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -181839,7 +182161,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -181969,7 +182291,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -182013,7 +182335,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -184757,7 +185079,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="2 (1W200B)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -185004,7 +185326,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="2 (1W200B)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -185048,7 +185370,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="2 (1W200B)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -187184,7 +187506,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -187319,7 +187641,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -187363,7 +187685,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -199464,7 +199786,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -200360,7 +200682,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -200404,7 +200726,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -215662,7 +215984,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -216558,7 +216880,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -216602,7 +216924,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -223378,7 +223700,6 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -223522,7 +223843,6 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -223566,7 +223886,6 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -225430,7 +225749,6 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -225578,7 +225896,6 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -225622,7 +225939,6 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -226373,82 +226689,182 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - -
+ + + +
+
+ - - - + + + + + + @@ -228846,7 +229262,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -228976,7 +229392,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -229020,7 +229436,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -230357,7 +230773,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -230487,7 +230903,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -230531,7 +230947,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -233275,7 +233691,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -233522,7 +233938,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -233566,7 +233982,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -235702,7 +236118,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -235837,7 +236253,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -235881,7 +236297,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -246869,7 +247285,6 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -247761,7 +248176,6 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -247805,7 +248219,6 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -263009,7 +263422,6 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -263901,7 +264313,6 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -263945,7 +264356,6 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -271292,7 +271702,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -271422,7 +271832,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -271466,7 +271876,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -272803,7 +273213,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -272933,7 +273343,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -272977,7 +273387,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -275721,7 +276131,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -275968,7 +276378,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -276012,7 +276422,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -278148,7 +278558,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -278283,7 +278693,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -278327,7 +278737,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -289436,7 +289846,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -290332,7 +290742,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -290376,7 +290786,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -305634,7 +306044,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -306530,7 +306940,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -306574,7 +306984,7 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -312959,7 +313369,6 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -313103,7 +313512,6 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -313147,7 +313555,6 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -315012,7 +315419,6 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -315160,7 +315566,6 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -315204,7 +315609,6 @@ exports[`Details Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -315955,86 +316359,190 @@ exports[`Details Matches snapshot with default props 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - -
+ + + +
+
+ - - - + + + + + + diff --git a/client/test/app/hearings/components/__snapshots__/HearingConversion.test.js.snap b/client/test/app/hearings/components/__snapshots__/HearingConversion.test.js.snap index 018c843fd40..41eff24ac97 100644 --- a/client/test/app/hearings/components/__snapshots__/HearingConversion.test.js.snap +++ b/client/test/app/hearings/components/__snapshots__/HearingConversion.test.js.snap @@ -353,66 +353,144 @@ exports[`HearingConversion Displays email fields when hearing type is switched f
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -5861,7 +5939,7 @@ exports[`HearingConversion Displays email fields when hearing type is switched f aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -6376,7 +6454,7 @@ exports[`HearingConversion Displays email fields when hearing type is switched f aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -6420,7 +6498,7 @@ exports[`HearingConversion Displays email fields when hearing type is switched f aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -19603,7 +19681,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -20499,7 +20577,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -20543,7 +20621,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -36182,7 +36260,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -37078,7 +37156,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -37122,7 +37200,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -48185,7 +48263,7 @@ exports[`HearingConversion Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -48700,7 +48778,7 @@ exports[`HearingConversion Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -48744,7 +48822,7 @@ exports[`HearingConversion Matches snapshot with default props 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="3:30 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -61926,7 +62004,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -62822,7 +62900,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -62866,7 +62944,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (12:30 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -78504,7 +78582,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -79400,7 +79478,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -79444,7 +79522,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Mountain Time (US & Canada) (1:30 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -85600,7 +85678,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -85730,7 +85808,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -85774,7 +85852,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" diff --git a/client/test/app/hearings/components/__snapshots__/ScheduleVeteran.test.js.snap b/client/test/app/hearings/components/__snapshots__/ScheduleVeteran.test.js.snap index beb2d859dc7..8c021d6f4b0 100644 --- a/client/test/app/hearings/components/__snapshots__/ScheduleVeteran.test.js.snap +++ b/client/test/app/hearings/components/__snapshots__/ScheduleVeteran.test.js.snap @@ -2478,7 +2478,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -2613,7 +2613,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -2657,7 +2657,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -15568,7 +15568,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -16590,7 +16590,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -16634,7 +16634,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -24704,7 +24704,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -24943,7 +24943,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -24987,7 +24987,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -26201,66 +26201,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -31709,7 +31787,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -32224,7 +32302,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -32268,7 +32346,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -45288,7 +45366,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -46184,7 +46262,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -46228,7 +46306,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -61628,7 +61706,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -62524,7 +62602,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -62568,7 +62646,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -69852,7 +69930,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -69987,7 +70065,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -70031,7 +70109,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -82697,7 +82775,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="null" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -83694,7 +83772,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="null" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -83738,7 +83816,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="null" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -92613,7 +92691,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -92748,7 +92826,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -92792,7 +92870,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -105687,7 +105765,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -106709,7 +106787,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -106753,7 +106831,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -114104,7 +114182,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -114287,7 +114365,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -114331,7 +114409,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -117397,7 +117475,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -117636,7 +117714,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -117680,7 +117758,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -118894,66 +118972,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -124402,7 +124558,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -124917,7 +125073,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -124961,7 +125117,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -130945,7 +131101,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -131080,7 +131236,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -131124,7 +131280,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -144019,7 +144175,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -145041,7 +145197,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -145085,7 +145241,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -151763,7 +151919,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -151893,7 +152049,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -151937,7 +152093,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -154697,7 +154853,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -154936,7 +155092,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -154980,7 +155136,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -156194,66 +156350,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -161702,7 +161936,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -162217,7 +162451,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -162261,7 +162495,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -168334,7 +168568,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -168469,7 +168703,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -168513,7 +168747,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -181408,7 +181642,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -182430,7 +182664,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -182474,7 +182708,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -189825,7 +190059,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -190008,7 +190242,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -190052,7 +190286,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -193118,7 +193352,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -193357,7 +193591,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -193401,7 +193635,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -194615,66 +194849,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -200123,7 +200435,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -200638,7 +200950,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -200682,7 +200994,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -206662,7 +206974,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -206797,7 +207109,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -206841,7 +207153,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -219736,7 +220048,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -220758,7 +221070,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -220802,7 +221114,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -227480,7 +227792,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -227610,7 +227922,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -227654,7 +227966,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -230414,7 +230726,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -230653,7 +230965,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -230697,7 +231009,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -231911,66 +232223,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -237419,7 +237809,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -237934,7 +238324,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -237978,7 +238368,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -243962,7 +244352,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -244097,7 +244487,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -244141,7 +244531,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -257036,7 +257426,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -258058,7 +258448,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -258102,7 +258492,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -264780,7 +265170,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -264910,7 +265300,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -264954,7 +265344,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -267714,7 +268104,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -267953,7 +268343,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -267997,7 +268387,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -269211,66 +269601,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -274719,7 +275187,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -275234,7 +275702,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -275278,7 +275746,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -281222,7 +281690,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -281357,7 +281825,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -281401,7 +281869,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -294299,7 +294767,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -295321,7 +295789,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -295365,7 +295833,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -302719,7 +303187,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -302902,7 +303370,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -302946,7 +303414,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -305933,7 +306401,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label=" " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -306164,7 +306632,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label=" " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -306208,7 +306676,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label=" " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -311922,7 +312390,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -312057,7 +312525,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -312101,7 +312569,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -325012,7 +325480,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -326034,7 +326502,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -326078,7 +326546,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -334148,7 +334616,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -334387,7 +334855,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -334431,7 +334899,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -335645,66 +336113,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -341153,7 +341699,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -341668,7 +342214,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -341712,7 +342258,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -354732,7 +355278,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -355628,7 +356174,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -355672,7 +356218,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -371072,7 +371618,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -371968,7 +372514,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -372012,7 +372558,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -380795,7 +381341,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -380930,7 +381476,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -380974,7 +381520,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -393885,7 +394431,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -394907,7 +395453,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -394951,7 +395497,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -403021,7 +403567,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -403260,7 +403806,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -403304,7 +403850,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="08/15/2020 (0/12) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -404518,66 +405064,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -410026,7 +410650,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -410541,7 +411165,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -410585,7 +411209,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -423605,7 +424229,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -424501,7 +425125,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -424545,7 +425169,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Mountain Time (US & Canada) (6:45 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -439945,7 +440569,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -440841,7 +441465,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -440885,7 +441509,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (5:45 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -449330,7 +449954,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -449465,7 +450089,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -449509,7 +450133,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -462404,7 +463028,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -463426,7 +464050,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -463470,7 +464094,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="St. Petersburg regional office" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -470821,7 +471445,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -471004,7 +471628,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -471048,7 +471672,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appeal-hearing-location-label" + aria-label="Holdrege, NE (VHA) 0 miles away" aria-owns="appeal-hearing-location-listbox" autoCapitalize="none" autoComplete="off" @@ -474114,7 +474738,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="10/04/2020 (0/12) 1 (1W200A) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -474353,7 +474977,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="10/04/2020 (0/12) 1 (1W200A) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -474397,7 +475021,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-date-label" + aria-label="10/04/2020 (0/12) 1 (1W200A) " aria-owns="hearing-date-listbox" autoCapitalize="none" autoComplete="off" @@ -475611,66 +476235,144 @@ SAN FRANCISCO, CA 94103
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -481119,7 +481821,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -481634,7 +482336,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -481678,7 +482380,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:45 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -487801,7 +488503,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -487936,7 +488638,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -487980,7 +488682,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -500646,7 +501348,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="null" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -501643,7 +502345,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="null" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" @@ -501687,7 +502389,7 @@ SAN FRANCISCO, CA 94103 aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="regional-office-label" + aria-label="null" aria-owns="regional-office-listbox" autoCapitalize="none" autoComplete="off" diff --git a/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap b/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap index d3136a6d5f7..57919dd08ee 100644 --- a/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap +++ b/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap @@ -306,7 +306,6 @@ exports[`DailyDocketRow renders correctly for non virtual, DVC 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-listbox" autocapitalize="none" autocomplete="off" @@ -538,7 +537,6 @@ exports[`DailyDocketRow renders correctly for non virtual, DVC 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-aod-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-aod-listbox" autocapitalize="none" autocomplete="off" @@ -895,7 +893,6 @@ exports[`DailyDocketRow renders correctly for non virtual, Transcriber 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-listbox" autocapitalize="none" autocomplete="off" @@ -1108,7 +1105,7 @@ exports[`DailyDocketRow renders correctly for non virtual, Transcriber 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autocapitalize="none" autocomplete="off" @@ -1473,7 +1470,6 @@ exports[`DailyDocketRow renders correctly for non virtual, VSO 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-listbox" autocapitalize="none" autocomplete="off" @@ -1636,7 +1632,7 @@ exports[`DailyDocketRow renders correctly for non virtual, VSO 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autocapitalize="none" autocomplete="off" @@ -2040,7 +2036,6 @@ exports[`DailyDocketRow renders correctly for non virtual, attorney 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-listbox" autocapitalize="none" autocomplete="off" @@ -2272,7 +2267,6 @@ exports[`DailyDocketRow renders correctly for non virtual, attorney 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-aod-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-aod-listbox" autocapitalize="none" autocomplete="off" @@ -2629,7 +2623,6 @@ exports[`DailyDocketRow renders correctly for non virtual, hearing cooridnator 1 aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-listbox" autocapitalize="none" autocomplete="off" @@ -2842,7 +2835,7 @@ exports[`DailyDocketRow renders correctly for non virtual, hearing cooridnator 1 aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autocapitalize="none" autocomplete="off" @@ -3246,7 +3239,6 @@ exports[`DailyDocketRow renders correctly for non virtual, judge 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-disposition-listbox" autocapitalize="none" autocomplete="off" @@ -3478,7 +3470,6 @@ exports[`DailyDocketRow renders correctly for non virtual, judge 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-aod-label" aria-owns="ae-4-c-4050-36-e-8-4-df-2-83-a-4-839-cf-73725-dc-aod-listbox" autocapitalize="none" autocomplete="off" @@ -3874,7 +3865,6 @@ exports[`DailyDocketRow renders correctly for virtual 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="3-c-170697-2-c-52-4-bad-b-7-bd-2-ccf-44-f-0-c-44-c-disposition-label" aria-owns="3-c-170697-2-c-52-4-bad-b-7-bd-2-ccf-44-f-0-c-44-c-disposition-listbox" autocapitalize="none" autocomplete="off" @@ -4087,7 +4077,7 @@ exports[`DailyDocketRow renders correctly for virtual 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="appeal-hearing-location-label" + aria-label="null" aria-owns="appeal-hearing-location-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap b/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap index 47155ee3647..b2d8d54ce02 100644 --- a/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap +++ b/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap @@ -934,7 +934,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -1064,7 +1064,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -1108,7 +1108,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -2445,7 +2445,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -2575,7 +2575,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -2619,7 +2619,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -5363,7 +5363,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -5610,7 +5610,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -5654,7 +5654,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -7658,7 +7658,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -7785,7 +7784,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -7829,7 +7827,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -18680,7 +18677,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -19576,7 +19573,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -19620,7 +19617,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -34877,7 +34874,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -35773,7 +35770,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -35817,7 +35814,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -42181,7 +42178,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -42325,7 +42321,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -42369,7 +42364,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -44227,7 +44221,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -44375,7 +44368,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -44419,7 +44411,6 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -45170,86 +45161,190 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - -
+ + + +
+
+ - - - + + + + + + @@ -46408,7 +46503,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -46538,7 +46633,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -46582,7 +46677,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -47919,7 +48014,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -48049,7 +48144,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -48093,7 +48188,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -50837,7 +50932,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -51084,7 +51179,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -51128,7 +51223,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -53132,7 +53227,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -53259,7 +53353,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -53303,7 +53396,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -64154,7 +64246,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -65050,7 +65142,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -65094,7 +65186,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -80351,7 +80443,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -81247,7 +81339,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -81291,7 +81383,7 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -87655,7 +87747,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -87799,7 +87890,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -87843,7 +87933,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="transcriber-label" aria-owns="transcriber-listbox" autoCapitalize="none" autoComplete="off" @@ -89701,7 +89790,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -89849,7 +89937,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -89893,7 +89980,6 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="problem-type-label" aria-owns="problem-type-listbox" autoCapitalize="none" autoComplete="off" @@ -90644,86 +90730,190 @@ exports[`DetailsForm Matches snapshot with for AMA hearing 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - -
+ + + +
+
+ - - - + + + + + + @@ -91882,7 +92072,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -92012,7 +92202,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -92056,7 +92246,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" + aria-label="null" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -93393,7 +93583,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -93523,7 +93713,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -93567,7 +93757,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-coordinator-dropdown-label" + aria-label="null" aria-owns="hearing-coordinator-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -96311,7 +96501,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -96558,7 +96748,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -96602,7 +96792,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-room-dropdown-label" + aria-label="1 (1W200A)" aria-owns="hearing-room-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -98606,7 +98796,6 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -98733,7 +98922,6 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -98777,7 +98965,6 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -109590,7 +109777,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -110486,7 +110673,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -110530,7 +110717,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="appellant-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="appellant-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -125787,7 +125974,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -126683,7 +126870,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" @@ -126727,7 +126914,7 @@ exports[`DetailsForm Matches snapshot with for legacy hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="representative-tz-label" + aria-label="Pacific Time (US & Canada) (3:00 AM)" aria-owns="representative-tz-listbox" autoCapitalize="none" autoComplete="off" diff --git a/client/test/app/hearings/components/details/__snapshots__/HearingTypeDropdown.test.js.snap b/client/test/app/hearings/components/details/__snapshots__/HearingTypeDropdown.test.js.snap index 5adf03a37cb..4e4677f56ca 100644 --- a/client/test/app/hearings/components/details/__snapshots__/HearingTypeDropdown.test.js.snap +++ b/client/test/app/hearings/components/details/__snapshots__/HearingTypeDropdown.test.js.snap @@ -992,7 +992,7 @@ exports[`HearingTypeDropdown Can change from central office hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -1131,7 +1131,7 @@ exports[`HearingTypeDropdown Can change from central office hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -1175,7 +1175,7 @@ exports[`HearingTypeDropdown Can change from central office hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -2758,7 +2758,7 @@ exports[`HearingTypeDropdown Can change from video hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -2897,7 +2897,7 @@ exports[`HearingTypeDropdown Can change from video hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -2941,7 +2941,7 @@ exports[`HearingTypeDropdown Can change from video hearing 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Virtual" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -4546,7 +4546,7 @@ exports[`HearingTypeDropdown Can change from virtual hearing to central 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Central" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -4685,7 +4685,7 @@ exports[`HearingTypeDropdown Can change from virtual hearing to central 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Central" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -4729,7 +4729,7 @@ exports[`HearingTypeDropdown Can change from virtual hearing to central 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Central" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -6356,7 +6356,7 @@ exports[`HearingTypeDropdown Can change from virtual hearing to video 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -6495,7 +6495,7 @@ exports[`HearingTypeDropdown Can change from virtual hearing to video 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" @@ -6539,7 +6539,7 @@ exports[`HearingTypeDropdown Can change from virtual hearing to video 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="hearing-type-label" + aria-label="Video" aria-owns="hearing-type-listbox" autoCapitalize="none" autoComplete="off" diff --git a/client/test/app/hearings/components/modalForms/__snapshots__/HearingTime.test.js.snap b/client/test/app/hearings/components/modalForms/__snapshots__/HearingTime.test.js.snap index d6eff75bfd6..c5dc2bd9272 100644 --- a/client/test/app/hearings/components/modalForms/__snapshots__/HearingTime.test.js.snap +++ b/client/test/app/hearings/components/modalForms/__snapshots__/HearingTime.test.js.snap @@ -58,66 +58,144 @@ exports[`HearingTime Matches snapshot when enableZone is true 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -5566,7 +5644,7 @@ exports[`HearingTime Matches snapshot when enableZone is true 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:15 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -6081,7 +6159,7 @@ exports[`HearingTime Matches snapshot when enableZone is true 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:15 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -6125,7 +6203,7 @@ exports[`HearingTime Matches snapshot when enableZone is true 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="8:15 AM Eastern Time (US & Canada)" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -9008,66 +9086,144 @@ exports[`HearingTime Matches snapshot when other time is not selected 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -9133,66 +9289,144 @@ exports[`HearingTime Matches snapshot when other time is selected 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -14641,7 +14875,7 @@ exports[`HearingTime Matches snapshot when other time is selected 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="1:45 pm" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -15156,7 +15390,7 @@ exports[`HearingTime Matches snapshot when other time is selected 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="1:45 pm" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -15200,7 +15434,7 @@ exports[`HearingTime Matches snapshot when other time is selected 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" + aria-label="1:45 pm" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -18082,66 +18316,144 @@ exports[`HearingTime Matches snapshot when readonly prop is set 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -23555,7 +23867,6 @@ exports[`HearingTime Matches snapshot when readonly prop is set 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -24067,7 +24378,6 @@ exports[`HearingTime Matches snapshot when readonly prop is set 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -24111,7 +24421,6 @@ exports[`HearingTime Matches snapshot when readonly prop is set 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -26984,66 +27293,144 @@ exports[`HearingTime Matches snapshot with default props when passed in 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - + + + + + + @@ -32457,7 +32844,6 @@ exports[`HearingTime Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -32969,7 +33355,6 @@ exports[`HearingTime Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" @@ -33013,7 +33398,6 @@ exports[`HearingTime Matches snapshot with default props when passed in 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="optional-hearing-time-0-label" aria-owns="optional-hearing-time-0-listbox" autoCapitalize="none" autoComplete="off" diff --git a/client/test/app/intake/components/__snapshots__/AddPoaPage.test.js.snap b/client/test/app/intake/components/__snapshots__/AddPoaPage.test.js.snap index 9634245a673..2f602cd9dfd 100644 --- a/client/test/app/intake/components/__snapshots__/AddPoaPage.test.js.snap +++ b/client/test/app/intake/components/__snapshots__/AddPoaPage.test.js.snap @@ -147,7 +147,7 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="listed-attorney-label" + aria-label="Name not listed" aria-owns="listed-attorney-listbox" autocapitalize="none" autocomplete="off" @@ -535,7 +535,6 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="state-label" aria-owns="state-listbox" autocapitalize="none" autocomplete="off" @@ -901,7 +900,7 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="listed-attorney-label" + aria-label="Name not listed" aria-owns="listed-attorney-listbox" autocapitalize="none" autocomplete="off" @@ -1223,7 +1222,6 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="state-label" aria-owns="state-listbox" autocapitalize="none" autocomplete="off" @@ -1589,7 +1587,7 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="listed-attorney-label" + aria-label="Name not listed" aria-owns="listed-attorney-listbox" autocapitalize="none" autocomplete="off" @@ -1982,7 +1980,6 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="state-label" aria-owns="state-listbox" autocapitalize="none" autocomplete="off" @@ -2353,7 +2350,7 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="listed-attorney-label" + aria-label="Name not listed" aria-owns="listed-attorney-listbox" autocapitalize="none" autocomplete="off" @@ -2746,7 +2743,6 @@ exports[`AddPoaPage Can select Name not listed and it renders individual and org aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="state-label" aria-owns="state-listbox" autocapitalize="none" autocomplete="off" @@ -3116,7 +3112,7 @@ exports[`AddPoaPage Can select an existing attorney and it renders correctly 1`] aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="listed-attorney-label" + aria-label="John Attorney" aria-owns="listed-attorney-listbox" autocapitalize="none" autocomplete="off" @@ -3404,7 +3400,6 @@ exports[`AddPoaPage renders default state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="listed-attorney-label" aria-owns="listed-attorney-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/intake/components/__snapshots__/EditClaimLabelModal.test.js.snap b/client/test/app/intake/components/__snapshots__/EditClaimLabelModal.test.js.snap index 932ab05c05e..ed861cf7768 100644 --- a/client/test/app/intake/components/__snapshots__/EditClaimLabelModal.test.js.snap +++ b/client/test/app/intake/components/__snapshots__/EditClaimLabelModal.test.js.snap @@ -107,7 +107,7 @@ exports[`EditClaimLabelModal renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="select-claim-label-label" + aria-label="040HDENR" aria-owns="select-claim-label-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/intake/components/addClaimants/__snapshots__/AddClaimantPage.test.js.snap b/client/test/app/intake/components/addClaimants/__snapshots__/AddClaimantPage.test.js.snap index be164ee81a6..3b656ca12c4 100644 --- a/client/test/app/intake/components/addClaimants/__snapshots__/AddClaimantPage.test.js.snap +++ b/client/test/app/intake/components/addClaimants/__snapshots__/AddClaimantPage.test.js.snap @@ -124,7 +124,6 @@ exports[`AddClaimantPage renders default state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/intake/components/addClaimants/__snapshots__/ClaimantForm.test.js.snap b/client/test/app/intake/components/addClaimants/__snapshots__/ClaimantForm.test.js.snap index 0e3110869fe..c84d4d126de 100644 --- a/client/test/app/intake/components/addClaimants/__snapshots__/ClaimantForm.test.js.snap +++ b/client/test/app/intake/components/addClaimants/__snapshots__/ClaimantForm.test.js.snap @@ -54,7 +54,7 @@ exports[`ClaimantForm default values prepopulates with default values 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" + aria-label="Other" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -458,7 +458,7 @@ exports[`ClaimantForm default values prepopulates with default values 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="state-label" + aria-label="CA" aria-owns="state-listbox" autocapitalize="none" autocomplete="off" @@ -760,7 +760,6 @@ exports[`ClaimantForm renders default state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -873,7 +872,7 @@ exports[`HlrScClaimantForm renders child relationship state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" + aria-label="Child" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -1265,7 +1264,6 @@ exports[`HlrScClaimantForm renders child relationship state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="state-label" aria-owns="state-listbox" autocapitalize="none" autocomplete="off" @@ -1555,7 +1553,6 @@ exports[`HlrScClaimantForm renders default state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -1668,7 +1665,7 @@ exports[`HlrScClaimantForm renders healthcare provider relationship state correc aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" + aria-label="Healthcare Provider" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -1831,7 +1828,7 @@ exports[`HlrScClaimantForm renders healthcare provider relationship state correc aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" + aria-label="Healthcare Provider" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -1994,7 +1991,7 @@ exports[`HlrScClaimantForm renders other relationship state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" + aria-label="Other" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -2157,7 +2154,7 @@ exports[`HlrScClaimantForm renders other relationship state correctly 2`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" + aria-label="Other" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -2320,7 +2317,7 @@ exports[`HlrScClaimantForm renders spouse relationship state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="relationship-label" + aria-label="Spouse" aria-owns="relationship-listbox" autocapitalize="none" autocomplete="off" @@ -2712,7 +2709,6 @@ exports[`HlrScClaimantForm renders spouse relationship state correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="state-label" aria-owns="state-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/nonComp/__snapshots__/Personnel.test.js.snap b/client/test/app/nonComp/__snapshots__/Personnel.test.js.snap index cef00e43b54..d2b62315ac3 100644 --- a/client/test/app/nonComp/__snapshots__/Personnel.test.js.snap +++ b/client/test/app/nonComp/__snapshots__/Personnel.test.js.snap @@ -57,7 +57,7 @@ exports[`Personnel component renders correctly renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="report-type-label" + aria-label="Event / Action" aria-owns="report-type-listbox" autocapitalize="none" autocomplete="off" @@ -218,7 +218,6 @@ exports[`Personnel component renders correctly renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="timing-range-label" aria-owns="timing-range-listbox" autocapitalize="none" autocomplete="off" @@ -416,7 +415,6 @@ exports[`Personnel component renders correctly renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="conditions-0-options-personnel-label" aria-owns="conditions-0-options-personnel-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/nonComp/components/reportPage/__snapshots__/DaysWaiting.test.js.snap b/client/test/app/nonComp/components/reportPage/__snapshots__/DaysWaiting.test.js.snap index 7794325e649..da716bd07ce 100644 --- a/client/test/app/nonComp/components/reportPage/__snapshots__/DaysWaiting.test.js.snap +++ b/client/test/app/nonComp/components/reportPage/__snapshots__/DaysWaiting.test.js.snap @@ -57,7 +57,7 @@ exports[`DaysWaiting renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="report-type-label" + aria-label="Event / Action" aria-owns="report-type-listbox" autocapitalize="none" autocomplete="off" @@ -218,7 +218,6 @@ exports[`DaysWaiting renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="timing-range-label" aria-owns="timing-range-listbox" autocapitalize="none" autocomplete="off" @@ -414,7 +413,6 @@ exports[`DaysWaiting renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="conditions-0-options-comparison-operator-label" aria-owns="conditions-0-options-comparison-operator-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/nonComp/pages/__snapshots__/ReportPage.test.js.snap b/client/test/app/nonComp/pages/__snapshots__/ReportPage.test.js.snap index 718c1604439..22c519ab7d1 100644 --- a/client/test/app/nonComp/pages/__snapshots__/ReportPage.test.js.snap +++ b/client/test/app/nonComp/pages/__snapshots__/ReportPage.test.js.snap @@ -57,7 +57,6 @@ exports[`ReportPage renders correctly renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="report-type-label" aria-owns="report-type-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/queue/__snapshots__/QueueTable.test.js.snap b/client/test/app/queue/__snapshots__/QueueTable.test.js.snap index 7ef3c929441..66a6ae27b33 100644 --- a/client/test/app/queue/__snapshots__/QueueTable.test.js.snap +++ b/client/test/app/queue/__snapshots__/QueueTable.test.js.snap @@ -399,6 +399,8 @@ exports[`QueueTable HeaderRow Can filter when using task pages API and column ha filterOptions={Array []} filterOptionsFromApi={Array []} header="Third" + isReceiptDateFilter={false} + isTaskCompletedDateFilter={false} tableData={ Array [ Object { @@ -849,7 +851,7 @@ exports[`QueueTable render() Can filter rows 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 3, + "total_task_count": undefined, }, } } @@ -1079,7 +1081,7 @@ exports[`QueueTable render() Can filter rows 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 3, + "total_task_count": undefined, }, } } @@ -1524,7 +1526,7 @@ exports[`QueueTable render() Can sort rows 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 3, + "total_task_count": undefined, }, } } @@ -1709,7 +1711,7 @@ exports[`QueueTable render() Can sort rows 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 3, + "total_task_count": undefined, }, } } @@ -2052,7 +2054,7 @@ exports[`QueueTable render() Matches snapshot with default props 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 3, + "total_task_count": undefined, }, } } @@ -2202,7 +2204,7 @@ exports[`QueueTable render() Matches snapshot with default props 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 3, + "total_task_count": undefined, }, } } @@ -7326,7 +7328,7 @@ exports[`QueueTable render() Renders pagination when set 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 20, + "total_task_count": undefined, }, } } @@ -9080,7 +9082,7 @@ exports[`QueueTable render() Renders pagination when set 1`] = ` }, ], "tasks_per_page": undefined, - "total_task_count": 20, + "total_task_count": undefined, }, } } diff --git a/client/test/app/queue/cavc/__snapshots__/AddCavcRemandView.test.js.snap b/client/test/app/queue/cavc/__snapshots__/AddCavcRemandView.test.js.snap index 57f1ad07fd8..914e3978d57 100644 --- a/client/test/app/queue/cavc/__snapshots__/AddCavcRemandView.test.js.snap +++ b/client/test/app/queue/cavc/__snapshots__/AddCavcRemandView.test.js.snap @@ -377,46 +377,94 @@ exports[`AddCavcRemandView renders correctly 1`] = `
-
- - -
-
+ + + +
+ + - - -
+ + + + + + @@ -2280,7 +2328,6 @@ exports[`AddCavcRemandView renders correctly 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -2496,7 +2543,6 @@ exports[`AddCavcRemandView renders correctly 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -2540,7 +2586,6 @@ exports[`AddCavcRemandView renders correctly 1`] = ` aria-autocomplete="list" aria-expanded={false} aria-haspopup={true} - aria-labelledby="judge-dropdown-label" aria-owns="judge-dropdown-listbox" autoCapitalize="none" autoComplete="off" @@ -3633,86 +3678,182 @@ exports[`AddCavcRemandView renders correctly 1`] = `
-
- - -
-
+ + + +
+ + - - -
-
+ + + +
+ + - - - -
+ + + +
+
+ - - - + + + + + + @@ -3760,26 +3901,50 @@ exports[`AddCavcRemandView renders correctly 1`] = `
-
- - -
+ + + + +
+ diff --git a/client/test/app/queue/cavc/__snapshots__/EditCavcRemandForm.test.js.snap b/client/test/app/queue/cavc/__snapshots__/EditCavcRemandForm.test.js.snap index 5a08d891aff..7bf687af63d 100644 --- a/client/test/app/queue/cavc/__snapshots__/EditCavcRemandForm.test.js.snap +++ b/client/test/app/queue/cavc/__snapshots__/EditCavcRemandForm.test.js.snap @@ -128,7 +128,6 @@ exports[`EditCavcRemandForm adding new all feature toggles enabled renders corre aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="judge-label" aria-owns="judge-listbox" autocapitalize="none" autocomplete="off" @@ -535,7 +534,7 @@ exports[`EditCavcRemandForm adding new with Remand decision renders correctly wi aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="judge-label" + aria-label="Panel" aria-owns="judge-listbox" autocapitalize="none" autocomplete="off" @@ -1022,7 +1021,7 @@ exports[`EditCavcRemandForm adding new with Remand decision renders correctly wi aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="judge-label" + aria-label="Panel" aria-owns="judge-listbox" autocapitalize="none" autocomplete="off" @@ -1509,7 +1508,7 @@ exports[`EditCavcRemandForm adding new with Remand decision renders correctly wi aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="judge-label" + aria-label="Panel" aria-owns="judge-listbox" autocapitalize="none" autocomplete="off" @@ -2011,7 +2010,7 @@ exports[`EditCavcRemandForm adding new with Reversal decision renders correctly aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="judge-label" + aria-label="Panel" aria-owns="judge-listbox" autocapitalize="none" autocomplete="off" @@ -2468,7 +2467,7 @@ exports[`EditCavcRemandForm adding new with Reversal decision renders correctly aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="judge-label" + aria-label="Panel" aria-owns="judge-listbox" autocapitalize="none" autocomplete="off" @@ -2939,7 +2938,7 @@ exports[`EditCavcRemandForm editing existing all feature toggles enabled renders aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="judge-label" + aria-label="Panel" aria-owns="judge-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/queue/colocatedTasks/AddAdminTaskForm/__snapshots__/AddAdminTaskForm.test.js.snap b/client/test/app/queue/colocatedTasks/AddAdminTaskForm/__snapshots__/AddAdminTaskForm.test.js.snap index 2d53c5ceedb..1afd574c186 100644 --- a/client/test/app/queue/colocatedTasks/AddAdminTaskForm/__snapshots__/AddAdminTaskForm.test.js.snap +++ b/client/test/app/queue/colocatedTasks/AddAdminTaskForm/__snapshots__/AddAdminTaskForm.test.js.snap @@ -65,7 +65,6 @@ exports[`AddAdminTaskForm renders correctly 1`] = ` aria-autocomplete="list" aria-expanded="false" aria-haspopup="true" - aria-labelledby="new-tasks-0-type-label" aria-owns="new-tasks-0-type-listbox" autocapitalize="none" autocomplete="off" diff --git a/client/test/app/queue/components/__snapshots__/CavcReviewExtensionRequestModal.test.js.snap b/client/test/app/queue/components/__snapshots__/CavcReviewExtensionRequestModal.test.js.snap index 6feb4adbfea..3626bc5daaf 100644 --- a/client/test/app/queue/components/__snapshots__/CavcReviewExtensionRequestModal.test.js.snap +++ b/client/test/app/queue/components/__snapshots__/CavcReviewExtensionRequestModal.test.js.snap @@ -157,62 +157,112 @@ exports[`CavcReviewExtensionRequestModal renders correctly 1`] = `
-
- - - -
+ + + + - Task will go on hold for selected number of days -
-
-
-
+ Task will go on hold for selected number of days +
+ +
+ + - - - -
+ + + + - Marks the extension request as denied -
-
- +
+ Marks the extension request as denied +
+ + +
@@ -261,6 +311,7 @@ exports[`CavcReviewExtensionRequestModal renders correctly 1`] = ` className="cf-modal-controls" > +)); + +jest.mock('app/components/TextareaField', () => ({ onChange, value }) => ( +