department-of-veterans-affairs/vets-website

View on GitHub
src/applications/vaos/new-appointment/components/DateTimeSelectPage/index.jsx

Summary

Maintainability
C
1 day
Test Coverage
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import moment from 'moment';

import InfoAlert from '../../../components/InfoAlert';

import {
  getAppointmentSlots,
  onCalendarChange,
  routeToNextAppointmentPage,
  routeToPreviousAppointmentPage,
  requestAppointmentDateChoice,
} from '../../redux/actions';
import { scrollAndFocus } from '../../../utils/scrollAndFocus';
import FormButtons from '../../../components/FormButtons';
import { getDateTimeSelect, selectEligibility } from '../../redux/selectors';
import CalendarWidget from '../../../components/calendar/CalendarWidget';
import WaitTimeAlert from './WaitTimeAlert';
import { FETCH_STATUS } from '../../../utils/constants';
import { getRealFacilityId } from '../../../utils/appointment';
import NewTabAnchor from '../../../components/NewTabAnchor';
import useIsInitialLoad from '../../../hooks/useIsInitialLoad';
import { getPageTitle } from '../../newAppointmentFlow';

const pageKey = 'selectDateTime';

function renderContent({ dispatch, isRequest, facilityId, history }) {
  // Display this content when the facility is configured to accept appointment
  // request
  if (isRequest) {
    return (
      <>
        To schedule this appointment, you can{' '}
        <button
          type="button"
          onClick={() => dispatch(requestAppointmentDateChoice(history))}
          className="va-button-link"
        >
          submit a request for a VA appointment
        </button>{' '}
        or{' '}
        <NewTabAnchor
          href={`/find-locations/facility/vha_${getRealFacilityId(facilityId)}`}
        >
          call your local VA medical center
        </NewTabAnchor>
        .
      </>
    );
  }

  return (
    <>
      To schedule this appointment, you can{' '}
      <NewTabAnchor
        href={`/find-locations/facility/vha_${getRealFacilityId(facilityId)}`}
      >
        call your local VA medical center
      </NewTabAnchor>
      .
    </>
  );
}

function ErrorMessage({ eligibility, facilityId, history }) {
  const { request: isRequest } = eligibility;
  const dispatch = useDispatch();

  return (
    <div
      aria-atomic="true"
      aria-live="assertive"
      className="vads-u-margin-bottom--2"
    >
      <InfoAlert
        status="error"
        level="2"
        headline="We’ve run into a problem trying to find an appointment time"
      >
        {renderContent({ dispatch, isRequest, facilityId, history })}
      </InfoAlert>
    </div>
  );
}
ErrorMessage.propTypes = {
  eligibility: PropTypes.object,
  facilityId: PropTypes.string,
  history: PropTypes.object,
};

function goForward({ dispatch, data, history, setSubmitted }) {
  setSubmitted(true);

  if (data.selectedDates?.length) {
    dispatch(routeToNextAppointmentPage(history, pageKey));
  } else {
    scrollAndFocus('.usa-input-error-message');
  }
}

export default function DateTimeSelectPage() {
  const pageTitle = useSelector(state => getPageTitle(state, pageKey));

  const {
    appointmentSlotsStatus,
    availableSlots,
    data,
    eligibleForRequests,
    facilityId,
    pageChangeInProgress,
    preferredDate,
    timezone,
    timezoneDescription,
  } = useSelector(state => getDateTimeSelect(state, pageKey), shallowEqual);

  const dispatch = useDispatch();
  const history = useHistory();
  const [submitted, setSubmitted] = useState(false);
  const fetchFailed = appointmentSlotsStatus === FETCH_STATUS.failed;
  const loadingSlots =
    appointmentSlotsStatus === FETCH_STATUS.loading ||
    appointmentSlotsStatus === FETCH_STATUS.notStarted;

  const isInitialLoad = useIsInitialLoad(loadingSlots);
  const eligibility = useSelector(selectEligibility);

  useEffect(
    () => {
      dispatch(
        getAppointmentSlots(
          moment(preferredDate)
            .startOf('month')
            .format('YYYY-MM-DD'),
          moment(preferredDate)
            .add(1, 'months')
            .endOf('month')
            .format('YYYY-MM-DD'),
          true,
        ),
      );
      document.title = `${pageTitle} | Veterans Affairs`;
    },
    [dispatch, preferredDate],
  );

  useEffect(
    () => {
      if (
        !isInitialLoad &&
        !loadingSlots &&
        appointmentSlotsStatus !== FETCH_STATUS.failed
      ) {
        scrollAndFocus('h2');
      } else if (
        (!loadingSlots && isInitialLoad) ||
        appointmentSlotsStatus === FETCH_STATUS.failed
      ) {
        scrollAndFocus();
      }
    },
    // Intentionally leaving isInitialLoad off, because it should trigger updates, it just
    // determines which update is made
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [loadingSlots, appointmentSlotsStatus],
  );

  const { selectedDates } = data;
  const startMonth = preferredDate
    ? moment(preferredDate).format('YYYY-MM')
    : null;

  return (
    <div>
      <h1 className="vads-u-font-size--h2">{pageTitle}</h1>
      {!loadingSlots && (
        <WaitTimeAlert
          eligibleForRequests={eligibleForRequests}
          facilityId={facilityId}
          nextAvailableApptDate={availableSlots?.[0]?.start}
          preferredDate={preferredDate}
          timezone={timezoneDescription}
        />
      )}
      {fetchFailed && (
        <ErrorMessage
          history={history}
          facilityId={facilityId}
          requestAppointmentDateChoice={(...args) =>
            dispatch(requestAppointmentDateChoice(...args))
          }
          eligibility={eligibility}
        />
      )}
      {!fetchFailed && (
        <>
          <p>
            Please select an available date and time from the calendar below.
            {timezone &&
              ` Appointment times are displayed in ${timezoneDescription}.`}
          </p>
          <CalendarWidget
            maxSelections={1}
            availableSlots={availableSlots}
            value={selectedDates}
            id="dateTime"
            timezone={timezone}
            additionalOptions={{
              required: true,
            }}
            disabled={loadingSlots}
            disabledMessage={
              <va-loading-indicator
                data-testid="loadingIndicator"
                set-focus
                message="Finding appointment availability..."
              />
            }
            onChange={(...args) => dispatch(onCalendarChange(...args))}
            onNextMonth={(...args) => dispatch(getAppointmentSlots(...args))}
            onPreviousMonth={(...args) =>
              dispatch(getAppointmentSlots(...args))
            }
            minDate={moment()
              .add(1, 'days')
              .format('YYYY-MM-DD')}
            maxDate={moment()
              .add(395, 'days')
              .format('YYYY-MM-DD')}
            required
            requiredMessage="Please choose your preferred date and time for your appointment"
            startMonth={startMonth}
            showValidation={submitted && !selectedDates?.length}
            showWeekends
          />
        </>
      )}
      <FormButtons
        onBack={() =>
          dispatch(routeToPreviousAppointmentPage(history, pageKey))
        }
        onSubmit={() =>
          goForward({
            dispatch,
            data,
            history,
            setSubmitted,
          })
        }
        disabled={loadingSlots || fetchFailed}
        pageChangeInProgress={pageChangeInProgress}
        loadingText="Page change in progress"
      />
    </div>
  );
}

ErrorMessage.propTypes = {
  facilityId: PropTypes.string.isRequired,
  history: PropTypes.object.isRequired,
};