MetaPhase-Consulting/State-TalentMAP

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

Summary

Maintainability
B
4 hrs
Test Coverage
F
3%
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Picky from 'react-picky';
import FA from 'react-fontawesome';
import PropTypes from 'prop-types';
import { checkFlag } from 'flags';
import { sortBy } from 'lodash';
import { usePrevious } from 'hooks';
import {
  projectedVacancyEdit,
  projectedVacancyFetchData,
  projectedVacancyFilters,
  saveProjectedVacancySelections,
} from 'actions/projectedVacancy';
import { onEditModeSearch, renderSelectionList, userHasPermissions } from 'utilities';
import { PANEL_MEETINGS_PAGE_SIZES } from 'Constants/Sort';
import Spinner from 'Components/Spinner';
import Alert from 'Components/Alert';
import SelectForm from 'Components/SelectForm';
import TotalResults from 'Components/TotalResults';
import ScrollUpButton from 'Components/ScrollUpButton';
import PaginationWrapper from 'Components/PaginationWrapper';
import ProfileSectionTitle from 'Components/ProfileSectionTitle/ProfileSectionTitle';
import ProjectedVacancyCard from '../../ProjectedVacancyCard/ProjectedVacancyCard';

const enableCycleImport = () => checkFlag('flags.projected_vacancy_cycle_import');
const enableEdit = () => checkFlag('flags.projected_vacancy_edit');

