MetaPhase-Consulting/State-TalentMAP

View on GitHub
src/Components/Panel/PanelMeetingAgendas/PanelMeetingAgendas.jsx

Summary

Maintainability
B
4 hrs
Test Coverage
F
56%
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router';
import { useEffect, useRef, useState } from 'react';
import { filter, flatten, get, has, includes, isEmpty, sortBy, uniqBy } from 'lodash';
import { format } from 'date-fns-v2';
import { panelMeetingAgendasFetchData, savePanelMeetingAgendasSelections } from 'actions/panelMeetingAgendas';
import { filtersFetchData } from 'actions/filters/filters';
import PositionManagerSearch from 'Components/BureauPage/PositionManager/PositionManagerSearch';
import ProfileSectionTitle from 'Components/ProfileSectionTitle/ProfileSectionTitle';
import ListItem from 'Components/BidderPortfolio/BidControls/BidCyclePicker/ListItem';
import Spinner from 'Components/Spinner';
import TotalResults from 'Components/TotalResults';
import ScrollUpButton from 'Components/ScrollUpButton';
import BackButton from 'Components/BackButton';
import PaginationWrapper from 'Components/PaginationWrapper';
import SelectForm from 'Components/SelectForm';
import { PANEL_MEETING } from 'Constants/PropTypes';
import { PANEL_MEETINGS_PAGE_SIZES } from 'Constants/Sort';
import { DEFAULT_TEXT } from 'Constants/SystemMessages';
import { useDataLoader } from 'hooks';
import Picky from 'react-picky';
import FA from 'react-fontawesome';
import AgendaItemRow from 'Components/Agenda/AgendaItemRow';
import PrintPanelMeetingAgendas from './PrintPanelMeetingAgendas';
import { meetingCategoryMap } from '../Constants';
import api from '../../../api';

