Skip to content

Commit

Permalink
Connects the backend and frontend for the schedule veteran directly w…
Browse files Browse the repository at this point in the history
…orkflow (#15212)

Resolves #14895

Stack
- [Part 2](#15214)
- [Part 3](#15215)

### Description
Updates frontend and backend code to connect the functionality and complete the workflow for scheduling veterans directly to either Central Office, Video, or Virtual hearings.

### Acceptance Criteria
- [ ] Code compiles correctly
- [ ] Behavior is unchanged when the feature flag is disabled

### Testing Plan

**With Feature Flag**

*Central Hearing*
NOTE: You need to do some setup work for the Central flow, first get the `uuid` of an Appeal without a hearing scheduled and open a rails console. Run the following commands to make the central office hearing available to schedule: 

```
appeal = Appeal.find_by(uuid: "")
appeal.update!(closest_regional_office: "C")
CachedAppeal.create!(appeal_id: appeal.id, appeal_type: appeal.class.name, closest_regional_office_key: "C")
```

1. Shadow user `BVASYELLOW` and navigate to the hearings schedule
1. Click the `Schedule Veterans` button change the dropdown to `Central`
1. Change to the `AMA Veterans Waiting` tab and choose the appeal you just updated
1. Select the Schedule Veteran option from the Actions dropdown
1. Ensure the full page schedule veteran component is loaded
1. Fill in the hearing details and click `Schedule`
1. Ensure you receive a success message with a link back to schedule veterans

*Video Hearing*

1. Shadow user `BVASYELLOW` and navigate to the hearings schedule
1. Click the `Schedule Veterans` button change the dropdown to `St. Petersburg, FL`
1. Change to the `AMA Veterans Waiting` tab and choose the appeal you just updated
1. Select the Schedule Veteran option from the Actions dropdown
1. Ensure the full page schedule veteran component is loaded
1. Fill in the hearing details and click `Schedule`
1. Ensure you receive a success message with a link back to schedule veterans

*Virtual Hearing*

1. Shadow user `BVASYELLOW` and navigate to the hearings schedule
1. Click the `Schedule Veterans` button change the dropdown to `St. Petersburg, FL`
1. Change to the `AMA Veterans Waiting` tab and choose the appeal you just updated
1. Select the Schedule Veteran option from the Actions dropdown
1. Ensure the full page schedule veteran component is loaded
1. Change the hearing type to `Virtual`
1. Fill in the hearing details and click `Schedule`
1. Ensure you receive a success message with a link back to schedule veterans

**Without Feature Flag**

*Central Hearing*
NOTE: You need to do some setup work for the Central flow, first get the `uuid` of an Appeal without a hearing scheduled and open a rails console. Run the following commands to make the central office hearing available to schedule: 

```
appeal = Appeal.find_by(uuid: "")
appeal.update!(closest_regional_office: "C")
CachedAppeal.create!(appeal_id: appeal.id, appeal_type: appeal.class.name, closest_regional_office_key: "C")
```

1. Shadow user `BVASYELLOW` and navigate to the hearings schedule
1. Click the `Schedule Veterans` button change the dropdown to `Central`
1. Change to the `AMA Veterans Waiting` tab and choose the appeal you just updated
1. Select the Schedule Veteran option from the Actions dropdown
1. Ensure the full page schedule veteran component is loaded
1. Fill in the hearing details and click `Schedule`
1. Ensure you receive a success message with a link back to schedule veterans

*Video Hearing*

1. Shadow user `BVASYELLOW` and navigate to the hearings schedule
1. Click the `Schedule Veterans` button change the dropdown to `St. Petersburg, FL`
1. Change to the `AMA Veterans Waiting` tab and choose the appeal you just updated
1. Select the Schedule Veteran option from the Actions dropdown
1. Ensure the full page schedule veteran component is loaded
1. Fill in the hearing details and click `Schedule`
1. Ensure you receive a success message with a link back to schedule veterans

### User Facing Changes
 - [ ] Screenshots of UI changes added to PR & Original Issue

 BEFORE|AFTER
 ---|---
![Before-ConvertToCentral](https://user-images.githubusercontent.com/61989526/92796584-2c14dd00-f366-11ea-967c-f7f404c3e443.png)|![After-ConvertToCentral](https://user-images.githubusercontent.com/61989526/92796567-27e8bf80-f366-11ea-821c-feb075d024f0.png)
![Before-PostponeRescheduleImmediately](https://user-images.githubusercontent.com/61989526/92796576-2a4b1980-f366-11ea-8b05-4a3ffc2e0d52.png)|![After-PostponeRescheduleImmediately](https://user-images.githubusercontent.com/61989526/92796597-2cad7380-f366-11ea-8853-02983d1bdfe9.png)
![Before-ConvertToVideo](https://user-images.githubusercontent.com/61989526/92796583-2b7c4680-f366-11ea-9813-bf75c698199c.png)|![After-ConvertToVideo](https://user-images.githubusercontent.com/61989526/92796601-2d460a00-f366-11ea-99eb-3a72fc7b790b.png)
| |![After-ConvertToVirtual](https://user-images.githubusercontent.com/61989526/92796599-2d460a00-f366-11ea-960b-ca46f85f6db8.png)
| |![After-SchedulingInProgress](https://user-images.githubusercontent.com/61989526/92796590-2c14dd00-f366-11ea-978d-cb105c2c3bb0.png)
| |![After-SchedulingComplete](https://user-images.githubusercontent.com/61989526/92796594-2cad7380-f366-11ea-839b-030b5e9f94ff.png)



### Storybook Story
*For Frontend (Presentationa) Components*
* [x] Add a [Storybook](https://github.com/department-of-veterans-affairs/caseflow/wiki/Documenting-React-Components-with-Storybook) file alongside the component file (e.g. create `MyComponent.stories.js` alongside `MyComponent.jsx`)
* [x] Give it a title that reflects the component's location within the overall Caseflow hierarchy
* [x] Write a separate story (within the same file) for each discrete variation of the component
  • Loading branch information
sahalliburton authored Sep 16, 2020
1 parent df5385a commit 5222cfd
Show file tree
Hide file tree
Showing 30 changed files with 227,711 additions and 38,996 deletions.
9 changes: 4 additions & 5 deletions app/controllers/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,14 @@ def update
rescue AssignHearingDispositionTask::HearingAssociationMissing => error
Raven.capture_exception(error)
render json: {
"errors": [
"title": "Missing Associated Hearing",
"detail": error
]
"errors": ["title": "Missing Associated Hearing", "detail": error]
}, status: :bad_request
rescue Caseflow::Error::VirtualHearingConversionFailed => error
Raven.capture_exception(error)

render json: { "errors": ["message": error.message] }, status: error.code
render json: {
"errors": ["title": COPY::FAILED_HEARING_UPDATE, "message": error.message, "code": error.code]
}, status: :bad_request
end

def for_appeal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def convert_hearing_to_virtual(hearing, virtual_hearing_attributes)
Caseflow::Error::VirtualHearingConversionFailed,
error_type: error.class,
message: error.message,
code: :bad_request
code: 1002
)
end
end
Expand Down
1 change: 1 addition & 0 deletions app/views/queue/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
userCanViewHearingSchedule: current_user.can_view_hearing_schedule?,
userCanViewOvertimeStatus: current_user.can_view_overtime_status?,
featureToggles: {
schedule_veteran_virtual_hearing: FeatureToggle.enabled?(:schedule_veteran_virtual_hearing, user: current_user),
special_issues_revamp: FeatureToggle.enabled?(:special_issues_revamp, user: current_user),
overtime_revamp: FeatureToggle.enabled?(:overtime_revamp, user: current_user)
}
Expand Down
1 change: 1 addition & 0 deletions client/COPY.json
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@
"ACCESS_DENIED_TITLE": "Additional access needed",
"UNAUTHORIZED_PAGE_ACCESS_MESSAGE": "You aren't authorized to use this part of Caseflow yet.",
"CASE_DETAILS_LOADING_FAILURE_TITLE": "Unable to load this case",
"FAILED_HEARING_UPDATE": "There was an error updating the hearing",

"CASE_MOVEMENT_TASK_LABEL": "Case Movement",
"JUDGE_ASSIGN_TASK_LABEL": "Assign",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ AppealHearingLocationsDropdown.propTypes = {
PropTypes.shape({
label: PropTypes.string,
value: PropTypes.shape({
facilityId: PropTypes.number,
facilityId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
type: PropTypes.string,
distance: PropTypes.number,
facilityType: PropTypes.string,
Expand Down
5 changes: 4 additions & 1 deletion client/app/components/common/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ export const ACTIONS = {
RECEIVE_ALERTS: 'RECEIVE_ALERTS',
RECEIVE_TRANSITIONING_ALERT: 'RECEIVE_TRANSITIONING_ALERT',
TRANSITION_ALERT: 'TRANSITION_ALERT',
CLEAR_ALERTS: 'CLEAR_ALERTS'
CLEAR_ALERTS: 'CLEAR_ALERTS',
START_POLLING: 'START_POLLING',
STOP_POLLING: 'STOP_POLLING',
SET_SCHEDULE_HEARING_PAYLOAD: 'SET_SCHEDULE_HEARING_PAYLOAD'
};
21 changes: 21 additions & 0 deletions client/app/components/common/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,24 @@ export const removeAlertsWithTimestamps = (timestamps) => ({
export const clearAlerts = () => ({
type: ACTIONS.CLEAR_ALERTS,
});

export const startPollingHearing = (externalId) => ({
type: ACTIONS.START_POLLING,
payload: {
externalId,
polling: true
}
});

export const stopPollingHearing = () => ({
type: ACTIONS.STOP_POLLING,
payload: {
externalId: null,
polling: false
}
});

export const setScheduledHearing = (payload) => ({
type: ACTIONS.SET_SCHEDULE_HEARING_PAYLOAD,
payload
});
19 changes: 18 additions & 1 deletion client/app/components/common/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ export const initialState = {
},
forms: {},
alerts: [],
transitioningAlerts: {}
transitioningAlerts: {},
// Used to handle communication between Case Details/ScheduleVeteran
scheduledHearing: {
taskId: null,
disposition: null,
externalId: null,
polling: false,
}
};

const dropdownsReducer = (state = {}, action = {}) => {
Expand Down Expand Up @@ -151,6 +158,16 @@ const commonComponentsReducer = (state = initialState, action = {}) => {
$set: formsReducer(state.forms, action)
}
});
case ACTIONS.SET_SCHEDULE_HEARING_PAYLOAD:
case ACTIONS.STOP_POLLING:
case ACTIONS.START_POLLING:
return {
...state,
scheduledHearing: {
...state.scheduledHearing,
...action.payload
}
};
default:
return state;
}
Expand Down
72 changes: 23 additions & 49 deletions client/app/hearings/components/Details.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { css } from 'glamor';
import { isEmpty, isUndefined, get } from 'lodash';
import { isUndefined, get } from 'lodash';
import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment';
import PropTypes from 'prop-types';
import React, { useState, useContext, useEffect } from 'react';
Expand All @@ -12,10 +12,16 @@ import {
HearingsFormContext,
updateHearingDispatcher,
RESET_HEARING,
RESET_VIRTUAL_HEARING
} from '../contexts/HearingsFormContext';
import { HearingsUserContext } from '../contexts/HearingsUserContext';
import { deepDiff, pollVirtualHearingData, getChanges, getAppellantTitle } from '../utils';
import {
deepDiff,
getChanges,
getAppellantTitle,
processAlerts,
startPolling,
parseVirtualHearingErrors
} from '../utils';
import { inputFix } from './details/style';
import {
onReceiveAlerts,
Expand Down Expand Up @@ -65,7 +71,7 @@ const HearingDetails = (props) => {
const appellantTitle = getAppellantTitle(hearing?.appellantIsNotVeteran);

// Method to reset the state
const reset = () => {
const resetState = () => {
// Reset the state
setVirtualHearingErrors({});
convertHearing('');
Expand Down Expand Up @@ -100,17 +106,6 @@ const HearingDetails = (props) => {
};
};

const processAlerts = (alerts) => {
alerts.forEach((alert) => {
if ('hearing' in alert) {
props.onReceiveAlerts(alert.hearing);
} else if ('virtual_hearing' in alert && !isEmpty(alert.virtual_hearing)) {
props.onReceiveTransitioningAlert(alert.virtual_hearing, 'virtualHearing');
setShouldStartPolling(true);
}
});
};

const submit = async (editedEmails) => {
try {
// Determine the current state and whether to error
Expand Down Expand Up @@ -167,11 +162,11 @@ const HearingDetails = (props) => {
const alerts = response.body?.alerts;

if (alerts) {
processAlerts(alerts);
processAlerts(alerts, props, setShouldStartPolling);
}

// Reset the state
reset();
resetState();
dispatch({ type: RESET_HEARING, payload: hearingResp });
} catch (respError) {
const code = get(respError, 'response.body.errors[0].code') || '';
Expand All @@ -196,15 +191,7 @@ const HearingDetails = (props) => {
// 1002 is returned with an invalid email. rethrow respError, then re-catch it in VirtualHearingModal
throw respError;
} else {
// Remove the validation string from th error
const messages = msg.split(':')[1];

// Set inline errors for hearing conversion page
const errors = messages.split(',').reduce((list, message) => ({
...list,
[(/Representative/).test(message) ? 'representativeEmail' : 'appellantEmail']:
message.replace('Appellant', appellantTitle)
}), {});
const errors = parseVirtualHearingErrors(msg);

document.getElementById('email-section').scrollIntoView();

Expand All @@ -216,25 +203,12 @@ const HearingDetails = (props) => {
}
};

const startPolling = () => {
return pollVirtualHearingData(hearing?.externalId, (response) => {
// response includes jobCompleted, aliasWithHost, guestPin, hostPin,
// guestLink, and hostLink
const resp = ApiUtil.convertToCamelCase(response);

if (resp.virtualHearing.jobCompleted) {
setShouldStartPolling(false);

// Reset the state with the new details
reset();
dispatch({ type: RESET_VIRTUAL_HEARING, payload: resp });
props.transitionAlert('virtualHearing');
}

// continue polling if return true (opposite of jobCompleted)
return !resp.virtualHearing.jobCompleted;
});
};
const poll = () => startPolling(hearing, {
resetState,
setShouldStartPolling,
dispatch,
props
});

const editedEmails = getEditedEmails();
const convertLabel = converting === 'change_to_virtual' ?
Expand All @@ -247,7 +221,7 @@ const HearingDetails = (props) => {
<div>
<Alert
type="error"
title={error === '' ? 'There was an error updating the hearing' : error}
title={error === '' ? COPY.FAILED_HEARING_UPDATE : error}
/>
</div>
)}
Expand Down Expand Up @@ -289,15 +263,15 @@ const HearingDetails = (props) => {
readOnly={disabled}
requestType={hearing?.readableRequestType}
/>
{shouldStartPolling && startPolling()}
{shouldStartPolling && poll()}
</div>
</AppSegment>
)}
<div {...css({ overflow: 'hidden' })}>
<Button
name="Cancel"
linkStyling
onClick={converting ? () => reset(initialHearing) : goBack}
onClick={converting ? () => resetState() : goBack}
styling={css({ float: 'left', paddingLeft: 0, paddingRight: 0 })}
>
Cancel
Expand All @@ -321,7 +295,7 @@ const HearingDetails = (props) => {
update={updateHearing}
submit={submit}
closeModal={closeVirtualHearingModal}
reset={() => reset(initialHearing)}
reset={() => resetState()}
type={virtualHearingModalType}
{...editedEmails}
/>
Expand Down
Loading

0 comments on commit 5222cfd

Please sign in to comment.