From 360486c8b7b0d60fbfc51a5dc1c8437ce2b9deb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Tue, 27 Jan 2026 01:10:40 +0100 Subject: [PATCH 01/14] feat: fetch enabled period types in 43+ (WIP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../PeriodDimension/FixedPeriodFilter.js | 38 ++-- .../PeriodDimension/PeriodDimension.js | 118 ++++++++++- .../PeriodDimension/PeriodTransfer.js | 126 ++++++++--- .../PeriodDimension/RelativePeriodFilter.js | 62 +++--- .../utils/enabledPeriodTypes.js | 199 ++++++++++++++++++ 5 files changed, 476 insertions(+), 67 deletions(-) create mode 100644 src/components/PeriodDimension/utils/enabledPeriodTypes.js diff --git a/src/components/PeriodDimension/FixedPeriodFilter.js b/src/components/PeriodDimension/FixedPeriodFilter.js index 52376291a..cca5e51b8 100644 --- a/src/components/PeriodDimension/FixedPeriodFilter.js +++ b/src/components/PeriodDimension/FixedPeriodFilter.js @@ -16,11 +16,29 @@ const FixedPeriodFilter = ({ onSelectPeriodType, onSelectYear, dataTest, + availableOptions = null, + supportsEnabledPeriodTypes = false, }) => { + // Determine which period options to show + let periodOptions + if (supportsEnabledPeriodTypes && availableOptions) { + // v43+: Use server-provided enabled period types + periodOptions = availableOptions + } else if (allowedPeriodTypes) { + // Legacy: Filter by allowedPeriodTypes if provided + periodOptions = getFixedPeriodsOptions().filter((option) => + allowedPeriodTypes.some((type) => type === option.id) + ) + } else { + // v40-42: Filter by legacy excluded period types (keyHide*Periods system settings) + periodOptions = filterPeriodTypesById( + getFixedPeriodsOptions(), + excludedPeriodTypes + ) + } + const onlyAllowedTypeIsSelected = - Array.isArray(allowedPeriodTypes) && - allowedPeriodTypes.length === 1 && - allowedPeriodTypes[0] === currentPeriodType + periodOptions.length === 1 && periodOptions[0].id === currentPeriodType return ( <> @@ -34,17 +52,7 @@ const FixedPeriodFilter = ({ className="filterElement" dataTest={`${dataTest}-period-type`} > - {(allowedPeriodTypes - ? getFixedPeriodsOptions().filter((option) => - allowedPeriodTypes.some( - (type) => type === option.id - ) - ) - : filterPeriodTypesById( - getFixedPeriodsOptions(), - excludedPeriodTypes - ) - ).map((option) => ( + {periodOptions.map((option) => ( { - const { systemInfo } = useConfig() - const result = useDataQuery(userSettingsQuery) + const config = useConfig() + const { systemInfo, serverVersion } = config + const userSettingsResult = useDataQuery(userSettingsQuery) + + const supportsEnabledPeriodTypes = serverVersion.minor >= 43 + + // Conditionally fetch enabled period types for v43+ + const enabledPeriodTypesResult = useDataQuery( + supportsEnabledPeriodTypes ? enabledPeriodTypesQuery : { skip: true } + ) + + // Conditionally fetch financial year start setting for v43+ + const financialYearStartResult = useDataQuery( + supportsEnabledPeriodTypes ? financialYearStartQuery : { skip: true } + ) + + // Conditionally fetch analysis relative period setting for v43+ + const analysisRelativePeriodResult = useDataQuery( + supportsEnabledPeriodTypes ? analysisRelativePeriodQuery : { skip: true } + ) const { calendar = 'gregory' } = systemInfo - const { data: { userSettings: { keyUiLocale: locale } = {} } = {} } = result + const { data: { userSettings: { keyUiLocale: locale } = {} } = {} } = + userSettingsResult const periodsSettings = { calendar, locale } + // Process enabled period types and validate financial year setting + const enabledPeriodTypesData = useMemo(() => { + if (!supportsEnabledPeriodTypes) { + return null + } + + const { data: enabledTypesData, error: enabledTypesError } = + enabledPeriodTypesResult + const { data: fyStartData, error: fyStartError } = + financialYearStartResult + const { data: analysisRpData, error: analysisRpError } = + analysisRelativePeriodResult + + if (enabledTypesError || fyStartError || analysisRpError) { + return null + } + + if (!enabledTypesData?.enabledPeriodTypes) { + return null + } + + const enabledTypes = enabledTypesData.enabledPeriodTypes + + // Handle empty enabled types + if (!enabledTypes || enabledTypes.length === 0) { + alert( + 'No period types are enabled in the system. Please contact your system administrator.' + ) + return { + enabledTypes: [], + financialYearStart: null, + analysisRelativePeriod: null + } + } + + // Process financial year start setting + let financialYearStart = null + if (fyStartData?.financialYearStart?.analyticsFinancialYearStart) { + const fyStartValue = + fyStartData.financialYearStart.analyticsFinancialYearStart + + // Map system setting to server PT name + 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', + } + + const mappedFyPt = FY_SETTING_TO_SERVER_PT[fyStartValue] + if ( + mappedFyPt && + enabledTypes.some((pt) => pt.name === mappedFyPt) + ) { + financialYearStart = fyStartValue + } + } + + // Process analysis relative period setting + const analysisRelativePeriod = + analysisRpData?.analysisRelativePeriod?.keyAnalysisRelativePeriod || null + + return { enabledTypes, financialYearStart, analysisRelativePeriod } + }, [ + supportsEnabledPeriodTypes, + enabledPeriodTypesResult, + financialYearStartResult, + analysisRelativePeriodResult, + ]) + const selectPeriods = (periods) => { onSelect({ dimensionId: DIMENSION_ID_PERIOD, @@ -47,6 +155,8 @@ const PeriodDimension = ({ excludedPeriodTypes={excludedPeriodTypes} periodsSettings={periodsSettings} height={height} + enabledPeriodTypesData={enabledPeriodTypesData} + supportsEnabledPeriodTypes={supportsEnabledPeriodTypes} /> ) } diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 58d89a310..b23c05472 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 PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { 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,20 @@ 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 { + filterEnabledFixedPeriodTypes, + filterEnabledRelativePeriodTypes, + findBestAvailableRelativePeriod, +} from './utils/enabledPeriodTypes.js' +import { + getFixedPeriodsOptionsById, + getFixedPeriodsOptions, +} from './utils/fixedPeriods.js' +import { MONTHLY, QUARTERLY, filterPeriodTypesById } from './utils/index.js' +import { + getRelativePeriodsOptionsById, + getRelativePeriodsOptions, +} from './utils/relativePeriods.js' const RightHeader = ({ infoBoxMessage }) => ( <> @@ -48,13 +59,73 @@ 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) + // Get filtered period options based on enabled types (v43+) or exclude list (v40-42) + const { filteredFixedOptions, filteredRelativeOptions } = useMemo(() => { + if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { + // v43+: Use server-provided enabled period types (ignore excludedPeriodTypes) + const { enabledTypes, financialYearStart } = enabledPeriodTypesData + + const filteredFixed = filterEnabledFixedPeriodTypes( + getFixedPeriodsOptions(periodsSettings), + enabledTypes + ) + + const filteredRelative = filterEnabledRelativePeriodTypes( + getRelativePeriodsOptions(), + enabledTypes, + financialYearStart + ) + + return { + filteredFixedOptions: filteredFixed, + filteredRelativeOptions: filteredRelative, + } + } else { + // v40-42: Fallback to old behavior with legacy excluded period types + // (based on keyHide*Periods system settings from consuming apps) + const allFixed = getFixedPeriodsOptions(periodsSettings) + const allRelative = getRelativePeriodsOptions() + + return { + filteredFixedOptions: filterPeriodTypesById( + allFixed, + excludedPeriodTypes + ), + filteredRelativeOptions: filterPeriodTypesById( + allRelative, + excludedPeriodTypes + ), + } + } + }, [ + supportsEnabledPeriodTypes, + enabledPeriodTypesData, + excludedPeriodTypes, + periodsSettings, + ]) + + // Choose default period types from filtered options + const bestRelativePeriod = useMemo(() => { + if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { + const { analysisRelativePeriod } = enabledPeriodTypesData + return findBestAvailableRelativePeriod(filteredRelativeOptions, analysisRelativePeriod) + } + return null + }, [supportsEnabledPeriodTypes, enabledPeriodTypesData, filteredRelativeOptions]) + + const defaultRelativePeriodType = supportsEnabledPeriodTypes && bestRelativePeriod + ? filteredRelativeOptions.find((opt) => opt.id === bestRelativePeriod.categoryId) + : (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) @@ -68,14 +139,14 @@ const PeriodTransfer = ({ }) const [allPeriods, setAllPeriods] = useState( - defaultRelativePeriodType.getPeriods() + defaultRelativePeriodType?.getPeriods() || [] ) 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(), }) @@ -124,13 +195,14 @@ const PeriodTransfer = ({ currentFilter={relativeFilter.periodType} onSelectFilter={(filter) => { setRelativeFilter({ periodType: filter }) - setAllPeriods( - getRelativePeriodsOptionsById( - filter - ).getPeriods() + const selectedOption = filteredRelativeOptions.find( + (opt) => opt.id === filter ) + setAllPeriods(selectedOption?.getPeriods() || []) }} dataTest={`${dataTest}-relative-period-filter`} + availableOptions={filteredRelativeOptions} + supportsEnabledPeriodTypes={supportsEnabledPeriodTypes} excludedPeriodTypes={excludedPeriodTypes} /> ) : ( @@ -150,6 +222,8 @@ const PeriodTransfer = ({ }) }} dataTest={`${dataTest}-fixed-period-filter`} + availableOptions={filteredFixedOptions} + supportsEnabledPeriodTypes={supportsEnabledPeriodTypes} excludedPeriodTypes={excludedPeriodTypes} /> )} @@ -162,14 +236,13 @@ const PeriodTransfer = ({ setFixedFilter(filter) if (filter.year.match(/[0-9]{4}/)) { + const selectedOption = filteredFixedOptions.find( + (opt) => opt.id === filter.periodType + ) setAllPeriods( - getFixedPeriodsOptionsById( - filter.periodType, - periodsSettings - ).getPeriods( - fixedPeriodConfig(Number(filter.year)), - periodsSettings - ) + selectedOption?.getPeriods( + fixedPeriodConfig(Number(filter.year)) + ) || [] ) } } @@ -227,6 +300,10 @@ const PeriodTransfer = ({ PeriodTransfer.propTypes = { onSelect: PropTypes.func.isRequired, dataTest: PropTypes.string, + enabledPeriodTypesData: PropTypes.shape({ + enabledTypes: PropTypes.array, + financialYearStart: PropTypes.string, + }), excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string), height: PropTypes.string, infoBoxMessage: PropTypes.string, @@ -242,6 +319,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..151325610 100644 --- a/src/components/PeriodDimension/RelativePeriodFilter.js +++ b/src/components/PeriodDimension/RelativePeriodFilter.js @@ -11,37 +11,49 @@ const RelativePeriodFilter = ({ onSelectFilter, dataTest, excludedPeriodTypes, -}) => ( -
- onSelectFilter(selected)} - dense - selected={currentFilter} - className="filterElement" - dataTest={`${dataTest}-period-type`} - > - {filterPeriodTypesById( - getRelativePeriodsOptions(), - excludedPeriodTypes - ).map((option) => ( - - ))} - - -
-) + availableOptions = null, + supportsEnabledPeriodTypes = false, +}) => { + // v43+: Use server-provided enabled options, v40-42: Use legacy excluded period types + const periodOptions = + supportsEnabledPeriodTypes && availableOptions + ? availableOptions // Server-provided enabled period types + : filterPeriodTypesById( + getRelativePeriodsOptions(), + excludedPeriodTypes // Legacy keyHide*Periods system settings + ) + + return ( +
+ onSelectFilter(selected)} + dense + selected={currentFilter} + className="filterElement" + dataTest={`${dataTest}-period-type`} + > + {periodOptions.map((option) => ( + + ))} + + +
+ ) +} RelativePeriodFilter.propTypes = { currentFilter: PropTypes.string.isRequired, onSelectFilter: PropTypes.func.isRequired, + availableOptions: PropTypes.array, dataTest: PropTypes.string, excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string), + supportsEnabledPeriodTypes: PropTypes.bool, } export default RelativePeriodFilter diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js new file mode 100644 index 000000000..91b3b0a05 --- /dev/null +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -0,0 +1,199 @@ +// 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', + WeeklySaturday: 'WEEKLYSAT', + WeeklySunday: 'WEEKLYSUN', + BiWeekly: 'BIWEEKLY', + Monthly: 'MONTHLY', + BiMonthly: 'BIMONTHLY', + Quarterly: 'QUARTERLY', + SixMonthly: 'SIXMONTHLY', + SixMonthlyApril: 'SIXMONTHLYAPR', + Yearly: 'YEARLY', + FinancialApril: 'FYAPR', + FinancialJuly: 'FYJUL', + 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', + 'WeeklySaturday', + 'WeeklySunday', + ], + BIWEEKLY: ['BiWeekly'], + MONTHLY: ['Monthly'], + BIMONTHLY: ['BiMonthly'], + QUARTERLY: ['Quarterly'], + SIXMONTHLY: ['SixMonthly', 'SixMonthlyApril'], + 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 = 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) + ) +} + +/** + * 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 + */ +export const filterEnabledRelativePeriodTypes = ( + allRelativePeriodOptions, + enabledServerPeriodTypes, + financialYearStart = null +) => { + if (!enabledServerPeriodTypes || enabledServerPeriodTypes.length === 0) { + return [] + } + + const enabledServerPtNames = 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.includes(fpType) + ) + }) +} + +// Mapping from keyAnalysisRelativePeriod system setting values (RP IDs) to categories +export const ANALYSIS_RELATIVE_PERIOD_MAPPING = { + THIS_WEEK: { id: 'THIS_WEEK', category: 'WEEKLY' }, + LAST_WEEK: { id: 'LAST_WEEK', category: 'WEEKLY' }, + LAST_4_WEEKS: { id: 'LAST_4_WEEKS', category: 'WEEKLY' }, + LAST_12_WEEKS: { id: 'LAST_12_WEEKS', category: 'WEEKLY' }, + LAST_52_WEEKS: { id: 'LAST_52_WEEKS', category: 'WEEKLY' }, + THIS_MONTH: { id: 'THIS_MONTH', category: 'MONTHLY' }, + LAST_MONTH: { id: 'LAST_MONTH', category: 'MONTHLY' }, + MONTHS_THIS_YEAR: { id: 'MONTHS_THIS_YEAR', category: 'MONTHLY' }, + // Note: MONTHS_LAST_YEAR does not exist in current RP definitions + LAST_3_MONTHS: { id: 'LAST_3_MONTHS', category: 'MONTHLY' }, + LAST_6_MONTHS: { id: 'LAST_6_MONTHS', category: 'MONTHLY' }, + LAST_12_MONTHS: { id: 'LAST_12_MONTHS', category: 'MONTHLY' }, + THIS_BIMONTH: { id: 'THIS_BIMONTH', category: 'BIMONTHLY' }, + LAST_BIMONTH: { id: 'LAST_BIMONTH', category: 'BIMONTHLY' }, + LAST_6_BIMONTHS: { id: 'LAST_6_BIMONTHS', category: 'BIMONTHLY' }, + THIS_QUARTER: { id: 'THIS_QUARTER', category: 'QUARTERLY' }, + LAST_QUARTER: { id: 'LAST_QUARTER', category: 'QUARTERLY' }, + QUARTERS_THIS_YEAR: { id: 'QUARTERS_THIS_YEAR', category: 'QUARTERLY' }, + // Note: QUARTERS_LAST_YEAR does not exist in current RP definitions + LAST_4_QUARTERS: { id: 'LAST_4_QUARTERS', category: 'QUARTERLY' }, + THIS_SIX_MONTH: { id: 'THIS_SIX_MONTH', category: 'SIXMONTHLY' }, + LAST_SIX_MONTH: { id: 'LAST_SIX_MONTH', category: 'SIXMONTHLY' }, + LAST_2_SIXMONTHS: { id: 'LAST_2_SIXMONTHS', category: 'SIXMONTHLY' }, + THIS_YEAR: { id: 'THIS_YEAR', category: 'YEARLY' }, + LAST_YEAR: { id: 'LAST_YEAR', category: 'YEARLY' }, + LAST_5_YEARS: { id: 'LAST_5_YEARS', category: 'YEARLY' }, + LAST_10_YEARS: { id: 'LAST_10_YEARS', category: 'YEARLY' }, + THIS_FINANCIAL_YEAR: { id: 'THIS_FINANCIAL_YEAR', category: 'FINANCIAL' }, + LAST_FINANCIAL_YEAR: { id: 'LAST_FINANCIAL_YEAR', category: 'FINANCIAL' }, + LAST_5_FINANCIAL_YEARS: { id: 'LAST_5_FINANCIAL_YEARS', category: 'FINANCIAL' }, +} + +// Fallback priority order for RP categories (closest to most commonly used) +const RP_CATEGORY_FALLBACK_ORDER = [ + 'MONTHLY', // Most common + 'QUARTERLY', // Close alternative to monthly + 'YEARLY', // Longer term view + 'WEEKLY', // More granular + 'SIXMONTHLY', // Mid-term + 'BIMONTHLY', // Less common + 'FINANCIAL', // Depends on system config + 'BIWEEKLY', // Least common + 'DAILY', // Very granular +] + +/** + * Find the best available relative period based on keyAnalysisRelativePeriod setting + * @param {Array} enabledRelativeOptions - Available RP categories + * @param {string|null} analysisRelativePeriod - System setting value + * @returns {Object} { categoryId, periodId } or null + */ +export const findBestAvailableRelativePeriod = (enabledRelativeOptions, analysisRelativePeriod) => { + if (!enabledRelativeOptions || enabledRelativeOptions.length === 0) { + return null + } + + const enabledCategoryIds = new Set(enabledRelativeOptions.map(opt => opt.id)) + + // Try to use the configured analysis relative period first + if (analysisRelativePeriod && ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod]) { + const { id: periodId, category: categoryId } = ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod] + + if (enabledCategoryIds.has(categoryId)) { + return { categoryId, periodId } + } + } + + // Fall back to the highest priority enabled category + for (const categoryId of RP_CATEGORY_FALLBACK_ORDER) { + if (enabledCategoryIds.has(categoryId)) { + // Use the first period from that category as default + const categoryOption = enabledRelativeOptions.find(opt => opt.id === categoryId) + const periods = categoryOption?.getPeriods() || [] + const defaultPeriod = periods[0] + + return { + categoryId, + periodId: defaultPeriod?.id || null + } + } + } + + // Last resort: use first available category and its first period + const firstCategory = enabledRelativeOptions[0] + const firstPeriod = firstCategory?.getPeriods()?.[0] + + return { + categoryId: firstCategory?.id || null, + periodId: firstPeriod?.id || null + } +} From 7319460eb962584c209524152c080314cfae6d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Tue, 27 Jan 2026 01:14:06 +0100 Subject: [PATCH 02/14] fix: add missing serverVersion mock in PeriodDimension test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test was failing because the useConfig mock was missing the serverVersion property that the component now requires for version detection. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 104 ++++++++++++++++++ .../PeriodDimension/PeriodDimension.js | 9 +- .../PeriodDimension/PeriodTransfer.js | 24 ++-- .../__tests__/PeriodDimension.spec.js | 5 +- .../utils/enabledPeriodTypes.js | 46 +++++--- 5 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..68a62aa6e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,104 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Development + +- `yarn start` - Start Storybook development server on port 5000 +- `yarn start-storybook` - Alternative command to start Storybook +- `yarn build` - Build the library using d2-app-scripts +- `yarn build-storybook` - Build Storybook for production + +### Testing + +- `yarn test` - Run tests using d2-app-scripts test +- Jest configuration is in `jest.config.js` with setup in `config/setupTestingLibrary.js` + +### Code Quality + +- `yarn lint` - Run d2-style linting checks +- `yarn format` - Apply d2-style formatting +- `yarn validate-commit` - Check staged files (used in git hooks) +- `yarn validate-push` - Run tests before push + +## Architecture Overview + +This is the DHIS2 Analytics library that provides shared components and utilities for DHIS2 analytics applications (dashboards, data visualizer, maps, line listing). + +### Core Structure + +**Components** (`src/components/`) + +- **DataDimension** - Data element/indicator selection +- **PeriodDimension** - Period selection with relative and fixed periods +- **OrgUnitDimension** - Organisation unit selection +- **DynamicDimension** - Generic dimension component for categories, etc. +- **DimensionsPanel** - Layout manager for drag/drop dimension arrangement +- **PivotTable** - Pivot table visualization component +- **FileMenu** - Save/load functionality +- **Interpretations** - Comments and interpretation system +- **Toolbar** - Common toolbar components +- **Options/VisualizationOptions** - Configuration dialogs + +**Modules** (`src/modules/`) + +- **layout/** - Core layout system managing dimensions across columns/rows/filters +- **predefinedDimensions.js** - Data (dx), Period (pe), OrgUnit (ou) dimension utilities +- **visTypes.js** - Visualization type constants and utilities +- **layoutUiRules/** - Business logic for dimension placement rules +- **axis.js** - Axis manipulation utilities +- **fontStyle.js** - Typography configuration +- **valueTypes.js** - DHIS2 value type definitions + +**Visualizations** (`src/visualizations/`) + +- Chart creation and rendering utilities +- Color sets and theming + +### Layout System + +The library centers around a **layout** object with three axes: + +```javascript +{ + columns: [dimensions], // Top/horizontal in tables, series in charts + rows: [dimensions], // Left/vertical in tables, category in charts + filters: [dimensions] // Applied as filters to data +} +``` + +Each **dimension** has: + +```javascript +{ + dimension: "dx", // Dimension ID (dx=data, pe=period, ou=orgunit) + items: [{id: "..."}] // Selected items +} +``` + +### Key Exports + +The library exports components, API utilities, and layout manipulation functions. Main categories: + +- **Components**: DataDimension, PeriodDimension, OrgUnitDimension, DimensionsPanel, PivotTable, FileMenu +- **Layout utilities**: Layout manipulation, axis utilities, dimension management +- **API utilities**: Analytics API wrapper, dimension fetching +- **Constants**: Visualization types, layout types, axis IDs, dimension IDs + +### Development Notes + +- Uses DHIS2 design system (`@dhis2/ui`) and app platform (`@dhis2/app-runtime`) +- Built with React, exports as both ES and CommonJS modules +- Styling with styled-jsx +- Internationalization with `@dhis2/d2-i18n` +- Uses d2-app-scripts for building/testing (DHIS2's CLI toolchain) +- ESLint extends `@dhis2/cli-style` React configuration + +### Testing Setup + +- Jest with React Testing Library +- Setup file: `config/setupTestingLibrary.js` +- Test files alongside source code +- Excludes `/node_modules/` and `/build/` from test paths diff --git a/src/components/PeriodDimension/PeriodDimension.js b/src/components/PeriodDimension/PeriodDimension.js index 7a441a7e0..8b649e81d 100644 --- a/src/components/PeriodDimension/PeriodDimension.js +++ b/src/components/PeriodDimension/PeriodDimension.js @@ -59,7 +59,9 @@ const PeriodDimension = ({ // Conditionally fetch analysis relative period setting for v43+ const analysisRelativePeriodResult = useDataQuery( - supportsEnabledPeriodTypes ? analysisRelativePeriodQuery : { skip: true } + supportsEnabledPeriodTypes + ? analysisRelativePeriodQuery + : { skip: true } ) const { calendar = 'gregory' } = systemInfo @@ -99,7 +101,7 @@ const PeriodDimension = ({ return { enabledTypes: [], financialYearStart: null, - analysisRelativePeriod: null + analysisRelativePeriod: null, } } @@ -129,7 +131,8 @@ const PeriodDimension = ({ // Process analysis relative period setting const analysisRelativePeriod = - analysisRpData?.analysisRelativePeriod?.keyAnalysisRelativePeriod || null + analysisRpData?.analysisRelativePeriod?.keyAnalysisRelativePeriod || + null return { enabledTypes, financialYearStart, analysisRelativePeriod } }, [ diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index b23c05472..80101e754 100644 --- a/src/components/PeriodDimension/PeriodTransfer.js +++ b/src/components/PeriodDimension/PeriodTransfer.js @@ -111,16 +111,26 @@ const PeriodTransfer = ({ const bestRelativePeriod = useMemo(() => { if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { const { analysisRelativePeriod } = enabledPeriodTypesData - return findBestAvailableRelativePeriod(filteredRelativeOptions, analysisRelativePeriod) + return findBestAvailableRelativePeriod( + filteredRelativeOptions, + analysisRelativePeriod + ) } return null - }, [supportsEnabledPeriodTypes, enabledPeriodTypesData, filteredRelativeOptions]) + }, [ + supportsEnabledPeriodTypes, + enabledPeriodTypesData, + filteredRelativeOptions, + ]) - const defaultRelativePeriodType = supportsEnabledPeriodTypes && bestRelativePeriod - ? filteredRelativeOptions.find((opt) => opt.id === bestRelativePeriod.categoryId) - : (filteredRelativeOptions.find((opt) => opt.id === MONTHLY) || - filteredRelativeOptions.find((opt) => opt.id === QUARTERLY) || - filteredRelativeOptions[0]) + const defaultRelativePeriodType = + supportsEnabledPeriodTypes && bestRelativePeriod + ? filteredRelativeOptions.find( + (opt) => opt.id === bestRelativePeriod.categoryId + ) + : filteredRelativeOptions.find((opt) => opt.id === MONTHLY) || + filteredRelativeOptions.find((opt) => opt.id === QUARTERLY) || + filteredRelativeOptions[0] const defaultFixedPeriodType = filteredFixedOptions.find((opt) => opt.id === MONTHLY) || diff --git a/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js b/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js index 307ac5066..cbb3684ad 100644 --- a/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js +++ b/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js @@ -4,7 +4,10 @@ import React from 'react' import PeriodDimension from '../PeriodDimension.js' jest.mock('@dhis2/app-runtime', () => ({ - useConfig: () => ({ systemInfo: {} }), + useConfig: () => ({ + systemInfo: {}, + serverVersion: { minor: 42 }, // Mock v42 to test legacy behavior + }), useDataQuery: () => ({ data: { userSettings: { keyUiLocale: 'en' } } }), })) diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js index 91b3b0a05..440771625 100644 --- a/src/components/PeriodDimension/utils/enabledPeriodTypes.js +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -135,20 +135,23 @@ export const ANALYSIS_RELATIVE_PERIOD_MAPPING = { LAST_10_YEARS: { id: 'LAST_10_YEARS', category: 'YEARLY' }, THIS_FINANCIAL_YEAR: { id: 'THIS_FINANCIAL_YEAR', category: 'FINANCIAL' }, LAST_FINANCIAL_YEAR: { id: 'LAST_FINANCIAL_YEAR', category: 'FINANCIAL' }, - LAST_5_FINANCIAL_YEARS: { id: 'LAST_5_FINANCIAL_YEARS', category: 'FINANCIAL' }, + LAST_5_FINANCIAL_YEARS: { + id: 'LAST_5_FINANCIAL_YEARS', + category: 'FINANCIAL', + }, } // Fallback priority order for RP categories (closest to most commonly used) const RP_CATEGORY_FALLBACK_ORDER = [ - 'MONTHLY', // Most common - 'QUARTERLY', // Close alternative to monthly - 'YEARLY', // Longer term view - 'WEEKLY', // More granular + 'MONTHLY', // Most common + 'QUARTERLY', // Close alternative to monthly + 'YEARLY', // Longer term view + 'WEEKLY', // More granular 'SIXMONTHLY', // Mid-term - 'BIMONTHLY', // Less common - 'FINANCIAL', // Depends on system config - 'BIWEEKLY', // Least common - 'DAILY', // Very granular + 'BIMONTHLY', // Less common + 'FINANCIAL', // Depends on system config + 'BIWEEKLY', // Least common + 'DAILY', // Very granular ] /** @@ -157,16 +160,25 @@ const RP_CATEGORY_FALLBACK_ORDER = [ * @param {string|null} analysisRelativePeriod - System setting value * @returns {Object} { categoryId, periodId } or null */ -export const findBestAvailableRelativePeriod = (enabledRelativeOptions, analysisRelativePeriod) => { +export const findBestAvailableRelativePeriod = ( + enabledRelativeOptions, + analysisRelativePeriod +) => { if (!enabledRelativeOptions || enabledRelativeOptions.length === 0) { return null } - const enabledCategoryIds = new Set(enabledRelativeOptions.map(opt => opt.id)) + const enabledCategoryIds = new Set( + enabledRelativeOptions.map((opt) => opt.id) + ) // Try to use the configured analysis relative period first - if (analysisRelativePeriod && ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod]) { - const { id: periodId, category: categoryId } = ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod] + if ( + analysisRelativePeriod && + ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod] + ) { + const { id: periodId, category: categoryId } = + ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod] if (enabledCategoryIds.has(categoryId)) { return { categoryId, periodId } @@ -177,13 +189,15 @@ export const findBestAvailableRelativePeriod = (enabledRelativeOptions, analysis for (const categoryId of RP_CATEGORY_FALLBACK_ORDER) { if (enabledCategoryIds.has(categoryId)) { // Use the first period from that category as default - const categoryOption = enabledRelativeOptions.find(opt => opt.id === categoryId) + const categoryOption = enabledRelativeOptions.find( + (opt) => opt.id === categoryId + ) const periods = categoryOption?.getPeriods() || [] const defaultPeriod = periods[0] return { categoryId, - periodId: defaultPeriod?.id || null + periodId: defaultPeriod?.id || null, } } } @@ -194,6 +208,6 @@ export const findBestAvailableRelativePeriod = (enabledRelativeOptions, analysis return { categoryId: firstCategory?.id || null, - periodId: firstPeriod?.id || null + periodId: firstPeriod?.id || null, } } From 85867b1f5807840014d4d0d9456a3d0d13db2db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Tue, 27 Jan 2026 11:17:15 +0100 Subject: [PATCH 03/14] fix: clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- i18n/en.pot | 14 ++- .../PeriodDimension/FixedPeriodFilter.js | 37 +------ .../PeriodDimension/FixedPeriodSelect.js | 10 +- .../PeriodDimension/PeriodDimension.js | 98 +++++++------------ .../PeriodDimension/PeriodTransfer.js | 58 +++++------ .../PeriodDimension/RelativePeriodFilter.js | 65 +++++------- .../__tests__/PeriodDimension.spec.js | 14 ++- 7 files changed, 126 insertions(+), 170 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 8616da74e..79bf877b8 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-27T09:57:38.828Z\n" +"PO-Revision-Date: 2026-01-27T09:57:38.828Z\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" diff --git a/src/components/PeriodDimension/FixedPeriodFilter.js b/src/components/PeriodDimension/FixedPeriodFilter.js index cca5e51b8..362ed801f 100644 --- a/src/components/PeriodDimension/FixedPeriodFilter.js +++ b/src/components/PeriodDimension/FixedPeriodFilter.js @@ -3,42 +3,18 @@ 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, onSelectYear, dataTest, - availableOptions = null, - supportsEnabledPeriodTypes = false, }) => { - // Determine which period options to show - let periodOptions - if (supportsEnabledPeriodTypes && availableOptions) { - // v43+: Use server-provided enabled period types - periodOptions = availableOptions - } else if (allowedPeriodTypes) { - // Legacy: Filter by allowedPeriodTypes if provided - periodOptions = getFixedPeriodsOptions().filter((option) => - allowedPeriodTypes.some((type) => type === option.id) - ) - } else { - // v40-42: Filter by legacy excluded period types (keyHide*Periods system settings) - periodOptions = filterPeriodTypesById( - getFixedPeriodsOptions(), - excludedPeriodTypes - ) - } - const onlyAllowedTypeIsSelected = - periodOptions.length === 1 && periodOptions[0].id === currentPeriodType + availableOptions.length === 1 && + availableOptions[0].id === currentPeriodType return ( <> @@ -52,7 +28,7 @@ const FixedPeriodFilter = ({ className="filterElement" dataTest={`${dataTest}-period-type`} > - {periodOptions.map((option) => ( + {availableOptions.map((option) => ( + this.props.allowedPeriodTypes.includes(option.id) + ) + : allOptions + return (
= 43 - // Conditionally fetch enabled period types for v43+ - const enabledPeriodTypesResult = useDataQuery( - supportsEnabledPeriodTypes ? enabledPeriodTypesQuery : { skip: true } - ) - - // Conditionally fetch financial year start setting for v43+ - const financialYearStartResult = useDataQuery( - supportsEnabledPeriodTypes ? financialYearStartQuery : { skip: true } - ) + const { + data: v43Data, + error: v43Error, + refetch: v43Refetch, + } = useDataQuery(v43Query, { lazy: true }) - // Conditionally fetch analysis relative period setting for v43+ - const analysisRelativePeriodResult = useDataQuery( - supportsEnabledPeriodTypes - ? analysisRelativePeriodQuery - : { skip: true } - ) + useEffect(() => { + if (supportsEnabledPeriodTypes) { + v43Refetch() + } + }, [supportsEnabledPeriodTypes, v43Refetch]) const { calendar = 'gregory' } = systemInfo const { data: { userSettings: { keyUiLocale: locale } = {} } = {} } = @@ -70,55 +67,30 @@ const PeriodDimension = ({ const periodsSettings = { calendar, locale } - // Process enabled period types and validate financial year setting const enabledPeriodTypesData = useMemo(() => { if (!supportsEnabledPeriodTypes) { return null } - const { data: enabledTypesData, error: enabledTypesError } = - enabledPeriodTypesResult - const { data: fyStartData, error: fyStartError } = - financialYearStartResult - const { data: analysisRpData, error: analysisRpError } = - analysisRelativePeriodResult - - if (enabledTypesError || fyStartError || analysisRpError) { + if (v43Error || !v43Data?.enabledPeriodTypes) { return null } - if (!enabledTypesData?.enabledPeriodTypes) { - return null - } + const enabledTypes = v43Data.enabledPeriodTypes - const enabledTypes = enabledTypesData.enabledPeriodTypes - - // Handle empty enabled types if (!enabledTypes || enabledTypes.length === 0) { - alert( - 'No period types are enabled in the system. Please contact your system administrator.' - ) return { enabledTypes: [], financialYearStart: null, analysisRelativePeriod: null, + noEnabledTypes: true, } } - // Process financial year start setting let financialYearStart = null - if (fyStartData?.financialYearStart?.analyticsFinancialYearStart) { + if (v43Data.financialYearStart?.analyticsFinancialYearStart) { const fyStartValue = - fyStartData.financialYearStart.analyticsFinancialYearStart - - // Map system setting to server PT name - 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', - } + v43Data.financialYearStart.analyticsFinancialYearStart const mappedFyPt = FY_SETTING_TO_SERVER_PT[fyStartValue] if ( @@ -129,18 +101,16 @@ const PeriodDimension = ({ } } - // Process analysis relative period setting const analysisRelativePeriod = - analysisRpData?.analysisRelativePeriod?.keyAnalysisRelativePeriod || - null - - return { enabledTypes, financialYearStart, analysisRelativePeriod } - }, [ - supportsEnabledPeriodTypes, - enabledPeriodTypesResult, - financialYearStartResult, - analysisRelativePeriodResult, - ]) + v43Data.analysisRelativePeriod?.keyAnalysisRelativePeriod || null + + return { + enabledTypes, + financialYearStart, + analysisRelativePeriod, + noEnabledTypes: false, + } + }, [supportsEnabledPeriodTypes, v43Data, v43Error]) const selectPeriods = (periods) => { onSelect({ diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 80101e754..5853a58bc 100644 --- a/src/components/PeriodDimension/PeriodTransfer.js +++ b/src/components/PeriodDimension/PeriodTransfer.js @@ -1,5 +1,5 @@ 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, useMemo } from 'react' import PeriodIcon from '../../assets/DimensionItemIcons/PeriodIcon.js' //TODO: Reimplement the icon.js @@ -18,15 +18,9 @@ import { filterEnabledRelativePeriodTypes, findBestAvailableRelativePeriod, } from './utils/enabledPeriodTypes.js' -import { - getFixedPeriodsOptionsById, - getFixedPeriodsOptions, -} from './utils/fixedPeriods.js' +import { getFixedPeriodsOptions } from './utils/fixedPeriods.js' import { MONTHLY, QUARTERLY, filterPeriodTypesById } from './utils/index.js' -import { - getRelativePeriodsOptionsById, - getRelativePeriodsOptions, -} from './utils/relativePeriods.js' +import { getRelativePeriodsOptions } from './utils/relativePeriods.js' const RightHeader = ({ infoBoxMessage }) => ( <> @@ -62,10 +56,8 @@ const PeriodTransfer = ({ enabledPeriodTypesData = null, supportsEnabledPeriodTypes = false, }) => { - // Get filtered period options based on enabled types (v43+) or exclude list (v40-42) const { filteredFixedOptions, filteredRelativeOptions } = useMemo(() => { if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { - // v43+: Use server-provided enabled period types (ignore excludedPeriodTypes) const { enabledTypes, financialYearStart } = enabledPeriodTypesData const filteredFixed = filterEnabledFixedPeriodTypes( @@ -84,8 +76,6 @@ const PeriodTransfer = ({ filteredRelativeOptions: filteredRelative, } } else { - // v40-42: Fallback to old behavior with legacy excluded period types - // (based on keyHide*Periods system settings from consuming apps) const allFixed = getFixedPeriodsOptions(periodsSettings) const allRelative = getRelativePeriodsOptions() @@ -107,7 +97,6 @@ const PeriodTransfer = ({ periodsSettings, ]) - // Choose default period types from filtered options const bestRelativePeriod = useMemo(() => { if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { const { analysisRelativePeriod } = enabledPeriodTypesData @@ -168,19 +157,34 @@ 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))) - ) + if (state) { + const selectedOption = filteredRelativeOptions.find( + (opt) => opt.id === relativeFilter.periodType + ) + setAllPeriods(selectedOption?.getPeriods() || []) + } else { + const selectedOption = filteredFixedOptions.find( + (opt) => opt.id === fixedFilter.periodType + ) + setAllPeriods( + selectedOption?.getPeriods( + fixedPeriodConfig(Number(fixedFilter.year)) + ) || [] + ) + } } } + if (enabledPeriodTypesData?.noEnabledTypes) { + return ( + + {i18n.t( + 'No period types are enabled in the system. Please contact your system administrator.' + )} + + ) + } + const renderLeftHeader = () => ( <> @@ -212,8 +216,6 @@ const PeriodTransfer = ({ }} dataTest={`${dataTest}-relative-period-filter`} availableOptions={filteredRelativeOptions} - supportsEnabledPeriodTypes={supportsEnabledPeriodTypes} - excludedPeriodTypes={excludedPeriodTypes} /> ) : ( )}
@@ -311,8 +311,10 @@ PeriodTransfer.propTypes = { onSelect: PropTypes.func.isRequired, dataTest: PropTypes.string, enabledPeriodTypesData: PropTypes.shape({ + analysisRelativePeriod: PropTypes.string, enabledTypes: PropTypes.array, financialYearStart: PropTypes.string, + noEnabledTypes: PropTypes.bool, }), excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string), height: PropTypes.string, diff --git a/src/components/PeriodDimension/RelativePeriodFilter.js b/src/components/PeriodDimension/RelativePeriodFilter.js index 151325610..1860d4345 100644 --- a/src/components/PeriodDimension/RelativePeriodFilter.js +++ b/src/components/PeriodDimension/RelativePeriodFilter.js @@ -3,57 +3,40 @@ 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 = null, - supportsEnabledPeriodTypes = false, -}) => { - // v43+: Use server-provided enabled options, v40-42: Use legacy excluded period types - const periodOptions = - supportsEnabledPeriodTypes && availableOptions - ? availableOptions // Server-provided enabled period types - : filterPeriodTypesById( - getRelativePeriodsOptions(), - excludedPeriodTypes // Legacy keyHide*Periods system settings - ) - - return ( -
- onSelectFilter(selected)} - dense - selected={currentFilter} - className="filterElement" - dataTest={`${dataTest}-period-type`} - > - {periodOptions.map((option) => ( - - ))} - - -
- ) -} + availableOptions, +}) => ( +
+ onSelectFilter(selected)} + dense + selected={currentFilter} + className="filterElement" + dataTest={`${dataTest}-period-type`} + > + {availableOptions.map((option) => ( + + ))} + + +
+) RelativePeriodFilter.propTypes = { + availableOptions: PropTypes.array.isRequired, currentFilter: PropTypes.string.isRequired, onSelectFilter: PropTypes.func.isRequired, - availableOptions: PropTypes.array, dataTest: PropTypes.string, - excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string), - supportsEnabledPeriodTypes: PropTypes.bool, } export default RelativePeriodFilter diff --git a/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js b/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js index cbb3684ad..ad8b0506d 100644 --- a/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js +++ b/src/components/PeriodDimension/__tests__/PeriodDimension.spec.js @@ -6,9 +6,19 @@ import PeriodDimension from '../PeriodDimension.js' jest.mock('@dhis2/app-runtime', () => ({ useConfig: () => ({ systemInfo: {}, - serverVersion: { minor: 42 }, // Mock v42 to test legacy behavior + 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' } } } }), - useDataQuery: () => ({ data: { userSettings: { keyUiLocale: 'en' } } }), })) global.ResizeObserver = jest.fn().mockImplementation(() => ({ From 456dc86fab700e7e84d037e1c416d1852e924ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Tue, 27 Jan 2026 11:22:52 +0100 Subject: [PATCH 04/14] fix: satisfy sonarqube MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../PeriodDimension/utils/enabledPeriodTypes.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js index 440771625..1ed74ad94 100644 --- a/src/components/PeriodDimension/utils/enabledPeriodTypes.js +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -51,7 +51,9 @@ export const filterEnabledFixedPeriodTypes = ( return [] } - const enabledServerPtNames = enabledServerPeriodTypes.map((pt) => pt.name) + const enabledServerPtNames = new Set( + enabledServerPeriodTypes.map((pt) => pt.name) + ) const enabledMultiCalendarPts = new Set() // Map server PT names to multi-calendar-dates constants @@ -84,7 +86,9 @@ export const filterEnabledRelativePeriodTypes = ( return [] } - const enabledServerPtNames = enabledServerPeriodTypes.map((pt) => pt.name) + const enabledServerPtNames = new Set( + enabledServerPeriodTypes.map((pt) => pt.name) + ) return allRelativePeriodOptions.filter((option) => { // Special handling for financial years @@ -99,7 +103,7 @@ export const filterEnabledRelativePeriodTypes = ( } return requiredFpTypes.some((fpType) => - enabledServerPtNames.includes(fpType) + enabledServerPtNames.has(fpType) ) }) } From 1425a8698201fe9e48f1d58c11f6149fbf69769d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Tue, 27 Jan 2026 11:31:06 +0100 Subject: [PATCH 05/14] chore: remove CLAUDE.md from tracking and gitignore it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 1 + CLAUDE.md | 104 ----------------------------------------------------- 2 files changed, 1 insertion(+), 104 deletions(-) delete mode 100644 CLAUDE.md 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/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 68a62aa6e..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,104 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -### Development - -- `yarn start` - Start Storybook development server on port 5000 -- `yarn start-storybook` - Alternative command to start Storybook -- `yarn build` - Build the library using d2-app-scripts -- `yarn build-storybook` - Build Storybook for production - -### Testing - -- `yarn test` - Run tests using d2-app-scripts test -- Jest configuration is in `jest.config.js` with setup in `config/setupTestingLibrary.js` - -### Code Quality - -- `yarn lint` - Run d2-style linting checks -- `yarn format` - Apply d2-style formatting -- `yarn validate-commit` - Check staged files (used in git hooks) -- `yarn validate-push` - Run tests before push - -## Architecture Overview - -This is the DHIS2 Analytics library that provides shared components and utilities for DHIS2 analytics applications (dashboards, data visualizer, maps, line listing). - -### Core Structure - -**Components** (`src/components/`) - -- **DataDimension** - Data element/indicator selection -- **PeriodDimension** - Period selection with relative and fixed periods -- **OrgUnitDimension** - Organisation unit selection -- **DynamicDimension** - Generic dimension component for categories, etc. -- **DimensionsPanel** - Layout manager for drag/drop dimension arrangement -- **PivotTable** - Pivot table visualization component -- **FileMenu** - Save/load functionality -- **Interpretations** - Comments and interpretation system -- **Toolbar** - Common toolbar components -- **Options/VisualizationOptions** - Configuration dialogs - -**Modules** (`src/modules/`) - -- **layout/** - Core layout system managing dimensions across columns/rows/filters -- **predefinedDimensions.js** - Data (dx), Period (pe), OrgUnit (ou) dimension utilities -- **visTypes.js** - Visualization type constants and utilities -- **layoutUiRules/** - Business logic for dimension placement rules -- **axis.js** - Axis manipulation utilities -- **fontStyle.js** - Typography configuration -- **valueTypes.js** - DHIS2 value type definitions - -**Visualizations** (`src/visualizations/`) - -- Chart creation and rendering utilities -- Color sets and theming - -### Layout System - -The library centers around a **layout** object with three axes: - -```javascript -{ - columns: [dimensions], // Top/horizontal in tables, series in charts - rows: [dimensions], // Left/vertical in tables, category in charts - filters: [dimensions] // Applied as filters to data -} -``` - -Each **dimension** has: - -```javascript -{ - dimension: "dx", // Dimension ID (dx=data, pe=period, ou=orgunit) - items: [{id: "..."}] // Selected items -} -``` - -### Key Exports - -The library exports components, API utilities, and layout manipulation functions. Main categories: - -- **Components**: DataDimension, PeriodDimension, OrgUnitDimension, DimensionsPanel, PivotTable, FileMenu -- **Layout utilities**: Layout manipulation, axis utilities, dimension management -- **API utilities**: Analytics API wrapper, dimension fetching -- **Constants**: Visualization types, layout types, axis IDs, dimension IDs - -### Development Notes - -- Uses DHIS2 design system (`@dhis2/ui`) and app platform (`@dhis2/app-runtime`) -- Built with React, exports as both ES and CommonJS modules -- Styling with styled-jsx -- Internationalization with `@dhis2/d2-i18n` -- Uses d2-app-scripts for building/testing (DHIS2's CLI toolchain) -- ESLint extends `@dhis2/cli-style` React configuration - -### Testing Setup - -- Jest with React Testing Library -- Setup file: `config/setupTestingLibrary.js` -- Test files alongside source code -- Excludes `/node_modules/` and `/build/` from test paths From 70a1447a55e9057b35c9aab722c1238a3b6f2817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Tue, 27 Jan 2026 12:52:51 +0100 Subject: [PATCH 06/14] fix: avoid edge case crash --- .../PeriodDimension/PeriodTransfer.js | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 5853a58bc..26593a4f6 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, NoticeBox, TabBar, Tab, Transfer } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useState, useMemo } from 'react' +import React, { useEffect, useState, useMemo } from 'react' import PeriodIcon from '../../assets/DimensionItemIcons/PeriodIcon.js' //TODO: Reimplement the icon.js import i18n from '../../locales/index.js' import { @@ -149,6 +149,33 @@ const PeriodTransfer = ({ year: defaultFixedPeriodYear.toString(), }) + useEffect(() => { + if (!defaultRelativePeriodType) { + return + } + setRelativeFilter({ periodType: defaultRelativePeriodType.id }) + if (isRelative) { + setAllPeriods(defaultRelativePeriodType.getPeriods()) + } + }, [defaultRelativePeriodType]) // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (!defaultFixedPeriodType) { + return + } + setFixedFilter((prev) => ({ + ...prev, + periodType: defaultFixedPeriodType.id, + })) + if (!isRelative) { + setAllPeriods( + defaultFixedPeriodType.getPeriods( + fixedPeriodConfig(Number(fixedFilter.year)) + ) || [] + ) + } + }, [defaultFixedPeriodType]) // eslint-disable-line react-hooks/exhaustive-deps + const isActive = (value) => { const item = selectedItems.find((item) => item.id === value) return !item || item.isActive From 41c304ec89a18f036270bcc358c26a563e202932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Thu, 29 Jan 2026 13:32:50 +0100 Subject: [PATCH 07/14] feat: add support for all period types from @dhis2/multi-calendar-dates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the period type mappings to include all 52 period types supported by @dhis2/multi-calendar-dates library, including all 12 financial year variants (FYJAN-FYDEC), weekly variants, quarterly variants, and six-monthly variants. This ensures that when a period type like FinancialSep is enabled on the server, it correctly appears in the fixed periods dropdown. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- i18n/en.pot | 40 ++++- .../PeriodDimension/PeriodTransfer.js | 103 +++++++----- .../__tests__/fixedPeriods.spec.js | 14 +- .../PeriodDimension/__tests__/utils.spec.js | 12 +- .../utils/enabledPeriodTypes.js | 69 +++++++- .../PeriodDimension/utils/fixedPeriods.js | 147 ++++++++++++------ src/components/PeriodDimension/utils/index.js | 14 +- 7 files changed, 292 insertions(+), 107 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 79bf877b8..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: 2026-01-27T09:57:38.828Z\n" -"PO-Revision-Date: 2026-01-27T09:57:38.828Z\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" @@ -880,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/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 26593a4f6..051b17045 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, NoticeBox, TabBar, Tab, Transfer } from '@dhis2/ui' import PropTypes from 'prop-types' -import React, { useEffect, useState, useMemo } 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 { @@ -137,9 +137,7 @@ 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 || '', @@ -149,32 +147,69 @@ const PeriodTransfer = ({ year: defaultFixedPeriodYear.toString(), }) - useEffect(() => { - if (!defaultRelativePeriodType) { - return + 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 }) } - setRelativeFilter({ periodType: defaultRelativePeriodType.id }) if (isRelative) { - setAllPeriods(defaultRelativePeriodType.getPeriods()) + setUserPeriods(null) } - }, [defaultRelativePeriodType]) // eslint-disable-line react-hooks/exhaustive-deps + } - useEffect(() => { - if (!defaultFixedPeriodType) { - return + if (prevEffectiveFixedRef.current !== effectiveFixedFilterType) { + prevEffectiveFixedRef.current = effectiveFixedFilterType + if (fixedFilter.periodType !== effectiveFixedFilterType) { + setFixedFilter((prev) => ({ + ...prev, + periodType: effectiveFixedFilterType, + })) } - setFixedFilter((prev) => ({ - ...prev, - periodType: defaultFixedPeriodType.id, - })) if (!isRelative) { - setAllPeriods( - defaultFixedPeriodType.getPeriods( - fixedPeriodConfig(Number(fixedFilter.year)) - ) || [] + 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))) || + [] ) } - }, [defaultFixedPeriodType]) // eslint-disable-line react-hooks/exhaustive-deps + }, [ + isRelative, + effectiveRelativeFilterType, + effectiveFixedFilterType, + filteredRelativeOptions, + filteredFixedOptions, + fixedFilter.year, + ]) + + const allPeriods = userPeriods !== null ? userPeriods : derivedPeriods const isActive = (value) => { const item = selectedItems.find((item) => item.id === value) @@ -184,21 +219,7 @@ const PeriodTransfer = ({ const onIsRelativeClick = (state) => { if (state !== isRelative) { setIsRelative(state) - if (state) { - const selectedOption = filteredRelativeOptions.find( - (opt) => opt.id === relativeFilter.periodType - ) - setAllPeriods(selectedOption?.getPeriods() || []) - } else { - const selectedOption = filteredFixedOptions.find( - (opt) => opt.id === fixedFilter.periodType - ) - setAllPeriods( - selectedOption?.getPeriods( - fixedPeriodConfig(Number(fixedFilter.year)) - ) || [] - ) - } + setUserPeriods(null) } } @@ -233,20 +254,20 @@ const PeriodTransfer = ({
{isRelative ? ( { setRelativeFilter({ periodType: filter }) const selectedOption = filteredRelativeOptions.find( (opt) => opt.id === filter ) - setAllPeriods(selectedOption?.getPeriods() || []) + setUserPeriods(selectedOption?.getPeriods() || []) }} dataTest={`${dataTest}-relative-period-filter`} availableOptions={filteredRelativeOptions} /> ) : ( { onSelectFixedPeriods({ @@ -276,7 +297,7 @@ const PeriodTransfer = ({ const selectedOption = filteredFixedOptions.find( (opt) => opt.id === filter.periodType ) - setAllPeriods( + setUserPeriods( selectedOption?.getPeriods( fixedPeriodConfig(Number(filter.year)) ) || [] 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/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js index 1ed74ad94..896a20643 100644 --- a/src/components/PeriodDimension/utils/enabledPeriodTypes.js +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -2,21 +2,55 @@ export const SERVER_PT_TO_MULTI_CALENDAR_PT = { Daily: 'DAILY', Weekly: 'WEEKLY', + WeeklyMonday: 'WEEKLYMON', + WeeklyTuesday: 'WEEKLYTUE', WeeklyWednesday: 'WEEKLYWED', WeeklyThursday: 'WEEKLYTHU', + WeeklyFriday: 'WEEKLYFRI', WeeklySaturday: 'WEEKLYSAT', WeeklySunday: 'WEEKLYSUN', BiWeekly: 'BIWEEKLY', Monthly: 'MONTHLY', BiMonthly: 'BIMONTHLY', Quarterly: 'QUARTERLY', + QuarterlyJan: 'QUARTERLYJAN', + QuarterlyFeb: 'QUARTERLYFEB', + QuarterlyMar: 'QUARTERLYMAR', + QuarterlyApr: 'QUARTERLYAPR', + QuarterlyMay: 'QUARTERLYMAY', + QuarterlyJun: 'QUARTERLYJUN', + QuarterlyJul: 'QUARTERLYJUL', + QuarterlyAug: 'QUARTERLYAUG', + QuarterlySep: 'QUARTERLYSEP', + QuarterlyOct: 'QUARTERLYOCT', + QuarterlyNov: 'QUARTERLYNOV', + QuarterlyDec: 'QUARTERLYDEC', SixMonthly: 'SIXMONTHLY', + SixMonthlyJan: 'SIXMONTHLYJAN', + SixMonthlyFeb: 'SIXMONTHLYFEB', + SixMonthlyMar: 'SIXMONTHLYMAR', SixMonthlyApril: 'SIXMONTHLYAPR', + SixMonthlyMay: 'SIXMONTHLYMAY', + SixMonthlyJun: 'SIXMONTHLYJUN', + SixMonthlyJul: 'SIXMONTHLYJUL', + SixMonthlyAug: 'SIXMONTHLYAUG', + SixMonthlySep: 'SIXMONTHLYSEP', + SixMonthlyOct: 'SIXMONTHLYOCT', + SixMonthlyNov: 'SIXMONTHLYNOV', + SixMonthlyDec: 'SIXMONTHLYDEC', Yearly: 'YEARLY', + FinancialJan: 'FYJAN', + FinancialFeb: 'FYFEB', + FinancialMar: 'FYMAR', FinancialApril: 'FYAPR', + FinancialMay: 'FYMAY', + FinancialJun: 'FYJUN', FinancialJuly: 'FYJUL', + FinancialAug: 'FYAUG', + FinancialSep: 'FYSEP', FinancialOct: 'FYOCT', FinancialNov: 'FYNOV', + FinancialDec: 'FYDEC', } // Mapping from relative period categories to their corresponding fixed period types @@ -24,16 +58,47 @@ export const RP_CATEGORY_TO_FP_DEPENDENCIES = { DAILY: ['Daily'], WEEKLY: [ 'Weekly', + 'WeeklyMonday', + 'WeeklyTuesday', 'WeeklyWednesday', 'WeeklyThursday', + 'WeeklyFriday', 'WeeklySaturday', 'WeeklySunday', ], BIWEEKLY: ['BiWeekly'], MONTHLY: ['Monthly'], BIMONTHLY: ['BiMonthly'], - QUARTERLY: ['Quarterly'], - SIXMONTHLY: ['SixMonthly', 'SixMonthlyApril'], + QUARTERLY: [ + 'Quarterly', + 'QuarterlyJan', + 'QuarterlyFeb', + 'QuarterlyMar', + 'QuarterlyApr', + 'QuarterlyMay', + 'QuarterlyJun', + 'QuarterlyJul', + 'QuarterlyAug', + 'QuarterlySep', + 'QuarterlyOct', + 'QuarterlyNov', + 'QuarterlyDec', + ], + SIXMONTHLY: [ + 'SixMonthly', + 'SixMonthlyJan', + 'SixMonthlyFeb', + 'SixMonthlyMar', + 'SixMonthlyApril', + 'SixMonthlyMay', + 'SixMonthlyJun', + 'SixMonthlyJul', + 'SixMonthlyAug', + 'SixMonthlySep', + 'SixMonthlyOct', + 'SixMonthlyNov', + 'SixMonthlyDec', + ], YEARLY: ['Yearly'], } 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 = [], From d322db7255a1d2ce7b97d868dd8c296f372723e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Fri, 13 Feb 2026 17:54:45 +0100 Subject: [PATCH 08/14] fix: custom relative period item names wip --- .../PeriodDimension/PeriodDimension.js | 15 +++++++--- .../PeriodDimension/PeriodTransfer.js | 28 +++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/components/PeriodDimension/PeriodDimension.js b/src/components/PeriodDimension/PeriodDimension.js index 92dadc4a3..17d644fdf 100644 --- a/src/components/PeriodDimension/PeriodDimension.js +++ b/src/components/PeriodDimension/PeriodDimension.js @@ -78,6 +78,7 @@ const PeriodDimension = ({ const enabledTypes = v43Data.enabledPeriodTypes + if (!enabledTypes || enabledTypes.length === 0) { return { enabledTypes: [], @@ -88,16 +89,21 @@ const PeriodDimension = ({ } let financialYearStart = null + let financialYearDisplayLabel = null if (v43Data.financialYearStart?.analyticsFinancialYearStart) { const fyStartValue = v43Data.financialYearStart.analyticsFinancialYearStart const mappedFyPt = FY_SETTING_TO_SERVER_PT[fyStartValue] - if ( - mappedFyPt && - enabledTypes.some((pt) => pt.name === mappedFyPt) - ) { + const matchingPt = enabledTypes.find( + (pt) => pt.name === mappedFyPt + ) + matchingPt.displayLabel = 'Academic year' + if (mappedFyPt && matchingPt) { financialYearStart = fyStartValue + if (matchingPt.displayLabel) { + financialYearDisplayLabel = matchingPt.displayLabel + } } } @@ -107,6 +113,7 @@ const PeriodDimension = ({ return { enabledTypes, financialYearStart, + financialYearDisplayLabel, analysisRelativePeriod, noEnabledTypes: false, } diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 051b17045..fdb05a1c9 100644 --- a/src/components/PeriodDimension/PeriodTransfer.js +++ b/src/components/PeriodDimension/PeriodTransfer.js @@ -58,19 +58,42 @@ const PeriodTransfer = ({ }) => { const { filteredFixedOptions, filteredRelativeOptions } = useMemo(() => { if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { - const { enabledTypes, financialYearStart } = enabledPeriodTypesData + const { enabledTypes, financialYearStart, financialYearDisplayLabel } = enabledPeriodTypesData const filteredFixed = filterEnabledFixedPeriodTypes( getFixedPeriodsOptions(periodsSettings), enabledTypes ) - const filteredRelative = filterEnabledRelativePeriodTypes( + let filteredRelative = filterEnabledRelativePeriodTypes( getRelativePeriodsOptions(), enabledTypes, financialYearStart ) + if (financialYearDisplayLabel) { + const fyPeriodLabels = { + THIS_FINANCIAL_YEAR: `This ${financialYearDisplayLabel}`, + LAST_FINANCIAL_YEAR: `Last ${financialYearDisplayLabel}`, + LAST_5_FINANCIAL_YEARS: `Last 5 ${financialYearDisplayLabel}`, + } + filteredRelative = filteredRelative.map((option) => + option.id === 'FINANCIAL' + ? { + ...option, + name: financialYearDisplayLabel, + getPeriods: () => + option.getPeriods().map((period) => ({ + ...period, + name: + fyPeriodLabels[period.id] || + period.name, + })), + } + : option + ) + } + return { filteredFixedOptions: filteredFixed, filteredRelativeOptions: filteredRelative, @@ -361,6 +384,7 @@ PeriodTransfer.propTypes = { enabledPeriodTypesData: PropTypes.shape({ analysisRelativePeriod: PropTypes.string, enabledTypes: PropTypes.array, + financialYearDisplayLabel: PropTypes.string, financialYearStart: PropTypes.string, noEnabledTypes: PropTypes.bool, }), From 60d94f564eeb15d2d84ed5ed8d54f30d20e7ddce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Mon, 16 Feb 2026 20:47:37 +0100 Subject: [PATCH 09/14] refactor: use data output period types hook --- .../PeriodDimension/PeriodDimension.js | 91 +------------- .../PeriodDimension/PeriodTransfer.js | 37 +++--- .../useDataOutputPeriodTypes.js | 113 ++++++++++++++++++ .../PivotTable/PivotTableTitleRows.js | 2 +- src/index.js | 1 + src/visualizations/util/getFilterText.js | 6 +- 6 files changed, 138 insertions(+), 112 deletions(-) create mode 100644 src/components/PeriodDimension/useDataOutputPeriodTypes.js diff --git a/src/components/PeriodDimension/PeriodDimension.js b/src/components/PeriodDimension/PeriodDimension.js index 17d644fdf..8a51672f9 100644 --- a/src/components/PeriodDimension/PeriodDimension.js +++ b/src/components/PeriodDimension/PeriodDimension.js @@ -1,8 +1,8 @@ import { useConfig, useDataQuery } from '@dhis2/app-runtime' import PropTypes from 'prop-types' -import React, { useEffect, useMemo } from 'react' import { DIMENSION_ID_PERIOD } from '../../modules/predefinedDimensions.js' import PeriodTransfer from './PeriodTransfer.js' +import { useDataOutputPeriodTypes } from './useDataOutputPeriodTypes.js' const userSettingsQuery = { userSettings: { @@ -13,26 +13,6 @@ const userSettingsQuery = { }, } -const v43Query = { - enabledPeriodTypes: { - resource: 'configuration/dataOutputPeriodTypes', - }, - financialYearStart: { - resource: 'systemSettings/analyticsFinancialYearStart', - }, - analysisRelativePeriod: { - resource: 'systemSettings/keyAnalysisRelativePeriod', - }, -} - -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', -} - const SELECTED_PERIODS_PROP_DEFAULT = [] const PeriodDimension = ({ @@ -44,22 +24,11 @@ const PeriodDimension = ({ height, }) => { const config = useConfig() - const { systemInfo, serverVersion } = config + const { systemInfo } = config const userSettingsResult = useDataQuery(userSettingsQuery) - const supportsEnabledPeriodTypes = serverVersion.minor >= 43 - - const { - data: v43Data, - error: v43Error, - refetch: v43Refetch, - } = useDataQuery(v43Query, { lazy: true }) - - useEffect(() => { - if (supportsEnabledPeriodTypes) { - v43Refetch() - } - }, [supportsEnabledPeriodTypes, v43Refetch]) + const { supportsEnabledPeriodTypes, enabledPeriodTypesData } = + useDataOutputPeriodTypes() const { calendar = 'gregory' } = systemInfo const { data: { userSettings: { keyUiLocale: locale } = {} } = {} } = @@ -67,58 +36,6 @@ const PeriodDimension = ({ const periodsSettings = { calendar, locale } - 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, - } - } - - 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 - ) - matchingPt.displayLabel = 'Academic year' - if (mappedFyPt && matchingPt) { - financialYearStart = fyStartValue - if (matchingPt.displayLabel) { - financialYearDisplayLabel = matchingPt.displayLabel - } - } - } - - const analysisRelativePeriod = - v43Data.analysisRelativePeriod?.keyAnalysisRelativePeriod || null - - return { - enabledTypes, - financialYearStart, - financialYearDisplayLabel, - analysisRelativePeriod, - noEnabledTypes: false, - } - }, [supportsEnabledPeriodTypes, v43Data, v43Error]) - const selectPeriods = (periods) => { onSelect({ dimensionId: DIMENSION_ID_PERIOD, diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index fdb05a1c9..2aedb33fe 100644 --- a/src/components/PeriodDimension/PeriodTransfer.js +++ b/src/components/PeriodDimension/PeriodTransfer.js @@ -58,7 +58,7 @@ const PeriodTransfer = ({ }) => { const { filteredFixedOptions, filteredRelativeOptions } = useMemo(() => { if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { - const { enabledTypes, financialYearStart, financialYearDisplayLabel } = enabledPeriodTypesData + const { enabledTypes, financialYearStart, financialYearDisplayLabel, metaData } = enabledPeriodTypesData const filteredFixed = filterEnabledFixedPeriodTypes( getFixedPeriodsOptions(periodsSettings), @@ -71,25 +71,20 @@ const PeriodTransfer = ({ financialYearStart ) - if (financialYearDisplayLabel) { - const fyPeriodLabels = { - THIS_FINANCIAL_YEAR: `This ${financialYearDisplayLabel}`, - LAST_FINANCIAL_YEAR: `Last ${financialYearDisplayLabel}`, - LAST_5_FINANCIAL_YEARS: `Last 5 ${financialYearDisplayLabel}`, - } + if (financialYearDisplayLabel && metaData) { filteredRelative = filteredRelative.map((option) => option.id === 'FINANCIAL' ? { - ...option, - name: financialYearDisplayLabel, - getPeriods: () => - option.getPeriods().map((period) => ({ - ...period, - name: - fyPeriodLabels[period.id] || - period.name, - })), - } + ...option, + name: financialYearDisplayLabel, + getPeriods: () => + option.getPeriods().map((period) => ({ + ...period, + name: + metaData[period.id]?.name || + period.name, + })), + } : option ) } @@ -138,11 +133,11 @@ const PeriodTransfer = ({ const defaultRelativePeriodType = supportsEnabledPeriodTypes && bestRelativePeriod ? filteredRelativeOptions.find( - (opt) => opt.id === bestRelativePeriod.categoryId - ) + (opt) => opt.id === bestRelativePeriod.categoryId + ) : filteredRelativeOptions.find((opt) => opt.id === MONTHLY) || - filteredRelativeOptions.find((opt) => opt.id === QUARTERLY) || - filteredRelativeOptions[0] + filteredRelativeOptions.find((opt) => opt.id === QUARTERLY) || + filteredRelativeOptions[0] const defaultFixedPeriodType = filteredFixedOptions.find((opt) => opt.id === MONTHLY) || diff --git a/src/components/PeriodDimension/useDataOutputPeriodTypes.js b/src/components/PeriodDimension/useDataOutputPeriodTypes.js new file mode 100644 index 000000000..d040303b5 --- /dev/null +++ b/src/components/PeriodDimension/useDataOutputPeriodTypes.js @@ -0,0 +1,113 @@ +import { useConfig, useDataQuery } from '@dhis2/app-runtime' +import { useEffect, useMemo } from 'react' + +const v43Query = { + enabledPeriodTypes: { + resource: 'configuration/dataOutputPeriodTypes', + }, + financialYearStart: { + resource: 'systemSettings/analyticsFinancialYearStart', + }, + analysisRelativePeriod: { + resource: 'systemSettings/keyAnalysisRelativePeriod', + }, +} + +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', +} + +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, + } + } + + 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 + ) + //TODO: remove + matchingPt.displayLabel = 'Academic year' + if (mappedFyPt && matchingPt) { + financialYearStart = fyStartValue + if (matchingPt.displayLabel) { + financialYearDisplayLabel = matchingPt.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}`, + }, + } + : null + + return { + enabledTypes, + financialYearStart, + financialYearDisplayLabel, + analysisRelativePeriod, + metaData, + noEnabledTypes: false, + } + }, [supportsEnabledPeriodTypes, v43Data, v43Error]) + + return { + supportsEnabledPeriodTypes, + enabledPeriodTypesData, + } +} + +export { useDataOutputPeriodTypes } diff --git a/src/components/PivotTable/PivotTableTitleRows.js b/src/components/PivotTable/PivotTableTitleRows.js index b29439261..051fd4339 100644 --- a/src/components/PivotTable/PivotTableTitleRows.js +++ b/src/components/PivotTable/PivotTableTitleRows.js @@ -6,7 +6,7 @@ import { PivotTableTitleRow } from './PivotTableTitleRow.js' export const PivotTableTitleRows = ({ clippingResult, width }) => { const engine = usePivotTableEngine() - + console.log("engine.rawData", engine.rawData) return ( <> {engine.options.title ? ( 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 From dd2298898d5564359a5d6691d7e701b258871747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Tue, 17 Feb 2026 15:36:46 +0100 Subject: [PATCH 10/14] chore: remove log --- src/components/PivotTable/PivotTableTitleRows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PivotTable/PivotTableTitleRows.js b/src/components/PivotTable/PivotTableTitleRows.js index 051fd4339..b29439261 100644 --- a/src/components/PivotTable/PivotTableTitleRows.js +++ b/src/components/PivotTable/PivotTableTitleRows.js @@ -6,7 +6,7 @@ import { PivotTableTitleRow } from './PivotTableTitleRow.js' export const PivotTableTitleRows = ({ clippingResult, width }) => { const engine = usePivotTableEngine() - console.log("engine.rawData", engine.rawData) + return ( <> {engine.options.title ? ( From 1ddd9df5b7a38ffa66853cb2d42d95f19ac79847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Thu, 19 Feb 2026 11:47:56 +0100 Subject: [PATCH 11/14] fix: show custom label when passing selected items --- src/components/PeriodDimension/PeriodDimension.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/PeriodDimension/PeriodDimension.js b/src/components/PeriodDimension/PeriodDimension.js index 8a51672f9..3c23386a3 100644 --- a/src/components/PeriodDimension/PeriodDimension.js +++ b/src/components/PeriodDimension/PeriodDimension.js @@ -42,10 +42,21 @@ const PeriodDimension = ({ items: periods, }) } + + // DHIS2-20270 Apply custom period type label to period names + const metaData = enabledPeriodTypesData?.metaData + const selectedPeriodsWithCustomDisplayNames = metaData + ? selectedPeriods.map((period) => + metaData[period.id] + ? { ...period, name: metaData[period.id].name } + : period + ) + : selectedPeriods + return ( Date: Fri, 20 Feb 2026 11:56:17 +0100 Subject: [PATCH 12/14] fix: do not try to rescue misconfiguration in settings app --- .../PeriodDimension/PeriodTransfer.js | 34 +++--- .../useDataOutputPeriodTypes.js | 7 +- .../utils/enabledPeriodTypes.js | 107 ------------------ 3 files changed, 18 insertions(+), 130 deletions(-) diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 2aedb33fe..0857c6dd4 100644 --- a/src/components/PeriodDimension/PeriodTransfer.js +++ b/src/components/PeriodDimension/PeriodTransfer.js @@ -16,7 +16,6 @@ import RelativePeriodFilter from './RelativePeriodFilter.js' import { filterEnabledFixedPeriodTypes, filterEnabledRelativePeriodTypes, - findBestAvailableRelativePeriod, } from './utils/enabledPeriodTypes.js' import { getFixedPeriodsOptions } from './utils/fixedPeriods.js' import { MONTHLY, QUARTERLY, filterPeriodTypesById } from './utils/index.js' @@ -115,29 +114,24 @@ const PeriodTransfer = ({ periodsSettings, ]) - const bestRelativePeriod = useMemo(() => { - if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { - const { analysisRelativePeriod } = enabledPeriodTypesData - return findBestAvailableRelativePeriod( - filteredRelativeOptions, - analysisRelativePeriod - ) - } - return null - }, [ - supportsEnabledPeriodTypes, - enabledPeriodTypesData, - filteredRelativeOptions, - ]) + const analysisRelativePeriod = + enabledPeriodTypesData?.analysisRelativePeriod - const defaultRelativePeriodType = - supportsEnabledPeriodTypes && bestRelativePeriod - ? filteredRelativeOptions.find( - (opt) => opt.id === bestRelativePeriod.categoryId + const defaultRelativePeriodType = (() => { + if (analysisRelativePeriod) { + const match = filteredRelativeOptions.find((opt) => + opt.getPeriods().some((p) => p.id === analysisRelativePeriod) ) - : filteredRelativeOptions.find((opt) => opt.id === MONTHLY) || + 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) || diff --git a/src/components/PeriodDimension/useDataOutputPeriodTypes.js b/src/components/PeriodDimension/useDataOutputPeriodTypes.js index d040303b5..94dc776de 100644 --- a/src/components/PeriodDimension/useDataOutputPeriodTypes.js +++ b/src/components/PeriodDimension/useDataOutputPeriodTypes.js @@ -5,6 +5,7 @@ const v43Query = { enabledPeriodTypes: { resource: 'configuration/dataOutputPeriodTypes', }, + // v43-only: analyticsFinancialYearStart is removed in v44 financialYearStart: { resource: 'systemSettings/analyticsFinancialYearStart', }, @@ -13,6 +14,7 @@ const v43Query = { }, } +// v43-only: analyticsFinancialYearStart is removed in v44 const FY_SETTING_TO_SERVER_PT = { FINANCIAL_YEAR_APRIL: 'FinancialApril', FINANCIAL_YEAR_JULY: 'FinancialJuly', @@ -57,6 +59,7 @@ const useDataOutputPeriodTypes = () => { } } + // v43-only: financial year logic goes away in v44 let financialYearStart = null let financialYearDisplayLabel = null if (v43Data.financialYearStart?.analyticsFinancialYearStart) { @@ -67,9 +70,7 @@ const useDataOutputPeriodTypes = () => { const matchingPt = enabledTypes.find( (pt) => pt.name === mappedFyPt ) - //TODO: remove - matchingPt.displayLabel = 'Academic year' - if (mappedFyPt && matchingPt) { + if (matchingPt) { financialYearStart = fyStartValue if (matchingPt.displayLabel) { financialYearDisplayLabel = matchingPt.displayLabel diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js index 896a20643..905894001 100644 --- a/src/components/PeriodDimension/utils/enabledPeriodTypes.js +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -173,110 +173,3 @@ export const filterEnabledRelativePeriodTypes = ( }) } -// Mapping from keyAnalysisRelativePeriod system setting values (RP IDs) to categories -export const ANALYSIS_RELATIVE_PERIOD_MAPPING = { - THIS_WEEK: { id: 'THIS_WEEK', category: 'WEEKLY' }, - LAST_WEEK: { id: 'LAST_WEEK', category: 'WEEKLY' }, - LAST_4_WEEKS: { id: 'LAST_4_WEEKS', category: 'WEEKLY' }, - LAST_12_WEEKS: { id: 'LAST_12_WEEKS', category: 'WEEKLY' }, - LAST_52_WEEKS: { id: 'LAST_52_WEEKS', category: 'WEEKLY' }, - THIS_MONTH: { id: 'THIS_MONTH', category: 'MONTHLY' }, - LAST_MONTH: { id: 'LAST_MONTH', category: 'MONTHLY' }, - MONTHS_THIS_YEAR: { id: 'MONTHS_THIS_YEAR', category: 'MONTHLY' }, - // Note: MONTHS_LAST_YEAR does not exist in current RP definitions - LAST_3_MONTHS: { id: 'LAST_3_MONTHS', category: 'MONTHLY' }, - LAST_6_MONTHS: { id: 'LAST_6_MONTHS', category: 'MONTHLY' }, - LAST_12_MONTHS: { id: 'LAST_12_MONTHS', category: 'MONTHLY' }, - THIS_BIMONTH: { id: 'THIS_BIMONTH', category: 'BIMONTHLY' }, - LAST_BIMONTH: { id: 'LAST_BIMONTH', category: 'BIMONTHLY' }, - LAST_6_BIMONTHS: { id: 'LAST_6_BIMONTHS', category: 'BIMONTHLY' }, - THIS_QUARTER: { id: 'THIS_QUARTER', category: 'QUARTERLY' }, - LAST_QUARTER: { id: 'LAST_QUARTER', category: 'QUARTERLY' }, - QUARTERS_THIS_YEAR: { id: 'QUARTERS_THIS_YEAR', category: 'QUARTERLY' }, - // Note: QUARTERS_LAST_YEAR does not exist in current RP definitions - LAST_4_QUARTERS: { id: 'LAST_4_QUARTERS', category: 'QUARTERLY' }, - THIS_SIX_MONTH: { id: 'THIS_SIX_MONTH', category: 'SIXMONTHLY' }, - LAST_SIX_MONTH: { id: 'LAST_SIX_MONTH', category: 'SIXMONTHLY' }, - LAST_2_SIXMONTHS: { id: 'LAST_2_SIXMONTHS', category: 'SIXMONTHLY' }, - THIS_YEAR: { id: 'THIS_YEAR', category: 'YEARLY' }, - LAST_YEAR: { id: 'LAST_YEAR', category: 'YEARLY' }, - LAST_5_YEARS: { id: 'LAST_5_YEARS', category: 'YEARLY' }, - LAST_10_YEARS: { id: 'LAST_10_YEARS', category: 'YEARLY' }, - THIS_FINANCIAL_YEAR: { id: 'THIS_FINANCIAL_YEAR', category: 'FINANCIAL' }, - LAST_FINANCIAL_YEAR: { id: 'LAST_FINANCIAL_YEAR', category: 'FINANCIAL' }, - LAST_5_FINANCIAL_YEARS: { - id: 'LAST_5_FINANCIAL_YEARS', - category: 'FINANCIAL', - }, -} - -// Fallback priority order for RP categories (closest to most commonly used) -const RP_CATEGORY_FALLBACK_ORDER = [ - 'MONTHLY', // Most common - 'QUARTERLY', // Close alternative to monthly - 'YEARLY', // Longer term view - 'WEEKLY', // More granular - 'SIXMONTHLY', // Mid-term - 'BIMONTHLY', // Less common - 'FINANCIAL', // Depends on system config - 'BIWEEKLY', // Least common - 'DAILY', // Very granular -] - -/** - * Find the best available relative period based on keyAnalysisRelativePeriod setting - * @param {Array} enabledRelativeOptions - Available RP categories - * @param {string|null} analysisRelativePeriod - System setting value - * @returns {Object} { categoryId, periodId } or null - */ -export const findBestAvailableRelativePeriod = ( - enabledRelativeOptions, - analysisRelativePeriod -) => { - if (!enabledRelativeOptions || enabledRelativeOptions.length === 0) { - return null - } - - const enabledCategoryIds = new Set( - enabledRelativeOptions.map((opt) => opt.id) - ) - - // Try to use the configured analysis relative period first - if ( - analysisRelativePeriod && - ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod] - ) { - const { id: periodId, category: categoryId } = - ANALYSIS_RELATIVE_PERIOD_MAPPING[analysisRelativePeriod] - - if (enabledCategoryIds.has(categoryId)) { - return { categoryId, periodId } - } - } - - // Fall back to the highest priority enabled category - for (const categoryId of RP_CATEGORY_FALLBACK_ORDER) { - if (enabledCategoryIds.has(categoryId)) { - // Use the first period from that category as default - const categoryOption = enabledRelativeOptions.find( - (opt) => opt.id === categoryId - ) - const periods = categoryOption?.getPeriods() || [] - const defaultPeriod = periods[0] - - return { - categoryId, - periodId: defaultPeriod?.id || null, - } - } - } - - // Last resort: use first available category and its first period - const firstCategory = enabledRelativeOptions[0] - const firstPeriod = firstCategory?.getPeriods()?.[0] - - return { - categoryId: firstCategory?.id || null, - periodId: firstPeriod?.id || null, - } -} From f56543311401ca067cbf0e9b067c51536ba066b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Fri, 20 Feb 2026 19:59:25 +0100 Subject: [PATCH 13/14] fix: weekly relative period names --- .../PeriodDimension/PeriodDimension.js | 14 +-- .../PeriodDimension/PeriodTransfer.js | 32 ++---- .../useDataOutputPeriodTypes.js | 59 +++++++++- .../utils/enabledPeriodTypes.js | 101 +++++++++--------- 4 files changed, 119 insertions(+), 87 deletions(-) diff --git a/src/components/PeriodDimension/PeriodDimension.js b/src/components/PeriodDimension/PeriodDimension.js index 3c23386a3..b46dc897f 100644 --- a/src/components/PeriodDimension/PeriodDimension.js +++ b/src/components/PeriodDimension/PeriodDimension.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { DIMENSION_ID_PERIOD } from '../../modules/predefinedDimensions.js' import PeriodTransfer from './PeriodTransfer.js' import { useDataOutputPeriodTypes } from './useDataOutputPeriodTypes.js' +import { applyPeriodNameOverrides } from './utils/enabledPeriodTypes.js' const userSettingsQuery = { userSettings: { @@ -43,15 +44,10 @@ const PeriodDimension = ({ }) } - // DHIS2-20270 Apply custom period type label to period names - const metaData = enabledPeriodTypesData?.metaData - const selectedPeriodsWithCustomDisplayNames = metaData - ? selectedPeriods.map((period) => - metaData[period.id] - ? { ...period, name: metaData[period.id].name } - : period - ) - : selectedPeriods + const selectedPeriodsWithCustomDisplayNames = applyPeriodNameOverrides( + selectedPeriods, + enabledPeriodTypesData?.metaData + ) return ( { const { filteredFixedOptions, filteredRelativeOptions } = useMemo(() => { if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { - const { enabledTypes, financialYearStart, financialYearDisplayLabel, metaData } = enabledPeriodTypesData + const { enabledTypes, financialYearStart, financialYearDisplayLabel, weeklyDisplayLabel, metaData } = enabledPeriodTypesData const filteredFixed = filterEnabledFixedPeriodTypes( getFixedPeriodsOptions(periodsSettings), enabledTypes ) - let filteredRelative = filterEnabledRelativePeriodTypes( - getRelativePeriodsOptions(), - enabledTypes, - financialYearStart + const filteredRelative = applyDisplayLabelOverrides( + filterEnabledRelativePeriodTypes( + getRelativePeriodsOptions(), + enabledTypes, + financialYearStart + ), + { financialYearDisplayLabel, weeklyDisplayLabel, metaData } ) - if (financialYearDisplayLabel && metaData) { - filteredRelative = filteredRelative.map((option) => - option.id === 'FINANCIAL' - ? { - ...option, - name: financialYearDisplayLabel, - getPeriods: () => - option.getPeriods().map((period) => ({ - ...period, - name: - metaData[period.id]?.name || - period.name, - })), - } - : option - ) - } - return { filteredFixedOptions: filteredFixed, filteredRelativeOptions: filteredRelative, diff --git a/src/components/PeriodDimension/useDataOutputPeriodTypes.js b/src/components/PeriodDimension/useDataOutputPeriodTypes.js index 94dc776de..6bda384a0 100644 --- a/src/components/PeriodDimension/useDataOutputPeriodTypes.js +++ b/src/components/PeriodDimension/useDataOutputPeriodTypes.js @@ -12,6 +12,10 @@ const v43Query = { analysisRelativePeriod: { resource: 'systemSettings/keyAnalysisRelativePeriod', }, + // v43-only: analyticsWeeklyStart is removed in v44 + weeklyStart: { + resource: 'systemSettings/analyticsWeeklyStart', + }, } // v43-only: analyticsFinancialYearStart is removed in v44 @@ -23,6 +27,16 @@ const FY_SETTING_TO_SERVER_PT = { 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 @@ -78,11 +92,27 @@ const useDataOutputPeriodTypes = () => { } } + // 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 - ? { + const metaData = { + ...(financialYearDisplayLabel && { THIS_FINANCIAL_YEAR: { name: `This ${financialYearDisplayLabel}`, }, @@ -92,13 +122,34 @@ const useDataOutputPeriodTypes = () => { LAST_5_FINANCIAL_YEARS: { name: `Last 5 ${financialYearDisplayLabel}`, }, - } - : null + }), + ...(weeklyDisplayLabel && { + THIS_WEEK: { + name: `This ${weeklyDisplayLabel}`, + }, + LAST_WEEK: { + name: `Last ${weeklyDisplayLabel}`, + }, + LAST_4_WEEKS: { + name: `Last 4 ${weeklyDisplayLabel}s`, + }, + LAST_12_WEEKS: { + name: `Last 12 ${weeklyDisplayLabel}s`, + }, + LAST_52_WEEKS: { + name: `Last 52 ${weeklyDisplayLabel}s`, + }, + WEEKS_THIS_YEAR: { + name: `${weeklyDisplayLabel}s this year`, + }, + }), + } return { enabledTypes, financialYearStart, financialYearDisplayLabel, + weeklyDisplayLabel, analysisRelativePeriod, metaData, noEnabledTypes: false, diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js index 905894001..3b0ef184e 100644 --- a/src/components/PeriodDimension/utils/enabledPeriodTypes.js +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -2,8 +2,6 @@ export const SERVER_PT_TO_MULTI_CALENDAR_PT = { Daily: 'DAILY', Weekly: 'WEEKLY', - WeeklyMonday: 'WEEKLYMON', - WeeklyTuesday: 'WEEKLYTUE', WeeklyWednesday: 'WEEKLYWED', WeeklyThursday: 'WEEKLYTHU', WeeklyFriday: 'WEEKLYFRI', @@ -13,44 +11,18 @@ export const SERVER_PT_TO_MULTI_CALENDAR_PT = { Monthly: 'MONTHLY', BiMonthly: 'BIMONTHLY', Quarterly: 'QUARTERLY', - QuarterlyJan: 'QUARTERLYJAN', - QuarterlyFeb: 'QUARTERLYFEB', - QuarterlyMar: 'QUARTERLYMAR', - QuarterlyApr: 'QUARTERLYAPR', - QuarterlyMay: 'QUARTERLYMAY', - QuarterlyJun: 'QUARTERLYJUN', - QuarterlyJul: 'QUARTERLYJUL', - QuarterlyAug: 'QUARTERLYAUG', - QuarterlySep: 'QUARTERLYSEP', - QuarterlyOct: 'QUARTERLYOCT', QuarterlyNov: 'QUARTERLYNOV', - QuarterlyDec: 'QUARTERLYDEC', SixMonthly: 'SIXMONTHLY', - SixMonthlyJan: 'SIXMONTHLYJAN', - SixMonthlyFeb: 'SIXMONTHLYFEB', - SixMonthlyMar: 'SIXMONTHLYMAR', SixMonthlyApril: 'SIXMONTHLYAPR', - SixMonthlyMay: 'SIXMONTHLYMAY', - SixMonthlyJun: 'SIXMONTHLYJUN', - SixMonthlyJul: 'SIXMONTHLYJUL', - SixMonthlyAug: 'SIXMONTHLYAUG', - SixMonthlySep: 'SIXMONTHLYSEP', - SixMonthlyOct: 'SIXMONTHLYOCT', SixMonthlyNov: 'SIXMONTHLYNOV', - SixMonthlyDec: 'SIXMONTHLYDEC', Yearly: 'YEARLY', - FinancialJan: 'FYJAN', FinancialFeb: 'FYFEB', - FinancialMar: 'FYMAR', FinancialApril: 'FYAPR', - FinancialMay: 'FYMAY', - FinancialJun: 'FYJUN', FinancialJuly: 'FYJUL', FinancialAug: 'FYAUG', FinancialSep: 'FYSEP', FinancialOct: 'FYOCT', FinancialNov: 'FYNOV', - FinancialDec: 'FYDEC', } // Mapping from relative period categories to their corresponding fixed period types @@ -58,8 +30,6 @@ export const RP_CATEGORY_TO_FP_DEPENDENCIES = { DAILY: ['Daily'], WEEKLY: [ 'Weekly', - 'WeeklyMonday', - 'WeeklyTuesday', 'WeeklyWednesday', 'WeeklyThursday', 'WeeklyFriday', @@ -71,33 +41,12 @@ export const RP_CATEGORY_TO_FP_DEPENDENCIES = { BIMONTHLY: ['BiMonthly'], QUARTERLY: [ 'Quarterly', - 'QuarterlyJan', - 'QuarterlyFeb', - 'QuarterlyMar', - 'QuarterlyApr', - 'QuarterlyMay', - 'QuarterlyJun', - 'QuarterlyJul', - 'QuarterlyAug', - 'QuarterlySep', - 'QuarterlyOct', 'QuarterlyNov', - 'QuarterlyDec', ], SIXMONTHLY: [ 'SixMonthly', - 'SixMonthlyJan', - 'SixMonthlyFeb', - 'SixMonthlyMar', 'SixMonthlyApril', - 'SixMonthlyMay', - 'SixMonthlyJun', - 'SixMonthlyJul', - 'SixMonthlyAug', - 'SixMonthlySep', - 'SixMonthlyOct', 'SixMonthlyNov', - 'SixMonthlyDec', ], YEARLY: ['Yearly'], } @@ -142,6 +91,56 @@ export const filterEnabledFixedPeriodTypes = ( * @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, From 02e5165977d586c4c3d21b4c282915bbdf6b64a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Henrik=20=C3=98verland?= Date: Fri, 20 Feb 2026 22:28:01 +0100 Subject: [PATCH 14/14] fix: use pt display label in fixed period names --- .../PeriodDimension/PeriodTransfer.js | 8 +++-- .../useDataOutputPeriodTypes.js | 8 ++--- .../utils/enabledPeriodTypes.js | 32 +++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/components/PeriodDimension/PeriodTransfer.js b/src/components/PeriodDimension/PeriodTransfer.js index 04f812290..b6ccadee1 100644 --- a/src/components/PeriodDimension/PeriodTransfer.js +++ b/src/components/PeriodDimension/PeriodTransfer.js @@ -15,6 +15,7 @@ import FixedPeriodFilter from './FixedPeriodFilter.js' import RelativePeriodFilter from './RelativePeriodFilter.js' import { applyDisplayLabelOverrides, + applyFixedPeriodTypeDisplayLabels, filterEnabledFixedPeriodTypes, filterEnabledRelativePeriodTypes, } from './utils/enabledPeriodTypes.js' @@ -60,8 +61,11 @@ const PeriodTransfer = ({ if (supportsEnabledPeriodTypes && enabledPeriodTypesData) { const { enabledTypes, financialYearStart, financialYearDisplayLabel, weeklyDisplayLabel, metaData } = enabledPeriodTypesData - const filteredFixed = filterEnabledFixedPeriodTypes( - getFixedPeriodsOptions(periodsSettings), + const filteredFixed = applyFixedPeriodTypeDisplayLabels( + filterEnabledFixedPeriodTypes( + getFixedPeriodsOptions(periodsSettings), + enabledTypes + ), enabledTypes ) diff --git a/src/components/PeriodDimension/useDataOutputPeriodTypes.js b/src/components/PeriodDimension/useDataOutputPeriodTypes.js index 6bda384a0..57e645c64 100644 --- a/src/components/PeriodDimension/useDataOutputPeriodTypes.js +++ b/src/components/PeriodDimension/useDataOutputPeriodTypes.js @@ -131,16 +131,16 @@ const useDataOutputPeriodTypes = () => { name: `Last ${weeklyDisplayLabel}`, }, LAST_4_WEEKS: { - name: `Last 4 ${weeklyDisplayLabel}s`, + name: `Last 4 ${weeklyDisplayLabel}`, }, LAST_12_WEEKS: { - name: `Last 12 ${weeklyDisplayLabel}s`, + name: `Last 12 ${weeklyDisplayLabel}`, }, LAST_52_WEEKS: { - name: `Last 52 ${weeklyDisplayLabel}s`, + name: `Last 52 ${weeklyDisplayLabel}`, }, WEEKS_THIS_YEAR: { - name: `${weeklyDisplayLabel}s this year`, + name: `${weeklyDisplayLabel} this year`, }, }), } diff --git a/src/components/PeriodDimension/utils/enabledPeriodTypes.js b/src/components/PeriodDimension/utils/enabledPeriodTypes.js index 3b0ef184e..c0b758ab1 100644 --- a/src/components/PeriodDimension/utils/enabledPeriodTypes.js +++ b/src/components/PeriodDimension/utils/enabledPeriodTypes.js @@ -84,6 +84,38 @@ export const filterEnabledFixedPeriodTypes = ( ) } +/** + * 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