components/Filter.js

/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable jsx-a11y/label-has-associated-control */

import React, { Component } from 'react';

import Calendar from './Calendar';
import IntervalDates from './IntervalDates';
import { withRouterHooks } from '../utils/router';
import CountAndExportSettledMobile from './Buttons/CountAndExportSettledMobile';
import InputWithOperator from './InputWithOperator';
import { PERMISSIONS } from '../constants';

/**
 * @module Filter
 */

/**
 * @typedef {object} props
 * @property {string|null} selectedMenuItem
 * @property {boolean} [info]
 * @property {boolean} [initialApiCall]
 * @property {Array} filterFields
 * @property {Array<view>} children
 * @property {object} filters
 * @property {object} [defaultFilter]
 * @property {boolean} [allowEmpty] Allow call without filters
 * @property {object} selectedItem values for dropdowns
 * @property {Function} search Callback function after filter update
 * @property {Function} setFilterData
 * @property {Function} clearFilterData
 */
class Filter extends Component {
  constructor(props) {
    super(props);
    /**
     * @member {object}
     * @property {object} filter
     * @property {object} selectedItem Values of all dropdowns
     */
    this.defaultState = {
      filter: props.filters ? { ...props.filters } : {},
      selectedItem: props.selectedItem ? { ...props.selectedItem } : {},
      isSearch: false,
    };

    this.state = {
      ...this.defaultState,
      hasExportCouponPermission:
        typeof this.props.checkPermission === 'function'
          ? this.props.checkPermission(PERMISSIONS.EXPORT_COUPONS)
          : false,
    };
    /**
     * @member {boolean}
     * @description Clear search only when filter was changed
     */
    this.filteredBefore = false;
  }

  /**
   * Load filter when section is changed
   * Perform search if params exist in URL upon componentDidMount
   *
   * @returns {void}
   */
  componentDidMount() {
    if (this.props.initialApiCall) {
      this.search();
    } else {
      this.handleFilterUrl();
    }
  }

  /**
   * Clear filter when section is changed
   * Perform search when params are changed in URL
   *
   * @param {object} prevProp
   * @returns {void}
   */
  componentDidUpdate(prevProp) {
    if (
      this.props.selectedMenuItem !== prevProp.selectedMenuItem ||
      this.props.clearFilters !== prevProp.clearFilters
    ) {
      this.clear();
    }
    if (prevProp.location !== this.props.location) {
      this.handleFilterUrl();
    }
  }

  /**
   * Handle filter params on back/forward browser button pressed
   * Perform search when params are changed in URL
   *
   * @returns {void}
   */
  handleFilterUrl = () => {
    const filterFromUrl = Object.fromEntries(new URLSearchParams(this.props.location.search));
    const keys = Object.keys(filterFromUrl);
    if (!this.state.isSearch) {
      if (keys.length > 0) {
        const filterProps = this.props.filterProps ? this.props.filterProps : {};
        if (Object.entries(filterFromUrl).toString() !== Object.entries(filterProps).toString()) {
          const filter = { ...filterFromUrl };
          const selectedItem = this.state.selectedItem;
          const filterObj = this.props.filterFields.filter((field) => field.dropdown && keys.includes(field.field));
          if (filterObj.length > 0) {
            this.props.filterFields.filter((field, index) => {
              if (field.dropdown && keys.includes(field.field)) {
                const f = field.dropdown.find((d) => d.id === filterFromUrl[field.field]);
                selectedItem[index] = f;
              }
            });
            this.setState({
              filter,
              selectedItem,
            });
          } else {
            this.setState({
              filter,
              selectedItem: {},
            });
          }
          this.props.setFilterData({
            filterFields: this.props.filterFields,
            keys,
            filter,
            callback: this.props.search,
            selectedTimezone: this.props.selectedTimezone,
          });
        }
      } else {
        this.props.clearFilterData();
        this.props.search();
        this.setState({
          filter: this.props.defaultFilter ? { ...this.props.defaultFilter } : {},
          selectedItem: {},
        });
      }
    }
    this.setState({ isSearch: false });
  };

