MetaPhase-Consulting/State-TalentMAP

View on GitHub
src/Components/Agenda/EmployeeAgendaSearch/EmployeeAgendaSearch.jsx

Summary

Maintainability
B
4 hrs
Test Coverage
F
58%
import PropTypes from 'prop-types';
import { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Picky from 'react-picky';
import DateRangePicker from '@wojtekmaj/react-daterange-picker';
import { filter, flatten, get, has, identity, isEmpty } from 'lodash';
import FA from 'react-fontawesome';
import { isDate, startOfDay } from 'date-fns-v2';
import { agendaEmployeesFetchData, agendaEmployeesFiltersFetchData, employeeAgendaSearchExport, saveAgendaEmployeesSelections } from 'actions/agendaEmployees';
import { bidderPortfolioCDOsFetchData } from 'actions/bidderPortfolio';
import PositionManagerSearch from 'Components/BureauPage/PositionManager/PositionManagerSearch';
import Spinner from 'Components/Spinner';
import TotalResults from 'Components/TotalResults';
import PaginationWrapper from 'Components/PaginationWrapper';
import ExportButton from 'Components/ExportButton';
import SelectForm from 'Components/SelectForm';
import { AGENDA_EMPLOYEES_PAGE_SIZES, AGENDA_EMPLOYEES_SORT } from 'Constants/Sort';
import shortid from 'shortid';
import ListItem from 'Components/BidderPortfolio/BidControls/BidCyclePicker/ListItem';
import Alert from 'Components/Alert';
import ToggleButton from 'Components/ToggleButton';
import { usePrevious } from 'hooks';
import EmployeeAgendaSearchCard from '../EmployeeAgendaSearchCard/EmployeeAgendaSearchCard';
import EmployeeAgendaSearchRow from '../EmployeeAgendaSearchRow/EmployeeAgendaSearchRow';
import ProfileSectionTitle from '../../ProfileSectionTitle';
import ResultsViewBy from '../../ResultsViewBy/ResultsViewBy';
import ScrollUpButton from '../../ScrollUpButton';

const EmployeeAgendaSearch = ({ isCDO, viewType }) => {
  const searchLastNameRef = useRef();
  const searchFirstNameRef = useRef();
  const searchEmpIDRef = useRef();

  const dispatch = useDispatch();

  const agendaEmployeesFilters = useSelector(state => state.agendaEmployeesFilters);
  const agendaEmployeesFiltersIsLoading = useSelector(state =>
    state.agendaEmployeesFiltersFetchDataLoading);
  // const agendaEmployeesFiltersHasErrored = useSelector(state =>
  //  state.agendaEmployeesFiltersFetchDataErrored);

  const cdos = useSelector(state => state.bidderPortfolioCDOs);
  // const cdosIsLoading = useSelector(state => state.bidderPortfolioCDOsIsLoading);

  const agendaEmployees$ = useSelector(state => state.agendaEmployees);
  const agendaEmployeesIsLoading = useSelector(state => state.agendaEmployeesFetchDataLoading);
  const agendaEmployeesHasErrored = useSelector(state => state.agendaEmployeesFetchDataErrored);
  const userSelections = useSelector(state => state.agendaEmployeesSelections);

  const agendaEmployees = get(agendaEmployees$, 'results') || [];

  const fsbidHandshakeStatusOptions = [{ description: 'Handshake', code: 'Y' }, { description: 'No Handshake', code: 'N' }];

  const isLoading = agendaEmployeesFiltersIsLoading;

  // Pagination
  const [page, setPage] = useState(get(userSelections, 'page') || 1);
  const [limit, setLimit] = useState(get(userSelections, 'limit') || AGENDA_EMPLOYEES_PAGE_SIZES.defaultSize);
  const [ordering, setOrdering] = useState(get(userSelections, 'ordering') || AGENDA_EMPLOYEES_SORT.defaultSort);
  // Filters
  const [selectedCurrentBureaus, setSelectedCurrentBureaus] = useState(get(userSelections, 'selectedCurrentBureaus') || []);
  const [selectedOngoingBureaus, setSelectedOngoingBureaus] = useState(get(userSelections, 'selectedOngoingBureaus') || []);
  const [selectedCDOs, setSelectedCDOs] = useState(get(userSelections, 'selectedCDOs') || []);
  // To-Do: Fake creator data
  const [selectedHandshakeStatus, setSelectedHandshakeStatus] = useState(get(userSelections, 'selectedHandshakeStatus') || []);
  const [selectedCurrentPosts, setSelectedCurrentPosts] = useState(get(userSelections, 'selectedCurrentPosts') || []);
  const [selectedOngoingPosts, setSelectedOngoingPosts] = useState(get(userSelections, 'selectedOngoingPosts') || []);
  const [selectedTED, setSelectedTED] = useState(get(userSelections, 'selectedTED') || null);

  const prevPage = usePrevious(page);

  // Controls
  const [cardView, setCardView] = useState(get(userSelections, 'cardView', true));
  const [clearFilters, setClearFilters] = useState(false);
  // Export
  const [exportIsLoading, setExportIsLoading] = useState(false);
  // Text Searches
  const [searchTextLastName, setSearchTextLastName] = useState(get(userSelections, 'searchTextLastName') || '');
  const [searchTextFirstName, setSearchTextFirstName] = useState(get(userSelections, 'searchTextFirstName') || '');
  const [searchTextEmpID, setSearchTextEmpID] = useState(get(userSelections, 'searchTextEmpID') || '');

  const [searchInputLastName, setSearchInputLastName] = useState(get(userSelections, 'searchInputLastName') || '');
  const [searchInputFirstName, setSearchInputFirstName] = useState(get(userSelections, 'searchInputFirstName') || '');
  const [searchInputEmpID, setSearchInputEmpID] = useState(get(userSelections, 'searchInputEmpID') || '');

  const [isInactiveSelected, setIsInactiveSelected] = useState(get(userSelections, 'isInactiveSelected') || false);

  const count = get(agendaEmployees$, 'count') || 0;

  const view = cardView ? 'card' : 'grid';

  const pageSizes = AGENDA_EMPLOYEES_PAGE_SIZES;
  const sorts = AGENDA_EMPLOYEES_SORT;

  const getQuery = () => ({
    // Pagination
    page,
    limit,
    ordering,
    // User Filters
    'current-bureaus': selectedCurrentBureaus.map(bureauObject => (get(bureauObject, 'code'))),
    'handshake-bureaus': selectedOngoingBureaus.map(bureauObject => (get(bureauObject, 'code'))),
    cdos: selectedCDOs.map(cdoObject => get(cdoObject, 'id')),
    'current-organizations': selectedCurrentPosts.map(postObject => (get(postObject, 'code'))),
    'handshake-organizations': selectedOngoingPosts.map(postObject => (get(postObject, 'code'))),
    handshake: selectedHandshakeStatus.map(hsObject => (get(hsObject, 'code'))),

    // need to set to beginning of the day to avoid timezone issues
    'ted-start': isDate(get(selectedTED, '[0]')) ? startOfDay(get(selectedTED, '[0]')).toJSON() : '',
    'ted-end': isDate(get(selectedTED, '[1]')) ? startOfDay(get(selectedTED, '[1]')).toJSON() : '',

    // Search Text
    lastName: searchTextLastName,
    firstName: searchTextFirstName,
    empID: searchTextEmpID,

    // Include Inactive Emps Toggle
    isInactiveSelected,
  });

  const getCurrentInputs = () => ({
    page,
    limit,
    ordering,
    selectedCurrentBureaus,
    selectedOngoingBureaus,
    selectedCDOs,
    selectedHandshakeStatus,
    selectedCurrentPosts,
    selectedOngoingPosts,
    selectedTED,
    cardView,
    searchTextLastName,
    searchTextFirstName,
    searchTextEmpID,
    searchInputLastName,
    searchInputFirstName,
    searchInputEmpID,
    isInactiveSelected,
  });

  useEffect(() => {
    dispatch(agendaEmployeesFetchData(getQuery()));
    dispatch(saveAgendaEmployeesSelections(getCurrentInputs()));
  }, []);

  useEffect(() => {
    dispatch(agendaEmployeesFiltersFetchData());
    dispatch(bidderPortfolioCDOsFetchData());
  }, []);

  const fetchAndSet = (resetPage = false) => {
    const filters$ = [
      selectedCurrentBureaus,
      selectedOngoingBureaus,
      selectedCDOs,
      selectedHandshakeStatus,
      selectedCurrentPosts,
      selectedOngoingPosts,
      selectedTED,
      searchTextLastName,
      searchTextFirstName,
      searchTextEmpID,
      isInactiveSelected,
    ];
    if (isEmpty(filter(flatten(filters$), identity)) && isEmpty(searchTextLastName)
      && isEmpty(searchTextFirstName) && isEmpty(searchTextEmpID)) {
      setClearFilters(false);
    } else {
      setClearFilters(true);
    }
    if (resetPage) {
      setPage(1);
    }
    dispatch(agendaEmployeesFetchData(getQuery()));
    dispatch(saveAgendaEmployeesSelections(getCurrentInputs()));
  };

  useEffect(() => {
    if (prevPage) {
      fetchAndSet(true);
    }
  }, [
    limit,
    ordering,
    selectedCurrentBureaus,
    selectedOngoingBureaus,
    selectedCDOs,
    selectedHandshakeStatus,
    selectedCurrentPosts,
    selectedOngoingPosts,
    selectedTED,
    searchTextLastName,
    searchTextFirstName,
    searchTextEmpID,
    isInactiveSelected,
  ]);

  useEffect(() => {
    fetchAndSet(false);
  }, [
    page,
  ]);

  useEffect(() => {
    dispatch(saveAgendaEmployeesSelections(getCurrentInputs()));
  }, [cardView]);

  function submitSearch() {
    setSearchTextLastName(searchInputLastName);
    setSearchTextFirstName(searchInputFirstName);
    setSearchTextEmpID(searchInputEmpID);
  }

  const exportAgendaEmployees = () => {
    if (!exportIsLoading) {
      setExportIsLoading(true);
      employeeAgendaSearchExport(getQuery())
        .then(() => {
          setExportIsLoading(false);
        })
        .catch(() => {
          setExportIsLoading(false);
        });
    }
  };

  const renderSelectionList = ({ items, selected, ...rest }) => {
    let codeOrID = 'code';
    // only Cycle needs to use 'id'
    if (!has(items[0], 'code')) {
      codeOrID = 'id';
    }
    const getSelected = item => !!selected.find(f => f[codeOrID] === item[codeOrID]);
    let queryProp = 'description';
    if (get(items, '[0].custom_description', false)) queryProp = 'custom_description';
    else if (get(items, '[0].long_description', false)) queryProp = 'long_description';
    else if (get(items, '[0].description', false)) queryProp = 'description';
    else queryProp = 'name';
    return items.map(item =>
      (<ListItem
        key={item[codeOrID]}
        item={item}
        {...rest}
        queryProp={queryProp}
        getIsSelected={getSelected}
      />),
    );
  };

  const pickyProps = {
    numberDisplayed: 2,
    multiple: true,
    includeFilter: true,
    dropdownHeight: 255,
    renderList: renderSelectionList,
    includeSelectAll: true,
  };

  const resetFilters = () => {
    Promise.resolve().then(() => {
      setSelectedCurrentBureaus([]);
      setSelectedOngoingBureaus([]);
      setSelectedCDOs([]);
      setSelectedHandshakeStatus([]);
      setSelectedCurrentPosts([]);
      setSelectedOngoingPosts([]);
      setSelectedTED(null);
      setSearchTextLastName('');
      setSearchTextFirstName('');
      setSearchTextEmpID('');
      searchFirstNameRef.current.clearText();
      searchLastNameRef.current.clearText();
      searchEmpIDRef.current.clearText();
      setIsInactiveSelected(false);
      setClearFilters(false);
    });
  };

  const getOverlay = () => {
    let toReturn;
    if (agendaEmployeesIsLoading) {
      toReturn = <Spinner type="bureau-results" class="homepage-position-results" size="big" />;
    } else if (agendaEmployeesHasErrored) {
      toReturn = <Alert type="error" title="Error loading employees" messages={[{ body: 'Please try again.' }]} />;
    } else if (count <= 0) {
      toReturn = <Alert type="info" title="No results found" messages={[{ body: 'Please broaden your search criteria and try again.' }]} />;
    } else {
      toReturn = false;
    }
    if (toReturn) {
      return <div className="usa-width-one-whole empl-search-lower-section results-dropdown">{toReturn}</div>;
    }
    return false;
  };

  const overlay = getOverlay();

  const exportDisabled = !agendaEmployees.length;

  return (
    isLoading ?
      <Spinner type="bureau-filters" size="small" /> :
      <>
        <div className="empl-search-page position-search">
          <div className="usa-grid-full position-search--header">
            <div className="results-search-bar">
              <div className="usa-grid-full search-bar-container">
                <ProfileSectionTitle title="Employee Agenda Search" icon="user-circle-o" />
                <div className="filterby-container">
                  <div className="filterby-clear">
                    <button className={`unstyled-button ${clearFilters ? '' : 'hide-clear-filters'}`} onClick={resetFilters}>
                      <FA name="times" />
                      Clear Filters
                    </button>
                  </div>
                </div>
                <div className="eas-inactive-toggle">
                  <ToggleButton
                    labelTextRight="Include Inactive Employees"
                    onChange={() => setIsInactiveSelected(!isInactiveSelected)}
                    checked={isInactiveSelected}
                    onColor="#0071BC"
                  />
                </div>
                <div className="usa-width-one-whole empl-search-filters">
                  <div className="filter-div">
                    <label htmlFor="last-name-search" className="label">
                    Last Name:
                    </label>
                    <div className="filter-search-bar fsb-240">
                      <PositionManagerSearch
                        id="last-name-search"
                        submitSearch={submitSearch}
                        onChange={setSearchInputLastName}
                        ref={searchLastNameRef}
                        placeHolder="Search by Last Name"
                        textSearch={searchTextLastName}
                        noButton
                        showIcon={false}
                      />
                    </div>
                  </div>
                  <div className="filter-div">
                    <label htmlFor="first-name-search" className="label">
                    First Name:
                    </label>
                    <div className="filter-search-bar fsb-240">
                      <PositionManagerSearch
                        id="first-name-search"
                        submitSearch={submitSearch}
                        onChange={setSearchInputFirstName}
                        ref={searchFirstNameRef}
                        placeHolder="Search by First Name"
                        textSearch={searchTextFirstName}
                        noButton
                        showIcon={false}
                      />
                    </div>
                  </div>
                  <div className="filter-div">
                    <label htmlFor="emp-id-search" className="label">
                    Employee ID:
                    </label>
                    <div className="filter-search-bar fsb-240">
                      <PositionManagerSearch
                        id="emp-id-search"
                        submitSearch={submitSearch}
                        onChange={setSearchInputEmpID}
                        ref={searchEmpIDRef}
                        textSearch={searchTextEmpID}
                        placeHolder="Search by Employee ID"
                        noButton
                        showIcon={false}
                      />
                    </div>
                  </div>
                </div>
                <div className="usa-width-one-whole empl-search-filters results-dropdown">
                  <div className="filter-div split-filter-div">
                    <div className="label">Post:</div>
                    <Picky
                      {...pickyProps}
                      placeholder="Current"
                      value={selectedCurrentPosts}
                      options={get(agendaEmployeesFilters, 'currentOrganizations', [])}
                      onChange={setSelectedCurrentPosts}
                      valueKey="code"
                      labelKey="name"
                      includeSelectAll={false}
                    />
                    <Picky
                      {...pickyProps}
                      placeholder="Ongoing"
                      value={selectedOngoingPosts}
                      options={get(agendaEmployeesFilters, 'handshakeOrganizations', [])}
                      onChange={setSelectedOngoingPosts}
                      valueKey="code"
                      labelKey="name"
                      includeSelectAll={false}
                    />
                  </div>
                  <div className="filter-div split-filter-div">
                    <div className="label">Bureau:</div>
                    <Picky
                      {...pickyProps}
                      placeholder="Current"
                      value={selectedCurrentBureaus}
                      options={get(agendaEmployeesFilters, 'currentBureaus', [])}
                      onChange={setSelectedCurrentBureaus}
                      valueKey="code"
                      labelKey="name"
                    />
                    <Picky
                      {...pickyProps}
                      placeholder="Ongoing"
                      value={selectedOngoingBureaus}
                      options={get(agendaEmployeesFilters, 'handshakeBureaus', [])}
                      onChange={setSelectedOngoingBureaus}
                      valueKey="code"
                      labelKey="name"
                    />
                  </div>
                  <div className="filter-div restrict-label-width">
                    <div className="label">Handshake:</div>
                    <Picky
                      {...pickyProps}
                      placeholder="Select Handshake Register Status"
                      value={selectedHandshakeStatus}
                      options={fsbidHandshakeStatusOptions}
                      onChange={setSelectedHandshakeStatus}
                      valueKey="code"
                      labelKey="description"
                      disabled={isLoading}
                    />
                  </div>
                  <div className="filter-div">
                    <div className="label">CDO:</div>
                    <Picky
                      {...pickyProps}
                      placeholder="Select CDOs"
                      value={selectedCDOs}
                      options={cdos}
                      onChange={setSelectedCDOs}
                      valueKey="id"
                      labelKey="name"
                      disabled={isLoading}
                    />
                  </div>
                  <div className="filter-div">
                    <div className="label">TED:</div>
                    <DateRangePicker
                      onChange={setSelectedTED}
                      value={selectedTED}
                      maxDetail="month"
                      calendarIcon={null}
                      showLeadingZeros
                      disabled={isLoading}
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>
          {
            !agendaEmployeesIsLoading && !isLoading &&
            <div className="usa-width-one-whole results-dropdown empl-search-controls-container">
              <TotalResults
                total={count}
                pageNumber={page}
                pageSize={limit}
                suffix="Results"
                isHidden={isLoading || agendaEmployeesIsLoading}
              />
              <div className="empl-search-controls-right">
                <ResultsViewBy initial={view} onClick={e => setCardView(e === 'card')} />
                <div className="empl-search-results-controls">
                  <SelectForm
                    id="empl-search-num-results"
                    options={sorts.options}
                    label="Sort by:"
                    defaultSort={ordering}
                    onSelectOption={value => setOrdering(value.target.value)}
                    disabled={isLoading}
                  />
                  <SelectForm
                    id="empl-search-num-results"
                    options={pageSizes.options}
                    label="Results:"
                    defaultSort={limit}
                    onSelectOption={value => setLimit(value.target.value)}
                    disabled={isLoading}
                  />
                </div>
                <div className="export-button-container">
                  <ExportButton
                    text="Export - Max. 500"
                    onClick={exportAgendaEmployees}
                    isLoading={exportIsLoading}
                    disabled={exportDisabled}
                  />
                </div>
                <ScrollUpButton />
              </div>
            </div>
          }
          {
            overlay ||
              <>
                <div className="usa-width-one-whole empl-search-lower-section results-dropdown">
                  {
                    cardView && !agendaEmployeesIsLoading &&
                    <div className="employee-agenda-card">
                      {
                        agendaEmployees.map(emp => (
                          <EmployeeAgendaSearchCard
                            key={shortid.generate()}
                            result={emp}
                            isCDO={isCDO}
                            viewType={viewType}
                          />
                        ))
                      }
                    </div>
                  }
                  {
                    !cardView && !agendaEmployeesIsLoading &&
                    <div className="employee-agenda-row">
                      {
                        agendaEmployees.map(emp => (
                          <EmployeeAgendaSearchRow
                            key={shortid.generate()}
                            result={emp}
                            isCDO={isCDO}
                            viewType={viewType}
                          />
                        ))
                      }
                    </div>
                  }
                </div>
                <div className="usa-grid-full react-paginate empl-search-pagination-controls">
                  <PaginationWrapper
                    pageSize={limit}
                    onPageChange={p => setPage(p.page)}
                    forcePage={page}
                    totalResults={count}
                    marginPagesDisplayed={4}
                    pageRangeDisplayed={3}
                  />
                </div>
              </>
          }
        </div>
      </>
  );
};

EmployeeAgendaSearch.propTypes = {
  isCDO: PropTypes.bool,
  viewType: PropTypes.string,
};

EmployeeAgendaSearch.defaultProps = {
  isCDO: false,
  viewType: '',
};

export default EmployeeAgendaSearch;