diff --git a/src/components/orgunits/OrgUnitData.js b/src/components/orgunits/OrgUnitData.js index bce650e02..4108d34d1 100644 --- a/src/components/orgunits/OrgUnitData.js +++ b/src/components/orgunits/OrgUnitData.js @@ -23,7 +23,7 @@ const ORGUNIT_PROFILE_QUERY = { // Only YEARLY period type is supported in first version const periodType = 'YEARLY' const currentYear = String(new Date().getFullYear()) -const periods = getFixedPeriodsByType(periodType, currentYear) +const periods = getFixedPeriodsByType({ periodType, currentYear }) const defaultPeriod = filterFuturePeriods(periods)[0] || periods[0] /* diff --git a/src/components/periods/PeriodSelect.js b/src/components/periods/PeriodSelect.js index f020e3ce4..671387b20 100644 --- a/src/components/periods/PeriodSelect.js +++ b/src/components/periods/PeriodSelect.js @@ -7,7 +7,8 @@ import { } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React, { Component } from 'react' +import React, { useState, useMemo, useCallback, useEffect } from 'react' +import usePrevious from '../../hooks/usePrevious.js' import { getFixedPeriodsByType, filterFuturePeriods, @@ -16,124 +17,127 @@ import { getYear } from '../../util/time.js' import { SelectField } from '../core/index.js' import styles from './styles/PeriodSelect.module.css' -class PeriodSelect extends Component { - static propTypes = { - onChange: PropTypes.func.isRequired, - className: PropTypes.string, - errorText: PropTypes.string, - period: PropTypes.shape({ - id: PropTypes.string.isRequired, - startDate: PropTypes.string, - }), - periodType: PropTypes.string, - } - - state = { - year: null, - periods: null, - } - - componentDidMount() { - this.setPeriods() - } - - componentDidUpdate(prevProps, prevState) { - const { periodType, period, onChange } = this.props - const { year, periods } = this.state - - if (periodType !== prevProps.periodType) { - this.setPeriods() - } else if (periods && !period) { - onChange(filterFuturePeriods(periods)[0] || periods[0]) // Autoselect most recent period - } - - // Change period if year is changed (but keep period index) - if (period && prevState.periods && year !== prevState.year) { - const periodIndex = prevState.periods.findIndex( - (item) => item.id === period.id - ) - onChange(periods[periodIndex]) - } - } - - render() { - const { periodType, period, onChange, className, errorText } = - this.props - const { periods } = this.state - - if (!periods) { - return null +const PeriodSelect = ({ + onChange, + className, + errorText, + firstDate, + lastDate, + period, + periodType, +}) => { + const [year, setYear] = useState(getYear(period?.startDate || lastDate)) + const prevYear = usePrevious(year) + + // Set periods when periodType or year changes + /* eslint-disable react-hooks/exhaustive-deps */ + const periods = useMemo( + () => + periodType + ? getFixedPeriodsByType({ + periodType, + year, + firstDate, + lastDate, + }) + : [period], // saved map period (not included in depency array by design) + [periodType, year, firstDate, lastDate] + ) + /* eslint-enable react-hooks/exhaustive-deps */ + + const periodIndex = useMemo( + () => period && periods.findIndex((p) => p.id === period.id), + [period, periods] + ) + + const prevPeriodIndex = usePrevious(periodIndex) + + // Increment/decrement year + const changeYear = useCallback( + (change) => { + const newYear = year + change + + if ( + (!firstDate || newYear >= getYear(firstDate)) && + (!lastDate || newYear <= getYear(lastDate)) + ) { + setYear(newYear) + } + }, + [year, firstDate, lastDate] + ) + + // Autoselect most recent period + useEffect(() => { + if (!period && periods) { + onChange(filterFuturePeriods(periods)[0] || periods[0]) } + }, [period, periods, onChange]) - const value = - period && periods.some((p) => p.id === period.id) ? period.id : null - - return ( -
- - {periodType && ( -
- -
- )} -
- ) - } - - setPeriods() { - const { periodType, period } = this.props - const year = this.state.year || getYear(period && period.startDate) - let periods + // Keep the same period position when year changes + useEffect(() => { + if (year !== prevYear && prevPeriodIndex >= 0) { + const newPeriod = periods[prevPeriodIndex] - if (periodType) { - periods = getFixedPeriodsByType(periodType, year) - } else if (period) { - periods = [period] // If period is loaded in favorite + if (newPeriod) { + onChange(newPeriod) + } } + }, [year, prevYear, periods, prevPeriodIndex, onChange]) - this.setState({ periods, year }) + if (!periods) { + return null } - nextYear = () => { - this.changeYear(1) - } - - previousYear = () => { - this.changeYear(-1) - } - - changeYear = (change) => { - const { periodType } = this.props - const year = this.state.year + change + const value = + period && periods.some((p) => p.id === period.id) ? period.id : null + + return ( +
+ + {periodType && ( +
+ +
+ )} +
+ ) +} - this.setState({ - year, - periods: getFixedPeriodsByType(periodType, year), - }) - } +PeriodSelect.propTypes = { + onChange: PropTypes.func.isRequired, + className: PropTypes.string, + errorText: PropTypes.string, + firstDate: PropTypes.string, + lastDate: PropTypes.string, + period: PropTypes.shape({ + id: PropTypes.string.isRequired, + startDate: PropTypes.string, + }), + periodType: PropTypes.string, } export default PeriodSelect diff --git a/src/util/__tests__/periods.spec.js b/src/util/__tests__/periods.spec.js new file mode 100644 index 000000000..0069b2102 --- /dev/null +++ b/src/util/__tests__/periods.spec.js @@ -0,0 +1,325 @@ +import { getFixedPeriodsByType } from '../periods.js' + +describe('util/periods', () => { + test('getFixedPeriodsByType - RELATIVE', () => { + expect( + getFixedPeriodsByType({ periodType: 'RELATIVE', year: 2020 }) + ).toBe(null) + }) + + test('getFixedPeriodsByType - YEAR not provided', () => { + expect(getFixedPeriodsByType({ periodType: 'MONTHLY' })).toStrictEqual( + [] + ) + }) + + test('getFixedPeriodsByType - MONTHLY first and last date', () => { + expect( + getFixedPeriodsByType({ + periodType: 'MONTHLY', + year: 2020, + firstDate: '2020-04-01', + lastDate: '2020-08-30', + }) + ).toStrictEqual([ + { + endDate: '2020-07-31', + id: '202007', + iso: '202007', + name: 'July 2020', + startDate: '2020-07-01', + }, + { + endDate: '2020-06-30', + id: '202006', + iso: '202006', + name: 'June 2020', + startDate: '2020-06-01', + }, + { + endDate: '2020-05-31', + id: '202005', + iso: '202005', + name: 'May 2020', + startDate: '2020-05-01', + }, + { + endDate: '2020-04-30', + id: '202004', + iso: '202004', + name: 'April 2020', + startDate: '2020-04-01', + }, + ]) + }) + + test('getFixedPeriodsByType - YEARLY first and last date', () => { + expect( + getFixedPeriodsByType({ + periodType: 'YEARLY', + year: 2020, + firstDate: '2018-04-01', + lastDate: '2021-04-01', + }) + ).toStrictEqual([ + { + endDate: '2020-12-31', + id: '2020', + iso: '2020', + name: '2020', + startDate: '2020-01-01', + }, + { + endDate: '2019-12-31', + id: '2019', + iso: '2019', + name: '2019', + startDate: '2019-01-01', + }, + ]) + }) + + test('getFixedPeriodsByType - YEARLY first date', () => { + expect( + getFixedPeriodsByType({ + periodType: 'YEARLY', + year: 2020, + firstDate: '2020-04-01', + }) + ).toStrictEqual([]) + }) + + test('getFixedPeriodsByType - FYAPR 2020', () => { + expect( + getFixedPeriodsByType({ periodType: 'FYAPR', year: 2020 }) + ).toStrictEqual([ + { + endDate: '2021-03-31', + id: '2020April', + name: 'April 2020 - March 2021', + startDate: '2020-04-01', + }, + { + endDate: '2020-03-31', + id: '2019April', + name: 'April 2019 - March 2020', + startDate: '2019-04-01', + }, + { + endDate: '2019-03-31', + id: '2018April', + name: 'April 2018 - March 2019', + startDate: '2018-04-01', + }, + { + endDate: '2018-03-31', + id: '2017April', + name: 'April 2017 - March 2018', + startDate: '2017-04-01', + }, + { + endDate: '2017-03-31', + id: '2016April', + name: 'April 2016 - March 2017', + startDate: '2016-04-01', + }, + { + endDate: '2016-03-31', + id: '2015April', + name: 'April 2015 - March 2016', + startDate: '2015-04-01', + }, + { + endDate: '2015-03-31', + id: '2014April', + name: 'April 2014 - March 2015', + startDate: '2014-04-01', + }, + { + endDate: '2014-03-31', + id: '2013April', + name: 'April 2013 - March 2014', + startDate: '2013-04-01', + }, + { + endDate: '2013-03-31', + id: '2012April', + name: 'April 2012 - March 2013', + startDate: '2012-04-01', + }, + { + endDate: '2012-03-31', + id: '2011April', + name: 'April 2011 - March 2012', + startDate: '2011-04-01', + }, + ]) + }) + + test('getFixedPeriodsByType YEARLY 2020', () => { + expect( + getFixedPeriodsByType({ periodType: 'YEARLY', year: 2020 }) + ).toStrictEqual([ + { + endDate: '2020-12-31', + id: '2020', + iso: '2020', + name: '2020', + startDate: '2020-01-01', + }, + { + endDate: '2019-12-31', + id: '2019', + iso: '2019', + name: '2019', + startDate: '2019-01-01', + }, + { + endDate: '2018-12-31', + id: '2018', + iso: '2018', + name: '2018', + startDate: '2018-01-01', + }, + { + endDate: '2017-12-31', + id: '2017', + iso: '2017', + name: '2017', + startDate: '2017-01-01', + }, + { + endDate: '2016-12-31', + id: '2016', + iso: '2016', + name: '2016', + startDate: '2016-01-01', + }, + { + endDate: '2015-12-31', + id: '2015', + iso: '2015', + name: '2015', + startDate: '2015-01-01', + }, + { + endDate: '2014-12-31', + id: '2014', + iso: '2014', + name: '2014', + startDate: '2014-01-01', + }, + { + endDate: '2013-12-31', + id: '2013', + iso: '2013', + name: '2013', + startDate: '2013-01-01', + }, + { + endDate: '2012-12-31', + id: '2012', + iso: '2012', + name: '2012', + startDate: '2012-01-01', + }, + { + endDate: '2011-12-31', + id: '2011', + iso: '2011', + name: '2011', + startDate: '2011-01-01', + }, + ]) + }) + test('getFixedPeriodsByType - MONTHLY 2020', () => { + expect( + getFixedPeriodsByType({ periodType: 'MONTHLY', year: 2020 }) + ).toStrictEqual([ + { + endDate: '2020-12-31', + id: '202012', + iso: '202012', + name: 'December 2020', + startDate: '2020-12-01', + }, + { + endDate: '2020-11-30', + id: '202011', + iso: '202011', + name: 'November 2020', + startDate: '2020-11-01', + }, + { + endDate: '2020-10-31', + id: '202010', + iso: '202010', + name: 'October 2020', + startDate: '2020-10-01', + }, + { + endDate: '2020-09-30', + id: '202009', + iso: '202009', + name: 'September 2020', + startDate: '2020-09-01', + }, + { + endDate: '2020-08-31', + id: '202008', + iso: '202008', + name: 'August 2020', + startDate: '2020-08-01', + }, + { + endDate: '2020-07-31', + id: '202007', + iso: '202007', + name: 'July 2020', + startDate: '2020-07-01', + }, + { + endDate: '2020-06-30', + id: '202006', + iso: '202006', + name: 'June 2020', + startDate: '2020-06-01', + }, + { + endDate: '2020-05-31', + id: '202005', + iso: '202005', + name: 'May 2020', + startDate: '2020-05-01', + }, + { + endDate: '2020-04-30', + id: '202004', + iso: '202004', + name: 'April 2020', + startDate: '2020-04-01', + }, + { + endDate: '2020-03-31', + id: '202003', + iso: '202003', + name: 'March 2020', + startDate: '2020-03-01', + }, + { + endDate: '2020-02-29', + id: '202002', + iso: '202002', + name: 'February 2020', + startDate: '2020-02-01', + }, + { + endDate: '2020-01-31', + id: '202001', + iso: '202001', + name: 'January 2020', + startDate: '2020-01-01', + }, + ]) + }) +}) diff --git a/src/util/periods.js b/src/util/periods.js index 2fdd0f826..cfc89b3e8 100644 --- a/src/util/periods.js +++ b/src/util/periods.js @@ -6,25 +6,43 @@ import { periodTypes, periodGroups } from '../constants/periods.js' const getYearOffsetFromNow = (year) => year - new Date(Date.now()).getFullYear() +const filterPeriods = (periods, firstDate, lastDate) => + periods.filter( + (p) => + (!firstDate || p.startDate >= firstDate) && + (!lastDate || p.endDate <= lastDate) + ) + export const getPeriodTypes = (includeRelativePeriods, hiddenPeriods = []) => periodTypes(includeRelativePeriods).filter( ({ group }) => !hiddenPeriods.includes(group) ) -export const getFixedPeriodsByType = (periodType, year) => { +export const getFixedPeriodsByType = ({ + periodType, + year, + firstDate, + lastDate, +}) => { const period = getFixedPeriodsOptionsById(periodType) const forceDescendingForYearTypes = !!periodType.match(/^FY|YEARLY/) const offset = getYearOffsetFromNow(year) - const periods = period?.getPeriods({ offset, reversePeriods: true }) || null - if (periods && forceDescendingForYearTypes) { - // TODO: the reverse() is a workaround for a bug in the analytics - // getPeriods function that no longer correctly reverses the order - // for YEARLY and FY period types - return periods.reverse() + let periods = period?.getPeriods({ offset, reversePeriods: true }) + + if (!periods) { + return null } - return periods + + if (firstDate || lastDate) { + periods = filterPeriods(periods, firstDate, lastDate) + } + + // TODO: the reverse() is a workaround for a bug in the analytics + // getPeriods function that no longer correctly reverses the order + // for YEARLY and FY period types + return forceDescendingForYearTypes ? periods.reverse() : periods } export const getRelativePeriods = (hiddenPeriods = []) => @@ -48,7 +66,7 @@ export const getPeriodNames = () => ({ }, {}), }) -export const filterFuturePeriods = (periods) => { - const now = new Date(Date.now()) +export const filterFuturePeriods = (periods, date) => { + const now = new Date(date || Date.now()) return periods.filter(({ startDate }) => new Date(startDate) < now) }