From 71616281ac997aa1a6dc3bacf110d087280e91fe Mon Sep 17 00:00:00 2001 From: xurxodev Date: Thu, 23 Apr 2026 09:56:35 +0200 Subject: [PATCH 1/4] fix: persist runAnalyticsBefore/After in MSF Settings --- i18n/en.pot | 4 ++-- .../msf-aggregate-data/pages/MSFEntities.tsx | 2 -- .../msf-aggregate-data/pages/MSFHomePage.tsx | 14 ++++---------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0a68ef12a..37696a306 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-04-20T04:35:46.003Z\n" -"PO-Revision-Date: 2026-04-20T04:35:46.003Z\n" +"POT-Creation-Date: 2026-04-23T07:13:29.327Z\n" +"PO-Revision-Date: 2026-04-23T07:13:29.328Z\n" msgid "" "THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION " diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx index 7ce21597e..c5fd307f8 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx @@ -13,8 +13,6 @@ export type MSFSettings = { lastExecutions: Record; }; -export type PersistedMSFSettings = Omit; - export type AdvancedSettings = { period?: ObjectWithPeriod; }; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 1c54ec16d..edd41caa2 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -10,8 +10,8 @@ import PageHeader from "../../../react/core/components/page-header/PageHeader"; import { useAppContext } from "../../../react/core/contexts/AppContext"; import { AdvancedSettingsDialog } from "../../../react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog"; import { MSFSettingsDialog } from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; -import { AdvancedSettings, defaultMSFSettings, MSFSettings, MSFStorageKey, PersistedMSFSettings } from "./MSFEntities"; -import { executeAggregateData, isGlobalInstance } from "./MSFHomePagePresenter"; +import { AdvancedSettings, defaultMSFSettings, MSFSettings, MSFStorageKey } from "./MSFEntities"; +import { executeAggregateData } from "./MSFHomePagePresenter"; export const MSFHomePage: React.FC = () => { const { api, compositionRoot } = useAppContext(); @@ -42,12 +42,10 @@ export const MSFHomePage: React.FC = () => { }, [api]); useEffect(() => { - compositionRoot.customData.get(MSFStorageKey).then(settings => { + compositionRoot.customData.get(MSFStorageKey).then(settings => { setMsfSettings(oldSettings => ({ ...oldSettings, ...settings, - runAnalyticsBefore: isGlobalInstance() ? "false" : "by-sync-rule-settings", - runAnalyticsAfter: isGlobalInstance() ? "false" : "by-sync-rule-settings", })); }); }, [compositionRoot]); @@ -83,11 +81,7 @@ export const MSFHomePage: React.FC = () => { const handleSaveMSFSettings = async (msfSettings: MSFSettings) => { setShowMSFSettingsDialog(false); setMsfSettings(msfSettings); - await compositionRoot.customData.save(MSFStorageKey, { - ...msfSettings, - runAnalyticsBefore: undefined, - runAnalyticsAfter: undefined, - }); + await compositionRoot.customData.save(MSFStorageKey, msfSettings); }; const snackbar = useSnackbar(); From 60b257fcd41c463f60b188d69ee00a1b1b2f13e9 Mon Sep 17 00:00:00 2001 From: xurxodev Date: Thu, 23 Apr 2026 10:23:44 +0200 Subject: [PATCH 2/4] refactor: model analytics options per panel (before/after) in MSF Settings --- .../msf-settings-dialog/MSFSettingsDialog.tsx | 16 ++++++-- .../msf-aggregate-data/pages/MSFEntities.tsx | 41 ++++++++++++++++++- .../msf-aggregate-data/pages/MSFHomePage.tsx | 16 +++++--- .../pages/MSFHomePagePresenter.ts | 8 ++-- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx index 63012b1a1..698fd0872 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx @@ -3,7 +3,11 @@ import { ConfirmationDialog } from "@eyeseetea/d2-ui-components"; import { Dictionary } from "lodash"; import React, { ChangeEvent, useMemo, useState } from "react"; import i18n from "../../../../../utils/i18n"; -import { MSFSettings, RunAnalyticsSettings } from "../../../../webapp/msf-aggregate-data/pages/MSFEntities"; +import { + defaultAnalyticsOptions, + MSFSettings, + RunAnalyticsSettings, +} from "../../../../webapp/msf-aggregate-data/pages/MSFEntities"; import Dropdown from "../../../core/components/dropdown/Dropdown"; import { Toggle } from "../../../core/components/toggle/Toggle"; import { NamedDate, OrgUnitDateSelector } from "../org-unit-date-selector/OrgUnitDateSelector"; @@ -45,8 +49,12 @@ export const MSFSettingsDialog: React.FC = ({ onClose, o }; const setAnalyticsYears = (event: ChangeEvent) => { - const analyticsYears = parseInt(event.target.value); - updateSettings(settings => ({ ...settings, analyticsYears })); + const lastYears = parseInt(event.target.value); + updateSettings(settings => ({ + ...settings, + analyticsBefore: { ...(settings.analyticsBefore ?? defaultAnalyticsOptions), lastYears }, + analyticsAfter: { ...(settings.analyticsAfter ?? defaultAnalyticsOptions), lastYears }, + })); }; const updateProjectMinimumDates = (projectStartDates: Dictionary) => { @@ -97,7 +105,7 @@ export const MSFSettingsDialog: React.FC = ({ onClose, o diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx index c5fd307f8..0363ca2fa 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx @@ -3,10 +3,22 @@ import { NamedDate } from "../../../react/msf-aggregate-data/components/org-unit export type RunAnalyticsSettings = "true" | "false" | "by-sync-rule-settings"; +export type AnalyticsOptions = { + lastYears: number; + skipAggregate: boolean; + skipResourceTables: boolean; + skipEvents: boolean; + skipEnrollment: boolean; + skipOrgUnitOwnership: boolean; + skipTrackedEntities: boolean; + skipOutliers: boolean; +}; + export type MSFSettings = { runAnalyticsBefore: RunAnalyticsSettings; runAnalyticsAfter: RunAnalyticsSettings; - analyticsYears: number; + analyticsBefore?: AnalyticsOptions; + analyticsAfter?: AnalyticsOptions; projectMinimumDates: Record; deleteDataValuesBeforeSync?: boolean; checkInPreviousPeriods?: boolean; @@ -19,12 +31,37 @@ export type AdvancedSettings = { export const MSFStorageKey = "msf-storage"; +export const defaultAnalyticsOptions: AnalyticsOptions = { + lastYears: 2, + skipAggregate: false, + skipResourceTables: false, + skipEvents: false, + skipEnrollment: false, + skipOrgUnitOwnership: false, + skipTrackedEntities: false, + skipOutliers: false, +}; + export const defaultMSFSettings: MSFSettings = { runAnalyticsBefore: "by-sync-rule-settings", runAnalyticsAfter: "by-sync-rule-settings", - analyticsYears: 2, projectMinimumDates: {}, deleteDataValuesBeforeSync: false, checkInPreviousPeriods: false, lastExecutions: {}, }; + +export type StoredMSFSettings = Partial & { analyticsYears?: number }; + +export function buildMSFSettings(raw: StoredMSFSettings | undefined | null): MSFSettings { + const { analyticsYears, analyticsBefore, analyticsAfter, ...rest } = raw ?? {}; + const legacyPanel: AnalyticsOptions | undefined = + analyticsYears !== undefined ? { ...defaultAnalyticsOptions, lastYears: analyticsYears } : undefined; + + return { + ...defaultMSFSettings, + ...rest, + analyticsBefore: analyticsBefore ?? legacyPanel, + analyticsAfter: analyticsAfter ?? legacyPanel, + }; +} diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index edd41caa2..ec701c60f 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -10,7 +10,14 @@ import PageHeader from "../../../react/core/components/page-header/PageHeader"; import { useAppContext } from "../../../react/core/contexts/AppContext"; import { AdvancedSettingsDialog } from "../../../react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog"; import { MSFSettingsDialog } from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; -import { AdvancedSettings, defaultMSFSettings, MSFSettings, MSFStorageKey } from "./MSFEntities"; +import { + AdvancedSettings, + buildMSFSettings, + defaultMSFSettings, + MSFSettings, + MSFStorageKey, + StoredMSFSettings, +} from "./MSFEntities"; import { executeAggregateData } from "./MSFHomePagePresenter"; export const MSFHomePage: React.FC = () => { @@ -42,11 +49,8 @@ export const MSFHomePage: React.FC = () => { }, [api]); useEffect(() => { - compositionRoot.customData.get(MSFStorageKey).then(settings => { - setMsfSettings(oldSettings => ({ - ...oldSettings, - ...settings, - })); + compositionRoot.customData.get(MSFStorageKey).then(settings => { + setMsfSettings(buildMSFSettings(settings)); }); }, [compositionRoot]); diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index a66ae78c8..681526ec2 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -16,7 +16,7 @@ import { promiseMap } from "../../../../utils/common"; import { formatDateLong } from "../../../../utils/date"; import { availablePeriods } from "../../../../utils/synchronization"; import { CompositionRoot } from "../../../CompositionRoot"; -import { AdvancedSettings, MSFSettings } from "./MSFEntities"; +import { AdvancedSettings, defaultAnalyticsOptions, MSFSettings } from "./MSFEntities"; import { NamedRef, Ref } from "../../../../domain/common/entities/Ref"; type LoggerFunction = (event: string, userType?: "user" | "admin") => void; @@ -92,7 +92,8 @@ export async function executeAggregateData( if (runAnalyticsBeforeIsRequired) { const localInstance = await compositionRoot.instances.getLocal(); - await runAnalytics(localInstance, addEventToProgress, msfSettings.analyticsYears); + const lastYears = msfSettings.analyticsBefore?.lastYears ?? defaultAnalyticsOptions.lastYears; + await runAnalytics(localInstance, addEventToProgress, lastYears); } const reports = await promiseMap(rulesWithoutRunAnalylics, syncRule => @@ -113,8 +114,9 @@ export async function executeAggregateData( await promiseMap(targetInstances, async instanceId => { const instance = await compositionRoot.instances.getById(instanceId); + const lastYears = msfSettings.analyticsAfter?.lastYears ?? defaultAnalyticsOptions.lastYears; instance.match({ - success: async instance => await runAnalytics(instance, addEventToProgress, msfSettings.analyticsYears), + success: async instance => await runAnalytics(instance, addEventToProgress, lastYears), error: () => { addEventToProgress( i18n.t(`An error has occurred retrieving the instance {{name}}`, { From 1964e26699d68006cbf1cc2c4c9e53898277687d Mon Sep 17 00:00:00 2001 From: xurxodev Date: Thu, 23 Apr 2026 10:45:38 +0200 Subject: [PATCH 3/4] feat: forward full AnalyticsOptions to DHIS2 analytics endpoint --- .../pages/MSFHomePagePresenter.ts | 14 +++++++------- src/utils/analytics.ts | 9 ++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index 681526ec2..6c3cb6753 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -16,7 +16,7 @@ import { promiseMap } from "../../../../utils/common"; import { formatDateLong } from "../../../../utils/date"; import { availablePeriods } from "../../../../utils/synchronization"; import { CompositionRoot } from "../../../CompositionRoot"; -import { AdvancedSettings, defaultAnalyticsOptions, MSFSettings } from "./MSFEntities"; +import { AdvancedSettings, AnalyticsOptions, defaultAnalyticsOptions, MSFSettings } from "./MSFEntities"; import { NamedRef, Ref } from "../../../../domain/common/entities/Ref"; type LoggerFunction = (event: string, userType?: "user" | "admin") => void; @@ -92,8 +92,8 @@ export async function executeAggregateData( if (runAnalyticsBeforeIsRequired) { const localInstance = await compositionRoot.instances.getLocal(); - const lastYears = msfSettings.analyticsBefore?.lastYears ?? defaultAnalyticsOptions.lastYears; - await runAnalytics(localInstance, addEventToProgress, lastYears); + const analyticsOptions = msfSettings.analyticsBefore ?? defaultAnalyticsOptions; + await runAnalytics(localInstance, addEventToProgress, analyticsOptions); } const reports = await promiseMap(rulesWithoutRunAnalylics, syncRule => @@ -114,9 +114,9 @@ export async function executeAggregateData( await promiseMap(targetInstances, async instanceId => { const instance = await compositionRoot.instances.getById(instanceId); - const lastYears = msfSettings.analyticsAfter?.lastYears ?? defaultAnalyticsOptions.lastYears; + const analyticsOptions = msfSettings.analyticsAfter ?? defaultAnalyticsOptions; instance.match({ - success: async instance => await runAnalytics(instance, addEventToProgress, lastYears), + success: async instance => await runAnalytics(instance, addEventToProgress, analyticsOptions), error: () => { addEventToProgress( i18n.t(`An error has occurred retrieving the instance {{name}}`, { @@ -371,8 +371,8 @@ async function getSyncRules( .value(); } -async function runAnalytics(instance: Instance, addEventToProgress: LoggerFunction, lastYears: number) { - for await (const message of executeAnalytics(instance, { lastYears })) { +async function runAnalytics(instance: Instance, addEventToProgress: LoggerFunction, options: AnalyticsOptions) { + for await (const message of executeAnalytics(instance, options)) { addEventToProgress(message, "admin"); } diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index 58be2080e..353824f85 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -10,7 +10,9 @@ export async function* executeAnalytics(instance: Instance, options?: AnalyticsO yield i18n.t("Running analytics for instance {{name}}", instance); const api = getD2APiFromInstance(instance); - const { response } = await api.analytics.run(options).getData(); + const { response } = await api + .post("/resourceTables/analytics", { ...options }) + .getData(); const endpoint = response.relativeNotifierEndpoint.replace("/api", ""); let done = false; @@ -33,10 +35,15 @@ export async function* executeAnalytics(instance: Instance, options?: AnalyticsO type AnalyticsMessage = { message: string; completed: boolean }; type AnalyticsResponse = AnalyticsMessage[] | null; +type RunAnalyticsTaskResponse = { response: { relativeNotifierEndpoint: string } }; + interface AnalyticsOptions { skipResourceTables?: boolean; skipAggregate?: boolean; skipEvents?: boolean; skipEnrollment?: boolean; + skipOrgUnitOwnership?: boolean; + skipTrackedEntities?: boolean; + skipOutliers?: boolean; lastYears?: number; } From 95f673716df541e2e4472ee4218c0855bcab1b21 Mon Sep 17 00:00:00 2001 From: xurxodev Date: Thu, 23 Apr 2026 11:51:00 +0200 Subject: [PATCH 4/4] feat: add two analytics panels with options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the single "Run Analytics" block with two dedicated panels that match the DHIS2 analytics generation screen: - Before sync · Individual data: resource tables, events, enrollment, organisation unit ownership and tracked entities. - After sync · Aggregate data: resource tables, aggregate+completeness and outliers. Each panel keeps its own "Run Analytics" dropdown and "Number of last years of data to include" --- .../msf-settings-dialog/AnalyticsPanel.tsx | 134 +++++++++++++++++ .../msf-settings-dialog/MSFSettingsDialog.tsx | 140 +++++++----------- .../msf-aggregate-data/pages/MSFEntities.tsx | 36 +++-- .../pages/MSFHomePagePresenter.ts | 18 ++- 4 files changed, 226 insertions(+), 102 deletions(-) create mode 100644 src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/AnalyticsPanel.tsx diff --git a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/AnalyticsPanel.tsx b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/AnalyticsPanel.tsx new file mode 100644 index 000000000..649c8cd47 --- /dev/null +++ b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/AnalyticsPanel.tsx @@ -0,0 +1,134 @@ +import { TextField } from "@material-ui/core"; +import React, { ChangeEvent, useMemo } from "react"; +import styled from "styled-components"; +import i18n from "../../../../../utils/i18n"; +import { + AnalyticsOptions, + AnalyticsPanelKind, + defaultAnalyticsOptions, + RunAnalyticsSettings, +} from "../../../../webapp/msf-aggregate-data/pages/MSFEntities"; +import Dropdown from "../../../core/components/dropdown/Dropdown"; +import { Toggle } from "../../../core/components/toggle/Toggle"; + +export interface AnalyticsPanelProps { + title: string; + kind: AnalyticsPanelKind; + runSetting: RunAnalyticsSettings; + onRunSettingChange(value: RunAnalyticsSettings): void; + options: AnalyticsOptions | undefined; + onOptionsChange(options: AnalyticsOptions): void; +} + +type SkipFlag = keyof Omit; + +type FlagDef = { key: SkipFlag; label: string }; + +const individualFlags = (): FlagDef[] => [ + { key: "skipResourceTables", label: i18n.t("Skip generation of resource tables") }, + { key: "skipEvents", label: i18n.t("Skip generation of event data") }, + { key: "skipEnrollment", label: i18n.t("Skip generation of enrollment data") }, + { key: "skipOrgUnitOwnership", label: i18n.t("Skip generation of organisation unit ownership data") }, + { key: "skipTrackedEntities", label: i18n.t("Skip generation of tracked entity data") }, +]; + +const aggregateFlags = (): FlagDef[] => [ + { key: "skipResourceTables", label: i18n.t("Skip generation of resource tables") }, + { key: "skipAggregate", label: i18n.t("Skip generation of aggregate data and completeness data") }, + { key: "skipOutliers", label: i18n.t("Skip generation of outlier data") }, +]; + +export const AnalyticsPanel: React.FC = ({ + title, + kind, + runSetting, + onRunSettingChange, + options, + onOptionsChange, +}) => { + const runSettingItems = useMemo( + () => [ + { id: "true" as const, name: i18n.t("True") }, + { id: "false" as const, name: i18n.t("False") }, + { id: "by-sync-rule-settings" as const, name: i18n.t("Use sync rule settings") }, + ], + [] + ); + + const flags = useMemo(() => (kind === "individual" ? individualFlags() : aggregateFlags()), [kind]); + + const effectiveOptions = options ?? defaultAnalyticsOptions; + const showOptions = runSetting === "true"; + + const setLastYears = (event: ChangeEvent) => { + const lastYears = parseInt(event.target.value); + onOptionsChange({ ...effectiveOptions, lastYears }); + }; + + const setFlag = (key: SkipFlag) => (value: boolean) => { + onOptionsChange({ ...effectiveOptions, [key]: value }); + }; + + return ( + + {title} + + + label={i18n.t("Run Analytics")} + items={runSettingItems} + onValueChange={onRunSettingChange} + value={runSetting} + hideEmpty + /> + + {showOptions && ( + + + {flags.map(({ key, label }) => ( + + ))} + + + + + )} + + ); +}; + +const Panel = styled.div` + flex: 1; + min-width: 320px; + padding: 8px 16px 16px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; +`; + +const PanelTitle = styled.h4` + margin-top: 0; +`; + +const Options = styled.div` + margin-top: 16px; +`; + +const Flags = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 16px; +`; + +const YearsField = styled(TextField)` + width: 300px; +`; diff --git a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx index 698fd0872..6a91eff4c 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx @@ -1,16 +1,17 @@ -import { Divider, makeStyles, TextField, Theme } from "@material-ui/core"; +import { Divider } from "@material-ui/core"; import { ConfirmationDialog } from "@eyeseetea/d2-ui-components"; import { Dictionary } from "lodash"; -import React, { ChangeEvent, useMemo, useState } from "react"; +import React, { useState } from "react"; +import styled from "styled-components"; import i18n from "../../../../../utils/i18n"; import { - defaultAnalyticsOptions, + AnalyticsOptions, MSFSettings, RunAnalyticsSettings, } from "../../../../webapp/msf-aggregate-data/pages/MSFEntities"; -import Dropdown from "../../../core/components/dropdown/Dropdown"; import { Toggle } from "../../../core/components/toggle/Toggle"; import { NamedDate, OrgUnitDateSelector } from "../org-unit-date-selector/OrgUnitDateSelector"; +import { AnalyticsPanel } from "./AnalyticsPanel"; export interface MSFSettingsDialogProps { settings: MSFSettings; @@ -19,27 +20,8 @@ export interface MSFSettingsDialogProps { } export const MSFSettingsDialog: React.FC = ({ onClose, onSave, settings: defaultSettings }) => { - const classes = useStyles(); - const [settings, updateSettings] = useState(defaultSettings); - const analyticsSettingItems = useMemo(() => { - return [ - { - id: "true" as const, - name: i18n.t("True"), - }, - { - id: "false" as const, - name: i18n.t("False"), - }, - { - id: "by-sync-rule-settings" as const, - name: i18n.t("Use sync rule settings"), - }, - ]; - }, []); - const setRunAnalyticsBefore = (runAnalyticsBefore: RunAnalyticsSettings) => { updateSettings(settings => ({ ...settings, runAnalyticsBefore })); }; @@ -48,13 +30,12 @@ export const MSFSettingsDialog: React.FC = ({ onClose, o updateSettings(settings => ({ ...settings, runAnalyticsAfter })); }; - const setAnalyticsYears = (event: ChangeEvent) => { - const lastYears = parseInt(event.target.value); - updateSettings(settings => ({ - ...settings, - analyticsBefore: { ...(settings.analyticsBefore ?? defaultAnalyticsOptions), lastYears }, - analyticsAfter: { ...(settings.analyticsAfter ?? defaultAnalyticsOptions), lastYears }, - })); + const setAnalyticsBefore = (analyticsBefore: AnalyticsOptions) => { + updateSettings(settings => ({ ...settings, analyticsBefore })); + }; + + const setAnalyticsAfter = (analyticsAfter: AnalyticsOptions) => { + updateSettings(settings => ({ ...settings, analyticsAfter })); }; const updateProjectMinimumDates = (projectStartDates: Dictionary) => { @@ -84,36 +65,31 @@ export const MSFSettingsDialog: React.FC = ({ onClose, o cancelText={i18n.t("Cancel")} saveText={i18n.t("Save")} > -
-

{i18n.t("Analytics")}

- -
- - label={i18n.t("Run Analytics Before")} - items={analyticsSettingItems} - onValueChange={setRunAnalyticsBefore} - value={settings.runAnalyticsBefore} - hideEmpty - /> - - label={i18n.t("Run Analytics After")} - items={analyticsSettingItems} - onValueChange={setRunAnalyticsAfter} - value={settings.runAnalyticsAfter} - hideEmpty +
+ {i18n.t("Analytics")} + + + - -
-
+ + -
-

{i18n.t("Data values settings")}

+
+ {i18n.t("Data values settings")}
= ({ onClose, o value={settings.checkInPreviousPeriods ?? false} />
-
+ - + -
-

{i18n.t("Project minimum dates")}

+
+ {i18n.t("Project minimum dates")}
= ({ onClose, o onChange={updateProjectMinimumDates} />
-
+ ); }; -const useStyles = makeStyles((theme: Theme) => ({ - selector: { - margin: theme.spacing(0, 0, 3, 0), - }, - yearsSelector: { - minWidth: 250, - marginTop: -8, - marginLeft: 15, - }, - info: { - margin: theme.spacing(0, 0, 2, 1), - fontSize: "0.8em", - }, - title: { - marginTop: 0, - }, - section: { - marginBottom: 20, - }, - divider: { - marginBottom: 20, - }, -})); +const Section = styled.div` + margin-bottom: 20px; +`; + +const SectionTitle = styled.h3` + margin-top: 0; +`; + +const Panels = styled.div` + display: flex; + gap: 16px; + flex-wrap: wrap; +`; + +const SpacedDivider = styled(Divider)` + margin-bottom: 20px; +`; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx index 0363ca2fa..91e0f2fca 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFEntities.tsx @@ -5,15 +5,30 @@ export type RunAnalyticsSettings = "true" | "false" | "by-sync-rule-settings"; export type AnalyticsOptions = { lastYears: number; - skipAggregate: boolean; - skipResourceTables: boolean; - skipEvents: boolean; - skipEnrollment: boolean; - skipOrgUnitOwnership: boolean; - skipTrackedEntities: boolean; - skipOutliers: boolean; + skipAggregate?: boolean; + skipResourceTables?: boolean; + skipEvents?: boolean; + skipEnrollment?: boolean; + skipOrgUnitOwnership?: boolean; + skipTrackedEntities?: boolean; + skipOutliers?: boolean; }; +export type AnalyticsPanelKind = "individual" | "aggregate"; + +const analyticsFlagsByKind: Record)[]> = { + individual: ["skipResourceTables", "skipEvents", "skipEnrollment", "skipOrgUnitOwnership", "skipTrackedEntities"], + aggregate: ["skipResourceTables", "skipAggregate", "skipOutliers"], +}; + +export function toAnalyticsRequest(options: AnalyticsOptions, kind: AnalyticsPanelKind): AnalyticsOptions { + const request: AnalyticsOptions = { lastYears: options.lastYears }; + for (const key of analyticsFlagsByKind[kind]) { + if (options[key] !== undefined) request[key] = options[key]; + } + return request; +} + export type MSFSettings = { runAnalyticsBefore: RunAnalyticsSettings; runAnalyticsAfter: RunAnalyticsSettings; @@ -33,13 +48,6 @@ export const MSFStorageKey = "msf-storage"; export const defaultAnalyticsOptions: AnalyticsOptions = { lastYears: 2, - skipAggregate: false, - skipResourceTables: false, - skipEvents: false, - skipEnrollment: false, - skipOrgUnitOwnership: false, - skipTrackedEntities: false, - skipOutliers: false, }; export const defaultMSFSettings: MSFSettings = { diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index 6c3cb6753..6ba6a67d6 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -16,7 +16,13 @@ import { promiseMap } from "../../../../utils/common"; import { formatDateLong } from "../../../../utils/date"; import { availablePeriods } from "../../../../utils/synchronization"; import { CompositionRoot } from "../../../CompositionRoot"; -import { AdvancedSettings, AnalyticsOptions, defaultAnalyticsOptions, MSFSettings } from "./MSFEntities"; +import { + AdvancedSettings, + AnalyticsOptions, + defaultAnalyticsOptions, + MSFSettings, + toAnalyticsRequest, +} from "./MSFEntities"; import { NamedRef, Ref } from "../../../../domain/common/entities/Ref"; type LoggerFunction = (event: string, userType?: "user" | "admin") => void; @@ -92,7 +98,10 @@ export async function executeAggregateData( if (runAnalyticsBeforeIsRequired) { const localInstance = await compositionRoot.instances.getLocal(); - const analyticsOptions = msfSettings.analyticsBefore ?? defaultAnalyticsOptions; + const analyticsOptions = toAnalyticsRequest( + msfSettings.analyticsBefore ?? defaultAnalyticsOptions, + "individual" + ); await runAnalytics(localInstance, addEventToProgress, analyticsOptions); } @@ -114,7 +123,10 @@ export async function executeAggregateData( await promiseMap(targetInstances, async instanceId => { const instance = await compositionRoot.instances.getById(instanceId); - const analyticsOptions = msfSettings.analyticsAfter ?? defaultAnalyticsOptions; + const analyticsOptions = toAnalyticsRequest( + msfSettings.analyticsAfter ?? defaultAnalyticsOptions, + "aggregate" + ); instance.match({ success: async instance => await runAnalytics(instance, addEventToProgress, analyticsOptions), error: () => {