Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-modal form for converting travel board request type to virtual #15217

Merged
merged 12 commits into from
Sep 14, 2020
8 changes: 8 additions & 0 deletions app/models/concerns/appeal_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def regional_office_name
"#{regional_office.city}, #{regional_office.state}"
end

def closest_regional_office_label
return if closest_regional_office.nil?

return "Central Office" if closest_regional_office == "C"

RegionalOffice::CITIES[closest_regional_office][:label]
rubaiyat22 marked this conversation as resolved.
Show resolved Hide resolved
end

def veteran_name
veteran_name_object.formatted(:form)
end
Expand Down
2 changes: 2 additions & 0 deletions app/models/serializers/work_queue/appeal_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ class WorkQueue::AppealSerializer

attribute :closest_regional_office

attribute :closest_regional_office_label

attribute(:available_hearing_locations) { |object| available_hearing_locations(object) }

attribute :external_id, &:uuid
Expand Down
2 changes: 2 additions & 0 deletions app/models/serializers/work_queue/legacy_appeal_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class WorkQueue::LegacyAppealSerializer

attribute :closest_regional_office

attribute :closest_regional_office_label

attribute(:available_hearing_locations) { |object| available_hearing_locations(object) }

attribute :docket_name do
Expand Down
6 changes: 5 additions & 1 deletion client/COPY.json
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,11 @@
"EMAIL_NOTIFICATION_HISTORY_TITLE": "Email Notification History",
"EMAIL_NOTIFICATION_HISTORY_INTRO": "Caseflow attempted to send the following email notifications, but this does not guarantee the recipients were able to receive them",
"VIRTUAL_HEARING_TIMEZONE_REQUIRED": "Timezone is required to send email notifications.",


"CONVERT_HEARING_TYPE_TITLE": "Convert Hearing To %s",
"CONVERT_HEARING_TYPE_SUBTITLE": "The hearing request will appear in the scheduling queue for <strong>%s</strong>.",
"CONVERT_HEARING_TYPE_SUBTITLE_2": "Review and update the following information in VBMS if needed. Caseflow won't send email notifications until you schedule the hearing.",

"JUDGE_TEAM_REMOVE_JUDGE_ERROR": "Cannot remove a judge from their judge team.",
"JUDGE_TEAM_ATTORNEY_RIGHTS_ERROR": "Cannot edit attorney rights on non-attorneys",

Expand Down
8 changes: 5 additions & 3 deletions client/app/hearings/components/Details.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
RESET_VIRTUAL_HEARING
} from '../contexts/HearingsFormContext';
import { HearingsUserContext } from '../contexts/HearingsUserContext';
import { deepDiff, pollVirtualHearingData, getChanges, getAppellantTitleForHearing } from '../utils';
import { deepDiff, pollVirtualHearingData, getChanges, getAppellantTitle } from '../utils';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only a name change in this file