// eslint-disable-next-line complexity
const ProjectedVacancy = ({ viewType }) => {
  const dispatch = useDispatch();

  const userProfile = useSelector(state => state.userProfile);
  const isAo = userHasPermissions(['ao_user'], userProfile?.permission_groups);
  const isBureau = userHasPermissions(['bureau_user'], userProfile?.permission_groups);
  const bureauPermissions = useSelector(state => state.userProfile.bureau_permissions);
  // const isBureauView = viewType === 'bureau';

  const userSelections = useSelector(state => state.projectedVacancySelections);
  const filters = useSelector(state => state.projectedVacancyFilters) || [];
  const filtersLoading = useSelector(state => state.projectedVacancyFiltersLoading);
  const filtersErrored = useSelector(state => state.projectedVacancyFiltersErrored);
  const positionsData = useSelector(state => state.projectedVacancy);
  const positionsLoading = useSelector(state => state.projectedVacancyFetchDataLoading);
  const positionsErrored = useSelector(state => state.projectedVacancyFetchDataErrored);

  const [page, setPage] = useState(userSelections?.page || 1);
  const [limit, setLimit] = useState(userSelections?.limit || 5);
  const positions = positionsData?.results?.length ? positionsData?.results : [];
  const prevPage = usePrevious(page);
  const pageSizes = PANEL_MEETINGS_PAGE_SIZES;

  const [importedPositions, setImportedPositions] = useState([]);
  const [cardsInEditMode, setCardsInEditMode] = useState([]);
  const [importInEditMode, setImportInEditMode] = useState(false);
  const [clearFilters, setClearFilters] = useState(false);
  const [selectedBureaus, setSelectedBureaus] =
    useState(userSelections?.selectedBureaus || []);
  const [selectedOrganizations, setSelectedOrganizations] =
    useState(userSelections?.selectedOrganizations || []);
  const [selectedGrades, setSelectedGrades] =
    useState(userSelections?.selectedGrade || []);
  const [selectedSkills, setSelectedSkills] =
    useState(userSelections?.selectedSkills || []);
  const [selectedLanguages, setSelectedLanguages] =
    useState(userSelections?.selectedLanguage || []);
  const [selectedBidSeasons, setSelectedBidSeasons] =
    useState(userSelections?.selectedBidSeasons || []);
  const [selectedCycle, setSelectedCycle] =
    useState(userSelections?.selectedCycle || null);

  const filterSelectionValid = () => {
    // valid if:
    // not Bureau user
    // a Bureau filter selected
    if (viewType === 'bureau') {
      return selectedBureaus.length > 0;
    }
    return true;
  };

  const grades = sortBy(filters?.grades || [], [o => o.code]);
  const skills = sortBy(filters?.skills || [], [o => o.description]);
  const languages = sortBy(filters?.languages || [], [o => o.description]);
  const bidSeasons = sortBy(filters?.bid_seasons || [], [o => o.description]);
  const organizations = sortBy(filters?.organizations || [], [o => o.description]);
  const statuses = sortBy(filters?.statuses || [], [o => o.description]);
  const bureaus = sortBy(bureauPermissions || [], [(b) => b.long_description]);

  const resultsLoading = positionsLoading;
  const resultsErrored = filtersErrored || positionsErrored;
  const disableSearch = cardsInEditMode?.length > 0 || importInEditMode;
  const disableInput = filtersLoading || resultsLoading || disableSearch;

  // TODO: Use real field for cycle import in this function
  const originalImport = positions?.filter(
    p => p.future_vacancy_exclude_import_indicator === 'N',
  )?.map(
    k => k.future_vacancy_seq_num,
  ) || [];

  const getQuery = () => ({
    limit,
    page,
    bureaus: selectedBureaus?.map(o => o?.code),
    organizations: selectedOrganizations?.map(o => o?.short_description),
    bidSeasons: selectedBidSeasons?.map(o => o?.code),
    languages: selectedLanguages?.map(o => o?.code),
    grades: selectedGrades?.map(o => o?.code),
    skills: selectedSkills?.map(o => o?.code),
    cycle: selectedCycle?.code,
  });

  const resetFilters = () => {
    setSelectedBureaus([]);
    setSelectedOrganizations([]);
    setSelectedGrades([]);
    setSelectedLanguages([]);
    setSelectedSkills([]);
    setSelectedBidSeasons([]);
    setSelectedCycle(null);
    setClearFilters(false);
  };

  const getCurrentInputs = () => ({
    limit,
    page,
    selectedBureaus,
    selectedOrganizations,
    selectedGrades,
    selectedLanguages,
    selectedSkills,
    selectedBidSeasons,
    selectedCycle,
  });

  const getOverlay = () => {
    let overlay;
    if (resultsLoading) {
      overlay = <Spinner type="standard-center" class="homepage-position-results" size="big" />;
    } else if (resultsErrored) {
      overlay = <Alert type="error" title="Error displaying Projected Vacancies" messages={[{ body: 'Please try again.' }]} />;
    } else if (!filterSelectionValid()) {
      overlay = <Alert type="info" title="Select Bureau Filter" messages={[{ body: 'Please select a Bureau Filter.' }]} />;
    } else if (!positionsData?.results?.length) {
      overlay = <Alert type="info" title="No results found" messages={[{ body: 'No projected vacancies for filter inputs.' }]} />;
    } else {
      return false;
    }
    return overlay;
  };

  const submitEdit = (editData) => {
    dispatch(projectedVacancyEdit(editData, () => dispatch(projectedVacancyFetchData(getQuery()))));
  };

  useEffect(() => {
    dispatch(saveProjectedVacancySelections(getCurrentInputs()));
    dispatch(projectedVacancyFilters());
  }, []);

  useEffect(() => {
    if (positions.length) {
      setImportedPositions(originalImport);
    }
  }, [positions]);

  useEffect(() => {
    const diffExists = originalImport?.sort().join(',') !== importedPositions?.sort().join(',');
    if (diffExists) {
      setImportInEditMode(true);
    } else {
      setImportInEditMode(false);
    }
  }, [importedPositions]);

  const fetchAndSet = (resetPage = false) => {
    const f = [
      selectedBureaus,
      selectedOrganizations,
      selectedGrades,
      selectedLanguages,
      selectedSkills,
      selectedBidSeasons,
    ];
    if (f.flat()?.length === 0 && !selectedCycle) {
      setClearFilters(false);
    } else {
      setClearFilters(true);
    }
    if (filterSelectionValid()) {
      if (resetPage) {
        setPage(1);
      }
      dispatch(projectedVacancyFetchData(getQuery()));
      dispatch(saveProjectedVacancySelections(getCurrentInputs()));
    }
  };

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

  useEffect(() => {
    if (prevPage) {
      fetchAndSet(true);
    } else {
      fetchAndSet(false);
    }
  }, [
    limit,
    selectedBureaus,
    selectedOrganizations,
    selectedGrades,
    selectedLanguages,
    selectedSkills,
    selectedBidSeasons,
    selectedCycle,
  ]);

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

  const onImportUpdate = (id, imported) => {
    if (imported) {
      setImportedPositions([...importedPositions, id]);
    } else {
      setImportedPositions(importedPositions?.filter(x => x !== id));
    }
  };

  const addToProposedCycle = () => {
    const updatedPvs = [];
    positions.forEach(p => {
      const imported = importedPositions.find(o => o === p.future_vacancy_seq_num);
      const currentValue = p.future_vacancy_exclude_import_indicator;
      const needsUpdate = (currentValue === 'Y' && imported) || (currentValue === 'N' && !imported);

      if (needsUpdate) {
        const overrideTED = p.fvoverrideteddate;
        updatedPvs.push({
          ...p,
          future_vacancy_override_tour_end_date: overrideTED ?
            new Date(overrideTED).toISOString().substring(0, 10) : null,
          // TODO: Change proposed cycle field
        });
      }
    });
    const editData = { projected_vacancy: updatedPvs };
    dispatch(projectedVacancyEdit(getQuery(), editData)); // swapped out PV Endpoints - this no longer works
  };

  return (filtersLoading ?
    <Spinner type="bureau-filters" size="small" /> :
    <div className="position-search">
      <div className="usa-grid-full position-search--header">
        <ProfileSectionTitle title="Projected Vacancy Management" icon="keyboard-o" className="xl-icon" />
        <div className="results-search-bar pt-20">
          <div className="filterby-container">
            <div className="filterby-label">Filter by:</div>
            <div className="filterby-clear">
              {clearFilters &&
                <button
                  className="unstyled-button"
                  onClick={resetFilters}
                  disabled={disableSearch}
                >
                  <FA name="times" />
                  Clear Filters
                </button>
              }
            </div>
          </div>
          <div className="usa-width-one-whole position-search--filters--pv-man results-dropdown">
            <div className="filter-div">
              <div className="label">Bid Season:</div>
              <Picky
                {...pickyProps}
                placeholder="Select Bid Season(s)"
                value={selectedBidSeasons}
                options={bidSeasons}
                onChange={setSelectedBidSeasons}
                valueKey="code"
                labelKey="description"
                disabled={disableInput}
              />
            </div>
            <div className="filter-div">
              <div className="label">Bureau:</div>
              <Picky
                {...pickyProps}
                placeholder="Select Bureau(s)"
                value={selectedBureaus}
                options={bureaus}
                onChange={setSelectedBureaus}
                valueKey="code"
                labelKey="short_description"
                disabled={disableInput}
              />
            </div>
            <div className="filter-div">
              <div className="label">Organization:</div>
              <Picky
                {...pickyProps}
                placeholder="Select Organization(s)"
                value={selectedOrganizations}
                options={organizations}
                onChange={setSelectedOrganizations}
                valueKey="short_description"
                labelKey="description"
                disabled={disableInput}
              />
            </div>
            <div className="filter-div">
              <div className="label">Skills:</div>
              <Picky
                {...pickyProps}
                placeholder="Select Skill(s)"
                value={selectedSkills}
                options={skills}
                onChange={setSelectedSkills}
                valueKey="code"
                labelKey="description"
                disabled={disableInput}
              />
            </div>
            <div className="filter-div">
              <div className="label">Grade:</div>
              <Picky
                {...pickyProps}
                placeholder="Select Grade(s)"
                value={selectedGrades}
                options={grades}
                onChange={setSelectedGrades}
                valueKey="code"
                labelKey="description"
                disabled={disableInput}
              />
            </div>
            <div className="filter-div">
              <div className="label">Language:</div>
              <Picky
                {...pickyProps}
                placeholder="Select Language(s)"
                value={selectedLanguages}
                options={languages}
                onChange={setSelectedLanguages}
                valueKey="code"
                labelKey="description"
                disabled={disableInput}
              />
            </div>
            {enableCycleImport() &&
              <div className="filter-div">
                <div className="label">Cycle:</div>
                <Picky
                  {...pickyProps}
                  multiple={false}
                  placeholder="Select Cycle"
                  value={selectedCycle}
                  options={languages}
                  onChange={setSelectedCycle}
                  valueKey="code"
                  labelKey="description"
                  disabled={disableInput}
                />
              </div>
            }
          </div>
        </div>
      </div>
      {disableSearch &&
        <Alert
          type="warning"
          title="Edit Mode"
          customClassName="mb-10"
          messages={[{
            body: 'Discard or save your edits before searching, marking for include, adding to proposed cycle, or editing other projected vacancies. ' +
              'Filters, "Included" checkboxes, "Import" checkboxes, and other edit buttons are disabled if one card is in Edit Mode.',
          }]}
        />
      }
      {getOverlay() || <>
        <div className="viewing-results-and-dropdown--fullscreen padding-top results-dropdown">
          <TotalResults
            total={positionsData?.count}
            pageNumber={page}
            pageSize={limit}
            suffix="Results"
            isHidden={false}
          />
          <ScrollUpButton />
          <SelectForm
            className="panel-select"
            id="panel-select"
            options={pageSizes.options}
            label="Results:"
            defaultSort={limit}
            onSelectOption={e => setLimit(e.target.value)}
            disabled={disableSearch}
          />
        </div>
        <div className="usa-width-one-whole position-search--results mt-20">
          {enableCycleImport() &&
            <div className="double-action-banner">
              <div className="selected-submission-row import-row">
                <span>
                  {importedPositions?.length} {importedPositions?.length === 1 ? 'Position' : 'Positions'} Selected for Import
                </span>
                {(isAo && importInEditMode) &&
                  <div>
                    <button
                      onClick={() => {
                        setImportInEditMode(false);
                        setImportedPositions(originalImport);
                      }}
                      disabled={!importedPositions?.length}
                    >
                      Cancel
                    </button>
                    <button
                      className="usa-button-secondary"
                      onClick={addToProposedCycle}
                      disabled={!importedPositions?.length}
                    >
                      Add to Proposed Cycle
                    </button>
                  </div>
                }
              </div>
            </div>
          }
          <div className="usa-grid-full position-list">
            {positions?.map(k => (
              <ProjectedVacancyCard
                key={k.future_vacancy_seq_num}
                result={k}
                updateImport={onImportUpdate}
                disableImport={cardsInEditMode?.length > 0 || !isAo || !selectedCycle || !enableEdit}
                disableEdit={importInEditMode || disableSearch || !enableEdit || !isBureau}
                isBureau={isBureau}
                onEditModeSearch={(editMode, id) =>
                  onEditModeSearch(editMode, id, setCardsInEditMode, cardsInEditMode)
                }
                onSubmit={editData => submitEdit(editData)}
                selectOptions={{
                  bidSeasons,
                  statuses,
                }}
              />
            ))}
          </div>
          <div className="usa-grid-full react-paginate">
            <PaginationWrapper
              pageSize={limit}
              onPageChange={(p) => setPage(p.page)}
              forcePage={page}
              totalResults={positionsData?.count}
            />
          </div>
        </div>
      </>}
    </div>
  );
};


ProjectedVacancy.propTypes = {
  viewType: PropTypes.string.isRequired,
  bureauFiltersIsLoading: PropTypes.bool,
};

ProjectedVacancy.defaultProps = {
  bureauFilters: { filters: [] },
  bureauPositions: { results: [] },
  bureauFiltersIsLoading: false,
  bureauPositionsIsLoading: false,
};

export default ProjectedVacancy;