From d8be02b72273b088775744a9c11896a019cd7e49 Mon Sep 17 00:00:00 2001 From: Hendrik de Graaf Date: Tue, 17 Aug 2021 13:49:41 +0200 Subject: [PATCH] feat(period-select): skip to previous year if no ended periods in current year (#50) * chore(fixed-periods): remove redundant functions, tests and deps * feat(period-select): set the initial year value based on period type * fix(period-select): allow workflow period type to be undefined * fix(period-select): avoid recomputing maxYear on every render * test(period-select): update failing year navigator test * test(fixed-periods): add unit test for getMostRecentCompletedYear * test(fixed-period): test period type with different start and end year * chore(period-select): remove redundant const currentYear --- package.json | 3 +- src/shared/fixed-periods/fixed-periods.js | 141 +++----- .../fixed-periods/fixed-periods.test.js | 327 ++---------------- src/top-bar/period-select/period-select.js | 18 +- src/top-bar/period-select/year-navigator.js | 8 +- .../period-select/year-navigator.test.js | 6 +- yarn.lock | 5 - 7 files changed, 89 insertions(+), 419 deletions(-) diff --git a/package.json b/package.json index 5e5bbd59..a11445b7 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ "@testing-library/user-event": "^13.1.9", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.6", - "identity-obj-proxy": "^3.0.0", - "mockdate": "^3.0.5" + "identity-obj-proxy": "^3.0.0" }, "dependencies": { "@dhis2/app-runtime": "^2.9.0-beta.6", diff --git a/src/shared/fixed-periods/fixed-periods.js b/src/shared/fixed-periods/fixed-periods.js index 8a895791..c2d40698 100644 --- a/src/shared/fixed-periods/fixed-periods.js +++ b/src/shared/fixed-periods/fixed-periods.js @@ -13,24 +13,24 @@ import i18n from '@dhis2/d2-i18n' */ // Period types -export const DAILY = 'Daily' -export const WEEKLY = 'Weekly' -export const WEEKLY_WEDNESDAY = 'WeeklyWednesday' -export const WEEKLY_THURSDAY = 'WeeklyThursday' -export const WEEKLY_SATURDAY = 'WeeklySaturday' -export const WEEKLY_SUNDAY = 'WeeklySunday' -export const BI_WEEKLY = 'BiWeekly' -export const MONTHLY = 'Monthly' -export const BI_MONTHLY = 'BiMonthly' -export const QUARTERLY = 'Quarterly' -export const SIX_MONTHLY = 'SixMonthly' -export const SIX_MONTHLY_APRIL = 'SixMonthlyApril' -// export const SIX_MONTHLY_NOV = 'SixMonthlyNov' -export const YEARLY = 'Yearly' -export const FINANCIAL_APRIL = 'FinancialApril' -export const FINANCIAL_JULY = 'FinancialJuly' -export const FINANCIAL_OCT = 'FinancialOct' -export const FINANCIAL_NOV = 'FinancialNov' +const DAILY = 'Daily' +const WEEKLY = 'Weekly' +const WEEKLY_WEDNESDAY = 'WeeklyWednesday' +const WEEKLY_THURSDAY = 'WeeklyThursday' +const WEEKLY_SATURDAY = 'WeeklySaturday' +const WEEKLY_SUNDAY = 'WeeklySunday' +const BI_WEEKLY = 'BiWeekly' +const MONTHLY = 'Monthly' +const BI_MONTHLY = 'BiMonthly' +const QUARTERLY = 'Quarterly' +const SIX_MONTHLY = 'SixMonthly' +const SIX_MONTHLY_APRIL = 'SixMonthlyApril' +// const SIX_MONTHLY_NOV = 'SixMonthlyNov' +const YEARLY = 'Yearly' +const FINANCIAL_APRIL = 'FinancialApril' +const FINANCIAL_JULY = 'FinancialJuly' +const FINANCIAL_OCT = 'FinancialOct' +const FINANCIAL_NOV = 'FinancialNov' const getMonthName = key => { const monthNames = [ @@ -777,87 +777,36 @@ export const getFixedPeriodsForTypeAndDateRange = ( return convertedPeriods.reverse() } -const FIXED_PERIODS_BY_LENGTH = [ - [DAILY], - [WEEKLY, WEEKLY_WEDNESDAY, WEEKLY_THURSDAY, WEEKLY_SATURDAY, WEEKLY_SUNDAY], - [BI_WEEKLY], - [MONTHLY], - [BI_MONTHLY], - [QUARTERLY], - [SIX_MONTHLY, SIX_MONTHLY_APRIL], - [YEARLY, FINANCIAL_APRIL, FINANCIAL_JULY, FINANCIAL_OCT, FINANCIAL_NOV], -] - -export const PERIOD_GREATER = 1 -export const PERIOD_EQUAL = 0 -export const PERIOD_SHORTER = -1 +export const getMostRecentCompletedYear = periodType => { + if (!periodType) { + throw new Error( + 'No "periodType" supplied to "getMostRecentCompletedYear"' + ) + } -/* - * IF left < right THEN return 1 - * IF left > right THEN return -1 - * IF left = right THEN return 0 - * - * Why right > left = 1 and not the other way? - * When partially applying this function, it makes sense this way: - * - * const compareWithWeekly = partial(compareFixedPeriodLength, 'WEEKLY') - * const isGreater = compareWithWeekly('YEARLY') === PERIOD_GREATER - * -> isGreater is true - * - * @param {string} right - * @param {string} left - * @returns {Int} - */ -export const compareFixedPeriodLength = (left, right) => { - const leftIndex = FIXED_PERIODS_BY_LENGTH.findIndex(types => - types.includes(left) - ) - const rightIndex = FIXED_PERIODS_BY_LENGTH.findIndex(types => - types.includes(right) + const endDate = new Date(Date.now()) + let year = endDate.getFullYear() + let periods = getFixedPeriodsForTypeAndDateRange( + periodType, + `${endDate.getFullYear()}-01-01`, + endDate ) - if (leftIndex === rightIndex) return PERIOD_EQUAL - return leftIndex > rightIndex ? PERIOD_SHORTER : PERIOD_GREATER -} - -export const getCurrentPeriodForType = type => { - const currentDate = new Date() - let year = currentDate.getFullYear() - - // cover this and the next years as that - // should cover all existing period types - for (let i = 0; i < 2; ++i) { - const periods = getFixedPeriodsByTypeAndYear(type, year).reverse() - - for (const period of periods) { - const periodStart = new Date(period.startDate) - const periodEnd = new Date(period.endDate) - const endsBeforePeriodEnd = currentDate <= periodEnd - const startsAfterPeriodStart = currentDate >= periodStart - - if (endsBeforePeriodEnd && startsAfterPeriodStart) { - return period - } + if (periods.length > 0) { + return year + } else { + /** + * Practically speaking we could also just return `year - 1` here + * but this logic would allow for periods to span over multiple years + */ + while (periods.length === 0) { + --year + periods = getFixedPeriodsForTypeAndDateRange( + periodType, + `${year}-01-01`, + `${year}-12-31` + ) } - - ++year + return year } - - return null -} - -export const isGreaterPeriodTypeEndDateWithinShorterPeriod = ( - greaterPeriodType, - shorterPeriod -) => { - const greaterPeriod = getCurrentPeriodForType(greaterPeriodType) - const greaterPeriodEndDate = new Date(greaterPeriod.endDate) - const shorterPeriodEndDate = new Date(shorterPeriod.endDate) - const shorterPeriodStartDate = new Date(shorterPeriod.startDate) - const greaterEndsAfterShorterStarts = - greaterPeriodEndDate >= shorterPeriodStartDate - const greaterEndsBeforeShorterEnds = - greaterPeriodEndDate <= shorterPeriodEndDate - - return greaterEndsAfterShorterStarts && greaterEndsBeforeShorterEnds } diff --git a/src/shared/fixed-periods/fixed-periods.test.js b/src/shared/fixed-periods/fixed-periods.test.js index a2fc3d98..dd48d112 100644 --- a/src/shared/fixed-periods/fixed-periods.test.js +++ b/src/shared/fixed-periods/fixed-periods.test.js @@ -1,33 +1,10 @@ -import MockDate from 'mockdate' import { - PERIOD_GREATER, - PERIOD_EQUAL, - PERIOD_SHORTER, - DAILY, - WEEKLY, - WEEKLY_WEDNESDAY, - WEEKLY_THURSDAY, - WEEKLY_SATURDAY, - WEEKLY_SUNDAY, - BI_WEEKLY, - MONTHLY, - BI_MONTHLY, - QUARTERLY, - SIX_MONTHLY, - SIX_MONTHLY_APRIL, - YEARLY, - FINANCIAL_APRIL, - FINANCIAL_JULY, - FINANCIAL_OCT, - FINANCIAL_NOV, - compareFixedPeriodLength, - getCurrentPeriodForType, getFixedPeriodType, getFixedPeriodTypes, getFixedPeriodsForTypeAndDateRange, getYearOffsetFromNow, - isGreaterPeriodTypeEndDateWithinShorterPeriod, parsePeriodId, + getMostRecentCompletedYear, } from './fixed-periods.js' describe('fixedPeriods utils', () => { @@ -937,293 +914,29 @@ describe('fixedPeriods utils', () => { }) }) - describe('compareFixedPeriodLength', () => { - const notEqual = [ - [DAILY, WEEKLY], - [DAILY, WEEKLY_WEDNESDAY], - [DAILY, WEEKLY_THURSDAY], - [DAILY, WEEKLY_SATURDAY], - [DAILY, WEEKLY_SUNDAY], - [DAILY, BI_WEEKLY], - [DAILY, MONTHLY], - [DAILY, BI_MONTHLY], - [DAILY, QUARTERLY], - [DAILY, SIX_MONTHLY], - [DAILY, SIX_MONTHLY_APRIL], - [DAILY, YEARLY], - [DAILY, FINANCIAL_APRIL], - [DAILY, FINANCIAL_JULY], - [DAILY, FINANCIAL_OCT], - [DAILY, FINANCIAL_NOV], - [WEEKLY, BI_WEEKLY], - [WEEKLY, MONTHLY], - [WEEKLY, BI_MONTHLY], - [WEEKLY, QUARTERLY], - [WEEKLY, SIX_MONTHLY], - [WEEKLY, SIX_MONTHLY_APRIL], - [WEEKLY, YEARLY], - [WEEKLY, FINANCIAL_APRIL], - [WEEKLY, FINANCIAL_JULY], - [WEEKLY, FINANCIAL_OCT], - [WEEKLY, FINANCIAL_NOV], - [WEEKLY_WEDNESDAY, BI_WEEKLY], - [WEEKLY_WEDNESDAY, MONTHLY], - [WEEKLY_WEDNESDAY, BI_MONTHLY], - [WEEKLY_WEDNESDAY, QUARTERLY], - [WEEKLY_WEDNESDAY, SIX_MONTHLY], - [WEEKLY_WEDNESDAY, SIX_MONTHLY_APRIL], - [WEEKLY_WEDNESDAY, YEARLY], - [WEEKLY_WEDNESDAY, FINANCIAL_APRIL], - [WEEKLY_WEDNESDAY, FINANCIAL_JULY], - [WEEKLY_WEDNESDAY, FINANCIAL_OCT], - [WEEKLY_WEDNESDAY, FINANCIAL_NOV], - [WEEKLY_THURSDAY, BI_WEEKLY], - [WEEKLY_THURSDAY, MONTHLY], - [WEEKLY_THURSDAY, BI_MONTHLY], - [WEEKLY_THURSDAY, QUARTERLY], - [WEEKLY_THURSDAY, SIX_MONTHLY], - [WEEKLY_THURSDAY, SIX_MONTHLY_APRIL], - [WEEKLY_THURSDAY, YEARLY], - [WEEKLY_THURSDAY, FINANCIAL_APRIL], - [WEEKLY_THURSDAY, FINANCIAL_JULY], - [WEEKLY_THURSDAY, FINANCIAL_OCT], - [WEEKLY_THURSDAY, FINANCIAL_NOV], - [WEEKLY_SATURDAY, BI_WEEKLY], - [WEEKLY_SATURDAY, MONTHLY], - [WEEKLY_SATURDAY, BI_MONTHLY], - [WEEKLY_SATURDAY, QUARTERLY], - [WEEKLY_SATURDAY, SIX_MONTHLY], - [WEEKLY_SATURDAY, SIX_MONTHLY_APRIL], - [WEEKLY_SATURDAY, YEARLY], - [WEEKLY_SATURDAY, FINANCIAL_APRIL], - [WEEKLY_SATURDAY, FINANCIAL_JULY], - [WEEKLY_SATURDAY, FINANCIAL_OCT], - [WEEKLY_SATURDAY, FINANCIAL_NOV], - [WEEKLY_SUNDAY, BI_WEEKLY], - [WEEKLY_SUNDAY, MONTHLY], - [WEEKLY_SUNDAY, BI_MONTHLY], - [WEEKLY_SUNDAY, QUARTERLY], - [WEEKLY_SUNDAY, SIX_MONTHLY], - [WEEKLY_SUNDAY, SIX_MONTHLY_APRIL], - [WEEKLY_SUNDAY, YEARLY], - [WEEKLY_SUNDAY, FINANCIAL_APRIL], - [WEEKLY_SUNDAY, FINANCIAL_JULY], - [WEEKLY_SUNDAY, FINANCIAL_OCT], - [WEEKLY_SUNDAY, FINANCIAL_NOV], - [BI_WEEKLY, MONTHLY], - [BI_WEEKLY, BI_MONTHLY], - [BI_WEEKLY, QUARTERLY], - [BI_WEEKLY, SIX_MONTHLY], - [BI_WEEKLY, SIX_MONTHLY_APRIL], - [BI_WEEKLY, YEARLY], - [BI_WEEKLY, FINANCIAL_APRIL], - [BI_WEEKLY, FINANCIAL_JULY], - [BI_WEEKLY, FINANCIAL_OCT], - [BI_WEEKLY, FINANCIAL_NOV], - [MONTHLY, BI_MONTHLY], - [MONTHLY, QUARTERLY], - [MONTHLY, SIX_MONTHLY], - [MONTHLY, SIX_MONTHLY_APRIL], - [MONTHLY, YEARLY], - [MONTHLY, FINANCIAL_APRIL], - [MONTHLY, FINANCIAL_JULY], - [MONTHLY, FINANCIAL_OCT], - [MONTHLY, FINANCIAL_NOV], - [BI_MONTHLY, QUARTERLY], - [BI_MONTHLY, SIX_MONTHLY], - [BI_MONTHLY, SIX_MONTHLY_APRIL], - [BI_MONTHLY, YEARLY], - [BI_MONTHLY, FINANCIAL_APRIL], - [BI_MONTHLY, FINANCIAL_JULY], - [BI_MONTHLY, FINANCIAL_OCT], - [BI_MONTHLY, FINANCIAL_NOV], - [SIX_MONTHLY, YEARLY], - [SIX_MONTHLY, FINANCIAL_APRIL], - [SIX_MONTHLY, FINANCIAL_JULY], - [SIX_MONTHLY, FINANCIAL_OCT], - [SIX_MONTHLY, FINANCIAL_NOV], - [SIX_MONTHLY_APRIL, YEARLY], - [SIX_MONTHLY_APRIL, FINANCIAL_APRIL], - [SIX_MONTHLY_APRIL, FINANCIAL_JULY], - [SIX_MONTHLY_APRIL, FINANCIAL_OCT], - [SIX_MONTHLY_APRIL, FINANCIAL_NOV], - ] - - it('should be shorter', () => { - notEqual.forEach(([shorter, greater]) => { - const result = compareFixedPeriodLength(greater, shorter) - expect(result).toBe(PERIOD_SHORTER) - }) - }) - - it('should be greater', () => { - notEqual.forEach(([shorter, greater]) => { - const result = compareFixedPeriodLength(shorter, greater) - expect(result).toBe(PERIOD_GREATER) - }) - }) - - const equal = [ - [DAILY, DAILY], - [WEEKLY, WEEKLY], - [WEEKLY, WEEKLY_WEDNESDAY], - [WEEKLY, WEEKLY_THURSDAY], - [WEEKLY, WEEKLY_SATURDAY], - [WEEKLY, WEEKLY_SUNDAY], - [WEEKLY_WEDNESDAY, WEEKLY_WEDNESDAY], - [WEEKLY_WEDNESDAY, WEEKLY_THURSDAY], - [WEEKLY_WEDNESDAY, WEEKLY_SATURDAY], - [WEEKLY_WEDNESDAY, WEEKLY_SUNDAY], - [WEEKLY_THURSDAY, WEEKLY_THURSDAY], - [WEEKLY_THURSDAY, WEEKLY_SATURDAY], - [WEEKLY_THURSDAY, WEEKLY_SUNDAY], - [WEEKLY_SATURDAY, WEEKLY_SATURDAY], - [WEEKLY_SATURDAY, WEEKLY_SUNDAY], - [WEEKLY_SUNDAY, WEEKLY_SUNDAY], - [BI_WEEKLY, BI_WEEKLY], - [MONTHLY, MONTHLY], - [BI_MONTHLY, BI_MONTHLY], - [QUARTERLY, QUARTERLY], - [SIX_MONTHLY, SIX_MONTHLY], - [SIX_MONTHLY, SIX_MONTHLY_APRIL], - [SIX_MONTHLY_APRIL, SIX_MONTHLY_APRIL], - [YEARLY, YEARLY], - [YEARLY, FINANCIAL_APRIL], - [YEARLY, FINANCIAL_JULY], - [YEARLY, FINANCIAL_OCT], - [YEARLY, FINANCIAL_NOV], - [FINANCIAL_APRIL, FINANCIAL_APRIL], - [FINANCIAL_APRIL, FINANCIAL_JULY], - [FINANCIAL_APRIL, FINANCIAL_OCT], - [FINANCIAL_APRIL, FINANCIAL_NOV], - [FINANCIAL_JULY, FINANCIAL_JULY], - [FINANCIAL_JULY, FINANCIAL_OCT], - [FINANCIAL_JULY, FINANCIAL_NOV], - [FINANCIAL_OCT, FINANCIAL_OCT], - [FINANCIAL_OCT, FINANCIAL_NOV], - [FINANCIAL_NOV, FINANCIAL_NOV], - ] - - it('should equal', () => { - equal.forEach(([left, right]) => { - const result = compareFixedPeriodLength(left, right) - expect(result).toBe(PERIOD_EQUAL) - }) - }) - }) - - describe('getCurrentPeriodForType', () => { - afterEach(() => { - MockDate.reset() - }) - - it('should return the current period that ends in the current year', () => { - MockDate.set(new Date('2021-10-01').getTime()) - - const periodType = SIX_MONTHLY - const actual = getCurrentPeriodForType(periodType) - const expected = { - startDate: '2021-07-01', - endDate: '2021-12-31', - displayName: 'July - December 2021', - iso: '2021S2', - id: '2021S2', - } - - expect(actual).toEqual(expected) - }) - - it('should return the current period that ends in the next year', () => { - MockDate.set(new Date('2021-10-01').getTime()) - - const periodType = SIX_MONTHLY_APRIL - const actual = getCurrentPeriodForType(periodType) - const expected = { - startDate: '2021-10-01', - endDate: '2022-03-31', - displayName: 'October 2021 - March 2022', - iso: '2021AprilS2', - id: '2021AprilS2', - } - - expect(actual).toEqual(expected) + describe('getMostRecentCompletedYear', () => { + it('throws an error if no periodType is specified', () => { + expect(() => getMostRecentCompletedYear()).toThrowError( + 'No "periodType" supplied to "getMostRecentCompletedYear"' + ) }) - - describe('yearly - edge case', () => { - it('should return the current period that ends in the current year', () => { - MockDate.set(new Date('2021-10-01').getTime()) - - const periodType = YEARLY - const actual = getCurrentPeriodForType(periodType) - const expected = { - endDate: '2021-12-31', - startDate: '2021-01-01', - displayName: '2021', - iso: '2021', - id: '2021', - } - - expect(actual).toEqual(expected) - }) - - it('should return the current period that ends in the next year', () => { - MockDate.set(new Date('2021-10-01').getTime()) - - const periodType = FINANCIAL_APRIL - const actual = getCurrentPeriodForType(periodType) - const expected = { - endDate: '2022-03-31', - startDate: '2021-04-01', - displayName: 'April 2021 - March 2022', - id: '2021April', - } - - expect(actual).toEqual(expected) - }) + it('returns the current year (2019) for a Monthly periodType', () => { + // Given the fact that today is 2019-06-17, + // there are completed Monthly periods in this year + expect(getMostRecentCompletedYear('Monthly')).toEqual(2019) }) - }) - - describe('isGreaterPeriodTypeEndDateWithinShorterPeriod', () => { - it('should return true when the short period spans over the greater periods end date', () => { - MockDate.set(new Date('2021-10-01').getTime()) - - const greaterPeriodType = YEARLY - const shorterPeriod = parsePeriodId('2021Q4') - const expected = true - const actual = isGreaterPeriodTypeEndDateWithinShorterPeriod( - greaterPeriodType, - shorterPeriod - ) - - expect(actual).toBe(expected) + it('returns the previous year (2018) for a Yearly periodType', () => { + // Given the fact that today is 2019-06-17, + // there are no completed Yearly periods in this year + expect(getMostRecentCompletedYear('Yearly')).toEqual(2018) }) - - it('should return false when the short period spans over the greater periods end date', () => { - MockDate.set(new Date('2021-10-01').getTime()) - - const greaterPeriodType = YEARLY - const shorterPeriod = parsePeriodId('2021Q3') - const expected = false - const actual = isGreaterPeriodTypeEndDateWithinShorterPeriod( - greaterPeriodType, - shorterPeriod - ) - - expect(actual).toBe(expected) + it('returns the previous year (2018) for a Monthly periodType if January has not completed', () => { + // 2019-01-10 + jest.spyOn(Date, 'now').mockImplementationOnce(() => 1547078400000) + expect(getMostRecentCompletedYear('Monthly')).toEqual(2018) }) - - it('should return true when a short period that ends in the following year spans over the greater periods end date', () => { - MockDate.set(new Date('2021-10-01').getTime()) - - const greaterPeriodType = YEARLY - const shorterPeriod = parsePeriodId('2021W52') - const expected = true - const actual = isGreaterPeriodTypeEndDateWithinShorterPeriod( - greaterPeriodType, - shorterPeriod - ) - - expect(actual).toBe(expected) + it('returns the previous year (2018) for a FinancialOct periodType', () => { + expect(getMostRecentCompletedYear('FinancialOct')).toEqual(2018) }) }) }) diff --git a/src/top-bar/period-select/period-select.js b/src/top-bar/period-select/period-select.js index 10280976..9e81ea4b 100644 --- a/src/top-bar/period-select/period-select.js +++ b/src/top-bar/period-select/period-select.js @@ -1,16 +1,23 @@ import i18n from '@dhis2/d2-i18n' import React, { useEffect, useState } from 'react' import { useSelectionContext } from '../../selection-context/index.js' +import { getMostRecentCompletedYear } from '../../shared/index.js' import { ContextSelect } from '../context-select/index.js' import { PeriodMenu } from './period-menu.js' -import { YearNavigator, currentYear } from './year-navigator.js' +import { YearNavigator } from './year-navigator.js' export const PERIOD = 'PERIOD' +const computeMaxYear = periodType => + periodType ? getMostRecentCompletedYear(periodType) : null + const PeriodSelect = () => { const { period, workflow, selectPeriod, openedSelect, setOpenedSelect } = useSelectionContext() - const [year, setYear] = useState(period?.year || currentYear) + const [maxYear, setMaxYear] = useState(() => + computeMaxYear(workflow?.periodType) + ) + const [year, setYear] = useState(period?.year || maxYear) const open = openedSelect === PERIOD const value = period?.displayName @@ -20,6 +27,12 @@ const PeriodSelect = () => { } }, [period?.year]) + useEffect(() => { + if (workflow?.periodType) { + setMaxYear(computeMaxYear(workflow?.periodType)) + } + }, [workflow?.periodType]) + return ( { requiredValuesMessage={i18n.t('Choose a workflow first')} > { selectPeriod(null) diff --git a/src/top-bar/period-select/year-navigator.js b/src/top-bar/period-select/year-navigator.js index 9bf0a18c..3c98e05a 100644 --- a/src/top-bar/period-select/year-navigator.js +++ b/src/top-bar/period-select/year-navigator.js @@ -3,11 +3,10 @@ import PropTypes from 'prop-types' import React from 'react' import classes from './year-navigator.module.css' -export const currentYear = new Date().getFullYear() // To avoid users from navigating too far back const startYear = 1970 -const YearNavigator = ({ year, onYearChange }) => ( +const YearNavigator = ({ maxYear, year, onYearChange }) => (