diff --git a/.gitignore b/.gitignore index 3d4526f73..1b41c0a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ bundle.stats.json # DHIS2 Platform .d2 src/locales +CLAUDE.md diff --git a/i18n/en.pot b/i18n/en.pot index 8616da74e..833198f8b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2025-09-09T09:32:04.677Z\n" -"PO-Revision-Date: 2025-09-09T09:32:04.678Z\n" +"POT-Creation-Date: 2026-01-29T11:57:17.738Z\n" +"PO-Revision-Date: 2026-01-29T11:57:17.738Z\n" msgid "view only" msgstr "view only" @@ -822,6 +822,16 @@ msgstr "Period" msgid "Selected Periods" msgstr "Selected Periods" +msgid "No period types available" +msgstr "No period types available" + +msgid "" +"No period types are enabled in the system. Please contact your system " +"administrator." +msgstr "" +"No period types are enabled in the system. Please contact your system " +"administrator." + msgid "Relative periods" msgstr "Relative periods" @@ -870,18 +880,42 @@ msgstr "Six-monthly April" msgid "Yearly" msgstr "Yearly" -msgid "Financial year (Start November)" -msgstr "Financial year (Start November)" +msgid "Financial year (Start January)" +msgstr "Financial year (Start January)" -msgid "Financial year (Start October)" -msgstr "Financial year (Start October)" +msgid "Financial year (Start February)" +msgstr "Financial year (Start February)" -msgid "Financial year (Start July)" -msgstr "Financial year (Start July)" +msgid "Financial year (Start March)" +msgstr "Financial year (Start March)" msgid "Financial year (Start April)" msgstr "Financial year (Start April)" +msgid "Financial year (Start May)" +msgstr "Financial year (Start May)" + +msgid "Financial year (Start June)" +msgstr "Financial year (Start June)" + +msgid "Financial year (Start July)" +msgstr "Financial year (Start July)" + +msgid "Financial year (Start August)" +msgstr "Financial year (Start August)" + +msgid "Financial year (Start September)" +msgstr "Financial year (Start September)" + +msgid "Financial year (Start October)" +msgstr "Financial year (Start October)" + +msgid "Financial year (Start November)" +msgstr "Financial year (Start November)" + +msgid "Financial year (Start December)" +msgstr "Financial year (Start December)" + msgid "Today" msgstr "Today" diff --git a/src/components/PeriodDimension/FixedPeriodFilter.js b/src/components/PeriodDimension/FixedPeriodFilter.js index 52376291a..362ed801f 100644 --- a/src/components/PeriodDimension/FixedPeriodFilter.js +++ b/src/components/PeriodDimension/FixedPeriodFilter.js @@ -3,14 +3,9 @@ import PropTypes from 'prop-types' import React from 'react' import i18n from '../../locales/index.js' import styles from './styles/PeriodFilter.style.js' -import { getFixedPeriodsOptions } from './utils/fixedPeriods.js' -import { filterPeriodTypesById } from './utils/index.js' - -const EXCLUDED_PERIOD_TYPES_PROP_DEFAULT = [] const FixedPeriodFilter = ({ - allowedPeriodTypes, - excludedPeriodTypes = EXCLUDED_PERIOD_TYPES_PROP_DEFAULT, + availableOptions, currentPeriodType, currentYear, onSelectPeriodType, @@ -18,9 +13,8 @@ const FixedPeriodFilter = ({ dataTest, }) => { const onlyAllowedTypeIsSelected = - Array.isArray(allowedPeriodTypes) && - allowedPeriodTypes.length === 1 && - allowedPeriodTypes[0] === currentPeriodType + availableOptions.length === 1 && + availableOptions[0].id === currentPeriodType return ( <> @@ -34,17 +28,7 @@ const FixedPeriodFilter = ({ className="filterElement" dataTest={`${dataTest}-period-type`} > - {(allowedPeriodTypes - ? getFixedPeriodsOptions().filter((option) => - allowedPeriodTypes.some( - (type) => type === option.id - ) - ) - : filterPeriodTypesById( - getFixedPeriodsOptions(), - excludedPeriodTypes - ) - ).map((option) => ( + {availableOptions.map((option) => ( + this.props.allowedPeriodTypes.includes(option.id) + ) + : allOptions + return (
{ - const { systemInfo } = useConfig() - const result = useDataQuery(userSettingsQuery) + const config = useConfig() + const { systemInfo } = config + const userSettingsResult = useDataQuery(userSettingsQuery) + + const { supportsEnabledPeriodTypes, enabledPeriodTypesData } = + useDataOutputPeriodTypes() const { calendar = 'gregory' } = systemInfo - const { data: { userSettings: { keyUiLocale: locale } = {} } = {} } = result + const { data: { userSettings: { keyUiLocale: locale } = {} } = {} } = + userSettingsResult const periodsSettings = { calendar, locale } @@ -37,16 +43,24 @@ const PeriodDimension = ({ items: periods, }) } + + const selectedPeriodsWithCustomDisplayNames = applyPeriodNameOverrides( + selectedPeriods, + enabledPeriodTypesData?.metaData + ) + return ( ) } diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 58d89a310..b6ccadee1 100644 --- a/src/components/PeriodDimension/PeriodTransfer.js +++ b/src/components/PeriodDimension/PeriodTransfer.js @@ -1,7 +1,7 @@ import { getNowInCalendar } from '@dhis2/multi-calendar-dates' -import { IconInfo16, TabBar, Tab, Transfer } from '@dhis2/ui' +import { IconInfo16, NoticeBox, TabBar, Tab, Transfer } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useRef, useState, useMemo } from 'react' import PeriodIcon from '../../assets/DimensionItemIcons/PeriodIcon.js' //TODO: Reimplement the icon.js import i18n from '../../locales/index.js' import { @@ -13,9 +13,15 @@ import styles from '../styles/DimensionSelector.style.js' import { TransferOption } from '../TransferOption.js' import FixedPeriodFilter from './FixedPeriodFilter.js' import RelativePeriodFilter from './RelativePeriodFilter.js' -import { getFixedPeriodsOptionsById } from './utils/fixedPeriods.js' -import { MONTHLY, QUARTERLY } from './utils/index.js' -import { getRelativePeriodsOptionsById } from './utils/relativePeriods.js' +import { + applyDisplayLabelOverrides, + applyFixedPeriodTypeDisplayLabels, + filterEnabledFixedPeriodTypes, + filterEnabledRelativePeriodTypes, +} from './utils/enabledPeriodTypes.js' +import { getFixedPeriodsOptions } from './utils/fixedPeriods.js' +import { MONTHLY, QUARTERLY, filterPeriodTypesById } from './utils/index.js' +import { getRelativePeriodsOptions } from './utils/relativePeriods.js' const RightHeader = ({ infoBoxMessage }) => ( <> @@ -48,13 +54,79 @@ const PeriodTransfer = ({ periodsSettings = PERIODS_SETTINGS_PROP_DEFAULT, infoBoxMessage, height = TRANSFER_HEIGHT, + enabledPeriodTypesData = null, + supportsEnabledPeriodTypes = false, }) => { - const defaultRelativePeriodType = excludedPeriodTypes.includes(MONTHLY) - ? getRelativePeriodsOptionsById(QUARTERLY) - : getRelativePeriodsOptionsById(MONTHLY) - const defaultFixedPeriodType = excludedPeriodTypes.includes(MONTHLY) - ? getFixedPeriodsOptionsById(QUARTERLY, periodsSettings) - : getFixedPeriodsOptionsById(MONTHLY, periodsSettings) + const { filteredFixedOptions, filteredRelativeOptions } = useMemo(() => { + if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { + const { enabledTypes, financialYearStart, financialYearDisplayLabel, weeklyDisplayLabel, metaData } = enabledPeriodTypesData + + const filteredFixed = applyFixedPeriodTypeDisplayLabels( + filterEnabledFixedPeriodTypes( + getFixedPeriodsOptions(periodsSettings), + enabledTypes + ), + enabledTypes + ) + + const filteredRelative = applyDisplayLabelOverrides( + filterEnabledRelativePeriodTypes( + getRelativePeriodsOptions(), + enabledTypes, + financialYearStart + ), + { financialYearDisplayLabel, weeklyDisplayLabel, metaData } + ) + + return { + filteredFixedOptions: filteredFixed, + filteredRelativeOptions: filteredRelative, + } + } else { + const allFixed = getFixedPeriodsOptions(periodsSettings) + const allRelative = getRelativePeriodsOptions() + + return { + filteredFixedOptions: filterPeriodTypesById( + allFixed, + excludedPeriodTypes + ), + filteredRelativeOptions: filterPeriodTypesById( + allRelative, + excludedPeriodTypes + ), + } + } + }, [ + supportsEnabledPeriodTypes, + enabledPeriodTypesData, + excludedPeriodTypes, + periodsSettings, + ]) + + const analysisRelativePeriod = + enabledPeriodTypesData?.analysisRelativePeriod + + const defaultRelativePeriodType = (() => { + if (analysisRelativePeriod) { + const match = filteredRelativeOptions.find((opt) => + opt.getPeriods().some((p) => p.id === analysisRelativePeriod) + ) + if (match) { + return match + } + } + return ( + filteredRelativeOptions.find((opt) => opt.id === MONTHLY) || + filteredRelativeOptions.find((opt) => opt.id === QUARTERLY) || + filteredRelativeOptions[0] + ) + })() + + const defaultFixedPeriodType = + filteredFixedOptions.find((opt) => opt.id === MONTHLY) || + filteredFixedOptions.find((opt) => opt.id === QUARTERLY) || + filteredFixedOptions[0] const now = getNowInCalendar(periodsSettings.calendar) // use ".eraYear" rather than ".year" because in Ethiopian calendar, eraYear is what our users expect to see (for other calendars, it doesn't matter) @@ -67,18 +139,80 @@ const PeriodTransfer = ({ reversePeriods: false, }) - const [allPeriods, setAllPeriods] = useState( - defaultRelativePeriodType.getPeriods() - ) + const [userPeriods, setUserPeriods] = useState(null) const [isRelative, setIsRelative] = useState(true) const [relativeFilter, setRelativeFilter] = useState({ - periodType: defaultRelativePeriodType.id, + periodType: defaultRelativePeriodType?.id || '', }) const [fixedFilter, setFixedFilter] = useState({ - periodType: defaultFixedPeriodType.id, + periodType: defaultFixedPeriodType?.id || '', year: defaultFixedPeriodYear.toString(), }) + const effectiveRelativeFilterType = filteredRelativeOptions.some( + (opt) => opt.id === relativeFilter.periodType + ) + ? relativeFilter.periodType + : defaultRelativePeriodType?.id || '' + + const effectiveFixedFilterType = filteredFixedOptions.some( + (opt) => opt.id === fixedFilter.periodType + ) + ? fixedFilter.periodType + : defaultFixedPeriodType?.id || '' + + const prevEffectiveRelativeRef = useRef(effectiveRelativeFilterType) + const prevEffectiveFixedRef = useRef(effectiveFixedFilterType) + + if (prevEffectiveRelativeRef.current !== effectiveRelativeFilterType) { + prevEffectiveRelativeRef.current = effectiveRelativeFilterType + if (relativeFilter.periodType !== effectiveRelativeFilterType) { + setRelativeFilter({ periodType: effectiveRelativeFilterType }) + } + if (isRelative) { + setUserPeriods(null) + } + } + + if (prevEffectiveFixedRef.current !== effectiveFixedFilterType) { + prevEffectiveFixedRef.current = effectiveFixedFilterType + if (fixedFilter.periodType !== effectiveFixedFilterType) { + setFixedFilter((prev) => ({ + ...prev, + periodType: effectiveFixedFilterType, + })) + } + if (!isRelative) { + setUserPeriods(null) + } + } + + const derivedPeriods = useMemo(() => { + if (isRelative) { + const opt = filteredRelativeOptions.find( + (o) => o.id === effectiveRelativeFilterType + ) + return opt?.getPeriods() || [] + } else { + const opt = filteredFixedOptions.find( + (o) => o.id === effectiveFixedFilterType + ) + return ( + opt?.getPeriods(fixedPeriodConfig(Number(fixedFilter.year))) || + [] + ) + } + }, [ + isRelative, + effectiveRelativeFilterType, + effectiveFixedFilterType, + filteredRelativeOptions, + filteredFixedOptions, + fixedFilter.year, + ]) + + const allPeriods = userPeriods !== null ? userPeriods : derivedPeriods + const isActive = (value) => { const item = selectedItems.find((item) => item.id === value) return !item || item.isActive @@ -87,19 +221,20 @@ const PeriodTransfer = ({ const onIsRelativeClick = (state) => { if (state !== isRelative) { setIsRelative(state) - setAllPeriods( - state - ? getRelativePeriodsOptionsById( - relativeFilter.periodType - ).getPeriods() - : getFixedPeriodsOptionsById( - fixedFilter.periodType, - periodsSettings - ).getPeriods(fixedPeriodConfig(Number(fixedFilter.year))) - ) + setUserPeriods(null) } } + if (enabledPeriodTypesData?.noEnabledTypes) { + return ( + + {i18n.t( + 'No period types are enabled in the system. Please contact your system administrator.' + )} + + ) + } + const renderLeftHeader = () => ( <> @@ -121,21 +256,20 @@ const PeriodTransfer = ({
{isRelative ? ( { setRelativeFilter({ periodType: filter }) - setAllPeriods( - getRelativePeriodsOptionsById( - filter - ).getPeriods() + const selectedOption = filteredRelativeOptions.find( + (opt) => opt.id === filter ) + setUserPeriods(selectedOption?.getPeriods() || []) }} dataTest={`${dataTest}-relative-period-filter`} - excludedPeriodTypes={excludedPeriodTypes} + availableOptions={filteredRelativeOptions} /> ) : ( { onSelectFixedPeriods({ @@ -150,7 +284,7 @@ const PeriodTransfer = ({ }) }} dataTest={`${dataTest}-fixed-period-filter`} - excludedPeriodTypes={excludedPeriodTypes} + availableOptions={filteredFixedOptions} /> )}
@@ -162,14 +296,13 @@ const PeriodTransfer = ({ setFixedFilter(filter) if (filter.year.match(/[0-9]{4}/)) { - setAllPeriods( - getFixedPeriodsOptionsById( - filter.periodType, - periodsSettings - ).getPeriods( - fixedPeriodConfig(Number(filter.year)), - periodsSettings - ) + const selectedOption = filteredFixedOptions.find( + (opt) => opt.id === filter.periodType + ) + setUserPeriods( + selectedOption?.getPeriods( + fixedPeriodConfig(Number(filter.year)) + ) || [] ) } } @@ -227,6 +360,13 @@ const PeriodTransfer = ({ PeriodTransfer.propTypes = { onSelect: PropTypes.func.isRequired, dataTest: PropTypes.string, + enabledPeriodTypesData: PropTypes.shape({ + analysisRelativePeriod: PropTypes.string, + enabledTypes: PropTypes.array, + financialYearDisplayLabel: PropTypes.string, + financialYearStart: PropTypes.string, + noEnabledTypes: PropTypes.bool, + }), excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string), height: PropTypes.string, infoBoxMessage: PropTypes.string, @@ -242,6 +382,7 @@ PeriodTransfer.propTypes = { name: PropTypes.string, }) ), + supportsEnabledPeriodTypes: PropTypes.bool, } export default PeriodTransfer diff --git a/src/components/PeriodDimension/RelativePeriodFilter.js b/src/components/PeriodDimension/RelativePeriodFilter.js index 60fc435a5..1860d4345 100644 --- a/src/components/PeriodDimension/RelativePeriodFilter.js +++ b/src/components/PeriodDimension/RelativePeriodFilter.js @@ -3,14 +3,12 @@ import PropTypes from 'prop-types' import React from 'react' import i18n from '../../locales/index.js' import styles from './styles/PeriodFilter.style.js' -import { filterPeriodTypesById } from './utils/index.js' -import { getRelativePeriodsOptions } from './utils/relativePeriods.js' const RelativePeriodFilter = ({ currentFilter, onSelectFilter, dataTest, - excludedPeriodTypes, + availableOptions, }) => (
- {filterPeriodTypesById( - getRelativePeriodsOptions(), - excludedPeriodTypes - ).map((option) => ( + {availableOptions.map((option) => ( ({ - useConfig: () => ({ systemInfo: {} }), - useDataQuery: () => ({ data: { userSettings: { keyUiLocale: 'en' } } }), + useConfig: () => ({ + systemInfo: {}, + serverVersion: { minor: 42 }, + }), + useDataQuery: jest.fn().mockImplementation((_query, options) => { + if (options?.lazy) { + return { + data: null, + error: undefined, + loading: false, + refetch: jest.fn(), + } + } + return { data: { userSettings: { keyUiLocale: 'en' } } } + }), })) global.ResizeObserver = jest.fn().mockImplementation(() => ({ diff --git a/src/components/PeriodDimension/__tests__/fixedPeriods.spec.js b/src/components/PeriodDimension/__tests__/fixedPeriods.spec.js index b7816ef9c..91b20cf06 100644 --- a/src/components/PeriodDimension/__tests__/fixedPeriods.spec.js +++ b/src/components/PeriodDimension/__tests__/fixedPeriods.spec.js @@ -31,10 +31,18 @@ describe('fixedPeriods utils', () => { 'SIXMONTHLY', 'SIXMONTHLYAPR', 'YEARLY', - 'FYNOV', - 'FYOCT', - 'FYJUL', + 'FYJAN', + 'FYFEB', + 'FYMAR', 'FYAPR', + 'FYMAY', + 'FYJUN', + 'FYJUL', + 'FYAUG', + 'FYSEP', + 'FYOCT', + 'FYNOV', + 'FYDEC', ]) }) }) diff --git a/src/components/PeriodDimension/__tests__/utils.spec.js b/src/components/PeriodDimension/__tests__/utils.spec.js index 6021309ef..38c6a95bb 100644 --- a/src/components/PeriodDimension/__tests__/utils.spec.js +++ b/src/components/PeriodDimension/__tests__/utils.spec.js @@ -27,9 +27,17 @@ describe('utils', () => { 'SIXMONTHLY', 'SIXMONTHLYAPR', 'YEARLY', - 'FYNOV', - 'FYJUL', + 'FYJAN', + 'FYFEB', + 'FYMAR', 'FYAPR', + 'FYMAY', + 'FYJUN', + 'FYJUL', + 'FYAUG', + 'FYSEP', + 'FYNOV', + 'FYDEC', ]) }) diff --git a/src/components/PeriodDimension/useDataOutputPeriodTypes.js b/src/components/PeriodDimension/useDataOutputPeriodTypes.js new file mode 100644 index 000000000..57e645c64 --- /dev/null +++ b/src/components/PeriodDimension/useDataOutputPeriodTypes.js @@ -0,0 +1,165 @@ +import { useConfig, useDataQuery } from '@dhis2/app-runtime' +import { useEffect, useMemo } from 'react' + +const v43Query = { + enabledPeriodTypes: { + resource: 'configuration/dataOutputPeriodTypes', + }, + // v43-only: analyticsFinancialYearStart is removed in v44 + financialYearStart: { + resource: 'systemSettings/analyticsFinancialYearStart', + }, + analysisRelativePeriod: { + resource: 'systemSettings/keyAnalysisRelativePeriod', + }, + // v43-only: analyticsWeeklyStart is removed in v44 + weeklyStart: { + resource: 'systemSettings/analyticsWeeklyStart', + }, +} + +// v43-only: analyticsFinancialYearStart is removed in v44 +const FY_SETTING_TO_SERVER_PT = { + FINANCIAL_YEAR_APRIL: 'FinancialApril', + FINANCIAL_YEAR_JULY: 'FinancialJuly', + FINANCIAL_YEAR_SEPTEMBER: 'FinancialSep', + FINANCIAL_YEAR_OCTOBER: 'FinancialOct', + FINANCIAL_YEAR_NOVEMBER: 'FinancialNov', +} + +// v43-only: analyticsWeeklyStart is removed in v44 +const WEEKLY_START_TO_SERVER_PT = { + WEEKLY: 'Weekly', + WEEKLY_WEDNESDAY: 'WeeklyWednesday', + WEEKLY_THURSDAY: 'WeeklyThursday', + WEEKLY_FRIDAY: 'WeeklyFriday', + WEEKLY_SATURDAY: 'WeeklySaturday', + WEEKLY_SUNDAY: 'WeeklySunday', +} + +const useDataOutputPeriodTypes = () => { + const { serverVersion } = useConfig() + const supportsEnabledPeriodTypes = serverVersion.minor >= 43 + + const { + data: v43Data, + error: v43Error, + refetch: v43Refetch, + } = useDataQuery(v43Query, { lazy: true }) + + useEffect(() => { + if (supportsEnabledPeriodTypes) { + v43Refetch() + } + }, [supportsEnabledPeriodTypes, v43Refetch]) + + const enabledPeriodTypesData = useMemo(() => { + if (!supportsEnabledPeriodTypes) { + return null + } + + if (v43Error || !v43Data?.enabledPeriodTypes) { + return null + } + + const enabledTypes = v43Data.enabledPeriodTypes + + if (!enabledTypes || enabledTypes.length === 0) { + return { + enabledTypes: [], + financialYearStart: null, + analysisRelativePeriod: null, + noEnabledTypes: true, + } + } + + // v43-only: financial year logic goes away in v44 + let financialYearStart = null + let financialYearDisplayLabel = null + if (v43Data.financialYearStart?.analyticsFinancialYearStart) { + const fyStartValue = + v43Data.financialYearStart.analyticsFinancialYearStart + + const mappedFyPt = FY_SETTING_TO_SERVER_PT[fyStartValue] + const matchingPt = enabledTypes.find( + (pt) => pt.name === mappedFyPt + ) + if (matchingPt) { + financialYearStart = fyStartValue + if (matchingPt.displayLabel) { + financialYearDisplayLabel = matchingPt.displayLabel + } + } + } + + // v43-only: weekly start logic goes away in v44 + let weeklyDisplayLabel = null + if (v43Data.weeklyStart?.analyticsWeeklyStart) { + const weeklyStartValue = + v43Data.weeklyStart.analyticsWeeklyStart + + const mappedWeeklyPt = + WEEKLY_START_TO_SERVER_PT[weeklyStartValue] + const matchingWeeklyPt = enabledTypes.find( + (pt) => pt.name === mappedWeeklyPt + ) + if (matchingWeeklyPt?.displayLabel) { + weeklyDisplayLabel = matchingWeeklyPt.displayLabel + } + } + + const analysisRelativePeriod = + v43Data.analysisRelativePeriod?.keyAnalysisRelativePeriod || null + + const metaData = { + ...(financialYearDisplayLabel && { + THIS_FINANCIAL_YEAR: { + name: `This ${financialYearDisplayLabel}`, + }, + LAST_FINANCIAL_YEAR: { + name: `Last ${financialYearDisplayLabel}`, + }, + LAST_5_FINANCIAL_YEARS: { + name: `Last 5 ${financialYearDisplayLabel}`, + }, + }), + ...(weeklyDisplayLabel && { + THIS_WEEK: { + name: `This ${weeklyDisplayLabel}`, + }, + LAST_WEEK: { + name: `Last ${weeklyDisplayLabel}`, + }, + LAST_4_WEEKS: { + name: `Last 4 ${weeklyDisplayLabel}`, + }, + LAST_12_WEEKS: { + name: `Last 12 ${weeklyDisplayLabel}`, + }, + LAST_52_WEEKS: { + name: `Last 52 ${weeklyDisplayLabel}`, + }, + WEEKS_THIS_YEAR: { + name: `${weeklyDisplayLabel} this year`, + }, + }), + } + + return { + enabledTypes, + financialYearStart, + financialYearDisplayLabel, + weeklyDisplayLabel, + analysisRelativePeriod, + metaData, + noEnabledTypes: false, + } + }, [supportsEnabledPeriodTypes, v43Data, v43Error]) + + return { + supportsEnabledPeriodTypes, + enabledPeriodTypesData, + } +} + +export { useDataOutputPeriodTypes } diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js new file mode 100644 index 000000000..c0b758ab1 --- /dev/null +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -0,0 +1,206 @@ +// Mapping from server period type names to multi-calendar-dates constants +export const SERVER_PT_TO_MULTI_CALENDAR_PT = { + Daily: 'DAILY', + Weekly: 'WEEKLY', + WeeklyWednesday: 'WEEKLYWED', + WeeklyThursday: 'WEEKLYTHU', + WeeklyFriday: 'WEEKLYFRI', + WeeklySaturday: 'WEEKLYSAT', + WeeklySunday: 'WEEKLYSUN', + BiWeekly: 'BIWEEKLY', + Monthly: 'MONTHLY', + BiMonthly: 'BIMONTHLY', + Quarterly: 'QUARTERLY', + QuarterlyNov: 'QUARTERLYNOV', + SixMonthly: 'SIXMONTHLY', + SixMonthlyApril: 'SIXMONTHLYAPR', + SixMonthlyNov: 'SIXMONTHLYNOV', + Yearly: 'YEARLY', + FinancialFeb: 'FYFEB', + FinancialApril: 'FYAPR', + FinancialJuly: 'FYJUL', + FinancialAug: 'FYAUG', + FinancialSep: 'FYSEP', + FinancialOct: 'FYOCT', + FinancialNov: 'FYNOV', +} + +// Mapping from relative period categories to their corresponding fixed period types +export const RP_CATEGORY_TO_FP_DEPENDENCIES = { + DAILY: ['Daily'], + WEEKLY: [ + 'Weekly', + 'WeeklyWednesday', + 'WeeklyThursday', + 'WeeklyFriday', + 'WeeklySaturday', + 'WeeklySunday', + ], + BIWEEKLY: ['BiWeekly'], + MONTHLY: ['Monthly'], + BIMONTHLY: ['BiMonthly'], + QUARTERLY: [ + 'Quarterly', + 'QuarterlyNov', + ], + SIXMONTHLY: [ + 'SixMonthly', + 'SixMonthlyApril', + 'SixMonthlyNov', + ], + YEARLY: ['Yearly'], +} + +/** + * Filter fixed period types based on enabled server period types + * @param {Array} allFixedPeriodOptions - All available fixed period options + * @param {Array} enabledServerPeriodTypes - Enabled period types from server + * @returns {Array} Filtered fixed period options + */ +export const filterEnabledFixedPeriodTypes = ( + allFixedPeriodOptions, + enabledServerPeriodTypes +) => { + if (!enabledServerPeriodTypes || enabledServerPeriodTypes.length === 0) { + return [] + } + + const enabledServerPtNames = new Set( + enabledServerPeriodTypes.map((pt) => pt.name) + ) + const enabledMultiCalendarPts = new Set() + + // Map server PT names to multi-calendar-dates constants + enabledServerPtNames.forEach((serverPtName) => { + const multiCalendarPt = SERVER_PT_TO_MULTI_CALENDAR_PT[serverPtName] + if (multiCalendarPt) { + enabledMultiCalendarPts.add(multiCalendarPt) + } + }) + + // Filter fixed period options to only include enabled ones + return allFixedPeriodOptions.filter((option) => + enabledMultiCalendarPts.has(option.id) + ) +} + +/** + * Apply displayLabel overrides to fixed period type names + * v43-only: in v44 the API provides these names directly + */ +export const applyFixedPeriodTypeDisplayLabels = ( + filteredFixedOptions, + enabledServerPeriodTypes +) => { + if (!enabledServerPeriodTypes) { + return filteredFixedOptions + } + + const displayLabelMap = new Map() + enabledServerPeriodTypes.forEach((pt) => { + if (pt.displayLabel) { + const multiCalendarPt = SERVER_PT_TO_MULTI_CALENDAR_PT[pt.name] + if (multiCalendarPt) { + displayLabelMap.set(multiCalendarPt, pt.displayLabel) + } + } + }) + + if (displayLabelMap.size === 0) { + return filteredFixedOptions + } + + return filteredFixedOptions.map((option) => { + const displayLabel = displayLabelMap.get(option.id) + return displayLabel ? { ...option, name: displayLabel } : option + }) +} + +/** + * Filter relative period categories based on enabled server period types + * @param {Array} allRelativePeriodOptions - All available relative period options + * @param {Array} enabledServerPeriodTypes - Enabled period types from server + * @param {string|null} financialYearStart - Financial year start setting (if enabled) + * @returns {Array} Filtered relative period options + */ +/** + * Apply metaData name overrides to a list of periods + * v43-only: in v44 the API provides these names directly + */ +export const applyPeriodNameOverrides = (periods, metaData) => { + if (!metaData) { + return periods + } + return periods.map((period) => + metaData[period.id] + ? { ...period, name: metaData[period.id].name } + : period + ) +} + +/** + * Apply display label overrides to relative period options + * v43-only: in v44 the API provides these names directly + */ +export const applyDisplayLabelOverrides = ( + filteredRelativeOptions, + { financialYearDisplayLabel, weeklyDisplayLabel, metaData } +) => { + const overrides = {} + + if (financialYearDisplayLabel) { + overrides['FINANCIAL'] = { name: financialYearDisplayLabel } + } + if (weeklyDisplayLabel) { + overrides['WEEKLY'] = { name: weeklyDisplayLabel } + } + + if (Object.keys(overrides).length === 0) { + return filteredRelativeOptions + } + + return filteredRelativeOptions.map((option) => { + const override = overrides[option.id] + if (!override) { + return option + } + return { + ...option, + name: override.name, + getPeriods: () => + applyPeriodNameOverrides(option.getPeriods(), metaData), + } + }) +} + +export const filterEnabledRelativePeriodTypes = ( + allRelativePeriodOptions, + enabledServerPeriodTypes, + financialYearStart = null +) => { + if (!enabledServerPeriodTypes || enabledServerPeriodTypes.length === 0) { + return [] + } + + const enabledServerPtNames = new Set( + enabledServerPeriodTypes.map((pt) => pt.name) + ) + + return allRelativePeriodOptions.filter((option) => { + // Special handling for financial years + if (option.id === 'FINANCIAL') { + return financialYearStart !== null + } + + // Check if any of the required FP dependencies are enabled + const requiredFpTypes = RP_CATEGORY_TO_FP_DEPENDENCIES[option.id] + if (!requiredFpTypes) { + return true // Show if no dependency mapping (shouldn't happen) + } + + return requiredFpTypes.some((fpType) => + enabledServerPtNames.has(fpType) + ) + }) +} + diff --git a/src/components/PeriodDimension/utils/fixedPeriods.js b/src/components/PeriodDimension/utils/fixedPeriods.js index a6cff6bba..54a892de0 100644 --- a/src/components/PeriodDimension/utils/fixedPeriods.js +++ b/src/components/PeriodDimension/utils/fixedPeriods.js @@ -17,10 +17,18 @@ import { SIXMONTHLY, SIXMONTHLYAPR, YEARLY, - FYNOV, - FYOCT, - FYJUL, + FYJAN, + FYFEB, + FYMAR, FYAPR, + FYMAY, + FYJUN, + FYJUL, + FYAUG, + FYSEP, + FYOCT, + FYNOV, + FYDEC, } from './index.js' export const PERIOD_TYPE_REGEX = { @@ -173,43 +181,10 @@ const getYearlyPeriodType = (fnFilter, periodSettings) => { } } -const getFinancialOctoberPeriodType = (fnFilter, periodSettings) => { - return (config) => { - return getPeriods({ - periodType: 'FYOCT', - config, - fnFilter, - periodSettings, - }) - } -} - -const getFinancialNovemberPeriodType = (fnFilter, periodSettings) => { +const getFinancialPeriodType = (periodType, fnFilter, periodSettings) => { return (config) => { return getPeriods({ - periodType: 'FYNOV', - config, - fnFilter, - periodSettings, - }) - } -} - -const getFinancialJulyPeriodType = (fnFilter, periodSettings) => { - return (config) => { - return getPeriods({ - periodType: 'FYJUL', - config, - fnFilter, - periodSettings, - }) - } -} - -const getFinancialAprilPeriodType = (fnFilter, periodSettings) => { - return (config) => { - return getPeriods({ - periodType: 'FYAPR', + periodType, config, fnFilter, periodSettings, @@ -339,37 +314,113 @@ const getOptions = (periodSettings) => { name: i18n.t('Yearly'), }, { - id: FYNOV, - getPeriods: getFinancialNovemberPeriodType( + id: FYJAN, + getPeriods: getFinancialPeriodType( + 'FYJAN', filterFuturePeriods, periodSettings ), - name: i18n.t('Financial year (Start November)'), + name: i18n.t('Financial year (Start January)'), }, { - id: FYOCT, - getPeriods: getFinancialOctoberPeriodType( + id: FYFEB, + getPeriods: getFinancialPeriodType( + 'FYFEB', filterFuturePeriods, periodSettings ), - name: i18n.t('Financial year (Start October)'), + name: i18n.t('Financial year (Start February)'), }, { - id: FYJUL, - getPeriods: getFinancialJulyPeriodType( + id: FYMAR, + getPeriods: getFinancialPeriodType( + 'FYMAR', filterFuturePeriods, periodSettings ), - name: i18n.t('Financial year (Start July)'), + name: i18n.t('Financial year (Start March)'), }, { id: FYAPR, - getPeriods: getFinancialAprilPeriodType( + getPeriods: getFinancialPeriodType( + 'FYAPR', filterFuturePeriods, periodSettings ), name: i18n.t('Financial year (Start April)'), }, + { + id: FYMAY, + getPeriods: getFinancialPeriodType( + 'FYMAY', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start May)'), + }, + { + id: FYJUN, + getPeriods: getFinancialPeriodType( + 'FYJUN', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start June)'), + }, + { + id: FYJUL, + getPeriods: getFinancialPeriodType( + 'FYJUL', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start July)'), + }, + { + id: FYAUG, + getPeriods: getFinancialPeriodType( + 'FYAUG', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start August)'), + }, + { + id: FYSEP, + getPeriods: getFinancialPeriodType( + 'FYSEP', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start September)'), + }, + { + id: FYOCT, + getPeriods: getFinancialPeriodType( + 'FYOCT', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start October)'), + }, + { + id: FYNOV, + getPeriods: getFinancialPeriodType( + 'FYNOV', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start November)'), + }, + { + id: FYDEC, + getPeriods: getFinancialPeriodType( + 'FYDEC', + filterFuturePeriods, + periodSettings + ), + name: i18n.t('Financial year (Start December)'), + }, ] } diff --git a/src/components/PeriodDimension/utils/index.js b/src/components/PeriodDimension/utils/index.js index 72611c067..a94baf52a 100644 --- a/src/components/PeriodDimension/utils/index.js +++ b/src/components/PeriodDimension/utils/index.js @@ -13,10 +13,18 @@ export const SIXMONTHLY = 'SIXMONTHLY' export const SIXMONTHLYAPR = 'SIXMONTHLYAPR' export const YEARLY = 'YEARLY' export const FINANCIAL = 'FINANCIAL' -export const FYNOV = 'FYNOV' -export const FYOCT = 'FYOCT' -export const FYJUL = 'FYJUL' +export const FYJAN = 'FYJAN' +export const FYFEB = 'FYFEB' +export const FYMAR = 'FYMAR' export const FYAPR = 'FYAPR' +export const FYMAY = 'FYMAY' +export const FYJUN = 'FYJUN' +export const FYJUL = 'FYJUL' +export const FYAUG = 'FYAUG' +export const FYSEP = 'FYSEP' +export const FYOCT = 'FYOCT' +export const FYNOV = 'FYNOV' +export const FYDEC = 'FYDEC' export const filterPeriodTypesById = ( allPeriodTypes = [], diff --git a/src/index.js b/src/index.js index 24ade0037..aa1567e90 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ export { default as DataDimension } from './components/DataDimension/DataDimensi export { default as PeriodDimension } from './components/PeriodDimension/PeriodDimension.js' export { default as FixedPeriodSelect } from './components/PeriodDimension/FixedPeriodSelect.js' +export { useDataOutputPeriodTypes } from './components/PeriodDimension/useDataOutputPeriodTypes.js' export { default as OrgUnitDimension } from './components/OrgUnitDimension/OrgUnitDimension.js' diff --git a/src/visualizations/util/getFilterText.js b/src/visualizations/util/getFilterText.js index 18bd7b9a1..23593341f 100644 --- a/src/visualizations/util/getFilterText.js +++ b/src/visualizations/util/getFilterText.js @@ -36,8 +36,8 @@ export default function (filters, metaData) { dimensionGetItemIds(filter) .map( (id) => - relativePeriodNames[id] || metaData.items[id]?.name || + relativePeriodNames[id] || id ) .join(', ') @@ -62,8 +62,8 @@ export default function (filters, metaData) { else { sectionParts.push( metaData.items[filter.dimension].name + - ': ' + - filterItems.join(', ') + ': ' + + filterItems.join(', ') ) break