MetaPhase-Consulting/State-TalentMAP

View on GitHub
src/Components/BureauPage/PositionManagerBidders/PositionManagerBidders.jsx

Summary

Maintainability
A
2 hrs
Test Coverage
F
23%
import { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { get, isEqual, keys, orderBy } from 'lodash';
import FA from 'react-fontawesome';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from 'react-tippy';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import Skeleton from 'react-loading-skeleton';
import { formatDate, move } from 'utilities';
import { checkFlag } from 'flags';
import { CLASSIFICATIONS, EMPTY_FUNCTION } from 'Constants/PropTypes';
import { Icons } from 'Constants/Classifications';
import { NO_CLASSIFICATIONS, NO_END_DATE, NO_GRADE, NO_LANGUAGE, NO_SKILL, NO_SUBMIT_DATE } from 'Constants/SystemMessages';
import { DECONFLICT_TOOLTIP_TEXT, OTHER_HANDSHAKE_TOOLTIP_TEXT } from 'Constants/Tooltips';
import { BUREAU_BIDDER_FILTERS } from 'Constants/Sort';
import SelectForm from 'Components/SelectForm';
import Alert from 'Components/Alert';
import HandshakeStatus from 'Components/Handshake/HandshakeStatus';
import HandshakeBureauButton from 'Components/Handshake/HandshakeBureauButton';
import InteractiveElement from 'Components/InteractiveElement';
import LoadingText from 'Components/LoadingText';
import PermissionsWrapper from 'Containers/PermissionsWrapper';
import ShortListLock from '../ShortListLock';
import BidderRankings from '../BidderRankings';
import MailToButton from '../../MailToButton';
import { tertiaryCoolBlueLight, tertiaryCoolBlueLightest } from '../../../sass/sass-vars/variables';
import HandshakeAnimation from '../../BidTracker/BidStep/HandshakeAnimation';
/**  removed BUREAU_BIDDER_SORT and sort option : line 547 temporarily until further notice per WS.
Will be a ticket in the future to add back in.
import { BUREAU_BIDDER_SORT } from 'Constants/Sort';
*/

const postHandshakeVisibility = () => checkFlag('flags.post_handshake');

const getClassificationsInfo = (userClassifications, refClassifications) => {
  const classificationsInfo = [];
  const shortCodesCache = [];
  refClassifications.forEach(a => {
    a.seasons.forEach(b => {
      const c = userClassifications.find(f => f === b.id);
      if (c) {
        const k = shortCodesCache.indexOf(Icons[a.code].shortCode);
        if (k < 0) {
          shortCodesCache.push(Icons[a.code].shortCode);
          classificationsInfo.push({
            icon: Icons[a.code].name,
            shortCode: Icons[a.code].shortCode,
            text: a.text,
            seasons: [b.season_text],
          });
        } else {
          classificationsInfo[k].seasons.push(b.season_text);
        }
      }
    });
  });
  return classificationsInfo;
};

const getClassificationsTooltip = (classifications) => (
  <div>
    {classifications.map(i => (
      <div className="classification-wrapper">
        <div className="classification-text">
          <FontAwesomeIcon
            icon={get(i, 'icon')}
            className="classification-icon"
          />
          {i.text}
        </div>
        <div className="classification-season-wrapper">
          {(get(i, 'seasons') || []).map(s => (
            <div className="classification-season">{s}</div>
          ))}
        </div>
      </div>
    ))}
  </div>
);

const getItemStyle = (isDragging, draggableStyle) => {
  const border = isDragging ? '1px solid black' : 'none';
  const height = isDragging ? '130px' : '';
  const overflowY = isDragging ? 'hidden' : '';
  return {
  // some basic styles to make the items look a bit nicer
    userSelect: 'none',

    // Make sure all border css is overridden
    borderTop: border,
    borderBottom: border,
    borderRight: border,
    borderLeft: border,
    height,
    overflowY,

    // styles we need to apply on draggables
    ...draggableStyle,
  };
};

const getListStyle = isDraggingOver => ({
  background: isDraggingOver ? tertiaryCoolBlueLightest : tertiaryCoolBlueLight,
  maxHeight: 1000,
  overflowY: 'scroll',
});

const rankedBids = (bids, ranking) => {
  const bids$ = orderBy((bids || []).map(m => {
    const m$ = { ...m };
    const match = ranking.find(f => +f.bidder_perdet === +m.emp_id);
    if (match) {
      const { rank } = match;
      m$.rank = rank;
      return m$;
    }
    return null;
  }).filter(f => f), ['rank']);
  return bids$;
};

const unrankedBids = (bids, ranking) => (bids || []).map(m => {
  const match = ranking.find(f => +f.bidder_perdet === +m.emp_id);
  if (match) {
    return null;
  }
  return m;
}).filter(f => f);

const getTooltipText = (title, text) => (
  <div>
    <FA name={'exclamation-triangle'} className={'deconflict-indicator-small'} />
    <div className={'tooltip-title'}>{title}</div>
    <div className={'tooltip-text'}>{text}</div>
  </div>
);

class PositionManagerBidders extends Component {
  constructor(props) {
    super(props);
    this.state = {
      shortList: this.getItems(rankedBids(props.allBids, props.ranking), 'shortList'),
      unranked: this.getItems(unrankedBids(props.bids, props.ranking), 'unranked'),
      hasLoaded: false,
      rankingUpdate: Date.now(), // track when the user performs an action
      shortListVisible: true,
      unrankedVisible: true,
      isMouseDown: false,
      mouseDownEmp: '',
    };
    this.onDragEnd = this.onDragEnd.bind(this);
  }

  UNSAFE_componentWillReceiveProps(nextProps) {
    let state = {};
    if (!nextProps.bidsIsLoading) {
      state = { ...state, hasLoaded: true };
    }
    state = {
      ...state,
      shortList: this.getItems(rankedBids(nextProps.allBids, nextProps.ranking), 'shortList', nextProps),
      unranked: this.getItems(unrankedBids(nextProps.bids, nextProps.ranking), 'unranked', nextProps),
    };

    this.setState(state);
  }

  // Logic to check that either one of the lists updated, or a manually triggered (drag or rank)
  // action took place. If so, re-calculate the lists to get the ranking dropdowns to re-render.
  // If the ranking change was triggered by the user, call this.props.setRanking.
  componentDidUpdate(prevProps, prevState) {
    const { rankingUpdate, shortList, unranked } = this.state;
    const { bidsIsLoading } = this.props;

    const shortListUpdated = !isEqual(
      shortList.map(m => m.emp_id), prevState.shortList.map(m => m.emp_id));

    const unrankedUpdated = !isEqual(
      unranked.map(m => m.emp_id), prevState.unranked.map(m => m.emp_id));

    const rankingUpdatedByUser = !isEqual(rankingUpdate, prevState.rankingUpdate);

    const loadingHasChanged = !isEqual(bidsIsLoading, prevProps.bidsIsLoading);

    if (shortListUpdated || unrankedUpdated || rankingUpdatedByUser || loadingHasChanged) {
      // Running setState should be safe since it's conditional on multiple isEqual statements
      // eslint-disable-next-line react/no-did-update-set-state
      this.setState({
        shortList: this.getItems(shortList.map(m => m.bid), 'shortList', this.props),
        unranked: this.getItems(unranked.map(m => m.bid), 'unranked', this.props),
      }, () => {
        if (rankingUpdatedByUser) {
          const ranking$ = shortList.map((m, i) => ({ rank: i, bidder_perdet: `${m.emp_id}` }));
          this.props.setRanking(ranking$);
        }
      });
    }
  }

  onDragEnd = result => {
    const { source, destination } = result;

    const rankingUpdate = Date.now();

    // dropped outside the list
    if (!destination) {
      return;
    }

    if (source.droppableId === destination.droppableId) {
      const shortList = move(
        this.getList(source.droppableId),
        source.index,
        destination.index,
      );

      let state = { shortList };

      if (source.droppableId === 'droppable2') {
        state = { unranked: shortList };
      }

      if (destination.droppableId === 'droppable') {
        state = { ...state, rankingUpdate };
      }

      this.setState({ ...state });
    } else {
      const result$ = this.move(
        this.getList(source.droppableId),
        this.getList(destination.droppableId),
        source,
        destination,
      );

      this.setState({
        shortList: result$.droppable,
        unranked: result$.droppable2,
        rankingUpdate,
      });
    }
  };

  // data generator
  getItems = (bids, type, props = this.props) =>
    bids.map((k, i) => ({
      id: `item-${k.emp_id}-${type}`,
      content: this.bid$(k, props, i, bids.length, type),
      emp_id: k.emp_id,
      bid: k, // the original bid
    }));

  getList = id => this.state[this.id2List[id]];

  // type = 'shortListVisible' or 'unrankedVisible'
  toggleVisibility = type => {
    const type$ = get(this.state, type);
    this.setState({ [type]: !type$ });
  }

  // Renders an individual bid
  bid$ = (m, props = this.props, iter, len, type) => {
    const ted = get(m, 'ted');
    const submitted = get(m, 'submitted_date');
    const formattedTed = ted ? formatDate(ted) : NO_END_DATE;
    const formattedSubmitted = submitted ? formatDate(submitted) : NO_SUBMIT_DATE;
    const deconflict = get(m, 'has_competing_rank');
    const handshake = get(m, 'handshake', {}) || {};
    const handshakeRegisteredDate = get(m, 'handshake_registered_date');
    const handshakeRegistered = get(m, 'handshake_registered') === 'Y';
    const active_hs_perdet = get(m, 'active_handshake_perdet');
    const hasAcceptedOtherOffer = get(m, 'has_accepted_other_offer');
    const positionHasHsReg = get(props, 'hasHsReg');

    const classifications = getClassificationsInfo(get(m, 'classifications') || [], props.classifications);
    const sections = {
      RetainedSpace: type === 'unranked' ? 'Unranked' :
        <select name="ranking" disabled={this.isDndDisabled()} value={iter} onChange={a => { this.setState({ rankingUpdate: Date.now(), shortList: move(this.state.shortList, iter, a.target.value) }); }}>
          {[...Array(len).keys()]
            .map((e) => (<option
              key={e}
              value={e}
            >{e + 1}</option>))}
        </select>,
      Deconflict: (
        <div className="alert-indicators">
          {!!deconflict && <Tooltip
            html={getTooltipText(DECONFLICT_TOOLTIP_TEXT.title, DECONFLICT_TOOLTIP_TEXT.text)}
            theme={'deconflict-indicator'}
            arrow
            tabIndex="0"
            interactive
            style={{ height: 'fit-content' }}
            useContext
            position="right"
          >
            <FA name={'exclamation-triangle'} className={'deconflict-indicator'} />
          </Tooltip>}
          {!!hasAcceptedOtherOffer && <Tooltip
            html={getTooltipText(OTHER_HANDSHAKE_TOOLTIP_TEXT.title,
              OTHER_HANDSHAKE_TOOLTIP_TEXT.text)}
            theme={'has-other-handshake-indicator'}
            arrow
            tabIndex="0"
            interactive
            style={{ height: 'fit-content' }}
            useContext
            position="right"
          >
            <FA name={'exclamation-triangle'} className={'has-other-handshake-indicator'} />
          </Tooltip>}
        </div>
      ),
      Name: (<Link to={`/profile/public/${m.emp_id}/bureau`}>{get(m, 'name')}</Link>),
      SubmittedDate: formattedSubmitted,
      Skill: get(m, 'skill') || NO_SKILL,
      Grade: get(m, 'grade') || NO_GRADE,
      Language: get(m, 'language') || NO_LANGUAGE,
      Classifications: classifications.length ?
        <Tooltip
          html={getClassificationsTooltip(classifications)}
          theme={'classifications'}
          arrow
          tabIndex="0"
          interactive
          style={{ height: 'fit-content' }}
          useContext
        >
          {classifications.map(c => c.shortCode).join(', ')}
        </Tooltip>
        : NO_CLASSIFICATIONS,
      TED: formattedTed,
      CDO: get(m, 'cdo.email') ? <MailToButton email={get(m, 'cdo.email')} textAfter={get(m, 'cdo.name')} /> : 'N/A',
      Action:
        <>
          <PermissionsWrapper
            permissions="bureau_user"
            fallback={
              postHandshakeVisibility() &&
              <HandshakeStatus
                handshake={handshake}
              />
            }
          >
            { handshakeRegistered ?
              <>
                <HandshakeAnimation isBidder />
                <HandshakeStatus
                  handshake={handshake}
                  handshakeRegistered={handshakeRegistered}
                  handshakeRegisteredDate={handshakeRegisteredDate}
                  infoIcon
                />
              </> :
              <HandshakeStatus
                handshake={handshake}
              />
            }
          </PermissionsWrapper>
          {
            type !== 'unranked' &&
            <PermissionsWrapper permissions="bureau_user">
              <HandshakeBureauButton
                handshake={handshake}
                positionID={props.id}
                personID={m.emp_id}
                activePerdet={active_hs_perdet}
                bidCycle={get(props, 'bidCycle', {})}
                positionHasHsReg={positionHasHsReg}
              />
            </PermissionsWrapper>
          }
        </>,
    };

    if (props.bidsIsLoading) {
      keys(sections).forEach(k => {
        sections[k] = <Skeleton />;
      });
    }

    const tableRows = (
      <tr>
        {keys(sections).map(i => (
          <td key={i}>{sections[i]}</td>
        ))}
      </tr>
    );
    return tableRows;
  };

  /**
   * Moves an item from one list to another list.
   */
  move = (source, destination, droppableSource, droppableDestination) => {
    const sourceClone = Array.from(source);
    const destClone = Array.from(destination);

    const [cloned] = [...sourceClone].splice(droppableSource.index, 1);
    destClone.splice(droppableDestination.index, 0, cloned);
    sourceClone.splice(droppableSource.index, 1);

    const result = {};
    result[droppableSource.droppableId] = sourceClone;
    result[droppableDestination.droppableId] = destClone;

    return result;
  };

  isDndDisabled = () => {
    const { bidsIsLoading, isLocked,
      hasBureauPermission } = this.props;
    return bidsIsLoading || (isLocked && !hasBureauPermission);
  }

    /**
       * A semi-generic way to handle multiple lists. Matches
       * the IDs of the droppable container to the names of the
       * source arrays stored in the state.
     */
    id2List = {
      droppable: 'shortList',
      droppable2: 'unranked',
    };

  handleEvent = (event, id) => {
    if (event.type === 'mousedown') {
      this.setState({ isMouseDown: true, mouseDownEmp: id });
    }
    if (event.type === 'mouseup') {
      this.setState({ isMouseDown: false, mouseDownEmp: '' });
    }
  }

  render() {
    const { bids, bidsIsLoading, filtersSelected, filters, id, isLocked,
      hasBureauPermission, bidCycle } = this.props;
    const { hasLoaded, shortListVisible, unrankedVisible } = this.state;

    const tableHeaders = ['Ranking', '', 'Name', 'Submitted Date', 'Skill', 'Grade', 'Language', 'Classifications', 'TED', 'CDO', ''].map(item => (
      <th scope="col">{item}</th>
    ));

    const shortListLock = (<ShortListLock
      id={id}
      biddersInShortList={this.state.shortList.length}
    />);

    const dndDisabled = this.isDndDisabled();

    const shortListSection = (
      <>
        <div className="list-toggle-container">
          <InteractiveElement title="Toggle visibility" onClick={() => this.toggleVisibility('shortListVisible')}><FA name={shortListVisible ? 'chevron-down' : 'chevron-up'} /></InteractiveElement>
          <h3>Short List ({this.state.shortList.length})</h3>
          {shortListLock}
        </div>
        {
          shortListVisible &&
          <table className="position-manager-bidders-table">
            <thead>
              <tr>
                {tableHeaders}
              </tr>
            </thead>
            <tbody>
              <Droppable droppableId="droppable" isDropDisabled={dndDisabled}>
                {(provided, snapshot) => (
                  <div
                    ref={provided.innerRef}
                    style={getListStyle(snapshot.isDraggingOver)}
                  >
                    {this.state.shortList.map((item, index) => (
                      <Draggable
                        key={item.id}
                        draggableId={item.id}
                        index={index}
                        isDragDisabled={dndDisabled}
                      >
                        {(provided$, snapshot$) => (
                          <div
                            role="row"
                            tabIndex={0}
                            onMouseDown={(e) => { this.handleEvent(e, item.bid.emp_id); }}
                            onMouseUp={this.handleEvent}
                            ref={provided$.innerRef}
                            {...provided$.draggableProps}
                            {...provided$.dragHandleProps}
                            style={getItemStyle(
                              snapshot$.isDragging,
                              provided$.draggableProps.style,
                            )}
                            className={snapshot$.isDragging ? 'is-dragging' : ''}
                          >
                            {item.content}
                            <BidderRankings
                              perdet={item.bid.emp_id}
                              cp_id={id}
                              is_dragging={snapshot.isDraggingOver}
                              is_mouse_down={this.state.isMouseDown}
                              mouse_down_emp={this.state.mouseDownEmp}
                            />
                          </div>
                        )}
                      </Draggable>
                    ))}
                    {provided.placeholder}
                  </div>
                )}
              </Droppable>
            </tbody>
          </table>
        }
      </>
    );

    return (
      <div className="usa-width-one-whole position-manager-bidders">
        { !bids.length && !!hasLoaded && shortListLock }
        <DragDropContext onDragEnd={this.onDragEnd}>
          {
            // >:)
            // eslint-disable-next-line no-nested-ternary
            bidsIsLoading ? <LoadingText /> :
              (
                !bids.length && !filtersSelected && !bidsIsLoading ?
                  <Alert type="info" title="There are no bids on this position" />
                  :
                  <>
                    {/* eslint-disable no-nested-ternary */}
                    {!get(bidCycle, 'handshake_allowed_date') && <Alert type="dark" title="Bureaus cannot offer handshakes for this cycle at this time" />}
                    {isLocked ?
                      hasBureauPermission ? shortListSection : <>
                        <Alert
                          type="info"
                          title="Short List Locked"
                          messages={[{ body: 'The short list has been locked by the bureau. You cannot modify the short list until it has been unlocked.' }]}
                        />
                        <div>
                          {shortListSection}
                        </div>
                      </>
                      : shortListSection }
                    {/* eslint-enable no-nested-ternary */}
                    <div className="bidders-controls">
                      {/* Removed the sort by dropdown temporarily per WS
                      Will be a ticket in the future to add back in.
                      */}
                      {/* <SelectForm
                        id="sort"
                        label="Sort by:"
                        defaultSort={filters.ordering || 'bidder_grade'}
                        options={BUREAU_BIDDER_SORT.options}
                        disabled={false}
                        onSelectOption={e => this.props.onSort(e.target.value)}
                      /> */}
                      <SelectForm
                        id="filter"
                        options={BUREAU_BIDDER_FILTERS.options}
                        label="Filter By:"
                        defaultSort={filters.handshake_code || ''}
                        disabled={false}
                        onSelectOption={e => this.props.onFilter('handshake_code', e.target.value)}
                      />
                    </div>
                    <div className="list-toggle-container">
                      <InteractiveElement title="Toggle visibility" onClick={() => this.toggleVisibility('unrankedVisible')}><FA name={unrankedVisible ? 'chevron-down' : 'chevron-up'} /></InteractiveElement>
                      <h3>Candidates ({this.state.unranked.length})</h3>
                    </div>
                    {
                      unrankedVisible &&
                        <table className="position-manager-bidders-table">
                          <thead>
                            <tr>
                              {tableHeaders}
                            </tr>
                          </thead>
                          <tbody>
                            <Droppable droppableId="droppable2" isDropDisabled={dndDisabled}>
                              {(provided, snapshot) => (
                                <div
                                  ref={provided.innerRef}
                                  style={getListStyle(snapshot.isDraggingOver)}
                                >
                                  {this.state.unranked.map((item, index) => (
                                    <Draggable
                                      key={item.id}
                                      draggableId={item.id}
                                      index={index}
                                      isDragDisabled={dndDisabled}
                                    >
                                      {(provided$, snapshot$) => (
                                        <div
                                          role="row"
                                          tabIndex={0}
                                          /* eslint-disable-next-line max-len */
                                          onMouseDown={(e) => { this.handleEvent(e, item.bid.emp_id); }}
                                          onMouseUp={this.handleEvent}
                                          ref={provided$.innerRef}
                                          {...provided$.draggableProps}
                                          {...provided$.dragHandleProps}
                                          style={getItemStyle(
                                            snapshot$.isDragging,
                                            provided$.draggableProps.style,
                                          )}
                                          className={snapshot$.isDragging ? 'is-dragging' : ''}
                                        >
                                          {item.content}
                                          <BidderRankings
                                            perdet={item.bid.emp_id}
                                            cp_id={id}
                                            is_dragging={snapshot.isDraggingOver}
                                            is_mouse_down={this.state.isMouseDown}
                                            mouse_down_emp={this.state.mouseDownEmp}
                                          />
                                        </div>
                                      )}
                                    </Draggable>
                                  ))}
                                  {provided.placeholder}
                                </div>
                              )}
                            </Droppable>
                          </tbody>
                        </table>
                    }
                  </>
              )
          }
        </DragDropContext>
      </div>
    );
  }
}

PositionManagerBidders.propTypes = {
  bids: PropTypes.arrayOf(PropTypes.shape({})),
  bidCycle: PropTypes.shape({}),
  bidsIsLoading: PropTypes.bool,
  onSort: PropTypes.func,
  onFilter: PropTypes.func,
  ranking: PropTypes.arrayOf(PropTypes.shape({})),
  setRanking: PropTypes.func,
  filtersSelected: PropTypes.bool,
  filters: PropTypes.shape({
    handshake_code: PropTypes.string,
    ordering: PropTypes.string,
  }),
  allBids: PropTypes.arrayOf(PropTypes.shape({})),
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
  isLocked: PropTypes.bool,
  hasBureauPermission: PropTypes.bool,
  hasPostPermission: PropTypes.bool,
  classifications: CLASSIFICATIONS,
  positionHasHsReg: PropTypes.bool,
};

PositionManagerBidders.defaultProps = {
  bids: [],
  bidCycle: {},
  bidsIsLoading: false,
  onSort: EMPTY_FUNCTION,
  onFilter: EMPTY_FUNCTION,
  ranking: [],
  setRanking: EMPTY_FUNCTION,
  filtersSelected: false,
  filters: {},
  allBids: [],
  isLocked: false,
  hasBureauPermission: false,
  hasPostPermission: false,
  classifications: [],
  positionHasHsReg: false,
};

export default PositionManagerBidders;