import { inputFix } from './details/style';
import {
onReceiveAlerts,
Expand Down Expand Up @@ -62,6 +62,8 @@ const HearingDetails = (props) => {
const [virtualHearingModalType, setVirtualHearingModalType] = useState(null);
const [shouldStartPolling, setShouldStartPolling] = useState(null);

const appellantTitle = getAppellantTitle(hearing?.appellantIsNotVeteran);

// Method to reset the state
const reset = () => {
// Reset the state
Expand Down Expand Up @@ -127,7 +129,7 @@ const HearingDetails = (props) => {
if (virtual && errors) {
// Set the Virtual Hearing errors
setVirtualHearingErrors({
[noAppellantEmail && 'appellantEmail']: `${getAppellantTitleForHearing(hearing)} email is required`,
[noAppellantEmail && 'appellantEmail']: `${appellantTitle} email is required`,
[noRepTimezone && 'representativeTz']: COPY.VIRTUAL_HEARING_TIMEZONE_REQUIRED,
[noAppellantTimezone && 'appellantTz']: COPY.VIRTUAL_HEARING_TIMEZONE_REQUIRED
});
Expand Down Expand Up @@ -201,7 +203,7 @@ const HearingDetails = (props) => {
const errors = messages.split(',').reduce((list, message) => ({
...list,
[(/Representative/).test(message) ? 'representativeEmail' : 'appellantEmail']:
message.replace('Appellant', getAppellantTitleForHearing(hearing))
message.replace('Appellant', appellantTitle)
}), {});

document.getElementById('email-section').scrollIntoView();
Expand Down
6 changes: 3 additions & 3 deletions client/app/hearings/components/HearingConversion.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { VirtualHearingSection } from './VirtualHearings/Section';
import { ReadOnly } from './details/ReadOnly';
import { HelperText } from './VirtualHearings/HelperText';
import { HearingTime } from './modalForms/HearingTime';
import { getAppellantTitleForHearing } from '../utils';
import { getAppellantTitle } from '../utils';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only a name change in this file

import { HEARING_CONVERSION_TYPES } from '../constants';
import { RepresentativeSection } from './VirtualHearings/RepresentativeSection';
import { AppellantSection } from './VirtualHearings/AppellantSection';
Expand All @@ -25,7 +25,7 @@ export const HearingConversion = ({
errors,
update,
}) => {
const appellantTitle = getAppellantTitleForHearing(hearing);
const appellantTitle = getAppellantTitle(hearing?.appellantIsNotVeteran);
const virtual = type === 'change_to_virtual';
const video = hearing.readableRequestType === 'Video';
const convertLabel = video ? COPY.VIDEO_CHANGE_FROM_VIRTUAL : COPY.CENTRAL_OFFICE_CHANGE_FROM_VIRTUAL;
Expand Down Expand Up @@ -74,7 +74,7 @@ export const HearingConversion = ({
return (
<AppSegment filledBackground>
<h1 className="cf-margin-bottom-0">{title}</h1>
<span>{sprintf(helperLabel, getAppellantTitleForHearing(hearing))}</span>
<span>{sprintf(helperLabel, getAppellantTitle(hearing?.appellantIsNotVeteran))}</span>
rubaiyat22 marked this conversation as resolved.
Show resolved Hide resolved
<ReadOnly label="Hearing Date" text={DateUtil.formatDateStr(scheduledFor)} />
<div className={classNames('usa-grid', { [marginTop(30)]: true })}>
<div className="usa-width-one-half">
Expand Down
100 changes: 100 additions & 0 deletions client/app/hearings/components/HearingTypeConversionForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new component

import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment';
import PropTypes from 'prop-types';
import { sprintf } from 'sprintf-js';

import { marginTop, saveButton, cancelButton } from './details/style';
import { HelperText } from './VirtualHearings/HelperText';
import COPY from '../../../COPY';
import { getAppellantTitle } from '../utils';
import { RepresentativeSection } from './VirtualHearings/RepresentativeSection';
import { AppellantSection } from './VirtualHearings/AppellantSection';
import Button from '../../components/Button';

export const HearingTypeConversionForm = ({
type,
appeal
}) => {
// Create and manage the loading state
const [loading, setLoading] = useState(false);

// reset any states
const reset = () => setLoading(false);

// 'Appellant' or 'Veteran'
const appellantTitle = getAppellantTitle(appeal?.appellantIsNotVeteran);

/* eslint-disable camelcase */
// powerOfAttorney gets loaded into redux store when case details page loads
const hearing = {
representative: appeal?.powerOfAttorney?.representative_name,
representativeType: appeal?.powerOfAttorney?.representative_type,
appellantFullName: appeal?.appellantFullName
};

// veteranInfo gets loaded into redux store when case details page loads
const virtualHearing = {
appellantEmail: appeal?.veteranInfo?.veteran?.email_address,
representativeEmail: appeal?.powerOfAttorney?.representative_email_address,
};
/* eslint-enable camelcase */

// Set the section props
const sectionProps = {
appellantTitle,
hearing,
readOnly: true,
showDivider: false,
showOnlyAppellantName: true,
type,
virtualHearing,
};

const convertTitle = sprintf(COPY.CONVERT_HEARING_TYPE_TITLE, type);

return (
<React.Fragment>
<AppSegment filledBackground>
<h1 className="cf-margin-bottom-0">{convertTitle}</h1>
<p dangerouslySetInnerHTML={
{ __html: sprintf(COPY.CONVERT_HEARING_TYPE_SUBTITLE, appeal?.closestRegionalOfficeLabel) }
}
/>
<HelperText label={COPY.CONVERT_HEARING_TYPE_SUBTITLE_2} />
<AppellantSection {...sectionProps} />
rubaiyat22 marked this conversation as resolved.
Show resolved Hide resolved
<RepresentativeSection {...sectionProps} />
</AppSegment>
<div {...marginTop(30)}>
<Button
name="Cancel"
linkStyling
onClick={
() => {
reset();
history.goBack();
}
}
styling={cancelButton}
>
Cancel
</Button>
<span {...saveButton}>
<Button
name={convertTitle}
loading={loading}
className="usa-button"
>
{convertTitle}
</Button>
</span>
</div>
</React.Fragment>
);
};

HearingTypeConversionForm.propTypes = {
appeal: PropTypes.object,
// Router inherited props
history: PropTypes.object,
type: PropTypes.string
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new storybook component


import { HearingTypeConversionForm } from './HearingTypeConversionForm';

import { amaAppealForTravelBoard } from '../../../test/data/appeals';

export default {
title: 'Hearings/Components/HearingTypeConversionForm',
component: HearingTypeConversionForm,
parameters: {
docs: {
inlineStories: false,
iframeHeight: 760,
},
}
};

const Template = (args) => (
<HearingTypeConversionForm {...args} />
);

export const Basic = Template.bind({});
Basic.args = {
appeal: {
...amaAppealForTravelBoard
},
type: "Virtual"
}

export const Appellant = Template.bind({});
Appellant.args = {
...Basic.args,
appeal: {
...Basic.args.appeal,
appellantIsNotVeteran: true
}
}

export const CentralOffice = Template.bind({});
CentralOffice.args = {
...Basic.args,
appeal: {
...Basic.args.appeal,
closestRegionalOfficeLabel: "Central Office"
}
}
47 changes: 47 additions & 0 deletions client/app/hearings/components/HearingTypeConversionForm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new jest file


import { HearingTypeConversionForm } from 'app/hearings/components/HearingTypeConversionForm';

import { mount } from 'enzyme';
import { amaAppealForTravelBoard } from 'test/data';
import { VirtualHearingSection } from 'app/hearings/components/VirtualHearings/Section';
import { AddressLine } from 'app/hearings/components/details/Address';
import { VirtualHearingEmail } from 'app/hearings/components/VirtualHearings/Emails';
import { getAppellantTitle } from 'app/hearings/utils';

describe('HearingTypeConversionForm', () => {
test('Matches snapshot with default props', () => {
const hearingTypeConversionForm = mount(
<HearingTypeConversionForm
appeal={amaAppealForTravelBoard}
type={'Virtual'}
/>
);

// Assertions
expect(hearingTypeConversionForm.find(VirtualHearingSection)).toHaveLength(2);
expect(hearingTypeConversionForm.find(AddressLine)).toHaveLength(1);
expect(hearingTypeConversionForm.find(VirtualHearingEmail)).toHaveLength(2);
expect(hearingTypeConversionForm).toMatchSnapshot();
});

test('Does not show a divider on top of Appellant Section', () => {
const hearingTypeConversionForm = mount(
<HearingTypeConversionForm
appeal={amaAppealForTravelBoard}
type={'Virtual'}
/>
)

expect(
hearingTypeConversionForm.
findWhere(
(node) => node.prop('label') === `${getAppellantTitle(amaAppealForTravelBoard.appellantIsNotVeteran)}`
).
prop('showDivider')
).toEqual(false);
expect(hearingTypeConversionForm.find('.cf-help-divider')).toHaveLength(1);
expect(hearingTypeConversionForm).toMatchSnapshot();
});
});

4 changes: 2 additions & 2 deletions client/app/hearings/components/ScheduleVeteran.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { formatDateStr } from '../../util/DateUtil';
import Alert from '../../components/Alert';
import { marginTop, regionalOfficeSection, saveButton, cancelButton } from './details/style';
import { find } from 'lodash';
import { getAppellantTitleForHearing } from '../utils';
import { getAppellantTitle } from '../utils';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only a name change in this file

import { onChangeFormData } from '../../components/common/actions';
import { ScheduleVeteranForm } from './ScheduleVeteranForm';
import { HEARING_REQUEST_TYPES } from '../constants';
Expand All @@ -37,7 +37,7 @@ export const ScheduleVeteran = ({
const [errors, setErrors] = useState({});

// Get the appellant title ('Veteran' or 'Appellant')
const appellantTitle = getAppellantTitleForHearing(appeal);
const appellantTitle = getAppellantTitle(appeal?.apppellantIsNotVeteran);

// Get the selected hearing day
const selectedHearingDay = assignHearingForm?.hearingDay || hearingDay;
Expand Down
8 changes: 4 additions & 4 deletions client/app/hearings/components/VirtualHearingModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import _ from 'lodash';
import moment from 'moment-timezone';

import { getAppellantTitleForHearing, zoneName } from '../utils';
import { getAppellantTitle, zoneName } from '../utils';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only a name change in this file

import Button from '../../components/Button';
import COPY from '../../../COPY';
import Modal from '../../components/Modal';
Expand Down Expand Up @@ -71,7 +71,7 @@ export const ReadOnlyEmails = ({
appellantTzEdited,
showAllEmails = false,
}) => {
const appellantTitle = getAppellantTitleForHearing(hearing);
const appellantTitle = getAppellantTitle(hearing?.appellantIsNotVeteran);

// Check for appellant edits
const appellantEdited = appellantTzEdited || appellantEmailEdited ?
Expand Down Expand Up @@ -201,7 +201,7 @@ export const ChangeToVirtual = (props) => {
const {
hearing, readOnly, representativeEmailError, update, appellantEmailError, virtualHearing
} = props;
const appellantTitle = getAppellantTitleForHearing(hearing);
const appellantTitle = getAppellantTitle(hearing?.appellantIsNotVeteran);

// Prefill appellant/veteran email address and representative email on mount.
useEffect(() => {
Expand Down Expand Up @@ -327,7 +327,7 @@ const VirtualHearingModal = (props) => {
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const typeSettings = TYPES[type];
const appellantTitle = getAppellantTitleForHearing(hearing);
const appellantTitle = getAppellantTitle(hearing?.appellantIsNotVeteran);
const modalTitle = sprintf(
typeSettings.title({
representativeEmailEdited,
Expand Down
Loading