From 3abc63c77ff7d110d572f9ba7e071269f28de941 Mon Sep 17 00:00:00 2001 From: Jim Foley Date: Tue, 26 Dec 2023 07:38:24 -0700 Subject: [PATCH 001/237] Merge branch 'master' into feature/APPEALS-27311 (#20342) * Initial Commit * Running tests * Adding appellant phone number to serializer * Adding paper case attribute * Add attributes to search hash * add KNAPSACK_PRO_FIXED_QUEUE_SPLIT to workflow.yml (#20329) * add KNAPSACK_PRO_FIXED_QUEUE_SPLIT to workflow.yml * update deprecated method for knapsack * add comment --------- Co-authored-by: AimanK Co-authored-by: raymond-hughes Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> --- .github/workflows/workflow.yml | 1 + .../work_queue/appeal_search_serializer.rb | 29 +++++++++++++++++++ client/app/queue/VeteranCasesView.jsx | 4 +-- client/app/queue/utils.js | 8 +++++ spec/spec_helper.rb | 4 +-- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 8b5b8a19bda..224d0b7488e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -72,6 +72,7 @@ jobs: KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }} KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }} KNAPSACK_PRO_LOG_LEVEL: info + KNAPSACK_PRO_FIXED_QUEUE_SPLIT: true WD_INSTALL_DIR: .webdrivers CI: true REDIS_URL_CACHE: redis://redis:6379/0/cache/ diff --git a/app/models/serializers/work_queue/appeal_search_serializer.rb b/app/models/serializers/work_queue/appeal_search_serializer.rb index d72ff4547a8..cdce3ff7be9 100644 --- a/app/models/serializers/work_queue/appeal_search_serializer.rb +++ b/app/models/serializers/work_queue/appeal_search_serializer.rb @@ -25,6 +25,25 @@ class WorkQueue::AppealSearchSerializer attribute :status + attribute :decision_issues do |object, params| + if params[:user].nil? + fail Caseflow::Error::MissingRequiredProperty, message: "Params[:user] is required" + end + + decision_issues = AppealDecisionIssuesPolicy.new(appeal: object, user: params[:user]).visible_decision_issues + decision_issues.uniq.map do |issue| + { + id: issue.id, + disposition: issue.disposition, + description: issue.description, + benefit_type: issue.benefit_type, + remand_reasons: issue.remand_reasons, + diagnostic_code: issue.diagnostic_code, + request_issue_ids: issue.request_decision_issues.pluck(:request_issue_id) + } + end + end + attribute(:hearings) do |object, params| # For substitution appeals after death dismissal, we need to show hearings from the source appeal # in addition to those on the new/target appeal; this avoids copying them to new appeal stream @@ -53,6 +72,10 @@ class WorkQueue::AppealSearchSerializer object.claimant&.name end + attribute :appellant_phone_number do |object| + object.claimant&.unrecognized_claimant? ? object.claimant&.phone_number : nil + end + attribute :veteran_death_date attribute :veteran_file_number @@ -61,6 +84,8 @@ class WorkQueue::AppealSearchSerializer object.veteran ? object.veteran.name.formatted(:readable_full) : "Cannot locate" end + attribute(:available_hearing_locations) { |object| available_hearing_locations(object) } + attribute :external_id, &:uuid attribute :type @@ -73,6 +98,10 @@ class WorkQueue::AppealSearchSerializer attribute :nod_date, &:receipt_date attribute :withdrawal_date + attribute :paper_case do + false + end + attribute :caseflow_veteran_id do |object| object.veteran ? object.veteran.id : nil end diff --git a/client/app/queue/VeteranCasesView.jsx b/client/app/queue/VeteranCasesView.jsx index 7498fc42c9e..43f23fa5149 100644 --- a/client/app/queue/VeteranCasesView.jsx +++ b/client/app/queue/VeteranCasesView.jsx @@ -12,7 +12,7 @@ import { setFetchedAllCasesFor } from './CaseList/CaseListActions'; import { hideVeteranCaseList } from './uiReducer/uiActions'; import { onReceiveAppealDetails } from './QueueActions'; import { appealsByCaseflowVeteranId } from './selectors'; -import { prepareAppealForStore } from './utils'; +import { prepareAppealForSearchStore } from './utils'; import COPY from '../../COPY'; import WindowUtil from '../util/WindowUtil'; @@ -50,7 +50,7 @@ class VeteranCasesView extends React.PureComponent { return Promise.reject(response); } - this.props.onReceiveAppealDetails(prepareAppealForStore(returnedObject.appeals)); + this.props.onReceiveAppealDetails(prepareAppealForSearchStore(returnedObject.appeals)); this.props.setFetchedAllCasesFor(caseflowVeteranId); return Promise.resolve(); diff --git a/client/app/queue/utils.js b/client/app/queue/utils.js index c02b4448e80..67b49d19d4e 100644 --- a/client/app/queue/utils.js +++ b/client/app/queue/utils.js @@ -556,16 +556,21 @@ export const prepareAppealForSearchStore = (appeals) => { distributedToJudge: appeal.attributes.distributed_to_a_judge, veteranFullName: appeal.attributes.veteran_full_name, veteranFileNumber: appeal.attributes.veteran_file_number, + isPaperCase: appeal.attributes.paper_case, vacateType: appeal.attributes.vacate_type, }; return accumulator; }, {}); + const appealDetailsHash = appeals.reduce((accumulator, appeal) => { accumulator[appeal.attributes.external_id] = { hearings: prepareAppealHearingsForStore(appeal), + issues: prepareAppealIssuesForStore(appeal), + decisionIssues: appeal.attributes.decision_issues, appellantFullName: appeal.attributes.appellant_full_name, + appellantPhoneNumber: appeal.attributes.appellant_phone_number, contestedClaim: appeal.attributes.contested_claim, assignedToLocation: appeal.attributes.assigned_to_location, veteranParticipantId: appeal.attributes.veteran_participant_id, @@ -573,6 +578,9 @@ export const prepareAppealForSearchStore = (appeals) => { status: appeal.attributes.status, decisionDate: appeal.attributes.decision_date, caseflowVeteranId: appeal.attributes.caseflow_veteran_id, + availableHearingLocations: prepareAppealAvailableHearingLocationsForStore( + appeal + ), locationHistory: prepareLocationHistoryForStore(appeal), }; diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 115b5eab77c..b4007c966c3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -61,8 +61,8 @@ # Allows us to use shorthand FactoryBot methods. config.include FactoryBot::Syntax::Methods - config.filter_run focus: true - config.run_all_when_everything_filtered = true + # allows test suite to only run tests tagged with :focus for specific testing + config.filter_run_when_matching :focus # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. From 9ae8f98dfc64c265d5353530dcc7067af9646299 Mon Sep 17 00:00:00 2001 From: HunJerBAH Date: Tue, 26 Dec 2023 12:19:45 -0500 Subject: [PATCH 002/237] Revert "Revert "Feature/appeals 27311" (#20343)" This reverts commit 084cd9819ca5e2151cd6daadb0273075d362cf66. --- Gemfile | 1 + Gemfile.lock | 17 + Makefile.example | 5 + app/controllers/appeals_controller.rb | 5 + app/controllers/application_controller.rb | 2 +- .../concerns/explain_timeline_concern.rb | 1 + app/controllers/correspondence_controller.rb | 357 +++++++++++ .../correspondence_document_controller.rb | 15 + .../correspondence_tasks_controller.rb | 73 +++ app/controllers/explain_controller.rb | 36 +- app/controllers/team_management_controller.rb | 3 +- app/models/appeal.rb | 2 + app/models/auto_text.rb | 4 + app/models/cached_appeal.rb | 1 + app/models/correspondence.rb | 45 ++ app/models/correspondence_document.rb | 40 ++ app/models/correspondence_intake.rb | 9 + app/models/correspondence_relation.rb | 35 ++ app/models/correspondence_type.rb | 5 + app/models/correspondences_appeal.rb | 6 + .../legacy_tasks/attorney_legacy_task.rb | 9 +- .../organizations/mail_team_supervisor.rb | 12 + app/models/package_document_type.rb | 5 + .../work_queue/appeal_serializer.rb | 4 + .../correspondence_document_serializer.rb | 17 + app/models/task.rb | 8 +- .../correspondence/reassign_package_task.rb | 4 + .../tasks/correspondence_intake_task.rb | 42 ++ app/models/tasks/correspondence_root_task.rb | 4 + app/models/tasks/correspondence_task.rb | 41 ++ .../tasks/efolder_upload_failed_task.rb | 4 + app/models/tasks/merge_package_task.rb | 4 + app/models/tasks/remove_package_task.rb | 4 + app/models/tasks/review_package_task.rb | 31 + app/models/tasks/split_package_task.rb | 4 + app/models/vbms_document_type.rb | 5 + app/models/veteran.rb | 1 + app/queries/appeals_updated_since_query.rb | 2 + app/views/appeals/edit.html.erb | 1 + .../application/under_construction.html.erb | 11 + .../correspondence_cases.html.erb | 17 + app/views/correspondence/intake.html.erb | 15 + .../correspondence/review_package.html.erb | 14 + app/views/explain/show.html.erb | 182 ++++++ app/views/queue/index.html.erb | 1 + .../correspondence_root_task_factory.rb | 49 ++ client/COPY.json | 30 +- client/app/components/Dropdown.jsx | 2 +- .../Pagination/CorrespondencePagination.jsx | 161 +++++ .../app/components/Pagination/Pagination.jsx | 7 +- client/app/components/ReactSelectDropdown.jsx | 25 +- .../icons/fontAwesome/UnlinkIcon.jsx | 38 ++ client/app/containers/UnderConstruction.jsx | 47 ++ client/app/index.js | 4 +- client/app/nonComp/components/Alerts.jsx | 2 + client/app/queue/CaseDetailsLink.jsx | 30 +- client/app/queue/CaseListTable.jsx | 99 +++- client/app/queue/OrganizationQueue.jsx | 7 +- client/app/queue/QueueApp.jsx | 48 +- client/app/queue/QueueTableBuilder.jsx | 3 +- .../components/QueueOrganizationDropdown.jsx | 17 +- client/app/queue/constants.js | 8 +- .../correspondence/CorrespondenceCases.jsx | 82 +++ .../CorrespondencePaginationWrapper.jsx | 52 ++ .../correspondence/CorrespondenceTable.jsx | 71 +++ .../component/EditDocumentTypeModal.jsx | 123 ++++ .../correspondenceActions.js | 126 ++++ .../correspondenceConstants.js | 15 + .../correspondenceReducer.js | 126 ++++ .../reviewPackageActions.js | 63 ++ .../reviewPackageConstants.js | 8 + .../reviewPackageReducer.js | 77 +++ .../AddCorrespondenceView.jsx | 271 +++++++++ .../intake/components/CheckboxModal.jsx | 100 ++++ .../ConfirmCorrespondenceView.jsx | 241 ++++++++ .../ConfirmTasksNotRelatedToAnAppeal.jsx | 45 ++ .../ConfirmTasksRelatedToAnAppeal.jsx | 154 +++++ .../CorrespondenceDetailsTable.jsx | 53 ++ .../SubmitCorrespondenceModal.jsx | 101 ++++ .../components/CorrespondenceIntake.jsx | 228 +++++++ .../TasksAppeals/AddAppealRelatedTaskView.jsx | 213 +++++++ .../AddEvidenceSubmissionTaskView.jsx | 86 +++ .../components/TasksAppeals/AddTaskView.jsx | 203 +++++++ .../TasksAppeals/AddTasksAppealsView.jsx | 134 +++++ .../TasksAppeals/AddUnrelatedTaskView.jsx | 144 +++++ .../TasksAppeals/ExistingAppealTasksView.jsx | 147 +++++ .../modals/PackageActionModal.jsx | 213 +++++++ .../queue/correspondence/modals/editModal.jsx | 214 +++++++ .../pdfPreview/CorrespondencePdfFooter.jsx | 50 ++ .../pdfPreview/CorrespondencePdfPage.jsx | 45 ++ .../pdfPreview/CorrespondencePdfToolBar.jsx | 113 ++++ .../pdfPreview/CorrespondencePdfUI.jsx | 267 +++++++++ .../review_package/CmpDocuments.jsx | 100 ++++ .../CorrespondenceReviewPackage.jsx | 235 ++++++++ .../review_package/ReviewForm.jsx | 295 +++++++++ .../review_package/ReviewPackageCaseTitle.jsx | 96 +++ .../review_package/ReviewPackageData.jsx | 111 ++++ .../ReviewPackageLoadingScreen.jsx | 81 +++ .../ReviewPackageNotificationBanner.jsx | 24 + .../correspondence/review_package/utils.js | 136 +++++ client/app/queue/reducers.js | 6 +- client/app/queue/utils.js | 6 + client/app/styles/_main.scss | 4 + client/app/styles/_table.scss | 58 ++ client/app/styles/app.scss | 1 + client/app/styles/queue/_correspondence.scss | 96 +++ .../QUEUE_INTAKE_FORM_TASK_TYPES.json | 194 ++++++ client/constants/TASK_ACTIONS.json | 4 + client/constants/TASK_STATUSES.json | 3 +- .../correspondences/CmpDocuments.test.js | 19 + .../CorrespondencePdfUI.test.js | 69 +++ .../CorrespondenceReviewPackage.test.js | 124 ++++ .../queue/correspondences/ReviewForm.test.js | 49 ++ .../correspondences/ReviewPackageData.test.js | 35 ++ .../modals/PackageActionModal.test.js | 158 +++++ client/test/data/correspondence.js | 65 ++ config/brakeman.ignore | 26 +- config/routes.rb | 21 + .../20231018185238_create_correspondence.rb | 25 + ...18192834_create_correspondence_document.rb | 10 + ...023155854_update_cmp_packet_number_type.rb | 13 + ...espondence_reference_to_correspondences.rb | 8 + ...d_timestamps_to_correspondence_document.rb | 6 + ...026122835_create_correspondence_intakes.rb | 17 + ...7_add_indexes_to_correspondence_intakes.rb | 6 + ...93346_create_correspondence_types_table.rb | 13 + ...031041759_create_package_document_types.rb | 9 + ...or_correspondence_id_on_correspondences.rb | 5 + ...231107103628_create_vbms_document_types.rb | 8 + ..._type_pages_to_correspondence_documents.rb | 6 + ...doc_type_id_in_correspondence_documents.rb | 15 + db/migrate/20231121184716_create_auto_text.rb | 7 + ...04193100_create_correspondences_appeals.rb | 10 + ...safe_indices_to_correspondences_appeals.rb | 6 + ...5160831_create_correspondence_relations.rb | 10 + ...afe_indices_to_correspondence_relations.rb | 6 + ..._correspondence_id_from_correspondences.rb | 5 + ...31214200955_drop_correspondence_intakes.rb | 21 + ...4201518_recreate_correspondence_intakes.rb | 16 + db/schema.rb | 138 ++++- db/seeds.rb | 6 + db/seeds/auto_texts.rb | 41 ++ db/seeds/correspondence.rb | 185 ++++++ db/seeds/correspondence_types.rb | 45 ++ db/seeds/multi_correspondences.rb | 167 ++++++ db/seeds/package_document_types.rb | 26 + db/seeds/users.rb | 10 + db/seeds/vbms_document_types.rb | 31 + lib/tasks/correspondence.rake | 561 ++++++++++++++++++ .../correspondence_controller_spec.rb | 205 +++++++ ...correspondence_document_controller_spec.rb | 24 + .../correspondence_tasks_controller_spec.rb | 91 +++ spec/controllers/explain_controller_spec.rb | 68 ++- spec/factories/correspondence.rb | 31 + spec/feature/explain_spec.rb | 42 +- .../add_related_correspondence_spec.rb | 151 +++++ .../correspondence_cases_spec.rb | 32 + .../edit_cmp_information_spec.rb | 155 +++++ .../correspondence/intake_form_persistence.rb | 51 ++ .../queue/correspondence/intake_form_spec.rb | 45 ++ .../queue/correspondence/intake_spec.rb | 343 +++++++++++ .../correspondence/mail_tasks_confirm_spec.rb | 66 +++ .../correspondence/review_package_spec.rb | 102 ++++ .../correspondence/submit_intake_form_spec.rb | 142 +++++ ...asks_related_to_an_existing_appeal_spec.rb | 304 ++++++++++ spec/models/correspondence_document_spec.rb | 35 ++ spec/models/correspondence_intake_spec.rb | 27 + spec/models/correspondence_spec.rb | 68 +++ spec/models/correspondences_appeal_spec.rb | 29 + ...l_notification_letter_holding_task_spec.rb | 2 - .../correspondence_controller_spec.rb | 50 ++ spec/seeds/correspondence.rb | 32 + spec/seeds/package_document_types.rb | 12 + spec/seeds/users_spec.rb | 4 +- spec/seeds/vbms_document_types.rb | 12 + spec/support/correspondence_helpers.rb | 114 ++++ vbms doc types.json | 100 ++++ 177 files changed, 11150 insertions(+), 81 deletions(-) create mode 100644 app/controllers/correspondence_controller.rb create mode 100644 app/controllers/correspondence_document_controller.rb create mode 100644 app/controllers/correspondence_tasks_controller.rb create mode 100644 app/models/auto_text.rb create mode 100644 app/models/correspondence.rb create mode 100644 app/models/correspondence_document.rb create mode 100644 app/models/correspondence_intake.rb create mode 100644 app/models/correspondence_relation.rb create mode 100644 app/models/correspondence_type.rb create mode 100644 app/models/correspondences_appeal.rb create mode 100644 app/models/organizations/mail_team_supervisor.rb create mode 100644 app/models/package_document_type.rb create mode 100644 app/models/serializers/work_queue/correspondence_document_serializer.rb create mode 100644 app/models/tasks/correspondence/reassign_package_task.rb create mode 100644 app/models/tasks/correspondence_intake_task.rb create mode 100644 app/models/tasks/correspondence_root_task.rb create mode 100644 app/models/tasks/correspondence_task.rb create mode 100644 app/models/tasks/efolder_upload_failed_task.rb create mode 100644 app/models/tasks/merge_package_task.rb create mode 100644 app/models/tasks/remove_package_task.rb create mode 100644 app/models/tasks/review_package_task.rb create mode 100644 app/models/tasks/split_package_task.rb create mode 100644 app/models/vbms_document_type.rb create mode 100644 app/views/application/under_construction.html.erb create mode 100644 app/views/correspondence/correspondence_cases.html.erb create mode 100644 app/views/correspondence/intake.html.erb create mode 100644 app/views/correspondence/review_package.html.erb create mode 100644 app/workflows/correspondence_root_task_factory.rb create mode 100644 client/app/components/Pagination/CorrespondencePagination.jsx create mode 100644 client/app/components/icons/fontAwesome/UnlinkIcon.jsx create mode 100644 client/app/containers/UnderConstruction.jsx create mode 100644 client/app/queue/correspondence/CorrespondenceCases.jsx create mode 100644 client/app/queue/correspondence/CorrespondencePaginationWrapper.jsx create mode 100644 client/app/queue/correspondence/CorrespondenceTable.jsx create mode 100644 client/app/queue/correspondence/component/EditDocumentTypeModal.jsx create mode 100644 client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js create mode 100644 client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js create mode 100644 client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js create mode 100644 client/app/queue/correspondence/correspondenceReducer/reviewPackageActions.js create mode 100644 client/app/queue/correspondence/correspondenceReducer/reviewPackageConstants.js create mode 100644 client/app/queue/correspondence/correspondenceReducer/reviewPackageReducer.js create mode 100644 client/app/queue/correspondence/intake/components/AddCorrespondence/AddCorrespondenceView.jsx create mode 100644 client/app/queue/correspondence/intake/components/CheckboxModal.jsx create mode 100644 client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmCorrespondenceView.jsx create mode 100644 client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksNotRelatedToAnAppeal.jsx create mode 100644 client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksRelatedToAnAppeal.jsx create mode 100644 client/app/queue/correspondence/intake/components/ConfirmCorrespondence/CorrespondenceDetailsTable.jsx create mode 100644 client/app/queue/correspondence/intake/components/ConfirmCorrespondence/SubmitCorrespondenceModal.jsx create mode 100644 client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx create mode 100644 client/app/queue/correspondence/intake/components/TasksAppeals/AddAppealRelatedTaskView.jsx create mode 100644 client/app/queue/correspondence/intake/components/TasksAppeals/AddEvidenceSubmissionTaskView.jsx create mode 100644 client/app/queue/correspondence/intake/components/TasksAppeals/AddTaskView.jsx create mode 100644 client/app/queue/correspondence/intake/components/TasksAppeals/AddTasksAppealsView.jsx create mode 100644 client/app/queue/correspondence/intake/components/TasksAppeals/AddUnrelatedTaskView.jsx create mode 100644 client/app/queue/correspondence/intake/components/TasksAppeals/ExistingAppealTasksView.jsx create mode 100644 client/app/queue/correspondence/modals/PackageActionModal.jsx create mode 100644 client/app/queue/correspondence/modals/editModal.jsx create mode 100644 client/app/queue/correspondence/pdfPreview/CorrespondencePdfFooter.jsx create mode 100644 client/app/queue/correspondence/pdfPreview/CorrespondencePdfPage.jsx create mode 100644 client/app/queue/correspondence/pdfPreview/CorrespondencePdfToolBar.jsx create mode 100644 client/app/queue/correspondence/pdfPreview/CorrespondencePdfUI.jsx create mode 100644 client/app/queue/correspondence/review_package/CmpDocuments.jsx create mode 100644 client/app/queue/correspondence/review_package/CorrespondenceReviewPackage.jsx create mode 100644 client/app/queue/correspondence/review_package/ReviewForm.jsx create mode 100644 client/app/queue/correspondence/review_package/ReviewPackageCaseTitle.jsx create mode 100644 client/app/queue/correspondence/review_package/ReviewPackageData.jsx create mode 100644 client/app/queue/correspondence/review_package/ReviewPackageLoadingScreen.jsx create mode 100644 client/app/queue/correspondence/review_package/ReviewPackageNotificationBanner.jsx create mode 100644 client/app/queue/correspondence/review_package/utils.js create mode 100644 client/app/styles/queue/_correspondence.scss create mode 100644 client/constants/QUEUE_INTAKE_FORM_TASK_TYPES.json create mode 100644 client/test/app/queue/correspondences/CmpDocuments.test.js create mode 100644 client/test/app/queue/correspondences/CorrespondencePdfUI.test.js create mode 100644 client/test/app/queue/correspondences/CorrespondenceReviewPackage.test.js create mode 100644 client/test/app/queue/correspondences/ReviewForm.test.js create mode 100644 client/test/app/queue/correspondences/ReviewPackageData.test.js create mode 100644 client/test/app/queue/correspondences/modals/PackageActionModal.test.js create mode 100644 client/test/data/correspondence.js create mode 100644 db/migrate/20231018185238_create_correspondence.rb create mode 100644 db/migrate/20231018192834_create_correspondence_document.rb create mode 100644 db/migrate/20231023155854_update_cmp_packet_number_type.rb create mode 100644 db/migrate/20231024151721_add_prior_correspondence_reference_to_correspondences.rb create mode 100644 db/migrate/20231024162133_add_timestamps_to_correspondence_document.rb create mode 100644 db/migrate/20231026122835_create_correspondence_intakes.rb create mode 100644 db/migrate/20231026123217_add_indexes_to_correspondence_intakes.rb create mode 100644 db/migrate/20231027193346_create_correspondence_types_table.rb create mode 100644 db/migrate/20231031041759_create_package_document_types.rb create mode 100644 db/migrate/20231101163717_change_prior_correspondence_id_on_correspondences.rb create mode 100644 db/migrate/20231107103628_create_vbms_document_types.rb create mode 100644 db/migrate/20231108075643_add_document_type_pages_to_correspondence_documents.rb create mode 100644 db/migrate/20231115170454_update_vbms_doc_type_id_in_correspondence_documents.rb create mode 100644 db/migrate/20231121184716_create_auto_text.rb create mode 100644 db/migrate/20231204193100_create_correspondences_appeals.rb create mode 100644 db/migrate/20231204195133_add_safe_indices_to_correspondences_appeals.rb create mode 100644 db/migrate/20231205160831_create_correspondence_relations.rb create mode 100644 db/migrate/20231205161301_add_safe_indices_to_correspondence_relations.rb create mode 100644 db/migrate/20231205171256_remove_prior_correspondence_id_from_correspondences.rb create mode 100644 db/migrate/20231214200955_drop_correspondence_intakes.rb create mode 100644 db/migrate/20231214201518_recreate_correspondence_intakes.rb create mode 100644 db/seeds/auto_texts.rb create mode 100644 db/seeds/correspondence.rb create mode 100644 db/seeds/correspondence_types.rb create mode 100644 db/seeds/multi_correspondences.rb create mode 100644 db/seeds/package_document_types.rb create mode 100644 db/seeds/vbms_document_types.rb create mode 100644 lib/tasks/correspondence.rake create mode 100644 spec/controllers/correspondence_controller_spec.rb create mode 100644 spec/controllers/correspondence_document_controller_spec.rb create mode 100644 spec/controllers/correspondence_tasks_controller_spec.rb create mode 100644 spec/factories/correspondence.rb create mode 100644 spec/feature/queue/correspondence/add_related_correspondence_spec.rb create mode 100644 spec/feature/queue/correspondence/correspondence_cases_spec.rb create mode 100644 spec/feature/queue/correspondence/edit_cmp_information_spec.rb create mode 100644 spec/feature/queue/correspondence/intake_form_persistence.rb create mode 100644 spec/feature/queue/correspondence/intake_form_spec.rb create mode 100644 spec/feature/queue/correspondence/intake_spec.rb create mode 100644 spec/feature/queue/correspondence/mail_tasks_confirm_spec.rb create mode 100644 spec/feature/queue/correspondence/review_package_spec.rb create mode 100644 spec/feature/queue/correspondence/submit_intake_form_spec.rb create mode 100644 spec/feature/queue/correspondence/tasks_related_to_an_existing_appeal_spec.rb create mode 100644 spec/models/correspondence_document_spec.rb create mode 100644 spec/models/correspondence_intake_spec.rb create mode 100644 spec/models/correspondence_spec.rb create mode 100644 spec/models/correspondences_appeal_spec.rb create mode 100644 spec/requests/correspondence_controller_spec.rb create mode 100644 spec/seeds/correspondence.rb create mode 100644 spec/seeds/package_document_types.rb create mode 100644 spec/seeds/vbms_document_types.rb create mode 100644 spec/support/correspondence_helpers.rb create mode 100644 vbms doc types.json diff --git a/Gemfile b/Gemfile index 02511aa1b49..f145cd914ba 100644 --- a/Gemfile +++ b/Gemfile @@ -66,6 +66,7 @@ gem "redis-namespace" gem "redis-rails", "~> 5.0.2" gem "request_store" gem "roo", "~> 2.7" +gem "ruby_claim_evidence_api", git: "https://github.com/department-of-veterans-affairs/ruby_claim_evidence_api.git", ref: "f1686404d448e1d8d13e533400817bcab175ed65" # Use SCSS for stylesheets gem "sass-rails", "~> 5.0" # Error reporting to Sentry diff --git a/Gemfile.lock b/Gemfile.lock index aa035d911aa..6b5ba418d69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,19 @@ GIT nokogiri (>= 1.11.0.rc4) savon (~> 2.12) +GIT + remote: https://github.com/department-of-veterans-affairs/ruby_claim_evidence_api.git + revision: f1686404d448e1d8d13e533400817bcab175ed65 + ref: f1686404d448e1d8d13e533400817bcab175ed65 + specs: + ruby_claim_evidence_api (0.0.1) + activesupport + base64 + faraday + faraday-multipart + httpi + railties + GIT remote: https://github.com/department-of-veterans-affairs/sniffybara.git revision: 351560b5789ca638ba7c9b093c2bb1a9a6fda4b3 @@ -153,6 +166,7 @@ GEM aws-sdk-core (= 2.10.112) aws-sigv4 (1.0.2) backport (1.2.0) + base64 (0.2.0) benchmark-ips (2.7.2) bootsnap (1.7.5) msgpack (~> 1.0) @@ -290,6 +304,8 @@ GEM multipart-post (>= 1.2, < 3) faraday-http-cache (2.4.1) faraday (>= 0.8) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday_middleware (0.13.1) faraday (>= 0.7.4, < 1.0) fast_jsonapi (1.5) @@ -815,6 +831,7 @@ DEPENDENCIES ruby-debug-ide ruby-oci8 (~> 2.2) ruby-prof (~> 1.4) + ruby_claim_evidence_api! sass-rails (~> 5.0) scss_lint sentry-raven diff --git a/Makefile.example b/Makefile.example index 45cc6e80d09..af35e88cb32 100644 --- a/Makefile.example +++ b/Makefile.example @@ -113,6 +113,11 @@ realclean: clean ## TODO rm -rf client/node_modules rm -f client/package-lock.json +precompile: ## Precompiles assets for testing + bundle exec rake assets:precompile + +yeet: clean precompile ## Cleans and precompiles assets for testing + facols-bash: ## Connect to the docker FACOLS instance docker exec --tty -i VACOLS_DB bash diff --git a/app/controllers/appeals_controller.rb b/app/controllers/appeals_controller.rb index 4685e39f5d1..85f77f8729f 100644 --- a/app/controllers/appeals_controller.rb +++ b/app/controllers/appeals_controller.rb @@ -188,6 +188,11 @@ def update end end + def active_evidence_submissions + appeal = Appeal.find(params[:appeal_id]) + render json: appeal.evidence_submission_task + end + private # :reek:DuplicateMethodCall { allow_calls: ['result.extra'] } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bb3e4cc7797..a53cfc593b1 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| 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/correspondence_controller.rb b/app/controllers/correspondence_controller.rb new file mode 100644 index 00000000000..21b8f85057f --- /dev/null +++ b/app/controllers/correspondence_controller.rb @@ -0,0 +1,357 @@ +# frozen_string_literal: true + +class CorrespondenceController < ApplicationController + before_action :verify_correspondence_access + before_action :verify_feature_toggle + before_action :correspondence + before_action :auto_texts + before_action :veteran_information + + def intake + respond_to do |format| + format.html { return render "correspondence/intake" } + format.json do + render json: { + currentCorrespondence: current_correspondence, + correspondence: correspondence_load, + veteranInformation: veteran_information + } + end + end + end + + def current_step + intake = CorrespondenceIntake.find_by(user: current_user, correspondence: current_correspondence) || + CorrespondenceIntake.new(user: current_user, correspondence: current_correspondence) + + intake.update( + current_step: params[:current_step], + redux_store: params[:redux_store] + ) + + if intake.valid? + intake.save! + + render json: {}, status: :ok and return + else + render json: intake.errors.full_messages, status: :unprocessable_entity and return + end + end + + def correspondence_cases + respond_to do |format| + format.html { "correspondence_cases" } + format.json do + render json: { vetCorrespondences: veterans_with_correspondences } + end + end + end + + def review_package + render "correspondence/review_package" + end + + def intake_update + tasks = Task.where("appeal_id = ? and appeal_type = ?", @correspondence.id, "Correspondence") + begin + tasks.map do |task| + if task.type == "ReviewPackageTask" + task.instructions.push("An appeal intake was started because this Correspondence is a 10182") + task.assigned_to_id = @correspondence.assigned_by_id + task.assigned_to = User.find(@correspondence.assigned_by_id) + end + task.status = "cancelled" + task.save + end + if upload_documents_to_claim_evidence + render json: { correspondence: @correspondence } + else + render json: {}, status: bad_request + end + rescue StandardError => error + Rails.logger.error(error.to_s) + render json: {}, status: bad_request + end + end + + def veteran + render json: { veteran_id: veteran_by_correspondence&.id, file_number: veteran_by_correspondence&.file_number } + end + + def package_documents + packages = PackageDocumentType.all + render json: { package_document_types: packages } + end + + def current_correspondence + @current_correspondence ||= correspondence + end + + def veteran_information + @veteran_information ||= veteran_by_correspondence + end + + def show + corres_docs = correspondence.correspondence_documents + response_json = { + correspondence: correspondence, + package_document_type: correspondence&.package_document_type, + general_information: general_information, + user_can_edit_vador: MailTeamSupervisor.singleton.user_has_access?(current_user), + correspondence_documents: corres_docs.map do |doc| + WorkQueue::CorrespondenceDocumentSerializer.new(doc).serializable_hash[:data][:attributes] + end + } + render({ json: response_json }, status: :ok) + end + + def update + veteran = Veteran.find_by(file_number: veteran_params["file_number"]) + if veteran && correspondence.update( + correspondence_params.merge( + veteran_id: veteran.id, + updated_by_id: RequestStore.store[:current_user].id + ) + ) + render json: { status: :ok } + else + render json: { error: "Please enter a valid Veteran ID" }, status: :unprocessable_entity + end + end + + def update_cmp + correspondence.update( + va_date_of_receipt: params["VADORDate"].in_time_zone, + package_document_type_id: params["packageDocument"]["value"].to_i + ) + render json: { status: 200, correspondence: correspondence } + end + + def document_type_correspondence + data = vbms_document_types + render json: { data: data } + end + + # :reek:UtilityFunction + def vbms_document_types + data = ExternalApi::ClaimEvidenceService.document_types + data["documentTypes"].map { |document_type| { id: document_type["id"], name: document_type["name"] } } + end + + def pdf + document = Document.find(params[:pdf_id]) + + document_disposition = "inline" + if params[:download] + document_disposition = "attachment; filename='#{params[:type]}-#{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 + + def process_intake + correspondence_id = Correspondence.find_by(uuid: params[:correspondence_uuid])&.id + ActiveRecord::Base.transaction do + begin + create_correspondence_relations(correspondence_id) + link_appeals_to_correspondence(correspondence_id) + add_tasks_to_related_appeals(correspondence_id) + complete_waived_evidence_submission_tasks(correspondence_id) + rescue ActiveRecord::RecordInvalid + render json: { error: "Failed to update records" }, status: :bad_request + raise ActiveRecord::Rollback + rescue ActiveRecord::RecordNotUnique + render json: { error: "Failed to update records" }, status: :bad_request + raise ActiveRecord::Rollback + else + set_flash_intake_success_message + render json: {}, status: :created + end + end + end + + private + + def vbms_document_types + begin + data = ExternalApi::ClaimEvidenceService.document_types + rescue StandardError => error + Rails.logger.error(error.full_message) + data ||= demo_data + end + data["documentTypes"].map { |document_type| { id: document_type["id"], name: document_type["description"] } } + end + + def demo_data + json_file_path = "vbms doc types.json" + JSON.parse(File.read(json_file_path)) + 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 + + def create_correspondence_relations(correspondence_id) + 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 link_appeals_to_correspondence(correspondence_id) + params[:related_appeal_ids].map do |appeal_id| + CorrespondencesAppeal.find_or_create_by(correspondence_id: correspondence_id, appeal_id: appeal_id) + end + end + + def complete_waived_evidence_submission_tasks(correspondence_id) + 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 add_tasks_to_related_appeals(correspondence_id) + params[:tasks_related_to_appeal]&.map do |data| + appeal = Appeal.find(data[:appeal_id]) + data[:klass].constantize.create_from_params( + { + appeal: appeal, + parent_id: appeal.root_task&.id, + assigned_to: data[:assigned_to].constantize.singleton, + instructions: data[:content] + }, current_user + ) + end + end + + def verify_correspondence_access + return true if MailTeamSupervisor.singleton.user_has_access?(current_user) || + MailTeam.singleton.user_has_access?(current_user) + + redirect_to "/unauthorized" + 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_types: CorrespondenceType.all + } + end + + def correspondence_params + params.require(:correspondence).permit(:notes, :correspondence_type_id) + end + + def veteran_params + params.require(:veteran).permit(:file_number) + end + + def verify_feature_toggle + if !FeatureToggle.enabled?(:correspondence_queue) + redirect_to "/unauthorized" + end + end + + def correspondence + return @correspondence if @correspondence.present? + + if params[:id].present? + @correspondence = Correspondence.find(params[:id]) + elsif params[:correspondence_uuid].present? + @correspondence = Correspondence.find_by(uuid: params[:correspondence_uuid]) + end + + @correspondence + end + + def correspondence_load + Correspondence.where(veteran_id: veteran_by_correspondence.id).where.not(uuid: params[:correspondence_uuid]) + end + + def veteran_by_correspondence + return unless correspondence&.veteran_id + + @veteran_by_correspondence ||= begin + veteran = Veteran.find_by(id: correspondence.veteran_id) + if veteran.nil? + # Handle the case where the veteran is not found + puts "Veteran not found for ID: #{correspondence.veteran_id}" + end + veteran + end + 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 vet_info_serializer(veteran, correspondence) + { + firstName: veteran.first_name, + lastName: veteran.last_name, + fileNumber: veteran.file_number, + cmPacketNumber: correspondence.cmp_packet_number, + correspondenceUuid: correspondence.uuid, + packageDocumentType: correspondence.correspondence_type_id + } + end + + def auto_texts + @auto_texts ||= AutoText.all.pluck(:name) + end + + def upload_documents_to_claim_evidence + if Rails.env.development? || Rails.env.demo? || Rails.env.test? + true + else + begin + correspondence.correspondence_documents.all.each do |doc| + ExternalApi::ClaimEvidenceService.upload_document( + doc.pdf_location, + veteran_by_correspondence.file_number, + doc.claim_evidence_upload_json + ) + end + true + rescue StandardError => error + Rails.logger.error(error.to_s) + create_efolder_upload_failed_task + false + end + end + end + + def create_efolder_upload_failed_task + rpt = ReviewPackageTask.find_by(appeal_id: correspondence.id, type: ReviewPackageTask.name) + euft = EfolderUploadFailedTask.find_or_create_by( + appeal_id: correspondence.id, + appeal_type: "Correspondence", + type: EfolderUploadFailedTask.name, + assigned_to: current_user, + parent_id: rpt.id + ) + + euft.update!(status: Constants.TASK_STATUSES.in_progress) + end +end diff --git a/app/controllers/correspondence_document_controller.rb b/app/controllers/correspondence_document_controller.rb new file mode 100644 index 00000000000..fb080304fe3 --- /dev/null +++ b/app/controllers/correspondence_document_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "paper_trail" + +class CorrespondenceDocumentController < ApplicationController + def update_document + document = CorrespondenceDocument.find(params[:id]) + document.update!(update_params) + render json: {} + end + + def update_params + params.permit(:vbms_document_type_id) + end +end diff --git a/app/controllers/correspondence_tasks_controller.rb b/app/controllers/correspondence_tasks_controller.rb new file mode 100644 index 00000000000..f3ee50e597c --- /dev/null +++ b/app/controllers/correspondence_tasks_controller.rb @@ -0,0 +1,73 @@ +# 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: params[:correspondence_id], type: ReviewPackageTask.name) + if review_package_task.children.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: params[:instructions], + assigned_to: MailTeamSupervisor.singleton, + appeal_id: 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: MailTeamSupervisor.singleton, status: :on_hold) + render json: { status: :ok } + end + end + + def create_correspondence_intake_task + review_package_task = ReviewPackageTask.find_by(appeal_id: params[:id], type: ReviewPackageTask.name) + current_parent = review_package_task.parent + current_cit = CorrespondenceIntakeTask.find_by(parent_id: current_parent.id, type: CorrespondenceIntakeTask.name) + + 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 + + private + + def task_to_create + case 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 d534d02cd31..56b8bb5e14e 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, + helper_method :legacy_appeal?, :correspondence?, :appeal, :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,12 +151,18 @@ def legacy_appeal? appeal.is_a?(LegacyAppeal) end + def correspondence? + appeal.is_a?(Correspondence) + end + def appeal @appeal ||= fetch_appeal 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/team_management_controller.rb b/app/controllers/team_management_controller.rb index 9cabc00d940..0476f5a3b6f 100644 --- a/app/controllers/team_management_controller.rb +++ b/app/controllers/team_management_controller.rb @@ -119,7 +119,8 @@ def other_orgs org.is_a?(Representative) || org.is_a?(VhaProgramOffice) || org.is_a?(VhaRegionalOffice) || - org.is_a?(EducationRpo) + org.is_a?(EducationRpo) || + org.is_a?(MailTeamSupervisor) end end diff --git a/app/models/appeal.rb b/app/models/appeal.rb index d7fad5ba102..98057f260d8 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -25,6 +25,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 :correspondences_appeals + has_many :correspondences, through: :correspondences_appeals # decision_documents is effectively a has_one until post decisional motions are supported has_many :decision_documents, as: :appeal diff --git a/app/models/auto_text.rb b/app/models/auto_text.rb new file mode 100644 index 00000000000..a7ce40e2902 --- /dev/null +++ b/app/models/auto_text.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +# this is a lookup table +class AutoText < ApplicationRecord +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/correspondence.rb b/app/models/correspondence.rb new file mode 100644 index 00000000000..465a9a5c568 --- /dev/null +++ b/app/models/correspondence.rb @@ -0,0 +1,45 @@ +# 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 + has_paper_trail + include PrintsTaskTree + + has_many :correspondence_documents + has_many :correspondence_intakes + has_many :correspondences_appeals + has_many :appeals, through: :correspondences_appeals + has_many :correspondence_relations + has_many :related_correspondences, through: :correspondence_relations, dependent: :destroy + belongs_to :correspondence_type + belongs_to :package_document_type + belongs_to :veteran + + after_create :initialize_correspondence_tasks + + def initialize_correspondence_tasks + CorrespondenceRootTaskFactory.new(self).create_root_and_sub_tasks! + end + + def type + "Correspondence" + end + + # Cannot use has_many :tasks - Task model does not contain a correspondence_id column + def tasks + Task.where(appeal_id: id, appeal_type: type) + end + + # Methods below are included to allow Correspondences to render in explain page + + # Alias for cmp_packet_number + def docket_number + cmp_packet_number + end + + # Alias for package_document_type.name + def docket_name + package_document_type.name + end +end diff --git a/app/models/correspondence_document.rb b/app/models/correspondence_document.rb new file mode 100644 index 00000000000..ad3e6e2cdda --- /dev/null +++ b/app/models/correspondence_document.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class CorrespondenceDocument < CaseflowRecord + belongs_to :correspondence + belongs_to :vbms_document_type + + S3_BUCKET_NAME = "documents" + + def pdf_name + "#{uuid}.pdf" + end + + def s3_location + "#{S3_BUCKET_NAME}/#{uuid}" + end + + def output_location + File.join(Rails.root, "tmp", "pdfs", pdf_name) + end + + def pdf_location + S3Service.fetch_file(s3_location, output_location) + end + + def claim_evidence_upload_json + { + 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 + } + }.to_json + end +end diff --git a/app/models/correspondence_intake.rb b/app/models/correspondence_intake.rb new file mode 100644 index 00000000000..6d2d884bbcf --- /dev/null +++ b/app/models/correspondence_intake.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class CorrespondenceIntake < ApplicationRecord + belongs_to :correspondence + belongs_to :user + + validates_presence_of :correspondence_id + validates_presence_of :user_id +end diff --git a/app/models/correspondence_relation.rb b/app/models/correspondence_relation.rb new file mode 100644 index 00000000000..5257ab1ae8b --- /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: :has_inverse? + after_destroy :destroy_inverses, if: :has_inverse? + + validates_presence_of :correspondence_id + validates_presence_of :related_correspondence_id + validates_numericality_of :correspondence_id + validates_numericality_of :related_correspondence_id + + def create_inverse + self.class.create(inverse_match_options) + end + + def destroy_inverses + inverses.destroy_all + end + + def has_inverse? + 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_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_appeal.rb b/app/models/correspondences_appeal.rb new file mode 100644 index 00000000000..907d172467b --- /dev/null +++ b/app/models/correspondences_appeal.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class CorrespondencesAppeal < ApplicationRecord + belongs_to :correspondence + belongs_to :appeal +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/organizations/mail_team_supervisor.rb b/app/models/organizations/mail_team_supervisor.rb new file mode 100644 index 00000000000..25782a4e1b5 --- /dev/null +++ b/app/models/organizations/mail_team_supervisor.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class MailTeamSupervisor < Organization + def self.singleton + MailTeamSupervisor.first || + MailTeamSupervisor.create(name: "Mail Team Supervisor", url: "mail-team-supervisor") + end + + def selectable_in_queue? + !Rails.env.production? + end +end diff --git a/app/models/package_document_type.rb b/app/models/package_document_type.rb new file mode 100644 index 00000000000..b5c5530e040 --- /dev/null +++ b/app/models/package_document_type.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class PackageDocumentType < ApplicationRecord + has_many :correspondences +end diff --git a/app/models/serializers/work_queue/appeal_serializer.rb b/app/models/serializers/work_queue/appeal_serializer.rb index 62409533333..d8b926efc84 100644 --- a/app/models/serializers/work_queue/appeal_serializer.rb +++ b/app/models/serializers/work_queue/appeal_serializer.rb @@ -320,4 +320,8 @@ class WorkQueue::AppealSerializer Constants.CAVC_DECISION_TYPES.settlement ]).count end + + attribute :evidence_submission_task do |object| + object.tasks.find_by(type: "EvidenceSubmissionWindowTask", status: "assigned") + 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/task.rb b/app/models/task.rb index 807bb780036..c0ed69b9a6e 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -54,7 +54,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: { @@ -154,7 +155,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) @@ -932,7 +933,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/tasks/correspondence/reassign_package_task.rb b/app/models/tasks/correspondence/reassign_package_task.rb new file mode 100644 index 00000000000..0feb2e7bcf9 --- /dev/null +++ b/app/models/tasks/correspondence/reassign_package_task.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ReassignPackageTask < ReviewPackageTask +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..a219756393a --- /dev/null +++ b/app/models/tasks/correspondence_intake_task.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class CorrespondenceIntakeTask < CorrespondenceTask + class << self + def create_from_params(params, user) + parent_task = params + params = { + instructions: [], + assigned_to: user, + appeal_id: parent_task.appeal_id, + appeal_type: "Correspondence", + status: Constants.TASK_STATUSES.in_progress, + type: "CorrespondenceIntakeTask" + } + fail Caseflow::Error::ChildTaskAssignedToSameUser if parent_of_same_type_has_same_assignee(parent_task, params) + + verify_current_user_can_create!(user) + + current_params = modify_params_for_create(params) + child = create_child_task(parent_task, user, current_params) + child + end + + def 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 + + private + + def verify_current_user_can_create!(user) + MailTeam.singleton.user_has_access?(user) + end + 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..5552936aa0e --- /dev/null +++ b/app/models/tasks/correspondence_root_task.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class CorrespondenceRootTask < CorrespondenceTask +end diff --git a/app/models/tasks/correspondence_task.rb b/app/models/tasks/correspondence_task.rb new file mode 100644 index 00000000000..e6aed2d3916 --- /dev/null +++ b/app/models/tasks/correspondence_task.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class CorrespondenceTask < Task + before_create :verify_org_task_unique + validate :status_is_valid_on_create, on: :create + validate :assignee_status_is_valid_on_create, on: :create + + def verify_org_task_unique + if Task.where( + appeal_id: appeal_id, + appeal_type: appeal_type, + type: type + ).any? + fail( + Caseflow::Error::DuplicateOrgTask, + task_type: self.class.name, + assignee_type: assigned_to.class.name + ) + end + end + + private + + def status_is_valid_on_create + if type == "ReviewPackageTask" && status != Constants.TASK_STATUSES.unassigned + update!(status: :unassigned) + elsif type != "ReviewPackageTask" && status != Constants.TASK_STATUSES.assigned + fail Caseflow::Error::InvalidStatusOnTaskCreate, task_type: type + end + + true + end + + 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 +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..3bafaf1c87f --- /dev/null +++ b/app/models/tasks/efolder_upload_failed_task.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class EfolderUploadFailedTask < ReviewPackageTask +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..fcc94564c1f --- /dev/null +++ b/app/models/tasks/merge_package_task.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class MergePackageTask < ReviewPackageTask +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..63af99adfbb --- /dev/null +++ b/app/models/tasks/remove_package_task.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class RemovePackageTask < ReviewPackageTask +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..2ef2b2c9a30 --- /dev/null +++ b/app/models/tasks/review_package_task.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ReviewPackageTask < CorrespondenceTask + class << self + def create_from_params(params, user) + parent_task = ReviewPackageTask.find(params[:parent_id]) + 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 + + def 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 + end + + def when_child_task_created(_child_task) + true + 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..d12f057fbf0 --- /dev/null +++ b/app/models/tasks/split_package_task.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class SplitPackageTask < ReviewPackageTask +end 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 37d1927a562..8e889e65566 100644 --- a/app/models/veteran.rb +++ b/app/models/veteran.rb @@ -7,6 +7,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 0e1bc75989d..58086203751 100644 --- a/app/queries/appeals_updated_since_query.rb +++ b/app/queries/appeals_updated_since_query.rb @@ -26,6 +26,8 @@ def call request_decision_issues request_issues_updates vbms_uploaded_documents + correspondences_appeals + correspondences ].freeze attr_reader :since_date diff --git a/app/views/appeals/edit.html.erb b/app/views/appeals/edit.html.erb index f543d778340..29e3c766cc1 100644 --- a/app/views/appeals/edit.html.erb +++ b/app/views/appeals/edit.html.erb @@ -18,6 +18,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), vhaPreDocketAppeals: false } 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..74298a297bc --- /dev/null +++ b/app/views/correspondence/correspondence_cases.html.erb @@ -0,0 +1,17 @@ +<% 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, + flash: flash, + 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..f508218d4c0 --- /dev/null +++ b/app/views/correspondence/intake.html.erb @@ -0,0 +1,15 @@ +<% 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)}, + featureToggles: { + correspondence_queue: FeatureToggle.enabled?(:correspondence_queue, user: current_user) + }, + correspondence: @correspondence, + autoTexts: @auto_texts, + veteranInformation: @veteran_information + }) %> +<% 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..0e8ae53b544 --- /dev/null +++ b/app/views/correspondence/review_package.html.erb @@ -0,0 +1,14 @@ +<% 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, + 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..efe7a93040c 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 %>
diff --git a/app/views/queue/index.html.erb b/app/views/queue/index.html.erb index b12500347a8..fcc3a13e934 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -50,6 +50,7 @@ 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), 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), cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), diff --git a/app/workflows/correspondence_root_task_factory.rb b/app/workflows/correspondence_root_task_factory.rb new file mode 100644 index 00000000000..668e15be1b8 --- /dev/null +++ b/app/workflows/correspondence_root_task_factory.rb @@ -0,0 +1,49 @@ +# 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! + @correspondence_task = CorrespondenceTask.find_or_create_by!( + appeal_id: @correspondence.id, + assigned_to: MailTeamSupervisor.singleton, + appeal_type: "Correspondence", + type: "CorrespondenceTask" + ) + + @correspondence_task.update(status: "on_hold") + + @root_task = CorrespondenceRootTask.find_or_create_by!( + appeal_id: @correspondence.id, + assigned_to: MailTeamSupervisor.singleton, + appeal_type: "Correspondence", + parent_id: @correspondence_task.id, + type: "CorrespondenceRootTask" + ) + + @root_task.update(status: "on_hold") + end + + def create_subtasks! + @review_package_task = ReviewPackageTask.find_or_create_by!( + appeal_id: @correspondence.id, + assigned_to: MailTeamSupervisor.singleton, + appeal_type: "Correspondence", + parent_id: @root_task.id, + type: "ReviewPackageTask" + ) + end +end diff --git a/client/COPY.json b/client/COPY.json index b92e0df6a5c..6e563a7ead0 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -50,6 +50,11 @@ "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", + "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", @@ -74,6 +79,14 @@ "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 a 10182, 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.", + "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", @@ -1394,5 +1407,20 @@ "REFRESH_POA": "Refresh POA", "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" + "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":"You have selected the following correspondence cases for reassignment to another users. Please confirm your selection(s) below:", + "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.", + "UNDER_CONSTRUCTION_MESSAGE": "This page is under construction." } 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..e8963893204 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() { @@ -125,6 +126,7 @@ class Pagination extends React.PureComponent {
{paginationSummary}
+ {this.props.table}
{paginationButtons}
@@ -139,7 +141,8 @@ Pagination.propTypes = { currentCases: PropTypes.number.isRequired, totalPages: PropTypes.number, totalCases: PropTypes.number, - updatePage: PropTypes.func.isRequired + updatePage: PropTypes.func.isRequired, + table: PropTypes.instanceOf(Table) }; export default Pagination; 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 ? +
+ + } + /> +
: +
); }; } 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 +}; + +CaseListTable.defaultProps = { + showCheckboxes: false, + paginate: false, + currentPage: 1 }; const mapStateToProps = (state) => ({ 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/QueueApp.jsx b/client/app/queue/QueueApp.jsx index 31c01fdf65d..5ea3a72a15d 100644 --- a/client/app/queue/QueueApp.jsx +++ b/client/app/queue/QueueApp.jsx @@ -8,6 +8,7 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { Route, Switch } from 'react-router-dom'; import StringUtil from '../util/StringUtil'; +import CorrespondenceCases from './correspondence/CorrespondenceCases'; import { setCanEditAod, @@ -35,6 +36,7 @@ import Footer from '@department-of-veterans-affairs/caseflow-frontend-toolkit/co import AppFrame from '../components/AppFrame'; import QueueLoadingScreen from './QueueLoadingScreen'; import CaseDetailsLoadingScreen from './CaseDetailsLoadingScreen'; +import ReviewPackageLoadingScreen from './correspondence/review_package/ReviewPackageLoadingScreen'; import AttorneyTaskListView from './AttorneyTaskListView'; import ColocatedTaskListView from './ColocatedTaskListView'; import JudgeDecisionReviewTaskListView from './JudgeDecisionReviewTaskListView'; @@ -85,6 +87,8 @@ import OrganizationUsers from './OrganizationUsers'; import OrganizationQueueLoadingScreen from './OrganizationQueueLoadingScreen'; import TeamManagement from './teamManagement/TeamManagement'; import UserManagement from './UserManagement'; +import CorrespondenceReviewPackage from './correspondence/review_package/CorrespondenceReviewPackage'; +import CorrespondenceIntake from './correspondence/intake/components/CorrespondenceIntake'; import { LOGO_COLORS } from '../constants/AppConstants'; import { PAGE_TITLES } from './constants'; @@ -612,6 +616,12 @@ class QueueApp extends React.PureComponent { ); + routedReviewPackage = (props) => ( + + + + ); + routedStartHoldModal = (props) => ; routedEndHoldModal = (props) => ; @@ -661,6 +671,14 @@ class QueueApp extends React.PureComponent { ); + routedCorrespondenceIntake = (props) => ( + + ); + + routedCorrespondenceCase = () => ( + + ); + routedCompleteHearingWithdrawalRequest = (props) => ( ); @@ -718,6 +736,14 @@ class QueueApp extends React.PureComponent { title={`${this.queueName()} | Caseflow`} render={this.routedQueueList} /> + + + + + + + + {motionToVacateRoutes.page} @@ -1402,7 +1446,6 @@ class QueueApp extends React.PureComponent { path="/team_management/lookup_participant_id" render={this.routedLookupParticipantIdModal} /> - {motionToVacateRoutes.modal} @@ -1459,6 +1502,9 @@ QueueApp.propTypes = { canEditCavcDashboards: PropTypes.bool, canViewCavcDashboards: PropTypes.bool, userIsCobAdmin: PropTypes.bool, + correspondence: PropTypes.object, + autoTexts: PropTypes.array, + veteranInformation: PropTypes.object, }; const mapStateToProps = (state) => ({ diff --git a/client/app/queue/QueueTableBuilder.jsx b/client/app/queue/QueueTableBuilder.jsx index 251dfce526c..9d421e23f0f 100644 --- a/client/app/queue/QueueTableBuilder.jsx +++ b/client/app/queue/QueueTableBuilder.jsx @@ -250,7 +250,7 @@ const QueueTableBuilder = (props) => { return

{config.table_title}

- + ; } @@ -43,5 +56,5 @@ QueueOrganizationDropdown.propTypes = { organizations: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string.isRequired, url: PropTypes.string.isRequired - })) + })), }; diff --git a/client/app/queue/constants.js b/client/app/queue/constants.js index a2cae86e8e7..159b930fe93 100644 --- a/client/app/queue/constants.js +++ b/client/app/queue/constants.js @@ -11,6 +11,7 @@ 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'; export const COLORS = { QUEUE_LOGO_PRIMARY: '#11598D', @@ -164,6 +165,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; @@ -212,7 +215,10 @@ 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' }; export const CUSTOM_HOLD_DURATION_TEXT = 'Custom'; diff --git a/client/app/queue/correspondence/CorrespondenceCases.jsx b/client/app/queue/correspondence/CorrespondenceCases.jsx new file mode 100644 index 00000000000..7ad68873b3c --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceCases.jsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import ApiUtil from '../../util/ApiUtil'; +import { loadVetCorrespondence } from './correspondenceReducer/correspondenceActions'; +import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; +import PropTypes from 'prop-types'; +import COPY from '../../../COPY'; +import { css } from 'glamor'; +import CorrespondenceTable from './CorrespondenceTable'; +import QueueOrganizationDropdown from '../components/QueueOrganizationDropdown'; + +// import { +// initialAssignTasksToUser, +// initialCamoAssignTasksToVhaProgramOffice +// } from '../QueueActions'; + +class CorrespondenceCases extends React.PureComponent { + + // grabs correspondences and loads into intakeCorrespondence redux store. + getVeteransWithCorrespondence() { + return ApiUtil.get('/queue/correspondence?json').then((response) => { + const returnedObject = response.body; + const vetCorrespondences = returnedObject.vetCorrespondences; + + this.props.loadVetCorrespondence(vetCorrespondences); + }). + catch((err) => { + // allow HTTP errors to fall on the floor via the console. + console.error(new Error(`Problem with GET /queue/correspondence?json ${err}`)); + }); + } + + // load veteran correspondence info on page load + componentDidMount() { + // Retry the request after a delay + setTimeout(() => { + this.getVeteransWithCorrespondence(); + }, 1000); + } + + render = () => { + const { + organizations + } = this.props; + + return ( + + +

