diff --git a/app/controllers/appeals_controller.rb b/app/controllers/appeals_controller.rb index d811c78f759..af5663ba258 100644 --- a/app/controllers/appeals_controller.rb +++ b/app/controllers/appeals_controller.rb @@ -64,6 +64,26 @@ def power_of_attorney } end + def hearings + most_recently_held_hearing = appeal.hearings + .select { |hearing| hearing.disposition.to_s == Constants.HEARING_DISPOSITION_TYPES.held } + .max_by(&:scheduled_for) + + render json: + if most_recently_held_hearing + { + held_by: most_recently_held_hearing.judge.present? ? most_recently_held_hearing.judge.full_name : "", + viewed_by_judge: !most_recently_held_hearing.hearing_views.empty?, + date: most_recently_held_hearing.scheduled_for, + type: most_recently_held_hearing.readable_request_type, + external_id: most_recently_held_hearing.external_id, + disposition: most_recently_held_hearing.disposition + } + else + {} + end + end + # For legacy appeals, veteran address and birth/death dates are # the only data that is being pulled from BGS, the rest are from VACOLS for now def veteran diff --git a/client/app/queue/AssignedCasesPage.jsx b/client/app/queue/AssignedCasesPage.jsx index a3a4ee71643..bfce988ca6d 100644 --- a/client/app/queue/AssignedCasesPage.jsx +++ b/client/app/queue/AssignedCasesPage.jsx @@ -84,6 +84,7 @@ class AssignedCasesPage extends React.Component { onTaskAssignment={(params) => props.reassignTasksToUser(params)} selectedTasks={selectedTasks} />