  /**
   * Search button
   *
   * @function
   * @returns {void}
   */
  search = () => {
    this.setState({ isSearch: true });
    const keys = Object.keys(this.state.filter);
    if (keys.length > 0 || this.props.allowEmpty === true) {
      this.filteredBefore = true;
      const filter = this.state.filter;
      this.props.setFilterData({
        filterFields: this.props.filterFields,
        keys,
        filter,
        callback: this.props.search,
        selectedTimezone: this.props.selectedTimezone,
      });
      this.props.navigate({
        pathname: this.props.location.pathname,
        search: new URLSearchParams(filter).toString(),
      });
    }
  };

  /**
   * Clear filter
   *
   * @function
   * @returns {void}
   */
  clear = () => {
    if (this.filteredBefore) {
      this.filteredBefore = false;
      this.props.clearFilterData();
      this.props.search();
    }
    this.setState({
      filter: this.props.defaultFilter
        ? { ...this.props.defaultFilter }
        : this.props.filters
        ? { ...this.props.filters }
        : {},
      selectedItem: this.props.selectedItem ? { ...this.props.selectedItem } : {},
    });
    this.props.navigate({
      pathname: this.props.location.pathname,
      search: '',
    });
  };

  /**
   * Change filter input field
   *
   * @function
   * @param {string} fieldValue
   * @param {string} field
   * @param {number} [dropdownIndex]
   * @returns {void}
   */
  changeFilter = (fieldValue, field, dropdownIndex) => {
    const filter = this.state.filter;
    const selectedItem = this.state.selectedItem;
    if (dropdownIndex !== undefined) {
      const f = field.dropdown.find((d) => d.label === fieldValue);
      filter[field.field] = f.id;
      selectedItem[dropdownIndex] = f;
    } else {
      filter[field] = fieldValue;
    }
    this.setState({ filter, selectedItem });
    if (this.props.resetExport) this.props.resetExport();
  };

  /**
   * Render dropdown filter
   *
   * @function
   * @param {object} field
   * @param {number} i
   * @returns {view}
   */
  renderDropdown = (field, i) => {
    const value = field.dropdown.find((f) => f.id === this.state.filter[field.field]);
    return (
      <select
        className="form-control"
        onChange={(e) => {
          this.changeFilter(e.target.value, field, i);
        }}
        id={field.field}
        value={value ? value.label : ''}
      >
        {field.dropdown.map((d) => (
          <option key={d.label}>{d.label}</option>
        ))}
      </select>
    );
  };

  /**
   * Render checkbox filter
   *
   * @function
   * @param {object} field
   * @returns {view}
   */
  renderCheckbox = (field) => (
    <div className="user__field" key={field.field}>
      <label className="checkbox">
        <input
          type="checkbox"
          checked={this.state.filter[field.field] ? 'checked' : ''}
          onChange={() => {
            this.changeFilter(this.state.filter[field.field] ? '' : 1, field.field);
          }}
        />
        <span className="checkbox-txt">Bonus</span>
      </label>
    </div>
  );

  /**
   *
   * @function
   * @returns {boolean}
   */
  checkForSpecificRequiredFilters = () => {
    const booleanArr = this.props.mustBePopulatedFilters?.map((filter) => this.state.filter[filter?.field]);

    return booleanArr?.every((elem) => !!elem);
  };

  /**
   * Disable SearchBtn
   *
   * @function
   * @returns {boolean}
   */
  disableSearchBtn = () => {
    const filterNames = Object.keys(this.state.filter);
    const mandatoryFilters = filterNames?.filter((f) => !this.props.notMandatoryFilters?.includes(f));

    // Run custom validations set in filter fields
    for (let index = 0; index < this.props.filterFields.length; index += 1) {
      if (typeof this.props.filterFields[index]?.isValid === 'function') {
        const filterIsValid = this.props.filterFields[index].isValid(this.state.filter);
        if (!filterIsValid) {
          return true; // If any filter is invalid, disable immediately
        }
      }
    }

    if (this.props.additionalFiltersCheck) {
      const checkSpecificFilters = this.checkForSpecificRequiredFilters();
      if (!checkSpecificFilters) return true;
    }

    // Check for mandatory filters only if all filters are valid
    for (let i = 0; i < mandatoryFilters.length; i += 1) {
      if (this.state.filter[mandatoryFilters[i]]) {
        return false;
      }
    }

    return true; // Disable button if no mandatory filter
  };