{COPY.CASE_LIST_TABLE_QUEUE_DROPDOWN_CORRESPONDENCE_CASES}

+ + {this.props.vetCorrespondences && + + } +
+
+ ); + } +} + +CorrespondenceCases.propTypes = { + organizations: PropTypes.array, + loadVetCorrespondence: PropTypes.func, + vetCorrespondences: PropTypes.array +}; + +const mapStateToProps = (state) => ({ + vetCorrespondences: state.intakeCorrespondence.vetCorrespondences +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + loadVetCorrespondence + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CorrespondenceCases); diff --git a/client/app/queue/correspondence/CorrespondencePaginationWrapper.jsx b/client/app/queue/correspondence/CorrespondencePaginationWrapper.jsx new file mode 100644 index 00000000000..4f67c537658 --- /dev/null +++ b/client/app/queue/correspondence/CorrespondencePaginationWrapper.jsx @@ -0,0 +1,52 @@ +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..7f828b94e1b --- /dev/null +++ b/client/app/queue/correspondence/CorrespondenceTable.jsx @@ -0,0 +1,71 @@ +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: 'packageDocumentType', + header: 'Package Document Type ', + align: 'left', + valueName: 'packageDocumentType', + enableFilter: true, + getSortValue: (row) => row.packageDocumentType, + backendCanSort: true + }, + { + 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, + loadVetCorrespondence: PropTypes.func, + vetCorrespondences: PropTypes.array, + history: PropTypes.object, + user: PropTypes.shape({ + userCanBuildHearingSchedule: PropTypes.bool + }) +}; + +export default CorrespondenceTable; diff --git a/client/app/queue/correspondence/component/EditDocumentTypeModal.jsx b/client/app/queue/correspondence/component/EditDocumentTypeModal.jsx new file mode 100644 index 00000000000..e1954d53e5a --- /dev/null +++ b/client/app/queue/correspondence/component/EditDocumentTypeModal.jsx @@ -0,0 +1,123 @@ +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 } from '../correspondenceReducer/reviewPackageActions'; +import { css } from 'glamor'; +import COPY from '../../../../COPY'; +import Modal from '../../../components/Modal'; +import Button from '../../../components/Button'; + +const modalStyle = css({ + width: '32%' +}); + +class EditDocumentTypeModal extends React.Component { + + constructor(props) { + super(props); + + this.state = { + packageDocument: {}, + 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 + } + }); + 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, + setModalState: PropTypes.func, + indexDoc: PropTypes.number +}; + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + updateDocumentTypeName +}, dispatch); + +export default connect( + null, + mapDispatchToProps, +)(EditDocumentTypeModal); diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js new file mode 100644 index 00000000000..d54bfd7d238 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js @@ -0,0 +1,126 @@ +import { ACTIONS } from './correspondenceConstants'; + +export const loadCurrentCorrespondence = (currentCorrespondence) => + (dispatch) => { + dispatch({ + type: ACTIONS.LOAD_CURRENT_CORRESPONDENCE, + payload: { + currentCorrespondence + } + }); + }; + +export const loadCorrespondences = (correspondences) => + (dispatch) => { + dispatch({ + type: ACTIONS.LOAD_CORRESPONDENCES, + payload: { + correspondences + } + }); + }; + +export const loadVeteranInformation = (veteranInformation) => + (dispatch) => { + dispatch({ + type: ACTIONS.LOAD_VETERAN_INFORMATION, + payload: { + veteranInformation + } + }); + }; + +export const loadVetCorrespondence = (vetCorrespondences) => + (dispatch) => { + dispatch({ + type: ACTIONS.LOAD_VET_CORRESPONDENCE, + payload: { + vetCorrespondences + } + }); + }; + +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 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, isChecked) => + (dispatch) => { + dispatch({ + type: ACTIONS.SAVE_MAIL_TASK_STATE, + payload: { + name, + isChecked + } + }); + }; + +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 + } + }); +}; diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js new file mode 100644 index 00000000000..5ccbf472a5b --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js @@ -0,0 +1,15 @@ +export const ACTIONS = { + LOAD_CURRENT_CORRESPONDENCE: 'LOAD_CURRENT_CORRESPONDENCE', + LOAD_CORRESPONDENCES: 'LOAD_CORRESPONDENCES', + LOAD_VETERAN_INFORMATION: 'LOAD_VETERAN_INFORMATION', + LOAD_VET_CORRESPONDENCE: 'LOAD_VET_CORRESPONDENCE', + 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', + 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', +}; diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js new file mode 100644 index 00000000000..ff021b34f25 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js @@ -0,0 +1,126 @@ +import { update } from '../../../util/ReducerUtil'; +import { ACTIONS } from './correspondenceConstants'; + +export const initialState = { + taskRelatedAppealIds: [], + newAppealRelatedTasks: [], + fetchedAppeals: [], + correspondences: [], + radioValue: '0', + relatedCorrespondences: [], + mailTasks: {}, + unrelatedTasks: [], + currentCorrespondence: [], + veteranInformation: [], + waivedEvidenceTasks: [], +}; + +export const intakeCorrespondenceReducer = (state = initialState, action = {}) => { + switch (action.type) { + case ACTIONS.LOAD_CURRENT_CORRESPONDENCE: + return update(state, { + currentCorrespondence: { + $set: action.payload.currentCorrespondence + } + }); + + case ACTIONS.LOAD_CORRESPONDENCES: + return update(state, { + correspondences: { + $set: action.payload.correspondences + } + }); + + case ACTIONS.LOAD_VETERAN_INFORMATION: + return update(state, { + veteranInformation: { + $set: action.payload.veteranInformation + } + }); + + case ACTIONS.LOAD_VET_CORRESPONDENCE: + return update(state, { + vetCorrespondences: { + $set: action.payload.vetCorrespondences + } + }); + + 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.id !== action.payload.correspondence.id) + } + }); + + case ACTIONS.CLEAR_CHECKBOX_STATE: + return update(state, { + relatedCorrespondences: { + $set: [] + } + }); + + 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: { + [action.payload.name]: { + $set: action.payload.isChecked + } + } + }); + + 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] + } + }); + + 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..90cfa557c87 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/reviewPackageActions.js @@ -0,0 +1,63 @@ +import { ACTIONS } from './reviewPackageConstants'; + +export const setCorrespondence = (correspondence) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE, + payload: { + correspondence + } + }); + }; + +export const setCorrespondenceDocuments = (correspondenceDocuments) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_CORRESPONDENCE_DOCUMENTS, + payload: { + correspondenceDocuments + } + }); + }; + +export const setPackageDocumentType = (packageDocumentType) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_PACKAGE_DOCUMENT_TYPE, + payload: { + packageDocumentType + } + }); + }; + +export const setVeteranInformation = (veteranInfo) => + (dispatch) => { + dispatch({ + type: ACTIONS.SET_VETERAN_INFORMATION, + payload: { + veteranInfo + } + }); + }; + +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 + } + }); + }; diff --git a/client/app/queue/correspondence/correspondenceReducer/reviewPackageConstants.js b/client/app/queue/correspondence/correspondenceReducer/reviewPackageConstants.js new file mode 100644 index 00000000000..153378f6c91 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/reviewPackageConstants.js @@ -0,0 +1,8 @@ +export const ACTIONS = { + SET_CORRESPONDENCE: 'SET_CORRESPONDENCE', + SET_CORRESPONDENCE_DOCUMENTS: 'SET_CORRESPONDENCE_DOCUMENTS', + SET_PACKAGE_DOCUMENT_TYPE: 'SET_PACKAGE_DOCUMENT_TYPE', + SET_VETERAN_INFORMATION: 'SET_VETERAN_INFORMATION', + UPDATE_CMP_INFORMATION: 'UPDATE_CMP_INFORMATION', + UPDATE_DOCUMENT_TYPE_NAME: 'UPDATE_DOCUMENT_TYPE_NAME', +}; diff --git a/client/app/queue/correspondence/correspondenceReducer/reviewPackageReducer.js b/client/app/queue/correspondence/correspondenceReducer/reviewPackageReducer.js new file mode 100644 index 00000000000..44a3f937954 --- /dev/null +++ b/client/app/queue/correspondence/correspondenceReducer/reviewPackageReducer.js @@ -0,0 +1,77 @@ +import { update } from '../../../util/ReducerUtil'; +import { ACTIONS } from './reviewPackageConstants'; + +export const initialState = { + correspondence: {}, + correspondenceDocuments: [], + packageDocumentType: {}, + veteranInformation: {} +}; + +export const reviewPackageReducer = (state = initialState, action = {}) => { + switch (action.type) { + case ACTIONS.SET_CORRESPONDENCE: + return update(state, { + correspondence: { + $set: action.payload.correspondence + } + }); + + case ACTIONS.SET_CORRESPONDENCE_DOCUMENTS: + return update(state, { + correspondenceDocuments: { + $set: action.payload.correspondenceDocuments + } + }); + + case ACTIONS.SET_PACKAGE_DOCUMENT_TYPE: + return update(state, { + packageDocumentType: { + $set: action.payload.packageDocumentType + } + }); + + case ACTIONS.SET_VETERAN_INFORMATION: + return update(state, { + veteranInformation: { + $set: action.payload.veteranInfo + } + }); + + 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, { + correspondenceDocuments: { + [action.payload.index]: { + vbms_document_type_id: { + $set: action.payload.newName.value + }, + document_title: { + $set: action.payload.newName.label + } + } + } + }); + + default: + return state; + } +}; + +export default reviewPackageReducer; 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..6ec5c8d04d2 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/AddCorrespondence/AddCorrespondenceView.jsx @@ -0,0 +1,271 @@ +/* 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 ApiUtil from '../../../../../util/ApiUtil'; +import CorrespondencePaginationWrapper from '../../../CorrespondencePaginationWrapper'; +import { + loadCurrentCorrespondence, + loadCorrespondences, + loadVeteranInformation, + updateRadioValue, + saveCheckboxState, + clearCheckboxState +} from '../../../correspondenceReducer/correspondenceActions'; + +const RELATED_NO = '0'; +const RELATED_YES = '1'; + +class AddCorrespondenceView extends React.Component { + constructor(props) { + super(props); + this.state = { + veteran_id: '', + va_date_of_receipt: '', + source_type: '', + package_document_type: '', + correspondence_type_id: '', + notes: '', + selectedCheckboxes: [] + }; + } + + // grabs correspondences and loads into intakeCorrespondence redux store. + getRowObjects(correspondenceUuid) { + return ApiUtil.get(`/queue/correspondence/${correspondenceUuid}/intake?json`).then((response) => { + const returnedObject = response.body; + const currentCorrespondence = returnedObject.currentCorrespondence; + const correspondences = returnedObject.correspondence; + const veteranInformation = returnedObject.veteranInformation; + + this.props.loadCurrentCorrespondence(currentCorrespondence); + this.props.loadCorrespondences(correspondences); + this.props.loadVeteranInformation(veteranInformation); + + }). + catch((err) => { + // allow HTTP errors to fall on the floor via the console. + console.error(new Error(`Problem with GET /queue/correspondence/${correspondenceUuid}/intake?json ${err}`)); + }); + } + + componentDidMount() { + this.getRowObjects(this.props.correspondenceUuid); + } + + onChange = (value) => { + this.props.updateRadioValue({ radioValue: value }); + this.props.onContinueStatusChange(value === RELATED_NO); + this.props.clearCheckboxState(); + } + + onChangeCheckbox = (correspondence, isChecked) => { + this.props.saveCheckboxState(correspondence, isChecked); + let selectedCheckboxes = this.props.checkboxes; + + if (isChecked) { + selectedCheckboxes.push(correspondence.id); + } else { + selectedCheckboxes = selectedCheckboxes.filter((checkboxId) => checkboxId !== 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.va_date_of_receipt); + + return ( + +

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

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

{correspondence.source_type}

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

{correspondence.package_document_type_id}

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

{correspondence.correspondence_type_id}

+
+ ) + }, + { + 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.

+

+

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

+ {/*

Viewing {this.props.correspondences.length} out of {this.props.correspondences.length} total

*/} +
+ + +
+
+ )} +
+ ); + } +} + +AddCorrespondenceView.propTypes = { + correspondence: PropTypes.arrayOf(PropTypes.object), + featureToggles: PropTypes.object, + correspondenceUuid: PropTypes.string, + loadVeteranInformation: PropTypes.func, + loadCurrentCorrespondence: PropTypes.func, + loadCorrespondences: PropTypes.func, + updateRadioValue: PropTypes.func, + radioValue: PropTypes.string, + saveCheckboxState: PropTypes.func, + correspondences: PropTypes.array, + onContinueStatusChange: PropTypes.func, + onCheckboxChange: PropTypes.func.isRequired, + clearCheckboxState: PropTypes.func.isRequired, + checkboxes: PropTypes.array +}; + +const mapStateToProps = (state) => ({ + currentCorrespondence: state.intakeCorrespondence.currentCorrespondence, + veteranInformation: state.intakeCorrespondence.veteranInformation, + correspondences: state.intakeCorrespondence.correspondences, + radioValue: state.intakeCorrespondence.radioValue, + checkboxes: state.intakeCorrespondence.relatedCorrespondences, +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + loadCurrentCorrespondence, + loadCorrespondences, + loadVeteranInformation, + updateRadioValue, + saveCheckboxState, + clearCheckboxState + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AddCorrespondenceView); 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..214777b5489 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/CheckboxModal.jsx @@ -0,0 +1,100 @@ +import React, { useState } from 'react'; +import PropTypes, { string } from 'prop-types'; +import Modal from '../../../../components/Modal'; +import Checkbox from '../../../../components/Checkbox'; +import { css } from 'glamor'; + +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([]); + }; + + const checkboxSizeStyling = css({ + transform: 'scale(1.3)', + translate: '11%', + }); + + 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..fb894d8294f --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmCorrespondenceView.jsx @@ -0,0 +1,241 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { css } from 'glamor'; +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'; + +const bodyStyling = css({ + '& > tr > td': { + backgroundColor: '#f5f5f5', + borderBottom: 'none', + borderColor: '#d6d7d9', + paddingTop: '0vh', + paddingBottom: '0vh', + }, +}); + +const tableStyling = css({ + marginBottom: '-2vh', + marginTop: '2vh' +}); +const bottonStyling = css({ + paddingRight: '0px' +}); + +export const ConfirmCorrespondenceView = (props) => { + + const checkedMailTasks = Object.keys(props.mailTasks).filter((name) => props.mailTasks[name]); + const relatedCorrespondences = useSelector((state) => state.intakeCorrespondence.relatedCorrespondences); + + // 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.va_date_of_receipt); + + return ( + +

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

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