const PanelMeetingAgendas = (props) => {
  const { isCDO, location } = props;

  const panelMeetings = location.state?.panelMeetings || [];
  const pmSeqNums = sortBy(panelMeetings.map(pm => pm.pmi_pm_seq_num), (n) => n);

  const agendasIsLoading = useSelector(state => state.panelMeetingAgendasFetchDataLoading);
  const agendas$ = useSelector(state => state.panelMeetingAgendas);
  const count = get(agendas$, 'count', 0);
  const agendas = get(agendas$, 'results', []);
  const agendasByPanelMeeting = panelMeetings.map((pm) => {
    // Filter agendas by panel meeting sequence number
    const filteredAgendas = agendas.filter((agenda) => agenda.pmi_pm_seq_num === pm.pmi_pm_seq_num);

    // Initialize an empty object with keys from meetingCategoryMap
    const categorizedAgendas = Object.keys(meetingCategoryMap).reduce((acc, key) => {
      acc[meetingCategoryMap[key]] = [];
      return acc;
    }, {});

    // Iterate over filteredAgendas and categorize them
    filteredAgendas.forEach((agenda) => {
      const category = meetingCategoryMap[agenda.pmi_mic_code];
      if (category) {
        categorizedAgendas[category].push(agenda);
      }
    });

    return {
      ...pm,
      agendas: categorizedAgendas,
    };
  });
  const isAllCategoriesEmpty = (agenda) => Object.values(agenda).every(category => category.length === 0);

  const childRef = useRef();
  const dispatch = useDispatch();

  const userSelections = useSelector(state => state.panelMeetingAgendasSelections);
  const [page, setPage] = useState(get(userSelections, 'page') || 1);
  const [limit, setLimit] = useState(get(userSelections, 'limit') || PANEL_MEETINGS_PAGE_SIZES.defaultSize);

  const genericFiltersIsLoading = useSelector(state => state.filtersIsLoading);
  const genericFilters = useSelector(state => state.filters);

  const genericFilters$ = get(genericFilters, 'filters') || [];
  const grades = genericFilters$.find(f => get(f, 'item.description') === 'grade');
  const gradesOptions = uniqBy(get(grades, 'data'), 'code');
  const skills = genericFilters$.find(f => get(f, 'item.description') === 'skill');
  const skillsOptions = uniqBy(sortBy(get(skills, 'data'), [(s) => s.description]), 'code');
  const languages = genericFilters$.find(f => get(f, 'item.description') === 'language');
  const languagesOptions = uniqBy(sortBy(get(languages, 'data'), [(c) => c.custom_description]), 'custom_description');
  const { data: remarks, loading: remarksLoading } = useDataLoader(api().get, '/fsbid/agenda/remarks/');
  const remarksOptions = uniqBy(sortBy(get(remarks, 'data.results'), [(c) => c.text]), 'text');
  const { data: statuses, loading: statusLoading } = useDataLoader(api().get, '/fsbid/agenda/statuses/');
  const statusesOptions = uniqBy(sortBy(get(statuses, 'data.results'), [(c) => c.desc_text]), 'desc_text');
  const { data: actions, loading: actionLoading } = useDataLoader(api().get, '/fsbid/agenda/leg_action_types/');
  const actionsOptions = uniqBy(sortBy(get(actions, 'data.results'), [(c) => c.desc_text]), 'desc_text');
  const { data: categories, loading: categoryLoading } = useDataLoader(api().get, '/fsbid/panel/reference/categories/');
  const categoriesOptions = uniqBy(sortBy(get(categories, 'data.results'), [(c) => c.mic_desc_text]), 'mic_desc_text');
  // Replace with real ref data endpoints
  const { data: orgs, loading: orgsLoading } = useDataLoader(api().get, '/fsbid/agenda_employees/reference/current-organizations/');
  const organizationOptions = sortBy(get(orgs, 'data'), [(o) => o.name]);

  const panelFiltersIsLoading =
    includes([orgsLoading, remarksLoading, statusLoading,
      actionLoading, categoryLoading], true);

  const [selectedOrgs, setSelectedOrgs] = useState(get(userSelections, 'selectedOrgs') || []);
  const [selectedCategories, setSelectedCategories] = useState(get(userSelections, 'selectedCategories') || []);
  const [selectedGrades, setSelectedGrades] = useState(get(userSelections, 'selectedGrades') || []);
  const [selectedActions, setSelectedActions] = useState(get(userSelections, 'selectedActions') || []);
  const [selectedStatuses, setSelectedStatuses] = useState(get(userSelections, 'selectedStatuses') || []);
  const [selectedLanguages, setSelectedLanguages] = useState(get(userSelections, 'selectedLanguages') || []);
  const [selectedRemarks, setSelectedRemarks] = useState(get(userSelections, 'selectedRemarks') || []);
  const [selectedSkills, setSelectedSkills] = useState(get(userSelections, 'selectedSkills') || []);
  const [textSearch, setTextSearch] = useState(get(userSelections, 'textSearch') || '');
  const [clearFilters, setClearFilters] = useState(false);
  const [printView, setPrintView] = useState(false);

  const isLoading = genericFiltersIsLoading || panelFiltersIsLoading;
  const getQuery = () => ({
    page,
    limit,
    ordering: ['-panel_date', 'agenda_id'],
    pmipmseqnum: pmSeqNums,
    orgs: selectedOrgs.map(orgObject => orgObject.code),
    categories: selectedCategories.map(categoryObject => categoryObject.mic_code),
    grades: selectedGrades.map(gradeObject => gradeObject.code),
    actions: selectedActions.map(actionObject => actionObject.code),
    statuses: selectedStatuses.map(statusObject => statusObject.code),
    languages: selectedLanguages.map(languageObject => languageObject.code),
    remarks: selectedRemarks.map(remarkObject => remarkObject.seq_num),
    skills: selectedSkills.map(skillObject => skillObject.code),
    freetext: textSearch,
  });

  const getCurrentInputs = () => ({
    selectedOrgs,
    selectedCategories,
    selectedGrades,
    selectedActions,
    selectedStatuses,
    selectedLanguages,
    selectedRemarks,
    selectedSkills,
    textSearch,
    limit,
    page,
  });

  const fetchAndSet = (resetPage = false) => {
    const filters = [
      selectedOrgs,
      selectedCategories,
      selectedGrades,
      selectedActions,
      selectedStatuses,
      selectedLanguages,
      selectedRemarks,
      selectedSkills,
    ];
    if (isEmpty(filter(flatten(filters))) && isEmpty(textSearch)) {
      setClearFilters(false);
    } else {
      setClearFilters(true);
    }
    if (resetPage) {
      setPage(1);
    }
    dispatch(savePanelMeetingAgendasSelections(getCurrentInputs()));
    dispatch(panelMeetingAgendasFetchData(getQuery()));
  };

  useEffect(() => {
    dispatch(panelMeetingAgendasFetchData(getQuery()));
    dispatch(filtersFetchData(genericFilters));
  }, []);

  useEffect(() => {
    fetchAndSet(true);
  }, [
    selectedOrgs,
    selectedCategories,
    selectedGrades,
    selectedActions,
    selectedStatuses,
    selectedLanguages,
    selectedRemarks,
    selectedSkills,
    textSearch,
    limit,
  ]);

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

  function renderSelectionList({ items, selected, ...rest }) {
    let codeOrText = 'code';
    // only Remarks needs to use 'short_desc_text'
    if (has(items[0], 'seq_num')) {
      codeOrText = 'short_desc_text';
    }
    // only Item Actions/Statuses need to use 'desc_text'
    if (has(items[0], 'desc_text')) {
      codeOrText = 'desc_text';
    }
    if (has(items[0], 'abbr_desc_text') && items[0].code === 'V') {
      codeOrText = 'abbr_desc_text';
    }
    // only Categories need to use 'mic_desc_text'
    if (has(items[0], 'mic_desc_text')) {
      codeOrText = 'mic_desc_text';
    }
    const getSelected = item => !!selected.find(f => f[codeOrText] === item[codeOrText]);
    let queryProp = 'description';
    if (get(items, '[0].custom_description', false)) queryProp = 'custom_description';
    else if (has(items[0], 'name')) queryProp = 'name';
    else if (codeOrText === 'mic_desc_text') queryProp = 'mic_desc_text';
    else if (codeOrText === 'desc_text') queryProp = 'desc_text';
    else if (codeOrText === 'abbr_desc_text') queryProp = 'abbr_desc_text';
    else if (codeOrText === 'short_desc_text') queryProp = 'short_desc_text';
    return items.map(item =>
      (<ListItem
        key={item[codeOrText]}
        item={item}
        {...rest}
        queryProp={queryProp}
        getIsSelected={getSelected}
      />),
    );
  }

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

  const resetFilters = () => {
    setSelectedOrgs([]);
    setSelectedCategories([]);
    setSelectedGrades([]);
    setSelectedActions([]);
    setSelectedStatuses([]);
    setSelectedLanguages([]);
    setSelectedRemarks([]);
    setSelectedSkills([]);
    setTextSearch('');
    childRef.current.clearText();
    setClearFilters(false);
  };

  const getOverlay = () => {
    let overlay;
    if (isLoading) overlay = <Spinner type="bureau-filters" size="small" />;
    if (printView) {
      overlay = (
        <PrintPanelMeetingAgendas
          closePrintView={() => setPrintView(false)}
          panelMeetingsWithAgendas={agendasByPanelMeeting}
        />
      );
    }
    return overlay;
  };

  return (
    getOverlay() ||
      <>
        <div className="panel-meeting-agenda-page position-search">
          <div className="usa-grid-full position-search--header search-bar-container">
            <BackButton />
            <ProfileSectionTitle title="Panel Meeting Agendas" icon="calendar" />
            <PositionManagerSearch
              onChange={setTextSearch}
              ref={childRef}
              textSearch={textSearch}
              label="Find Agenda Item"
              placeHolder="Search for Agenda using Position Name, Position Title, or Employee Name"
              noButton
            />
            <div className="filterby-container">
              <div className="filterby-label">Filter by:</div>
              <div className="filterby-clear">
                {clearFilters &&
                <button className="unstyled-button" onClick={resetFilters}>
                  <FA name="times" />
                Clear Filters
                </button>
                }
              </div>
            </div>
            <div className="usa-width-one-whole position-search--filters--panel-m-agendas">
              <div className="filter-div">
                <div className="label">Location/Org:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Organization(s)"
                  value={selectedOrgs}
                  options={organizationOptions}
                  onChange={setSelectedOrgs}
                  valueKey="code"
                  labelKey="name"
                  disabled={isLoading}
                />
              </div>
              <div className="filter-div">
                <div className="label">Category:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Category(-ies)"
                  value={selectedCategories}
                  options={categoriesOptions}
                  onChange={setSelectedCategories}
                  valueKey="mic_code"
                  labelKey="mic_desc_text"
                  disabled={isLoading}
                />
              </div>
              <div className="filter-div">
                <div className="label">Grade:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Grade(s)"
                  value={selectedGrades}
                  options={gradesOptions}
                  onChange={setSelectedGrades}
                  valueKey="code"
                  labelKey="custom_description"
                  disabled={isLoading}
                />
              </div>
              <div className="filter-div">
                <div className="label">Action:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Action(s)"
                  value={selectedActions}
                  options={actionsOptions}
                  onChange={setSelectedActions}
                  valueKey="code"
                  labelKey="desc_text"
                  disabled={isLoading}
                />
              </div>
              <div className="filter-div">
                <div className="label">Status:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Item Status(es)"
                  value={selectedStatuses}
                  options={statusesOptions}
                  onChange={setSelectedStatuses}
                  valueKey="code"
                  labelKey="desc_text"
                  disabled={isLoading}
                />
              </div>
              <div className="filter-div">
                <div className="label">Language:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Language(s)"
                  value={selectedLanguages}
                  options={languagesOptions}
                  onChange={setSelectedLanguages}
                  valueKey="code"
                  labelKey="custom_description"
                  disabled={isLoading}
                />
              </div>
              <div className="filter-div">
                <div className="label">Remarks:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Remarks"
                  value={selectedRemarks}
                  options={remarksOptions}
                  onChange={setSelectedRemarks}
                  valueKey="seq_num"
                  labelKey="text"
                  disabled={isLoading}
                />
              </div>
              <div className="filter-div">
                <div className="label">Skill:</div>
                <Picky
                  {...pickyProps}
                  placeholder="Select Skill(s)"
                  value={selectedSkills}
                  options={skillsOptions}
                  onChange={setSelectedSkills}
                  valueKey="code"
                  labelKey="custom_description"
                  disabled={isLoading}
                />
              </div>
            </div>
          </div>
          <ScrollUpButton />
          {
            <div className="panel-meeting-agendas-container">
              <div className="total-results results-dropdown">
                <TotalResults
                  total={count}
                  pageNumber={page}
                  pageSize={limit}
                  suffix="Results"
                  isHidden={agendasIsLoading}
                />
                <div className="panel-results-controls">
                  <SelectForm
                    id="panel-limit"
                    className="panel-select"
                    options={PANEL_MEETINGS_PAGE_SIZES.options}
                    label="Results:"
                    defaultSort={limit}
                    onSelectOption={e => setLimit(e.target.value)}
                    disabled={agendasIsLoading}
                  />
                  { <button onClick={() => setPrintView(true)} disabled={agendasIsLoading}>Print View</button> }
                </div>
              </div>
              {
                agendasIsLoading ? <Spinner type="bureau-results" size="medium" /> :
                  <>
                    {
                      agendasByPanelMeeting.map((pm) => {
                        // Find panel meeting date
                        const panelMeetingDate = pm.panelMeetingDates.find(pmd => pmd.mdt_code === 'MEET') || DEFAULT_TEXT;
                        return (
                          <div className="pma-pm-container" key={pm.pmi_pm_seq_num}>
                            <div className="pm-header">
                              {get(pm, 'pmt_code')} {panelMeetingDate !== DEFAULT_TEXT ? format(new Date(panelMeetingDate.pmd_dttm), 'MM/dd/yy') : ''} - {get(pm, 'pms_desc_text')}
                            </div>
                            {
                              isAllCategoriesEmpty(pm.agendas) ? (
                                <div className="pm-empty-row">No agendas</div>
                              ) : (
                                <div className="pm-agendas-container">
                                  {
                                    Object.keys(pm.agendas).map((category) => (
                                      <div key={category} className="category-container">
                                        <div className="category-header">
                                          {category}
                                        </div>
                                        <div className="agenda-item-row-container">
                                          {
                                            pm.agendas[category].length > 0 ? (
                                              pm.agendas[category].map(agenda => (
                                                <AgendaItemRow
                                                  agenda={agenda}
                                                  key={agenda.id}
                                                  isCDO={isCDO}
                                                  isPanelMeetingView
                                                />
                                              ))
                                            ) : (
                                              <div className="ai-empty-row">(No Agendas)</div>
                                            )
                                          }
                                        </div>
                                      </div>
                                    ))
                                  }
                                </div>
                              )
                            }
                          </div>
                        );
                      })
                    }
                    <div className="usa-grid-full react-paginate">
                      <PaginationWrapper
                        pageSize={limit}
                        onPageChange={p => setPage(p.page)}
                        forcePage={page}
                        totalResults={count}
                      />
                    </div>
                  </>
              }
            </div>
          }
        </div>
      </>
  );
};

PanelMeetingAgendas.propTypes = {
  isCDO: PropTypes.bool,
  location: PropTypes.shape({
    pathname: PropTypes.string,
    state: PropTypes.shape({
      panelMeetings: PropTypes.arrayOf(PANEL_MEETING),
    }),
  }).isRequired };

PanelMeetingAgendas.defaultProps = {
  isCDO: false,
};

export default withRouter(PanelMeetingAgendas);