  renderClearAndSearchButtons = () => (
    <>
      <button
        id="searchBtn_filter"
        type="button"
        className="btn btn-primary"
        onClick={this.search}
        disabled={this.disableSearchBtn()}
      >
        Search
      </button>
      <button id="clearFiltersBtn_filter" type="button" className="btn btn-secondary" onClick={this.clear}>
        Clear Filters
      </button>
    </>
  );

  changeInputWithOperator = (value, config) => {
    // value is of type object { input: '', operator: ''}
    // Ensure value.input is a number
    let inputValue = parseInt(value.input);
    if (Number.isNaN(inputValue)) inputValue = 0;

    // Define min and max limits from config or default values
    const minLimit = config.minimumValue;
    const maxLimit = config.maximumValue;

    const filter = this.state.filter;
    filter[config.field] =
      typeof config.formatValue === 'function' && value.input
        ? config.formatValue(Math.max(minLimit, Math.min(maxLimit, inputValue)))
        : value.input;
    filter[config.operatorField] = value.operator;

    this.setState({ filter });
  };

  getInputValue = (config) =>
    typeof config.getValue === 'function' && this.state.filter[config.field]
      ? config.getValue(parseInt(this.state.filter[config.field]))
      : this.state.filter[config.field];

  /**
   * Render
   *
   * @returns {view}
   */
  render() {
    return (
      <>
        <div className="user__filter">
          <div className="user__filter-fields">
            {this.props.filterFields.map((field, i) =>
              field.bonus ? (
                this.renderCheckbox(field)
              ) : field.inputWithOperator ? (
                <React.Fragment key={field.field}>
                  <InputWithOperator
                    inputValue={this.getInputValue(field) || ''}
                    operatorValue={this.state.filter[field.operatorField] || ''}
                    onChangeFilter={this.changeInputWithOperator}
                    config={field}
                  />
                </React.Fragment>
              ) : (
                <div className="user__field" key={i}>
                  <label className="txt">{field.text}</label>
                  {field.dateInterval ? (
                    <IntervalDates
                      fields={field.fields}
                      filter={this.state.filter}
                      defaultFilter={this.props.defaultFilter}
                      changeFilter={this.changeFilter}
                      selectedTimezone={this.props.selectedTimezone}
                      dateFilterFields={this.props.dateFilterFields}
                      {...this.props}
                    />
                  ) : field.dateIntervalSecond ? (
                    <IntervalDates
                      fields={field.fields}
                      filter={this.state.filter}
                      defaultFilter={this.props.defaultFilter}
                      changeFilter={this.changeFilter}
                      selectedTimezone={this.props.selectedTimezone}
                      {...this.props}
                      dateFilterFields={this.props.dateFilterFieldsSecond}
                    />
                  ) : field.dateTime ? (
                    <Calendar
                      field={field.field}
                      filter={this.state.filter}
                      defaultFilter={this.props.defaultFilter}
                      changeFilter={this.changeFilter}
                      className="calendar"
                      actions
                      selectedTimezone={this.props.selectedTimezone}
                    />
                  ) : field.dropdown ? (
                    this.renderDropdown(field, i)
                  ) : (
                    <input
                      type={field.isNumber ? 'number' : 'text'}
                      className="form-control"
                      id={`${field.field}_filter`}
                      placeholder="-"
                      value={this.state.filter[field.field] || ''}
                      onChange={(e) => {
                        this.changeFilter(e.target.value, field.field);
                      }}
                    />
                  )}
                </div>
              )
            )}
          </div>
        </div>
        {this.props.info && (
          <div className="user__info mt-20">*selected Information will be displayed in the table(s) below</div>
        )}
        {this.props.isSettledCouponsForMobile ? (
          <div className="user__filter-button d-flex justify-content-between">
            <div className="d-flex flow-right align-items-center">{this.renderClearAndSearchButtons()}</div>
            {this.state.hasExportCouponPermission && (
              <div className="d-flex flow-right align-items-center">
                <CountAndExportSettledMobile />
              </div>
            )}
          </div>
        ) : (
          <div className="user__filter-button">
            {this.renderClearAndSearchButtons()}
            {this.props.children}
          </div>
        )}
      </>
    );
  }
}
export default withRouterHooks(Filter);