{correspondence.source_type}

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

{correspondence.package_document_type_id}

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

{correspondence.correspondence_type_id}

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

{correspondence.notes}

+
+ ) + }, + ]; + }; + + 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. +

+

+
+ +
+
+
+

+ Associated Prior Mail +

+
+ +
+
+ +
+ +
+
+ + + +
+

Completed Mail Tasks

+
+ +
+
+
+
+ Completed Mail Tasks +
+ {checkedMailTasks.map((name, index, array) => ( +
+ {props.mailTasks[name] && {name}} +
+ ))} +
+ +
+
+

Tasks not related to an Appeal

+
+ +
+
+ +
+

Linked Appeals & New Tasks

+
+ +
+
+ + +
+ + ); +}; + +ConfirmCorrespondenceView.propTypes = { + goToStep: PropTypes.func, + mailTasks: PropTypes.objectOf(PropTypes.bool) +}; +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..a869e57c8ac --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksNotRelatedToAnAppeal.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +const styling = { backgroundColor: '#f5f5f5' }; + +const ConfirmTasksNotRelatedToAnAppeal = () => { + const tasks = useSelector((state) => state.intakeCorrespondence.unrelatedTasks); + + const rowObjects = tasks.map((task) => { + return ( + + + + + ); + }); + + return ( +
+
+
+
+
+ {task.label} + + {task.content} +
+ + + + + + + + {rowObjects} + +
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..8e32582dd9a --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/ConfirmTasksRelatedToAnAppeal.jsx @@ -0,0 +1,154 @@ +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 ? '5px' : '35px'; + + 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} + + )} + + + ); + }); + + return ( + <> +
+
+ + + {rowObjects} + +
+
+
+ ); +}; + +ConfirmTasksRelatedToAnAppeal.propTypes = { + bottonStyling: PropTypes.object, + goToStepTwo: PropTypes.func.isRequired +}; + +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..310ec9467e2 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/CorrespondenceDetailsTable.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import moment from 'moment'; +import { useSelector } from 'react-redux'; + +export const CorrespondenceDetailsTable = () => { + + const currentCorrespondence = useSelector((state) => state.intakeCorrespondence.currentCorrespondence); + const veteranInformation = useSelector((state) => state.intakeCorrespondence.veteranInformation); + + return ( +
+

About the Correspondence

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Portal Entry DateSource TypePackage Document TypeCM Packet NumberCMP Queue NameVA DOR
{moment(currentCorrespondence.portal_entry_date).format('MM/DD/YYYY')}{currentCorrespondence.source_type}{currentCorrespondence.package_document_type_id}{currentCorrespondence.cmp_packet_number}{currentCorrespondence.cmp_queue_id}{moment(currentCorrespondence.va_date_of_receipt).format('MM/DD/YYYY')}
VeteranCorrespondence Type
+ {veteranInformation.first_name} {veteranInformation.last_name} ({veteranInformation.file_number}) + {currentCorrespondence.correspondence_type_id}
Notes
{currentCorrespondence.notes}
+
+ ); +}; + +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..e15f671d3ae --- /dev/null +++ b/client/app/queue/correspondence/intake/components/ConfirmCorrespondence/SubmitCorrespondenceModal.jsx @@ -0,0 +1,101 @@ +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 }) => { + + const correspondence = useSelector((state) => state.intakeCorrespondence.currentCorrespondence); + 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 [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 submitData = { + related_correspondence_uuids: relatedUuids, + tasks_related_to_appeal: serializedTasksRelatedToAppeal, + waived_evidence_submission_window_tasks: serializedWaivedEvidenceTasks, + related_appeal_ids: relatedAppealIds + }; + + setLoading(true); + // Where data goes to be submitted before redirecting back to correspondence queue + let status; + + await ApiUtil.post(`/queue/correspondence/${correspondence.uuid}`, { 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 = { + 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..c242b3862db --- /dev/null +++ b/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx @@ -0,0 +1,228 @@ +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, useSelector } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { setUnrelatedTasks } from '../../correspondenceReducer/correspondenceActions'; +import { useHistory, useLocation } from 'react-router-dom'; +import { ConfirmCorrespondenceView } from './ConfirmCorrespondence/ConfirmCorrespondenceView'; +import { SubmitCorrespondenceModal } from './ConfirmCorrespondence/SubmitCorrespondenceModal'; +import Alert from 'app/components/Alert'; +import ApiUtil from '../../../../util/ApiUtil'; +import { + CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TITLE, + CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TEXT +} from '../../../../../COPY'; + +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 intakeCorrespondence = useSelector((state) => state.intakeCorrespondence); + const [currentStep, setCurrentStep] = useState(1); + const [isContinueEnabled, setContinueEnabled] = useState(true); + const [addTasksVisible, setAddTasksVisible] = useState(false); + const [submitCorrespondenceModalVisible, setSubmitCorrespondenceModalVisible] = useState(false); + const [errorBannerVisible, setErrorBannerVisible] = useState(false); + const { pathname, hash, key } = useLocation(); + const history = useHistory(); + // For hash routing - Add element id and which step it lives on here + const SECTION_MAP = { 'task-not-related-to-an-appeal': 2 }; + + 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 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(() => { + const data = { + correspondence_uuid: props.correspondence_uuid, + current_step: currentStep, + redux_store: intakeCorrespondence + }; + + ApiUtil.post(`/queue/correspondence/${props.correspondence_uuid}/current_step`, { data }). + then( + (response) => { + if (!response.ok) { + console.error(response); + } + } + ); + }, [currentStep]); + + useEffect(() => { + if (hash === '') { + window.scrollTo(0, 0); + } else { + setTimeout(() => { + const id = hash.replace('#', ''); + + setCurrentStep(SECTION_MAP[id]); + const element = document.getElementById(id); + + if (element) { + element.scrollIntoView(); + } + }, 0); + } + }, [pathname, hash, key]); + + return
+ { errorBannerVisible && + + {CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER_TEXT} + + } + + {currentStep === 1 && + + } + {currentStep === 2 && + + } + {currentStep === 3 && +
+ + props.toggledCorrespondences.indexOf(String(currentCorrespondence.id)) !== -1)} + /> +
+ } +
+ + } + {currentStep === 3 && + } + {currentStep > 1 && + } + {currentStep === 3 && submitCorrespondenceModalVisible && + + } +
+
; +}; + +CorrespondenceIntake.propTypes = { + correspondence_uuid: PropTypes.string, + currentCorrespondence: PropTypes.object, + veteranInformation: PropTypes.object, + toggledCorrespondences: PropTypes.array, + correspondences: PropTypes.array, + unrelatedTasks: PropTypes.arrayOf(Object), + setUnrelatedTasks: PropTypes.func, + mailTasks: PropTypes.objectOf(PropTypes.bool), + autoTexts: PropTypes.arrayOf(PropTypes.string) +}; + +const mapStateToProps = (state) => ({ + correspondences: state.intakeCorrespondence.correspondences, + unrelatedTasks: state.intakeCorrespondence.unrelatedTasks, + mailTasks: state.intakeCorrespondence.mailTasks, + toggledCorrespondences: state.intakeCorrespondence.relatedCorrespondences +}); + +const mapDispatchToProps = (dispatch) => ( + bindActionCreators({ + setUnrelatedTasks + }, dispatch) +); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(CorrespondenceIntake); 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..68989009b90 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/TasksAppeals/AddAppealRelatedTaskView.jsx @@ -0,0 +1,213 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import CaseListTable from '../../../../CaseListTable'; +import ApiUtil from '../../../../../util/ApiUtil'; +import { prepareAppealForStore } from '../../../../utils'; +import LoadingContainer from '../../../../../components/LoadingContainer'; +import { LOGO_COLORS } from '../../../../../constants/AppConstants'; +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(() => { + setNextTaskId((prevId) => prevId + 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; + + newTasks.forEach((task) => { + canContinue = canContinue && ((task.content !== '') && (task.type !== '')); + }); + + waivedTasks.forEach((task) => { + canContinue = canContinue && (task.isWaived ? (task.waiveReason !== '') : true); + }); + + props.setRelatedTasksCanContinue(canContinue); + }, [newTasks, waivedTasks]); + + const veteranFileNumber = props.veteranInformation.file_number; + + 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 = prepareAppealForStore(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 = { + correspondenceUuid: PropTypes.string.isRequired, + setRelatedTasksCanContinue: PropTypes.func.isRequired, + filterUnavailableTaskTypeOptions: PropTypes.func.isRequired, + allTaskTypeOptions: PropTypes.array.isRequired, + autoTexts: PropTypes.arrayOf(PropTypes.string).isRequired, + veteranInformation: PropTypes.object.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..2e073abc424 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/TasksAppeals/AddEvidenceSubmissionTaskView.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import TextareaField from '../../../../../components/TextareaField'; +import ReactSelectDropdown from '../../../../../components/ReactSelectDropdown'; +import Checkbox from '../../../../../components/Checkbox'; +import PropTypes from 'prop-types'; +import { css } from 'glamor'; + +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..642dc1201a4 --- /dev/null +++ b/client/app/queue/correspondence/intake/components/TasksAppeals/AddTaskView.jsx @@ -0,0 +1,203 @@ +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 { css } from 'glamor'; +import PropTypes from 'prop-types'; + +const customSelectStyless = { + dropdownIndicator: () => ({ + width: '80%' + }), + + control: (styles) => { + return { + ...styles, + alignContent: 'center', + borderRadius: 0, + border: '1px solid black' + }; + }, + + menu: () => ({ + boxShadow: '1px 1px 10px grey', + }), + + 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: '25px', + paddingTop: '10px', + paddingBottom: '10px', + paddingLeft: '20px', + backgroundColor: isFocused ? 'white' : 'null', + ':hover': { + ...styles[':hover'], + backgroundColor: '#5c9ceb', + color: 'black', + } + }) +}; +const selectContainerStyless = css({ + width: '100%', + display: 'inline-block', +}); + +const AddTaskView = (props) => { + const task = props.task; + const [modalVisible, setModalVisible] = useState(false); + + const objectForSelectedTaskType = () => { + return props.allTaskTypeOptions.find((option) => { + return option.value === task.type; + }); + }; + + 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 && + + } +
+
+ +
+ + + + + {option.displayElement && option.element} + {option.help && } +
+ ); + + return maybeAddTooltip(option, 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/queue/components/TaskTableColumns.jsx b/client/app/queue/components/TaskTableColumns.jsx index fe6378cce3a..2385d718c8f 100644 --- a/client/app/queue/components/TaskTableColumns.jsx +++ b/client/app/queue/components/TaskTableColumns.jsx @@ -18,7 +18,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { setShowReassignPackageModal, setShowRemovePackageModal, - setSelectedTasks + setSelectedTasks, + setSelectedVeteranDetails } from 'app/queue/correspondence/correspondenceReducer/correspondenceActions'; import { taskHasCompletedHold, hasDASRecord, collapseColumn, regionalOfficeCity, renderAppealType } from '../utils'; @@ -232,12 +233,14 @@ export const assignedByColumn = () => { export const veteranDetails = () => { const dispatch = useDispatch(); - const showReassignPackageModal = () => { - dispatch(setShowReassignPackageModal(true)); + const handleRemoveClick = (task) => { + dispatch(setSelectedVeteranDetails(task)); + dispatch(setShowRemovePackageModal(true)); }; - const showRemovePackageModal = () => { - dispatch(setShowRemovePackageModal(true)); + const handleReassignClick = (task) => { + dispatch(setSelectedVeteranDetails(task)); + dispatch(setShowReassignPackageModal(true)); }; return { @@ -250,7 +253,7 @@ export const veteranDetails = () => { if (task.taskUrl === '/modal/reassign_package') { return handleReassignClick(task)} aria-label={`${task.label } Link`} id="task-link" > @@ -259,7 +262,7 @@ export const veteranDetails = () => { } else if (task.taskUrl === '/modal/remove_package') { return handleRemoveClick(task)} aria-label={`${task.label } Link`} id="task-link" > diff --git a/client/app/queue/correspondence/CorrespondenceCases.jsx b/client/app/queue/correspondence/CorrespondenceCases.jsx index 8c0c2dd77d6..778f907d88e 100644 --- a/client/app/queue/correspondence/CorrespondenceCases.jsx +++ b/client/app/queue/correspondence/CorrespondenceCases.jsx @@ -12,7 +12,9 @@ import { sprintf } from 'sprintf-js'; import CorrespondenceTableBuilder from './CorrespondenceTableBuilder'; import Alert from '../../components/Alert'; import Modal from 'app/components/Modal'; -import Button from '../../components/Button'; +import RadioFieldWithChildren from '../../components/RadioFieldWithChildren'; +import ReactSelectDropdown from '../../components/ReactSelectDropdown'; +import TextareaField from '../../components/TextareaField'; const CorrespondenceCases = (props) => { const dispatch = useDispatch(); @@ -20,8 +22,87 @@ const CorrespondenceCases = (props) => { const currentAction = useSelector((state) => state.reviewPackage.lastAction); const veteranInformation = useSelector((state) => state.reviewPackage.veteranInformation); + const currentSelectedVeteran = useSelector((state) => state.intakeCorrespondence.selectedVeteranDetails); + const reassignModalVisible = useSelector((state) => state.intakeCorrespondence.showReassignPackageModal); const [vetName, setVetName] = useState(''); + 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 approveElement = (
+ setSelectedMailTeamUser(val.value)} + options={buildMailUserData(props.mailTeamUsers)} + /> +
); + + const textAreaElement = ( +
+ +
); useEffect(() => { dispatch(loadCorrespondenceConfig(configUrl)); @@ -31,14 +112,93 @@ const CorrespondenceCases = (props) => { const showReassignPackageModal = useSelector((state) => state.intakeCorrespondence.showReassignPackageModal); const showRemovePackageModal = useSelector((state) => state.intakeCorrespondence.showRemovePackageModal); - const closeReassignPackageModal = () => { - dispatch(setShowReassignPackageModal(false)); - }; + const reassignOptions = [ + { displayText: 'Approve request', + value: 'approve', + element: approveElement, + displayElement: selectedRequestChoice === 'approve' + }, + { displayText: 'Reject request', + value: 'reject', + element: textAreaElement, + displayElement: selectedRequestChoice === 'reject' + } + ]; - const closeRemovePackageModal = () => { - dispatch(setShowRemovePackageModal(false)); + const removeOptions = [ + { displayText: 'Approve request', + value: 'approve', + displayElement: selectedRequestChoice === 'approve' + }, + { displayText: 'Reject request', + value: 'reject', + element: textAreaElement, + displayElement: selectedRequestChoice === 'reject' + } + ]; + + const handleConfirmReassignRemoveClick = (operation) => { + const newUrl = new URL(window.location.href); + const searchParams = new URLSearchParams(newUrl.search); + + // Encode and set the query parameters + searchParams.set('user', encodeURIComponent(selectedMailTeamUser)); + searchParams.set('taskId', encodeURIComponent(currentSelectedVeteran.uniqueId)); + searchParams.set('userAction', encodeURIComponent(selectedRequestChoice)); + searchParams.set('decisionReason', encodeURIComponent(decisionReason)); + searchParams.set('operation', encodeURIComponent(operation)); + + // Construct the new URL with encoded query parameters + newUrl.search = searchParams.toString(); + window.location.href = newUrl.href; }; + 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 + } + ]; + useEffect(() => { if ( veteranInformation?.veteranName?.firstName && @@ -77,16 +237,41 @@ const CorrespondenceCases = (props) => { isMailSupervisor={props.isMailSupervisor} />} {showReassignPackageModal && Cancel} - />} + > + Reason for reassignment: +

{currentSelectedVeteran.instructions}

+
+ setSelectedRequestChoice(val)} + value={selectedRequestChoice} + optionsStyling={{ width: '180px' }} + /> +
+
} {showRemovePackageModal && Cancel} - />} + buttons={removeModalButtons} + closeHandler={handleRemoveClose}> + Reason for removal: +

{currentSelectedVeteran.instructions}

+ setSelectedRequestChoice(val)} + value={selectedRequestChoice} + optionsStyling={{ width: '180px' }} + /> +
} ); diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js index bb794e17edc..f8be4a41710 100644 --- a/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js @@ -202,6 +202,14 @@ export const setShowRemovePackageModal = (isVisible) => (dispatch) => { }); }; +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, diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js index 156ae2a7556..e23300cd886 100644 --- a/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceConstants.js @@ -18,6 +18,7 @@ export const ACTIONS = { SET_WAIVED_EVIDENCE_TASKS: 'SET_WAIVED_EVIDENCE_TASKS', 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', - SET_SELECTED_TASKS: 'SET_SELECTED_TASKS' }; diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js index 3d6f63d71fb..f4f98cfc095 100644 --- a/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceReducer.js @@ -14,6 +14,7 @@ export const initialState = { currentCorrespondence: [], veteranInformation: [], waivedEvidenceTasks: [], + selectedVeteranDetails: {}, showReassignPackageModal: false, showRemovePackageModal: false, showErrorBanner: false @@ -162,6 +163,13 @@ export const intakeCorrespondenceReducer = (state = initialState, action = {}) = } }); + 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: { diff --git a/client/app/queue/utils.js b/client/app/queue/utils.js index f8839de4b90..187095c6585 100644 --- a/client/app/queue/utils.js +++ b/client/app/queue/utils.js @@ -86,6 +86,7 @@ const correspondenceTaskAttributesFromRawTask = (task) => { vaDor: task.attributes.va_date_of_receipt, label: task.attributes.label, taskUrl: task.attributes.task_url, + parentTaskUrl: task.attributes.parent_task_url.parent_task_url, status: task.attributes.status, assignedAt: task.attributes.assigned_at, assignedTo: { diff --git a/db/seeds/queue_correspondences.rb b/db/seeds/queue_correspondences.rb index c30c2002f7b..8b9cbb2694a 100644 --- a/db/seeds/queue_correspondences.rb +++ b/db/seeds/queue_correspondences.rb @@ -83,6 +83,15 @@ def create_queue_correspondences(user, veteran = {}) create_correspondence_with_action_required_tasks(user, veteran) end + # 20 correspondences with reassign / remove task for action required + 20.times do + create_correspondences_with_review_remove_package_tasks + end + + # 15 Correspondences with in-progress CorrespondenceRootTask and completed Mail Task + 15.times do + create_correspondence_with_completed_mail_task(user, veteran) + end # 15 Correspondences with in-progress CorrespondenceRootTask and completed Mail Task 15.times do create_correspondence_with_completed_mail_task(user, veteran) @@ -182,6 +191,22 @@ def create_correspondence_with_action_required_tasks(user = {}, veteran = {}) end end + def create_correspondences_with_review_remove_package_tasks + corres_array = (1..2).map { create(:correspondence) } + task_array = [ReassignPackageTask, RemovePackageTask] + + corres_array.each_with_index do |corres, index| + rpt = ReviewPackageTask.find_by(appeal_id: corres.id) + task_array[index].create!( + parent_id: rpt.id, + appeal_type: "Correspondence", + appeal_id: corres.id, + assigned_to: InboundOpsTeam.singleton, + assigned_by_id: rpt.assigned_to_id + ) + end + end + def create_correspondence_with_completed_mail_task(user, veteran = {}) correspondence = create_correspondence(user, veteran) create_and_complete_mail_task(correspondence, user) @@ -191,5 +216,5 @@ def create_correspondence_with_canceled_root_task(user, veteran = {}) corres = create_correspondence(user, veteran) corres.root_task.update!(status: Constants.TASK_STATUSES.cancelled) end - end +end end diff --git a/spec/feature/queue/correspondence/correspondence_cases_spec.rb b/spec/feature/queue/correspondence/correspondence_cases_spec.rb index 12c941acf9a..440717db6c6 100644 --- a/spec/feature/queue/correspondence/correspondence_cases_spec.rb +++ b/spec/feature/queue/correspondence/correspondence_cases_spec.rb @@ -1214,6 +1214,78 @@ end end + context "Banner alert for approval and reject request" do + let(:current_user) { create(:user) } + let(:mail_team_user) { create(:user) } + before :each do + MailTeam.singleton.add_user(current_user) + InboundOpsTeam.singleton.add_user(current_user) + User.authenticate!(user: current_user) + FeatureToggle.enable!(:correspondence_queue) + end + + before do + 5.times do + corres_array = (1..2).map { create(:correspondence) } + task_array = [ReassignPackageTask, RemovePackageTask] + + corres_array.each_with_index do |corres, index| + rpt = ReviewPackageTask.find_by(appeal_id: corres.id) + task_array[index].create!( + parent_id: rpt.id, + appeal_type: "Correspondence", + appeal_id: corres.id, + assigned_to: InboundOpsTeam.singleton, + instructions: ["This was the default"], + assigned_by_id: rpt.assigned_to_id + ) + end + end + end + + it "approve request to reassign" do + visit "queue/correspondence/team?tab=correspondence_action_required&page=1&sort_by=vaDor&order=asc" + all("[aria-label='Reassign Package Task Link']")[0].click + find('[for="vertical-radio_approve"]').click + find("#react-select-2-input").find(:xpath, "..").find(:xpath, "..").find(:xpath, "..").click + find("#react-select-2-option-0").click + find("#Review-request-button-id-1").click + expect(page).to have_content("You have successfully reassigned a mail record for") + end + + it "deny request to reassign" do + visit "queue/correspondence/team?tab=correspondence_action_required&page=1&sort_by=vaDor&order=asc" + all("[aria-label='Reassign Package Task Link']")[0].click + find('[for="vertical-radio_reject"]').click + find(".cf-form-textarea", visible: false).find(:xpath, "./*").fill_in with: "this is a rejection reason" + find("#Review-request-button-id-1").click + expect(page).to have_content("You have successfully rejected a package request for") + end + + it "approve request to remove" do + visit "queue/correspondence/team?tab=correspondence_action_required&page=1&sort_by=vaDor&order=asc" + all("[aria-label='Remove Package Task Link']")[0].click + find('[for="vertical-radio_approve"]').click + find("#Review-request-button-id-1").click + expect(page).to have_content("You have successfully removed a mail package for") + end + + it "deny request to remove" do + visit "queue/correspondence/team?tab=correspondence_action_required&page=1&sort_by=vaDor&order=asc" + all("[aria-label='Remove Package Task Link']")[0].click + find('[for="vertical-radio_reject"]').click + find(".cf-form-textarea", visible: false).find(:xpath, "./*").fill_in with: "this is a rejection reason" + find("#Review-request-button-id-1").click + expect(page).to have_content("You have successfully rejected a package request") + end + + it "goes to Task Package" do + visit "queue/correspondence/team?tab=correspondence_action_required&page=1&sort_by=vaDor&order=asc" + all("[aria-label='Reassign Package Task Link']")[0].click + find("[id='Review-request-button-id-2']").click + expect(page).to have_content("Review the mail package details below.") + end + end context "correspondence tasks completed tab testing filters date " do let(:current_user) { create(:user) } before :each do From 424d92ba1d60ff93c006d1e2ba55e7ab34f44c28 Mon Sep 17 00:00:00 2001 From: HunJerBAH Date: Thu, 21 Mar 2024 13:49:38 -0400 Subject: [PATCH 173/237] added back in redux store value on intake load. --- app/controllers/correspondence_intake_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/correspondence_intake_controller.rb b/app/controllers/correspondence_intake_controller.rb index 3f2d52c48f1..afbe86182ab 100644 --- a/app/controllers/correspondence_intake_controller.rb +++ b/app/controllers/correspondence_intake_controller.rb @@ -2,6 +2,11 @@ class CorrespondenceIntakeController < CorrespondenceController 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(user: current_user, + correspondence: current_correspondence)&.redux_store + respond_to do |format| format.html { return render "correspondence/intake" } format.json do From 645614c919f44c5414f94275b1f5ff2440c00b9f Mon Sep 17 00:00:00 2001 From: divyadasari-va <135847343+divyadasari-va@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:01:29 -0600 Subject: [PATCH 174/237] Div/appeals 38964 (#21099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit for search bar * Search bar chnages and removed post request * Modified the search results display * Removed unused code * Removed searchValue from unnecessary places * Search results on page load * APPEALS-38964 - Search criteria for all columns * moved search logic from table builder into queue table * Removed unused method * Made the changes back for tabPaginationOptions * Remove Console Log * Feature test cases for Search bar * Added few more feature test cases for Search bar * Added pagination spec * Failing spec * Added few more test cases * APPEALS-38964 - Additioanl RSpec tests, Fixed page visits * Edit VA DOR access for InboundOpsTeamAdmin user (#21150) * 🔀 jcroteau/APPEALS-38492 - Fix duplicate results from joins on certain AR associations (#21097) * refactored seed scripts for correspondence to have more detailed notes (#21170) * Modified the test spec for sorted by veteran details * APPEALS-38964 - Updated Flaky Test * APPEALS-38964 - Notes Update * Added serializer for few more columns * Added serializer for label and closed_at columns * APPEALS-38964 - Linting, Assigned_By * Sort and pagination column fix * Removed logic for sort and pagination fix * Modified column_names with self class in all the tab class * DRY all the tabs * Extra Linting and Test Updates * Can able to search with number * Updates for tabs and linting * Linting * fix for completed date serializer * Reverting Conditional for VetDetails and VADOR --------- Co-authored-by: KiMauVA Co-authored-by: HunJerBAH Co-authored-by: Kamala Madamanchi <110078646+kamala-07@users.noreply.github.com> Co-authored-by: Jeremy Croteau Co-authored-by: HunJerBAH <99915461+HunJerBAH@users.noreply.github.com> --- .../concerns/task_pagination_concern.rb | 5 +- app/models/correspondence_queue_tab.rb | 4 + .../correspondence_assigned_tasks_tab.rb | 2 +- .../correspondence_completed_tasks_tab.rb | 2 +- .../correspondence_in_progress_tasks_tab.rb | 2 +- ...orrespondence_action_required_tasks_tab.rb | 2 +- ...ation_correspondence_assigned_tasks_tab.rb | 2 +- ...tion_correspondence_completed_tasks_tab.rb | 2 +- ...zation_correspondence_pending_tasks_tab.rb | 2 +- ...ion_correspondence_unassigned_tasks_tab.rb | 2 +- .../correspondence_task_column_serializer.rb | 97 ++++++-- client/app/queue/QueueTable.jsx | 13 +- .../CorrespondenceTableBuilder.jsx | 70 +++++- .../correspondence_search_bar_spec.rb | 223 ++++++++++++++++++ .../correspondence_controller_spec.rb | 2 +- 15 files changed, 392 insertions(+), 38 deletions(-) create mode 100644 spec/feature/queue/correspondence/correspondence_search_bar_spec.rb diff --git a/app/controllers/concerns/task_pagination_concern.rb b/app/controllers/concerns/task_pagination_concern.rb index b3c61cef704..e162a59bf1d 100644 --- a/app/controllers/concerns/task_pagination_concern.rb +++ b/app/controllers/concerns/task_pagination_concern.rb @@ -51,9 +51,12 @@ 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 + is_collection: true, + params: { columns: tab.column_names } ).serializable_hash[:data] } end diff --git a/app/models/correspondence_queue_tab.rb b/app/models/correspondence_queue_tab.rb index fee50b3bd03..f9d078fa422 100644 --- a/app/models/correspondence_queue_tab.rb +++ b/app/models/correspondence_queue_tab.rb @@ -27,4 +27,8 @@ def self.from_name(tab_name) tab end + + def column_names + self.class.column_names + end end diff --git a/app/models/queue_tabs/correspondence_assigned_tasks_tab.rb b/app/models/queue_tabs/correspondence_assigned_tasks_tab.rb index 084abd93293..df5c0233876 100644 --- a/app/models/queue_tabs/correspondence_assigned_tasks_tab.rb +++ b/app/models/queue_tabs/correspondence_assigned_tasks_tab.rb @@ -22,7 +22,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, diff --git a/app/models/queue_tabs/correspondence_completed_tasks_tab.rb b/app/models/queue_tabs/correspondence_completed_tasks_tab.rb index fddeef7ce2b..727aa42cfcb 100644 --- a/app/models/queue_tabs/correspondence_completed_tasks_tab.rb +++ b/app/models/queue_tabs/correspondence_completed_tasks_tab.rb @@ -31,7 +31,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, diff --git a/app/models/queue_tabs/correspondence_in_progress_tasks_tab.rb b/app/models/queue_tabs/correspondence_in_progress_tasks_tab.rb index 70d91fb3afc..a86958d565d 100644 --- a/app/models/queue_tabs/correspondence_in_progress_tasks_tab.rb +++ b/app/models/queue_tabs/correspondence_in_progress_tasks_tab.rb @@ -24,7 +24,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, 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 index 0fb6a305525..8e096558c8d 100644 --- a/app/models/queue_tabs/organization_correspondence_action_required_tasks_tab.rb +++ b/app/models/queue_tabs/organization_correspondence_action_required_tasks_tab.rb @@ -28,7 +28,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, diff --git a/app/models/queue_tabs/organization_correspondence_assigned_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_assigned_tasks_tab.rb index 355335fe744..ad3c179b687 100644 --- a/app/models/queue_tabs/organization_correspondence_assigned_tasks_tab.rb +++ b/app/models/queue_tabs/organization_correspondence_assigned_tasks_tab.rb @@ -22,7 +22,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.CHECKBOX_COLUMN.name, Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, diff --git a/app/models/queue_tabs/organization_correspondence_completed_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_completed_tasks_tab.rb index bbcc01327e2..e0ea9174b36 100644 --- a/app/models/queue_tabs/organization_correspondence_completed_tasks_tab.rb +++ b/app/models/queue_tabs/organization_correspondence_completed_tasks_tab.rb @@ -34,7 +34,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, diff --git a/app/models/queue_tabs/organization_correspondence_pending_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_pending_tasks_tab.rb index 19f06462b8e..e59ccb36b2b 100644 --- a/app/models/queue_tabs/organization_correspondence_pending_tasks_tab.rb +++ b/app/models/queue_tabs/organization_correspondence_pending_tasks_tab.rb @@ -23,7 +23,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, Constants.QUEUE_CONFIG.COLUMNS.VA_DATE_OF_RECEIPT.name, diff --git a/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb b/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb index 60e84ba378c..a087b24f658 100644 --- a/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb +++ b/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb @@ -22,7 +22,7 @@ def tasks end # :reek:UtilityFunction - def column_names + def self.column_names [ Constants.QUEUE_CONFIG.COLUMNS.CHECKBOX_COLUMN.name, Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.name, diff --git a/app/models/serializers/work_queue/correspondence_task_column_serializer.rb b/app/models/serializers/work_queue/correspondence_task_column_serializer.rb index 52dffb2a2c8..04001d423b9 100644 --- a/app/models/serializers/work_queue/correspondence_task_column_serializer.rb +++ b/app/models/serializers/work_queue/correspondence_task_column_serializer.rb @@ -3,6 +3,10 @@ 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 @@ -14,23 +18,47 @@ class WorkQueue::CorrespondenceTaskColumnSerializer "#{vet.first_name} #{vet.last_name} (#{vet.file_number})" end - attribute :notes do |object| - object.correspondence.notes + attribute :notes do |object, params| + columns = [Constants.QUEUE_CONFIG.COLUMNS.NOTES.name] + if serialize_attribute?(params, columns) + object.correspondence.notes + end end attribute :cmp_packet_number do |object| object.correspondence.cmp_packet_number end - attribute :closed_at, &:completed_by_date + attribute :closed_at do |object, params| + columns = [Constants.QUEUE_CONFIG.COLUMNS.CORRESPONDENCE_TASK_CLOSED_DATE.name] - attribute :days_waiting + 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 + 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 :status @@ -45,24 +73,51 @@ class WorkQueue::CorrespondenceTaskColumnSerializer { parent_task_url: "" } end end - - attribute :assigned_to do |object| + + attribute :assigned_to do |object, params| + columns = [ + Constants.QUEUE_CONFIG.COLUMNS.TASK_ASSIGNEE.name + ] assignee = object.assigned_to - { - css_id: assignee.try(:css_id), - is_organization: assignee.is_a?(Organization), - name: assignee.is_a?(Organization) ? assignee.name : assignee.css_id, - type: assignee.class.name, - id: assignee.id - } + + if serialize_attribute?(params, columns) + { + css_id: assignee.try(:css_id), + name: assignee.is_a?(Organization) ? assignee.name : assignee.css_id, + is_organization: assignee.is_a?(Organization), + type: assignee.class.name, + id: assignee.id + } + else + { + css_id: nil, + is_organization: nil, + name: nil, + type: nil, + id: nil + } + end end - attribute :assigned_by do |object| - { - first_name: object.assigned_by_display_name.first, - last_name: object.assigned_by_display_name.last, - css_id: object.assigned_by.try(:css_id), - pg_id: object.assigned_by.try(:id) - } + 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, + css_id: object.assigned_by.try(:css_id), + pg_id: object.assigned_by.try(:id) + } + else + { + first_name: nil, + last_name: nil, + css_id: nil, + pg_id: nil + } + end end end diff --git a/client/app/queue/QueueTable.jsx b/client/app/queue/QueueTable.jsx index b87e6c38636..395cab3304a 100644 --- a/client/app/queue/QueueTable.jsx +++ b/client/app/queue/QueueTable.jsx @@ -657,6 +657,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, @@ -673,7 +677,8 @@ export default class QueueTable extends React.PureComponent { styling, bodyStyling, enablePagination, - useTaskPagesApi + useTaskPagesApi, + searchValue } = this.props; let { totalTaskCount, numberOfPages, rowObjects, casesPerPage } = this.props; @@ -778,7 +783,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} @@ -847,7 +852,9 @@ HeaderRow.propTypes = FooterRow.propTypes = Row.propTypes = BodyRows.propTypes = }), onHistoryUpdate: PropTypes.func, preserveFilter: PropTypes.bool, - isCorrespondenceTable: PropTypes.bool + isCorrespondenceTable: PropTypes.bool, + searchValue: PropTypes.string, + taskMatchesSearch: PropTypes.func }; Row.propTypes.rowObjects = PropTypes.arrayOf(PropTypes.object); diff --git a/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx b/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx index f85738d5eb2..4194dce8bd7 100644 --- a/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx +++ b/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx @@ -6,10 +6,12 @@ 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 { actionType, assignedToColumn, @@ -55,6 +57,7 @@ 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)); @@ -101,6 +104,48 @@ const CorrespondenceTableBuilder = (props) => { return index === -1 ? 0 : index; }; + const handleSearchChange = (value) => { + setSearchValue(value); + }; + + const handleClearSearch = () => { + setSearchValue(''); + }; + + const taskMatchesSearch = (task) => { + if (searchValue === '' || searchValue.length < 3) { + // Return all tasks when search value is empty or less than three characters + return true; + } + + 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 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()) || + taskLabel.toLowerCase().includes(searchValueTrimmed.toLowerCase()) || + (isNumericSearchValue && daysWaiting.trim() === searchValueTrimmed) || + moment(closedAt).format('MM/DD/YYYY'). + includes(searchValue) + ); + }; + const queueConfig = () => { const { config } = props; @@ -125,7 +170,7 @@ const CorrespondenceTableBuilder = (props) => { [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.ACTION_TYPE.name]: actionType() }; return functionForColumn[column.name]; @@ -229,9 +274,24 @@ const CorrespondenceTableBuilder = (props) => { } -

- {noCasesMessage || tabConfig.description} -

+
+

+ {noCasesMessage || tabConfig.description} +

+
+ handleSearchChange(value)} + onClearSearch={handleClearSearch} + value={searchValue} + /> +
+
+ { } enablePagination isCorrespondenceTable + searchValue={searchValue} + taskMatchesSearch={taskMatchesSearch} /> ), diff --git a/spec/feature/queue/correspondence/correspondence_search_bar_spec.rb b/spec/feature/queue/correspondence/correspondence_search_bar_spec.rb new file mode 100644 index 00000000000..9274f46ec2f --- /dev/null +++ b/spec/feature/queue/correspondence/correspondence_search_bar_spec.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +RSpec.feature("Search Bar for Correspondence") do + include CorrespondenceTaskHelpers + # alias this to avoid the method name collision + alias_method :create_efolderupload_task, :create_efolderupload_failed_task + + context "correspondece cases feature toggle" do + let(:current_user) { create(:user) } + before :each do + MailTeam.singleton.add_user(current_user) + User.authenticate!(user: current_user) + @correspondence_uuid = "123456789" + end + + it "routes user to /under_construction if the feature toggle is disabled" do + FeatureToggle.disable!(:correspondence_queue) + visit "/queue/correspondence" + expect(page).to have_current_path("/under_construction") + end + + it "routes to correspondence cases if feature toggle is enabled" do + FeatureToggle.enable!(:correspondence_queue) + visit "/queue/correspondence" + expect(page).to have_current_path( + "/queue/correspondence?tab=correspondence_assigned&page=1&sort_by=vaDor&order=asc" + ) + end + end + + context "correspondence assigned tab - locate the search bar" do + let(:current_user) { create(:user) } + before :each do + MailTeam.singleton.add_user(current_user) + User.authenticate!(user: current_user) + end + + before do + 20.times do + @review_correspondence = create(:correspondence) + rpt = ReviewPackageTask.find_by(appeal_id: @review_correspondence.id) + rpt.update!(assigned_to: current_user, status: "assigned") + rpt.save! + end + 1.times do + params = { + first_name: "Zzzane", + last_name: "Zzzans" + } + veteran = create(:veteran, params) + review_correspondence = create(:correspondence, veteran_id: veteran.id) + rpt = ReviewPackageTask.find_by(appeal_id: review_correspondence.id) + rpt.update!(assigned_to: current_user, + status: "assigned", + assigned_at: 42.days.ago) + rpt.save! + end + FeatureToggle.enable!(:correspondence_queue) + end + + before :each do + FeatureToggle.enable!(:correspondence_queue) + FeatureToggle.enable!(:user_queue_pagination) + end + + it "successfully opens the assigned tab, finds the search box, and enters data there." do + visit "/queue/correspondence?tab=correspondence_assigned&page=1&sort_by=vaDor&order=asc" + expect(page).to have_content("Filter table by any of its columns") + expect(find("#searchBar")).to match_xpath("//input[@placeholder='Type to filter...']") + veteran = Veteran.first + find_by_id("searchBar").fill_in with: veteran.last_name + search_value = find("tbody > tr:nth-child(1) > td:nth-child(1)").text + expect(search_value.include?(veteran.last_name)) + end + + it "should display the search bar with text even we shift to other tabs " do + visit "/queue/correspondence?tab=correspondence_assigned&page=1&sort_by=vaDor&order=asc" + expect(page).to have_content("Filter table by any of its columns") + expect(find("#searchBar")).to match_xpath("//input[@placeholder='Type to filter...']") + veteran = Veteran.first + find_by_id("searchBar").fill_in with: veteran.last_name + find_by_id("tasks-tabwindow-tab-1").click + expect(find_by_id("searchBar").value).to eq veteran.last_name + end + + it "Should display Only search results even when we hit pagination " do + visit "/queue/correspondence?tab=correspondence_assigned&page=1&sort_by=vaDor&order=asc" + expect(page).to have_content("Filter table by any of its columns") + expect(find("#searchBar")).to match_xpath("//input[@placeholder='Type to filter...']") + veteran = Veteran.first + find_by_id("searchBar").fill_in with: veteran.last_name + expect(page).to have_button("Next") + expect(page).not_to have_button("Previous") + click_button("Next", match: :first) + expect(page).not_to have_button("Next") + click_button("Previous", match: :first) + search_value = find("tbody > tr:nth-child(1) > td:nth-child(1)").text + expect(search_value.include?(veteran.last_name)) + end + + it "Verify the user can clear the search bar by clicking the 'x' in the search bar" do + visit "/queue/correspondence?tab=correspondence_assigned&page=1&sort_by=vaDor&order=asc" + expect(page).to have_content("Filter table by any of its columns") + expect(find("#searchBar")).to match_xpath("//input[@placeholder='Type to filter...']") + veteran = Veteran.first + find_by_id("searchBar").fill_in with: veteran.last_name + search_value = find("tbody > tr:nth-child(1) > td:nth-child(1)").text + expect(search_value.include?(veteran.last_name)) + find_by_id("button-clear-search").click + expect(all("tbody > tr:nth-child(1) > td:nth-child(1)").length == 1) + end + + it "Verify the user can have search results sorted by veteran details." do + visit "/queue/correspondence?tab=correspondence_assigned&page=1&sort_by=vaDor&order=asc" + + expect(page).to have_content("Filter table by any of its columns") + expect(find("#searchBar")).to match_xpath("//input[@placeholder='Type to filter...']") + + # Find Zzzane and get details + find_by_id("searchBar").fill_in with: "Zzzane" + first("[aria-label='Page 2']").click + only_vet_info = page.all("#task-link")[0].text + + # Return to first page, should not exist + first("[aria-label='Page 1']").click + expect(page).not_to have_content("Zzzans") + + # Sort Z-A, should return details from Zzzane + sort_icon = find("[aria-label='Sort by Veteran Details']") + sort_icon.click + expect(page.all("#task-link")[0].text).to eq(only_vet_info) + + # Sort A-Z, should result in no results + sort_icon = find("[aria-label='Sort by Veteran Details']") + sort_icon.click + expect(page).not_to have_content("Zzzans") + end + + it "Verify the user can have search results filtered by receipt date on filter correctly" do + visit "/queue/correspondence?tab=correspondence_assigned&page=1&sort_by=vaDor&order=asc" + veteran = Veteran.first + find_by_id("searchBar").fill_in with: veteran.last_name + search_value = find("tbody > tr:nth-child(1) > td:nth-child(1)").text + expect(search_value.include?(veteran.last_name)) + all(".unselected-filter-icon")[0].click + find_by_id("reactSelectContainer").click + find_by_id("react-select-2-option-3").click + all("div.input-container > input")[0].fill_in( + with: @review_correspondence.va_date_of_receipt.strftime("%m/%d/%Y") + ) + find(".cf-submit").click + expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) + end + end + + # Tested on Correspondence Cases page + context "correspondence paginationg search testing" do + let(:current_user) { create(:user) } + before :each do + InboundOpsTeam.singleton.add_user(current_user) + User.authenticate!(user: current_user) + end + + before do + 25.times do + review_correspondence = create(:correspondence) + rpt = ReviewPackageTask.find_by(appeal_id: review_correspondence.id) + rpt.update!(assigned_to: current_user, status: "assigned") + rpt.save! + end + 1.times do + params = { + first_name: "Zzzane", + last_name: "Zzzans" + } + veteran = create(:veteran, params) + review_correspondence = create(:correspondence, veteran_id: veteran.id) + rpt = ReviewPackageTask.find_by(appeal_id: review_correspondence.id) + rpt.update!(assigned_to: current_user, + status: "assigned", + assigned_at: 42.days.ago) + rpt.save! + end + FeatureToggle.enable!(:correspondence_queue) + end + + it "should display the search bar with text even when switching pages in pagination" do + visit "/queue/correspondence/team?tab=correspondence_team_assigned" + expect(page).to have_content("Filter table by any of its columns") + veteran = Veteran.first + find_by_id("searchBar").fill_in with: veteran.last_name + first("[aria-label='Page 2']").click + expect(find_by_id("searchBar").value).to eq veteran.last_name + end + + it "should display the correct results with pagination and filtering" do + visit "/queue/correspondence/team?tab=correspondence_team_assigned" + find_by_id("searchBar").fill_in with: "Zzzans" + first("[aria-label='Page 2']").click + only_vet_info = page.all("#task-link")[0].text + expect(page.all("#task-link")[0].text == only_vet_info) + # put page in the sorted Z-A state (filtering changes page Zzzane Should exist on) + find("[aria-label='Sort by Veteran Details']").click + first("[aria-label='Page 1']").click + expect(page.all("#task-link")[0].text == only_vet_info) + # check if first result is the last vet + end + + it "should be able to search by different columns" do + visit "/queue/correspondence/team?tab=correspondence_team_assigned" + # searches by days waiting + find_by_id("searchBar").fill_in with: "42" + first("[aria-label='Page 2']").click + only_vet_info = page.all("#task-link")[0].text + expect(page.all("#task-link")[0].text == only_vet_info) + # put page in the sorted Z-A state (filtering changes page Zzzane Should exist on) + find("[aria-label='Sort by Veteran Details']").click + first("[aria-label='Page 1']").click + expect(page.all("#task-link")[0].text == only_vet_info) + # check if first result is the last vet + end + end +end diff --git a/spec/requests/correspondence_controller_spec.rb b/spec/requests/correspondence_controller_spec.rb index 91bf09b67f4..7ba7702e621 100644 --- a/spec/requests/correspondence_controller_spec.rb +++ b/spec/requests/correspondence_controller_spec.rb @@ -140,7 +140,7 @@ expect(task[:attributes][:completion_date]).to eq(nil).or be_a(String) expect(task[:attributes][:days_waiting]).to be_a(Integer) expect(task[:attributes][:va_date_of_receipt]).to be_a(String) - expect(task[:attributes][:label]).to be_a(String) + expect(task[:attributes][:label]).to eq(nil).or be_a(String) expect(task[:attributes][:status]).to be_a(String) expect(task[:attributes][:assigned_to]).to be_a(Hash) expect(task[:attributes][:assigned_by]).to be_a(Hash) From c983c1d17ff778444d0b399fc44102b91f176719 Mon Sep 17 00:00:00 2001 From: Rnmarshall93 <110805785+Rnmarshall93@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:16:35 -0700 Subject: [PATCH 175/237] Correspondence Cases - Unassigned Tab - Superuser | Remove Select Column (#21228) * fixed select column appearing in superuser table * linting fixes * fixed linter issues * fixed merge conflicts --------- Co-authored-by: Jim Foley --- app/controllers/correspondence_controller.rb | 2 +- ...ion_correspondence_unassigned_tasks_tab.rb | 6 ++-- client/app/components/ReceiptDatePicker.jsx | 1 - .../correspondenceActions.js | 2 +- .../correspondence_cases_spec.rb | 32 +++++++++---------- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/app/controllers/correspondence_controller.rb b/app/controllers/correspondence_controller.rb index e6f16fcdde5..8e01924940b 100644 --- a/app/controllers/correspondence_controller.rb +++ b/app/controllers/correspondence_controller.rb @@ -156,4 +156,4 @@ def verify_feature_toggle redirect_to "/unauthorized" end end -end \ No newline at end of file +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 index a087b24f658..35ec294f89b 100644 --- a/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb +++ b/app/models/queue_tabs/organization_correspondence_unassigned_tasks_tab.rb @@ -23,12 +23,14 @@ def tasks # :reek:UtilityFunction def self.column_names - [ - Constants.QUEUE_CONFIG.COLUMNS.CHECKBOX_COLUMN.name, + user = RequestStore.store[:current_user] + columns = [ Constants.QUEUE_CONFIG.COLUMNS.VETERAN_DETAILS.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.mail_superuser? + columns end end diff --git a/client/app/components/ReceiptDatePicker.jsx b/client/app/components/ReceiptDatePicker.jsx index 8e58f9f29d1..9bad32977c5 100644 --- a/client/app/components/ReceiptDatePicker.jsx +++ b/client/app/components/ReceiptDatePicker.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import ReactSelectDropdown from '../../../client/app/components/ReactSelectDropdown'; import DateSelector from './DateSelector'; import Button from './Button'; -import { style } from 'glamor'; const dateDropdownMap = [ { value: 0, label: 'Between these dates' }, diff --git a/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js index f8be4a41710..0208eeb1c70 100644 --- a/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js +++ b/client/app/queue/correspondence/correspondenceReducer/correspondenceActions.js @@ -209,7 +209,7 @@ export const setSelectedVeteranDetails = (selectedVeteranDetails) => (dispatch) selectedVeteranDetails } }); -} +}; export const setErrorBanner = (isVisible) => (dispatch) => { dispatch({ type: ACTIONS.SET_SHOW_CORRESPONDENCE_INTAKE_FORM_ERROR_BANNER, diff --git a/spec/feature/queue/correspondence/correspondence_cases_spec.rb b/spec/feature/queue/correspondence/correspondence_cases_spec.rb index 440717db6c6..fd0f76ebbef 100644 --- a/spec/feature/queue/correspondence/correspondence_cases_spec.rb +++ b/spec/feature/queue/correspondence/correspondence_cases_spec.rb @@ -173,8 +173,8 @@ find_by_id("reactSelectContainer").click find_by_id("react-select-2-option-2").click current_date = Time.zone.today - myDate = current_date.strftime("%m/%d/%Y") - all("div.input-container > input")[0].fill_in(with: myDate) + my_date = current_date.strftime("%m/%d/%Y") + all("div.input-container > input")[0].fill_in(with: my_date) find(".cf-submit").click expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) end @@ -255,7 +255,7 @@ find("#react-select-2-option-0").click current_date = Time.zone.today start_date = (current_date - 1).strftime("%m/%d/%Y") - end_date = (current_date).strftime("%m/%d/%Y") + end_date = current_date.strftime("%m/%d/%Y") all("div.input-container > input")[0].fill_in(with: start_date) all("div.input-container > input")[1].fill_in(with: end_date) @@ -274,7 +274,7 @@ find("#reactSelectContainer").click find("#react-select-2-option-1").click current_date = Time.zone.today - start_date = (current_date -3).strftime("%m/%d/%Y") + start_date = (current_date - 3).strftime("%m/%d/%Y") all("div.input-container > input")[0].fill_in(with: start_date) expect(page).to have_button("Apply filter", disabled: false) @@ -490,8 +490,8 @@ find_by_id("reactSelectContainer").click find_by_id("react-select-2-option-2").click current_date = Time.zone.today - myDate = (current_date - 4).strftime("%m/%d/%Y") - all("div.input-container > input")[0].fill_in(with: myDate) + my_date = (current_date - 4).strftime("%m/%d/%Y") + all("div.input-container > input")[0].fill_in(with: my_date) find(".cf-submit").click expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) end @@ -643,8 +643,8 @@ find_by_id("reactSelectContainer").click find_by_id("react-select-3-option-2").click current_date = Time.zone.today - myDate = (current_date - 5).strftime("%m/%d/%Y") - all("div.input-container > input")[0].fill_in(with: myDate) + my_date = (current_date - 5).strftime("%m/%d/%Y") + all("div.input-container > input")[0].fill_in(with: my_date) click_button("Apply Filter") expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) end @@ -797,8 +797,8 @@ find_by_id("reactSelectContainer").click find_by_id("react-select-3-option-1").click current_date = Time.zone.today - myDate = (current_date - 5).strftime("%m/%d/%Y") - all("div.input-container > input")[0].fill_in(with: myDate) + my_date = (current_date - 5).strftime("%m/%d/%Y") + all("div.input-container > input")[0].fill_in(with: my_date) click_button("Apply Filter") expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) end @@ -809,8 +809,8 @@ find_by_id("reactSelectContainer").click find_by_id("react-select-3-option-2").click current_date = Time.zone.today - myDate = (current_date - 5).strftime("%m/%d/%Y") - all("div.input-container > input")[0].fill_in(with: myDate) + my_date = (current_date - 5).strftime("%m/%d/%Y") + all("div.input-container > input")[0].fill_in(with: my_date) click_button("Apply Filter") expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) end @@ -1023,8 +1023,8 @@ find_by_id("reactSelectContainer").click find_by_id("react-select-2-option-2").click current_date = Time.zone.today - myDate = current_date.strftime("%m/%d/%Y") - all("div.input-container > input")[0].fill_in(with: myDate) + my_date = current_date.strftime("%m/%d/%Y") + all("div.input-container > input")[0].fill_in(with: my_date) find(".cf-submit").click expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) end @@ -1141,8 +1141,8 @@ find_by_id("reactSelectContainer").click find_by_id("react-select-2-option-2").click current_date = Time.zone.today - myDate = (current_date -3).strftime("%m/%d/%Y") - all("div.input-container > input")[0].fill_in(with: myDate) + my_date = (current_date - 3).strftime("%m/%d/%Y") + all("div.input-container > input")[0].fill_in(with: my_date) find(".cf-submit").click expect(all("tbody > tr:nth-child(1) > td:nth-child(4)").length == 1) end From b015e8ba2a8fd419f8c82f7718661ef8ed72b55c Mon Sep 17 00:00:00 2001 From: HunJerBAH Date: Fri, 22 Mar 2024 09:12:09 -0400 Subject: [PATCH 176/237] added back in redux store value that was removed --- app/controllers/correspondence_intake_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/correspondence_intake_controller.rb b/app/controllers/correspondence_intake_controller.rb index 3f2d52c48f1..afbe86182ab 100644 --- a/app/controllers/correspondence_intake_controller.rb +++ b/app/controllers/correspondence_intake_controller.rb @@ -2,6 +2,11 @@ class CorrespondenceIntakeController < CorrespondenceController 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(user: current_user, + correspondence: current_correspondence)&.redux_store + respond_to do |format| format.html { return render "correspondence/intake" } format.json do From f1463e0cea7b917c02b347e712ce699790629170 Mon Sep 17 00:00:00 2001 From: Will Medders <93014155+wmedders21@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:23:23 -0500 Subject: [PATCH 177/237] Wmedders21/appeals 42698 (#21230) * prevent overwriting correspondence uuid on page load * provide saved redux store to view if the intake form has been previously worked * Update test expectations * Merge with feature --- .../correspondence/intake/components/CorrespondenceIntake.jsx | 4 +++- .../correspondence/correspondence_queue_task_link_spec.rb | 4 ++-- spec/models/correspondence_config_spec.rb | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx b/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx index 116d98aa1a9..418a0dad308 100644 --- a/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx +++ b/client/app/queue/correspondence/intake/components/CorrespondenceIntake.jsx @@ -99,7 +99,9 @@ export const CorrespondenceIntake = (props) => { ); useEffect(() => { - props.saveCurrentIntake(intakeCorrespondence, exportStoredata); + if (currentStep !== 1) { + props.saveCurrentIntake(intakeCorrespondence, exportStoredata); + } }, [currentStep]); useEffect(() => { diff --git a/spec/feature/queue/correspondence/correspondence_queue_task_link_spec.rb b/spec/feature/queue/correspondence/correspondence_queue_task_link_spec.rb index e3e1d9c4a72..0f5b2da1aa9 100644 --- a/spec/feature/queue/correspondence/correspondence_queue_task_link_spec.rb +++ b/spec/feature/queue/correspondence/correspondence_queue_task_link_spec.rb @@ -81,7 +81,7 @@ end it "user remains on Correspondence Cases" do - expect(page).to have_content("Correspondence cases") + expect(page).to have_content("Correspondence Cases") end it "a modal appears on the screen" do @@ -105,7 +105,7 @@ end it "user remains on Correspondence Cases" do - expect(page).to have_content("Correspondence cases") + expect(page).to have_content("Correspondence Cases") end it "a modal appears on the screen" do diff --git a/spec/models/correspondence_config_spec.rb b/spec/models/correspondence_config_spec.rb index 87b8e9b3ddd..d76385e1eb9 100644 --- a/spec/models/correspondence_config_spec.rb +++ b/spec/models/correspondence_config_spec.rb @@ -20,7 +20,7 @@ describe "title" do context "when assigned to an org" do it "is formatted as expected" do - expect(subject[:table_title]).to eq("Correspondence cases") + expect(subject[:table_title]).to eq("Correspondence Cases") end end @@ -28,7 +28,7 @@ let(:assignee) { user } it "is formatted as expected" do - expect(subject[:table_title]).to eq("Your correspondence") + expect(subject[:table_title]).to eq("Your Correspondence") end end end From 4f19273c294c869caafa9b9b3aab5e9e9c0fb430 Mon Sep 17 00:00:00 2001 From: Alex Smith Date: Fri, 22 Mar 2024 10:03:13 -0600 Subject: [PATCH 178/237] Restore Code Accidentally Removed in Merge (#21241) --- app/controllers/correspondence_controller.rb | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/controllers/correspondence_controller.rb b/app/controllers/correspondence_controller.rb index e9935fc91d6..a69168974dc 100644 --- a/app/controllers/correspondence_controller.rb +++ b/app/controllers/correspondence_controller.rb @@ -120,6 +120,43 @@ def pdf ) end + 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) + rescue StandardError => error + Rails.logger.error(error.full_message) + ensure + render json: { batch_auto_assignment_attempt_id: batch&.id }, status: :ok + end + end + + def auto_assign_status + batch = BatchAutoAssignmentAttempt.includes(:individual_auto_assignment_attempts) + .find_by!(user: current_user, id: 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 # :reek:FeatureEnvy From ae232c1d4c47668d34d5ff596acbdcc1cfff8652 Mon Sep 17 00:00:00 2001 From: Rnmarshall93 <110805785+Rnmarshall93@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:23:32 -0700 Subject: [PATCH 179/237] 35485 text fix (#21242) * fixed missing text * updated instructions text * added instructions to action required tasks --------- Co-authored-by: HunJerBAH --- client/app/queue/correspondence/CorrespondenceCases.jsx | 4 +++- db/seeds/queue_correspondences.rb | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/app/queue/correspondence/CorrespondenceCases.jsx b/client/app/queue/correspondence/CorrespondenceCases.jsx index 778f907d88e..e16996a3f73 100644 --- a/client/app/queue/correspondence/CorrespondenceCases.jsx +++ b/client/app/queue/correspondence/CorrespondenceCases.jsx @@ -101,7 +101,9 @@ const CorrespondenceCases = (props) => { const textAreaElement = (
- +
); useEffect(() => { diff --git a/db/seeds/queue_correspondences.rb b/db/seeds/queue_correspondences.rb index 8b9cbb2694a..50b463f6489 100644 --- a/db/seeds/queue_correspondences.rb +++ b/db/seeds/queue_correspondences.rb @@ -185,7 +185,8 @@ def create_correspondence_with_action_required_tasks(user = {}, veteran = {}) appeal_id: corres.id, appeal_type: "Correspondence", assigned_to: InboundOpsTeam.singleton, - assigned_by_id: rpt.assigned_to_id + assigned_by_id: rpt.assigned_to_id, + instructions: ["Test instructions for #{task_array[index]&.name}."] ) pat.update(assigned_at: corres.va_date_of_receipt) end @@ -202,7 +203,8 @@ def create_correspondences_with_review_remove_package_tasks appeal_type: "Correspondence", appeal_id: corres.id, assigned_to: InboundOpsTeam.singleton, - assigned_by_id: rpt.assigned_to_id + assigned_by_id: rpt.assigned_to_id, + instructions: ["Test instructions for #{task_array[index]&.name}."] ) end end From 24de5525c9db2b54ab94bbf1cc8966f281692a05 Mon Sep 17 00:00:00 2001 From: Rnmarshall93 <110805785+Rnmarshall93@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:23:20 -0700 Subject: [PATCH 180/237] text fix (#21246) Co-authored-by: HunJerBAH <99915461+HunJerBAH@users.noreply.github.com> --- app/models/concerns/correspondence_controller_util.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/correspondence_controller_util.rb b/app/models/concerns/correspondence_controller_util.rb index f33e66944ca..1e87aabb191 100644 --- a/app/models/concerns/correspondence_controller_util.rb +++ b/app/models/concerns/correspondence_controller_util.rb @@ -61,7 +61,7 @@ def reassign_remove_banner_action(mail_team_user) end set_reassign_remove_banner_params(mail_team_user, operation_type) rescue StandardError - set_error_banner_params(operation_type, mail_team_user) + set_error_banner_params(mail_team_user) end end @@ -200,8 +200,8 @@ def set_reassign_remove_banner_params(user, operation_type) end end - def set_error_banner_params(operation_type, mail_team_user) - operation_verb = operation_type == "approve" ? "approved" : "rejected" + def set_error_banner_params(mail_team_user) + operation_verb = @action_type == "approve" ? "approved" : "rejected" @response_header = "Package request for #{mail_team_user.css_id} could not be #{operation_verb}" @response_message = "Please try again at a later time or contact the Help Desk." @response_type = "error" From 0bb292bd71a20b57601f7e484d76d48bdf91d7e2 Mon Sep 17 00:00:00 2001 From: HunJerBAH <99915461+HunJerBAH@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:00:23 -0400 Subject: [PATCH 181/237] changed contents of correspondence seed notes (#21259) --- db/seeds/correspondence.rb | 8 ++++---- spec/factories/correspondence.rb | 2 +- .../correspondence/add_related_correspondence_spec.rb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/db/seeds/correspondence.rb b/db/seeds/correspondence.rb index f2499959d8f..dd45083b4c7 100644 --- a/db/seeds/correspondence.rb +++ b/db/seeds/correspondence.rb @@ -54,7 +54,7 @@ def create_correspondences cmp_queue_id: 1, cmp_packet_number: @cmp_packet_number, va_date_of_receipt: Time.zone.yesterday, - notes: "This is a note from CMP.", + notes: "This is a test note", assigned_by_id: 81, updated_by_id: 81, veteran_id: veteran.id, @@ -74,7 +74,7 @@ def create_correspondences cmp_queue_id: 1, cmp_packet_number: @cmp_packet_number, va_date_of_receipt: Time.zone.yesterday, - notes: "This is a note from CMP.", + notes: "This is a test note.", assigned_by_id: 81, updated_by_id: 81, veteran_id: veteran.id, @@ -101,7 +101,7 @@ def create_correspondences cmp_queue_id: 1, cmp_packet_number: @cmp_packet_number, va_date_of_receipt: Time.zone.yesterday, - notes: "This is a note from CMP.", + notes: "This is a test note.", assigned_by_id: 81, updated_by_id: 81, veteran_id: veteran.id, @@ -128,7 +128,7 @@ def create_correspondences cmp_queue_id: cmp_queue_id, cmp_packet_number: @cmp_packet_number, va_date_of_receipt: Time.zone.yesterday, - notes: "This is a note from CMP.", + notes: "This is a test note.", assigned_by_id: 81, updated_by_id: 81, veteran_id: veteran.id, diff --git a/spec/factories/correspondence.rb b/spec/factories/correspondence.rb index 5687efee792..281881dbd76 100644 --- a/spec/factories/correspondence.rb +++ b/spec/factories/correspondence.rb @@ -8,7 +8,7 @@ cmp_queue_id { 1 } cmp_packet_number { rand(1_000_000_000..9_999_999_999) } va_date_of_receipt { Time.zone.yesterday } - notes { "This is a note from CMP." } + notes { "This is a test note." } assigned_by factory: :user correspondence_type diff --git a/spec/feature/queue/correspondence/add_related_correspondence_spec.rb b/spec/feature/queue/correspondence/add_related_correspondence_spec.rb index d35f075c537..b7fe8cb57ee 100644 --- a/spec/feature/queue/correspondence/add_related_correspondence_spec.rb +++ b/spec/feature/queue/correspondence/add_related_correspondence_spec.rb @@ -88,7 +88,7 @@ expect(page).to have_content("Mail") expect(page).to have_content("15") expect(page).to have_content("9") - expect(page).to have_content("This is a note from CMP") + expect(page).to have_content("This is a test note") end it "table displays 15 items per page" do From 21ac0f670b57930ca3dea8bf0d224520bd69e3e1 Mon Sep 17 00:00:00 2001 From: HunJerBAH Date: Tue, 26 Mar 2024 11:02:07 -0400 Subject: [PATCH 182/237] added Auto Assign button to Correspondence Cases table --- app/controllers/correspondence_controller.rb | 4 ++-- .../app/queue/correspondence/CorrespondenceTableBuilder.jsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/controllers/correspondence_controller.rb b/app/controllers/correspondence_controller.rb index 4f3207ecc8a..5d299613ce3 100644 --- a/app/controllers/correspondence_controller.rb +++ b/app/controllers/correspondence_controller.rb @@ -108,8 +108,6 @@ def document_type_correspondence render json: { data: data } end - private - def handle_mail_superuser_or_supervisor set_handle_mail_superuser_or_supervisor_params(current_user, params) mail_team_user = User.find_by(css_id: params[:user].strip) if params[:user].present? @@ -159,6 +157,8 @@ def auto_assign_status render json: status_details, status: :ok end + private + def handle_reassign_or_remove_task(mail_team_user) return unless @reassign_remove_task_id.present? && @action_type.present? diff --git a/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx b/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx index 4194dce8bd7..64f37985b3f 100644 --- a/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx +++ b/client/app/queue/correspondence/CorrespondenceTableBuilder.jsx @@ -12,6 +12,7 @@ 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, @@ -228,9 +229,7 @@ const CorrespondenceTableBuilder = (props) => { disabled={!isDropdownItemSelected || !isAnyCheckboxSelected} /> - diff --git a/client/app/components/TaskCompletedDatePicker.jsx b/client/app/components/TaskCompletedDatePicker.jsx index 111a16b85ed..0593f5953ab 100644 --- a/client/app/components/TaskCompletedDatePicker.jsx +++ b/client/app/components/TaskCompletedDatePicker.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import ReactSelectDropdown from '../../../client/app/components/ReactSelectDropdown'; import DateSelector from './DateSelector'; import Button from './Button'; +import { css } from 'glamor'; const dateDropdownMap = [ { value: 0, label: 'Between these dates' }, @@ -45,6 +46,51 @@ const TaskCompletedDatePicker = (props) => { } }; + const styles = { + optSelect: css({ + '.receiptDate': { + }, + '& .css-yk16xz-control': { + borderRadius: '0px' + }, + '& .css-1pahdxg-control': { + borderColor: 'hsl(0, 0%, 100%)', + boxShadow: '0 0 0 1px #5B616B !important', + borderRadius: '0px !important', + ':hover': { + borderColor: 'hsl(0, 0%, 80%)', + } + } + }) + }; + + const selectContainerStyles = css({ + '& .data-css-1co2jut': { + margin: '0 5% 0 5%' + }, + '& .cf-form-textinput.usa-input-error': { + borderLeft: '4px solid #cd2026', + marginTop: '0px', + paddingBottom: '0px', + paddingLeft: '0.5rem', + paddingTop: '0px', + position: 'relative', + right: '2px' + }, + '& .usa-input-error input': { + width: 'inherit' + }, + '& .cf-form-textinput': { + paddingTop: '11px !important', + marginBottom: 0, + '& .input-container': { + '& input': { + margin: 0 + } + } + } + }); + const taskCompletedDateFilterStates = props.taskCompletedDateFilterStates; const isApplyFilterButtonDisabled = props.taskCompletedDateState === taskCompletedDateFilterStates.BETWEEN ? @@ -55,7 +101,7 @@ const TaskCompletedDatePicker = (props) => { switch (props.taskCompletedDateState) { case taskCompletedDateFilterStates.BETWEEN: return ( -
+
{ errorMessage={errorMessagesNode(dateErrorsTo, 'toDate')} />
); case taskCompletedDateFilterStates.BEFORE: return ( -
+
{
); case taskCompletedDateFilterStates.AFTER: return ( -
+
{
); case taskCompletedDateFilterStates.ON: return ( -
+
{ } }; - return