{description}

} { - return appeal.hearings.length; - })); + const doAnyAppealsHaveHeldHearings = Boolean( + _.find(this.props.appeals, (appeal) => { + return appeal.hearings. + filter((hearing) => hearing.disposition === HEARING_DISPOSITION_TYPES.held). + length; + })); - if (doAnyAppealsHaveHearings) { + if (doAnyAppealsHaveHeldHearings) { const hearingColumn = { valueFunction: (appeal) => { - const hearings = appeal.hearings.sort((h1, h2) => h1.date < h2.date ? 1 : -1); + const hearings = appeal.hearings. + filter((hearing) => hearing.disposition === HEARING_DISPOSITION_TYPES.held). + sort((h1, h2) => h1.date < h2.date ? 1 : -1); return ; } diff --git a/client/app/queue/ColocatedTaskListView.jsx b/client/app/queue/ColocatedTaskListView.jsx index 82637d12c15..8075694cc79 100644 --- a/client/app/queue/ColocatedTaskListView.jsx +++ b/client/app/queue/ColocatedTaskListView.jsx @@ -119,6 +119,7 @@ const NewTasksTab = connect( return

{COPY.COLOCATED_QUEUE_PAGE_NEW_TASKS_DESCRIPTION}

{COPY.COLOCATED_QUEUE_PAGE_PENDING_TASKS_DESCRIPTION}

{COPY.COLOCATED_QUEUE_PAGE_ON_HOLD_TASKS_DESCRIPTION}

; } else { tableContent =

{description}

{description}

({ } }); +export const setMostRecentlyHeldHearingForAppeal = (appealId: string, hearing: Object) => ({ + type: ACTIONS.SET_MOST_RECENTLY_HELD_HEARING_FOR_APPEAL, + payload: prepareMostRecentlyHeldHearingForStore(appealId, hearing) +}); + export const setDecisionOptions = (opts: Object) => (dispatch: Dispatch) => { dispatch(hideErrorMessage()); dispatch({ diff --git a/client/app/queue/UnassignedCasesPage.jsx b/client/app/queue/UnassignedCasesPage.jsx index c1ddf2e829c..bfd1b6ce609 100644 --- a/client/app/queue/UnassignedCasesPage.jsx +++ b/client/app/queue/UnassignedCasesPage.jsx @@ -72,6 +72,7 @@ class UnassignedCasesPage extends React.PureComponent { } {!this.props.distributionCompleteCasesLoading && + * + */ const badgeStyling = css({ display: 'inline-block', @@ -30,27 +42,63 @@ const listStyling = css({ } }); -const HearingBadge = ({ hearing }) => { - if (!hearing) { - return null; +class HearingBadge extends React.PureComponent { + componentDidMount = () => { + if (!this.props.mostRecentlyHeldHearingForAppeal && !this.props.hearing) { + ApiUtil.get(`/appeals/${this.props.externalId}/hearings`).then((response) => { + this.props.setMostRecentlyHeldHearingForAppeal(this.props.externalId, JSON.parse(response.text)); + }); + } + } + + render = () => { + const hearing = this.props.mostRecentlyHeldHearingForAppeal || this.props.hearing; + + if (!hearing || !hearing.date) { + return null; + } + + const tooltipText =
+ This case has a hearing associated with it. +
    +
  • Judge: {hearing.heldBy}
  • +
  • Disposition: {_.startCase(hearing.disposition)}
  • +
  • Date:
  • +
  • Type: {_.startCase(hearing.type)}
  • +
+
; + + return
+ + H + +
; } +} - const tooltipText =
- This case has a hearing associated with it. -
    -
  • Judge: {hearing.heldBy}
  • -
  • Disposition: {_.startCase(hearing.disposition)}
  • -
  • Date:
  • -
  • Type: {_.startCase(hearing.type)}
  • -
-
; - - // We expect this badge to be shown in a table, so we use this to get rid of the standard table padding. - return
- - H - -
; +HearingBadge.propTypes = { + task: PropTypes.object, + hearing: PropTypes.object }; -export default HearingBadge; +const mapStateToProps = (state, ownProps) => { + let externalId, hearing; + + if (ownProps.hearing) { + hearing = ownProps.hearing; + } else if (ownProps.task) { + externalId = ownProps.task.appeal.externalId; + } + + return { + hearing, + externalId, + mostRecentlyHeldHearingForAppeal: state.queue.mostRecentlyHeldHearingForAppeal[externalId] || null + }; +}; + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + setMostRecentlyHeldHearingForAppeal +}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(HearingBadge); diff --git a/client/app/queue/components/TaskTable.jsx b/client/app/queue/components/TaskTable.jsx index 5bc2022fa4b..be36a223241 100644 --- a/client/app/queue/components/TaskTable.jsx +++ b/client/app/queue/components/TaskTable.jsx @@ -91,7 +91,7 @@ export class TaskTableUnconnected extends React.PureComponent { caseHearingColumn = () => { return this.props.includeHearingBadge ? { header: '', - valueFunction: (task: TaskWithAppeal) => + valueFunction: (task: TaskWithAppeal) => } : null; } diff --git a/client/app/queue/constants.js b/client/app/queue/constants.js index 7cfa0ceb88e..745a18f3cf9 100644 --- a/client/app/queue/constants.js +++ b/client/app/queue/constants.js @@ -47,6 +47,8 @@ export const ACTIONS = { ERROR_TASKS_AND_APPEALS_OF_ATTORNEY: 'ERROR_TASKS_AND_APPEALS_OF_ATTORNEY', SET_SELECTION_OF_TASK_OF_USER: 'SET_SELECTION_OF_TASK_OF_USER', SET_SELECTED_ASSIGNEE_OF_USER: 'SET_SELECTED_ASSIGNEE_OF_USER', + SET_MOST_RECENTLY_HELD_HEARING_FOR_APPEAL: 'SET_MOST_RECENTLY_HELD_HEARING_FOR_APPEAL', + ERROR_ON_RECEIVE_HEARING_FOR_APPEAL: 'ERROR_ON_RECEIVE_HEARING_FOR_APPEAL', START_ASSIGN_TASKS_TO_USER: 'START_ASSIGN_TASKS_TO_USER', SET_PENDING_DISTRIBUTION: 'SET_PENDING_DISTRIBUTION', RECEIVE_ALL_ATTORNEYS: 'RECEIVE_ALL_ATTORNEYS', diff --git a/client/app/queue/reducers.js b/client/app/queue/reducers.js index 39c016e1447..13177bb8417 100644 --- a/client/app/queue/reducers.js +++ b/client/app/queue/reducers.js @@ -25,6 +25,7 @@ export const initialState = { claimReviews: {}, editingIssue: {}, docCountForAppeal: {}, + mostRecentlyHeldHearingForAppeal: {}, newDocsForAppeal: {}, specialIssues: {}, @@ -171,6 +172,14 @@ export const workQueueReducer = (state: QueueState = initialState, action: Objec } } }); + case ACTIONS.SET_MOST_RECENTLY_HELD_HEARING_FOR_APPEAL: + return update(state, { + mostRecentlyHeldHearingForAppeal: { + [action.payload.appealId]: { + $set: action.payload.hearing + } + } + }); case ACTIONS.SET_DECISION_OPTIONS: return update(state, { stagedChanges: { diff --git a/client/app/queue/types/state.js b/client/app/queue/types/state.js index 0bc642865b7..cfb2c0d0ab9 100644 --- a/client/app/queue/types/state.js +++ b/client/app/queue/types/state.js @@ -86,6 +86,7 @@ export type QueueState = {| claimReviews: ClaimReviews, editingIssue: Object, docCountForAppeal: {[string]: Object}, + mostRecentlyHeldHearingForAppeal: {[string]: Object}, stagedChanges: { appeals: {[string]: Object}, taskDecision: { diff --git a/client/app/queue/utils.js b/client/app/queue/utils.js index 85f45ae039e..3cd51c1c349 100644 --- a/client/app/queue/utils.js +++ b/client/app/queue/utils.js @@ -47,6 +47,20 @@ export const getUndecidedIssues = (issues: Issues) => _.filter(issues, (issue) = } }); +export const prepareMostRecentlyHeldHearingForStore = (appealId: string, hearing) => { + return { + appealId, + hearing: { + heldBy: hearing.held_by, + viewedByJudge: hearing.viewed_by_judge, + date: hearing.date, + type: hearing.type, + externalId: hearing.external_id, + disposition: hearing.disposition + } + }; +}; + export const prepareTasksForStore = (tasks: Array): Tasks => tasks.reduce((acc, task: Object): Tasks => { const decisionPreparedBy = task.attributes.decision_prepared_by.first_name ? { diff --git a/client/constants/HEARING_DISPOSITION_TYPES.json b/client/constants/HEARING_DISPOSITION_TYPES.json new file mode 100644 index 00000000000..8492d5e7106 --- /dev/null +++ b/client/constants/HEARING_DISPOSITION_TYPES.json @@ -0,0 +1,6 @@ +{ + "held": "held", + "cancelled": "cancelled", + "postponed": "postponed", + "no_show": "no_show" +} diff --git a/client/test/karma/queue/ColocatedTaskListView-test.js b/client/test/karma/queue/ColocatedTaskListView-test.js index 6ba065d8a20..d89ab570da6 100644 --- a/client/test/karma/queue/ColocatedTaskListView-test.js +++ b/client/test/karma/queue/ColocatedTaskListView-test.js @@ -148,15 +148,17 @@ describe('ColocatedTaskListView', () => { const cells = wrapper.find('td'); - expect(cells).to.have.length(6); + expect(cells).to.have.length(7); const wrappers = []; for (let i = 0; i < cells.length; i++) { wrappers.push(cells.at(i)); } - const [caseDetails, columnTasks, types, docketNumber, daysWaiting, documents] = wrappers; + const [hearings, caseDetails, columnTasks, types, docketNumber, daysWaiting, documents] = wrappers; const task = taskNewAssigned; + expect(hearings.text()).to.include(''); + expect(caseDetails.text()).to.include(appeal.veteranFullName); expect(caseDetails.text()).to.include(appeal.veteranFullName); expect(caseDetails.text()).to.include(appeal.veteranFileNumber); expect(columnTasks.text()).to.include(CO_LOCATED_ADMIN_ACTIONS[task.label]); @@ -226,14 +228,14 @@ describe('ColocatedTaskListView', () => { const cells = wrapper.find('td'); - expect(cells).to.have.length(6); + expect(cells).to.have.length(7); const wrappers = []; for (let i = 0; i < cells.length; i++) { wrappers.push(cells.at(i)); } { - const [daysOnHold, documents] = wrappers.slice(4); + const [daysOnHold, documents] = wrappers.slice(5); expect(daysOnHold.text()).to.equal('1 of 30'); expect(documents.html()).to.include(`/reader/appeal/${taskWithNewDocs.externalAppealId}/documents`); @@ -298,14 +300,15 @@ describe('ColocatedTaskListView', () => { const cells = wrapper.find('td'); - expect(cells).to.have.length(6); + expect(cells).to.have.length(7); const wrappers = []; for (let i = 0; i < cells.length; i++) { wrappers.push(cells.at(i)); } - const [caseDetails, columnTasks, types, docketNumber, daysOnHold, documents] = wrappers; + const [hearings, caseDetails, columnTasks, types, docketNumber, daysOnHold, documents] = wrappers; + expect(hearings.text()).to.include(''); expect(caseDetails.text()).to.include(appeal.veteranFullName); expect(caseDetails.text()).to.include(appeal.veteranFileNumber); expect(columnTasks.text()).to.include(CO_LOCATED_ADMIN_ACTIONS[task.label]); diff --git a/config/routes.rb b/config/routes.rb index 962a6617160..38e7a8d0680 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -102,6 +102,7 @@ get :new_documents get :veteran get :power_of_attorney + get :hearings resources :issues, only: [:create, :update, :destroy], param: :vacols_sequence_id resources :special_issues, only: [:create, :index] resources :advance_on_docket_motions, only: [:create] diff --git a/spec/feature/queue/search_spec.rb b/spec/feature/queue/search_spec.rb index d5290dbfd09..4685ff86bab 100644 --- a/spec/feature/queue/search_spec.rb +++ b/spec/feature/queue/search_spec.rb @@ -213,11 +213,11 @@ expect(find(".cf-hearing-badge")).to have_content("H") end - it "shows information for the correct hearing when there are multiple hearings" do + it "shows information for the most recently held hearing" do expect(page).to have_css( ".__react_component_tooltip div ul li:nth-child(3) strong span", visible: :hidden, - text: 2.days.ago.strftime("%m/%d/%y") + text: 4.days.ago.strftime("%m/%d/%y") ) end end diff --git a/spec/feature/queue/task_queue_spec.rb b/spec/feature/queue/task_queue_spec.rb index b22764dcfcc..6a1aca49b39 100644 --- a/spec/feature/queue/task_queue_spec.rb +++ b/spec/feature/queue/task_queue_spec.rb @@ -52,10 +52,39 @@ expect(find("tbody").find_all("tr").length).to eq(vacols_tasks.length) end + context "hearings" do + context "if a task has a hearing" do + let!(:attorney_task_with_hearing) do + FactoryBot.create( + :ama_attorney_task, + :in_progress, + assigned_to: attorney_user + ) + end + + let!(:hearing) { create(:hearing, appeal: attorney_task_with_hearing.appeal, disposition: "held") } + + before do + visit "/queue" + end + + it "shows the hearing badge" do + expect(page).to have_selector(".cf-hearing-badge") + expect(find(".cf-hearing-badge")).to have_content("H") + end + end + + context "if no tasks have hearings" do + it "does not show the hearing badge" do + expect(page).not_to have_selector(".cf-hearing-badge") + end + end + end + it "supports custom sorting" do docket_number_column_header = page.find(:xpath, "//thead/tr/th[3]/span/span[1]") docket_number_column_header.click - docket_number_column_vals = page.find_all(:xpath, "//tbody/tr/td[3]/span[3]") + docket_number_column_vals = page.find_all(:xpath, "//tbody/tr/td[4]/span[3]") expect(docket_number_column_vals.map(&:text)).to eq vacols_tasks.map(&:docket_number).sort.reverse docket_number_column_header.click expect(docket_number_column_vals.map(&:text)).to eq vacols_tasks.map(&:docket_number).sort.reverse