diff --git a/src/components/dnd-table/dnd-table-bottom-right-buttons.tsx b/src/components/dnd-table/dnd-table-bottom-right-buttons.tsx index 16cafac7c..95dc9bdb3 100644 --- a/src/components/dnd-table/dnd-table-bottom-right-buttons.tsx +++ b/src/components/dnd-table/dnd-table-bottom-right-buttons.tsx @@ -44,8 +44,8 @@ export function DndTableBottomRightButtons({ }); const noRowsSelected = currentRows ? !currentRows.some((row) => row[SELECTED]) : true; - const firstRowSelected = currentRows[0]?.[SELECTED]; - const lastRowSelected = currentRows[currentRows.length - 1]?.[SELECTED]; + const firstRowSelected = noRowsSelected ? undefined : currentRows[0]?.[SELECTED]; + const lastRowSelected = noRowsSelected ? undefined : currentRows[currentRows.length - 1]?.[SELECTED]; return ( diff --git a/src/components/dnd-table/dnd-table.tsx b/src/components/dnd-table/dnd-table.tsx index 4cd273333..c611111d7 100644 --- a/src/components/dnd-table/dnd-table.tsx +++ b/src/components/dnd-table/dnd-table.tsx @@ -34,6 +34,7 @@ import { ErrorInput, FieldErrorAlert, RawReadOnlyInput, + SwitchInput, TableNumericalInput, TableTextInput, } from '../inputs'; @@ -168,12 +169,15 @@ function EditableTableCell({ {column.type === DndColumnType.CHIP_ITEMS && ( )} + {column.type === DndColumnType.SWITCH && ( + + )} {column.type === DndColumnType.CUSTOM && column.component(rowIndex)} ); } -interface DndTableProps { +export interface DndTableProps { arrayFormName: string; useFieldArrayOutput: UseFieldArrayReturn; columnsDefinition: DndColumn[]; diff --git a/src/components/dnd-table/dnd-table.type.ts b/src/components/dnd-table/dnd-table.type.ts index 0d26e30ef..48fa96100 100644 --- a/src/components/dnd-table/dnd-table.type.ts +++ b/src/components/dnd-table/dnd-table.type.ts @@ -17,6 +17,7 @@ export enum DndColumnType { AUTOCOMPLETE = 'AUTOCOMPLETE', CHIP_ITEMS = 'CHIP_ITEMS', DIRECTORY_ITEMS = 'DIRECTORY_ITEMS', + SWITCH = 'SWITCH', CUSTOM = 'CUSTOM', } @@ -28,6 +29,7 @@ export interface ColumnBase { extra?: JSX.Element; editable?: boolean; type: DndColumnType; + initialValue?: any; // should conform to the type field } export interface ColumnText extends ColumnBase { @@ -58,6 +60,10 @@ export interface ColumnChipsItem extends ColumnBase { type: DndColumnType.CHIP_ITEMS; } +export interface ColumnSwitchItem extends ColumnBase { + type: DndColumnType.SWITCH; +} + export interface ColumnCustom extends ColumnBase { type: DndColumnType.CUSTOM; component: (rowIndex: number) => ReactNode; @@ -69,4 +75,5 @@ export type DndColumn = | ColumnText | ColumnDirectoryItem | ColumnChipsItem + | ColumnSwitchItem | ColumnCustom; diff --git a/src/components/parameters/common/ProviderParam.tsx b/src/components/parameters/common/ProviderParam.tsx index ab82c4df5..a89ffd717 100644 --- a/src/components/parameters/common/ProviderParam.tsx +++ b/src/components/parameters/common/ProviderParam.tsx @@ -26,7 +26,7 @@ const styles = { export function ProviderParam({ options }: Readonly) { return ( <> - + @@ -34,7 +34,7 @@ export function ProviderParam({ options }: Readonly) { - + diff --git a/src/components/parameters/common/parameter-dnd-table-field.tsx b/src/components/parameters/common/parameter-dnd-table-field.tsx new file mode 100644 index 000000000..ba72300b7 --- /dev/null +++ b/src/components/parameters/common/parameter-dnd-table-field.tsx @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Grid, SxProps, Tooltip, TooltipProps, Typography } from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { useFieldArray } from 'react-hook-form'; +import { Info as InfoIcon } from '@mui/icons-material'; +import { useCallback, useMemo } from 'react'; +import { DndTable, DndTableProps } from '../../dnd-table'; + +export type ParameterDndTableFieldProps = { + name: string; + label: string; + tooltipProps?: Omit; + sxContainerProps?: SxProps; +} & Omit; + +export default function ParameterDndTableField({ + name, + label, + columnsDefinition, + tooltipProps, + sxContainerProps, + ...otherProps +}: Readonly) { + const useFieldArrayOutput = useFieldArray({ + name, + }); + + const newDefaultRowData = useMemo(() => { + const newRowData: Record = {}; + columnsDefinition.forEach((columnDefinition) => { + newRowData[columnDefinition.dataKey] = columnDefinition.initialValue || null; + }); + return newRowData; + }, [columnsDefinition]); + + const createRows = useCallback(() => [newDefaultRowData], [newDefaultRowData]); + + const { title, ...otherTooltipProps } = tooltipProps || {}; + return ( + + + + + + {tooltipProps && ( + : title} + placement="right-start" + sx={{ marginLeft: 1 }} + {...otherTooltipProps} + > + + + )} + + + + ); +} diff --git a/src/components/parameters/loadflow/load-flow-parameter-field.tsx b/src/components/parameters/common/parameter-field.tsx similarity index 88% rename from src/components/parameters/loadflow/load-flow-parameter-field.tsx rename to src/components/parameters/common/parameter-field.tsx index e1285ef02..7f1a24113 100644 --- a/src/components/parameters/loadflow/load-flow-parameter-field.tsx +++ b/src/components/parameters/common/parameter-field.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Grid, Tooltip, Chip, Typography } from '@mui/material'; +import { Chip, Grid, SxProps, Tooltip, Typography } from '@mui/material'; import { FormattedMessage } from 'react-intl'; import { parametersStyles } from '../parameters-style'; import { ParameterType } from '../../../utils/types/parameters.type'; @@ -19,30 +19,24 @@ import { SwitchInput, TextInput, } from '../../inputs'; -import { LineSeparator } from '../common'; +import { LineSeparator } from './index'; -interface LoadFlowParameterFieldProps { +interface ParameterFieldProps { id: string; name: string; type: string; label?: string; description?: string; possibleValues?: { id: string; label: string }[] | string[]; + sx?: SxProps; } -function LoadFlowParameterField({ - id, - name, - type, - label, - description, - possibleValues, -}: Readonly) { +function ParameterField({ id, name, type, label, description, possibleValues, sx }: Readonly) { const renderField = () => { switch (type) { case ParameterType.STRING: return possibleValues ? ( - + ) : ( ); @@ -94,4 +88,4 @@ function LoadFlowParameterField({ ); } -export default LoadFlowParameterField; +export default ParameterField; diff --git a/src/components/parameters/dynamic-margin-calculation/constants.ts b/src/components/parameters/dynamic-margin-calculation/constants.ts new file mode 100644 index 000000000..b52a29663 --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/constants.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// tab TAB_TIME_DELAY +export const START_TIME = 'startTime'; +export const STOP_TIME = 'stopTime'; +// tab TAB_LOADS_VARIATIONS +export const MARGIN_CALCULATION_START_TIME = 'marginCalculationStartTime'; +export const LOAD_INCREASE_START_TIME = 'loadIncreaseStartTime'; +export const LOAD_INCREASE_STOP_TIME = 'loadIncreaseStopTime'; +export const CALCULATION_TYPE = 'calculationType'; +export const ACCURACY = 'accuracy'; +export const LOAD_MODELS_RULE = 'loadModelsRule'; +export const LOADS_VARIATIONS = 'loadsVariations'; +export const LOAD_FILTERS = 'loadFilters'; +export const VARIATION = 'variation'; +export const ACTIVE = 'active'; diff --git a/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-dialog.tsx b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-dialog.tsx new file mode 100644 index 000000000..06b7e633f --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-dialog.tsx @@ -0,0 +1,6 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ diff --git a/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-form.tsx b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-form.tsx new file mode 100644 index 000000000..7637bef12 --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-form.tsx @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ReactNode } from 'react'; +import { Grid, LinearProgress, Tab, Tabs } from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { UseDynamicMarginCalculationParametersFormReturn } from './use-dynamic-margin-calculation-parameters-form'; +import { mergeSx } from '../../../utils'; +import { CustomFormProvider } from '../../inputs'; +import { ProviderParam } from '../common'; +import { getTabStyle, parametersStyles } from '../parameters-style'; +import { TabPanel } from '../common/parameters'; +import TimeDelayParameters from './time-delay-parameters'; +import LoadsVariationsParameters from './loads-variations-parameters'; + +import { TabValues } from './dynamic-margin-calculation.type'; + +type DynamicMarginCalculationFormProps = { + dynamicMarginCalculationMethods: UseDynamicMarginCalculationParametersFormReturn; + renderTitleFields?: () => ReactNode; + renderActions?: () => ReactNode; +}; + +export function DynamicMarginCalculationForm({ + dynamicMarginCalculationMethods, + renderTitleFields, + renderActions, +}: Readonly) { + const { formMethods, formSchema, paramsLoaded, formattedProviders, selectedTab, onTabChange, tabsWithError } = + dynamicMarginCalculationMethods; + return ( + + {renderTitleFields?.()} + {paramsLoaded ? ( + + + + + + + } + value={TabValues.TAB_TIME_DELAY} + sx={getTabStyle(tabsWithError, TabValues.TAB_TIME_DELAY)} + /> + } + value={TabValues.TAB_LOADS_VARIATIONS} + sx={getTabStyle(tabsWithError, TabValues.TAB_LOADS_VARIATIONS)} + /> + + + + + + + + + + + {renderActions?.()} + + ) : ( + + )} + + ); +} diff --git a/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx new file mode 100644 index 000000000..a188bc404 --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation-inline.tsx @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import type { UUID } from 'node:crypto'; +import { Grid } from '@mui/material'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { useCallback, useEffect, useState } from 'react'; +import { FieldValues } from 'react-hook-form'; +import { UseParametersBackendReturnProps } from '../../../utils/types/parameters.type'; +import { ComputingType } from '../common/computing-type'; +import { ElementType, mergeSx, snackWithFallback } from '../../../utils'; +import { DynamicMarginCalculationForm } from './dynamic-margin-calculation-form'; +import { + toFormValues, + toParamsInfos, + useDynamicMarginCalculationParametersForm, +} from './use-dynamic-margin-calculation-parameters-form'; +import { LabelledButton } from '../common/parameters'; +import { SubmitButton } from '../../inputs/reactHookForm/utils/SubmitButton'; +import { PopupConfirmationDialog } from '../../dialogs/popupConfirmationDialog/PopupConfirmationDialog'; +import { parametersStyles } from '../parameters-style'; +import { CreateParameterDialog } from '../common'; +import { DirectoryItemSelector } from '../../directoryItemSelector'; +import { TreeViewFinderNodeProps } from '../../treeViewFinder'; +import { fetchDynamicMarginCalculationParameters } from '../../../services/dynamic-margin-calculation'; +import { useSnackMessage } from '../../../hooks'; + +type DynamicMarginCalculationInlineProps = { + studyUuid: UUID | null; + parametersBackend: UseParametersBackendReturnProps; + setHaveDirtyFields: (isDirty: boolean) => void; +}; +export function DynamicMarginCalculationInline({ + studyUuid, + parametersBackend, + setHaveDirtyFields, +}: Readonly) { + const [providers, , , , , params, , updateParams, resetParams, ,] = parametersBackend; + const dynamicMarginCalculationMethods = useDynamicMarginCalculationParametersForm({ + providers, + params, + name: null, + description: null, + }); + const intl = useIntl(); + const { snackError } = useSnackMessage(); + + const [openCreateParameterDialog, setOpenCreateParameterDialog] = useState(false); + const [openSelectParameterDialog, setOpenSelectParameterDialog] = useState(false); + const [openResetConfirmation, setOpenResetConfirmation] = useState(false); + + const { formMethods, onError } = dynamicMarginCalculationMethods; + const { reset, handleSubmit, getValues, formState } = formMethods; + + const handleResetClick = useCallback(() => { + setOpenResetConfirmation(true); + }, []); + const handleCancelReset = useCallback(() => { + setOpenResetConfirmation(false); + }, []); + + const handleReset = useCallback(() => { + resetParams(); + setOpenResetConfirmation(false); + }, [resetParams]); + + const onSubmit = useCallback( + (formData: FieldValues) => { + // update params after convert form representation to dto representation + updateParams(toParamsInfos(formData)); + }, + [updateParams] + ); + + const handleLoadParameter = useCallback( + (newParams: TreeViewFinderNodeProps[]) => { + if (newParams?.length) { + setOpenSelectParameterDialog(false); + const parametersUuid = newParams[0].id; + fetchDynamicMarginCalculationParameters(parametersUuid) + .then((_params) => { + reset(toFormValues(_params), { + keepDefaultValues: true, + }); + }) + .catch((error: Error) => { + snackWithFallback(snackError, error, { headerId: 'paramsRetrievingError' }); + }); + } + setOpenSelectParameterDialog(false); + }, + [reset, snackError] + ); + + useEffect(() => { + setHaveDirtyFields(!!Object.keys(formState.dirtyFields).length); + }, [formState, setHaveDirtyFields]); + + const renderActions = () => { + return ( + <> + + + setOpenSelectParameterDialog(true)} + label="settings.button.chooseSettings" + /> + setOpenCreateParameterDialog(true)} label="save" /> + + + + + + + {openCreateParameterDialog && ( + setOpenCreateParameterDialog(false)} + parameterValues={getValues} + parameterFormatter={toParamsInfos} + parameterType={ElementType.DYNAMIC_MARGIN_CALCULATION_PARAMETERS} + /> + )} + {openSelectParameterDialog && ( + + )} + {/* Reset Confirmation Dialog */} + {openResetConfirmation && ( + + )} + + ); + }; + return ( + + ); +} diff --git a/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation.type.ts b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation.type.ts new file mode 100644 index 000000000..ca8af931f --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/dynamic-margin-calculation.type.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +export enum TabValues { + TAB_TIME_DELAY = 'TAB_TIME_DELAY', + TAB_LOADS_VARIATIONS = 'TAB_LOADS_VARIATIONS', +} diff --git a/src/components/parameters/dynamic-margin-calculation/index.ts b/src/components/parameters/dynamic-margin-calculation/index.ts new file mode 100644 index 000000000..7557ec830 --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +export * from './constants'; +export * from './dynamic-margin-calculation-inline'; diff --git a/src/components/parameters/dynamic-margin-calculation/loads-variations-parameters.tsx b/src/components/parameters/dynamic-margin-calculation/loads-variations-parameters.tsx new file mode 100644 index 000000000..3d16f71ca --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/loads-variations-parameters.tsx @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { Grid, SxProps } from '@mui/material'; +import yup from '../../../utils/yupConfig'; +import { + ACCURACY, + ACTIVE, + CALCULATION_TYPE, + LOAD_FILTERS, + LOAD_MODELS_RULE, + LOADS_VARIATIONS, + VARIATION, +} from './constants'; +import { + CalculationType, + ElementType, + EquipmentType, + ID, + LoadModelsRule, + ParameterType, + SpecificParameterInfos, +} from '../../../utils'; +import ParameterField from '../common/parameter-field'; +import { NAME } from '../../inputs'; +import ParameterDndTableField from '../common/parameter-dnd-table-field'; +import { DndColumn, DndColumnType } from '../../dnd-table'; + +export const formSchema = yup.object().shape({ + [CALCULATION_TYPE]: yup.string().required(), + [ACCURACY]: yup.number().required(), + [LOAD_MODELS_RULE]: yup.string().required(), + [LOADS_VARIATIONS]: yup.array().of( + yup.object().shape({ + [ID]: yup.string().nullable(), // not shown in form, used to identify a row + [LOAD_FILTERS]: yup + .array() + .of( + yup.object().shape({ + [ID]: yup.string().required(), + [NAME]: yup.string().nullable().notRequired(), + }) + ) + .min(1), + [VARIATION]: yup.number().min(0).required(), + [ACTIVE]: yup.boolean().nullable().notRequired(), + }) + ), +}); + +export const emptyFormData = { + [CALCULATION_TYPE]: '', + [ACCURACY]: 0, + [LOAD_MODELS_RULE]: '', + [LOADS_VARIATIONS]: [], +}; + +const params: (SpecificParameterInfos & { sx?: SxProps })[] = [ + { + name: CALCULATION_TYPE, + type: ParameterType.STRING, + label: 'DynamicMarginCalculationCalculationType', + possibleValues: [ + { id: CalculationType.GLOBAL_MARGIN, label: 'DynamicMarginCalculationCalculationTypeGlobalMargin' }, + { id: CalculationType.LOCAL_MARGIN, label: 'DynamicMarginCalculationCalculationTypeLocalMargin' }, + ], + sx: { width: '100%' }, + }, + { + name: ACCURACY, + type: ParameterType.DOUBLE, + label: 'DynamicMarginCalculationAccuracy', + }, + { + name: LOAD_MODELS_RULE, + type: ParameterType.STRING, + label: 'DynamicMarginCalculationLoadModelsRule', + possibleValues: [ + { id: LoadModelsRule.ALL_LOADS, label: 'DynamicMarginCalculationLoadModelsRuleAllLoads' }, + { id: LoadModelsRule.TARGETED_LOADS, label: 'DynamicMarginCalculationLoadModelsRuleTargetedLoads' }, + ], + sx: { width: '100%' }, + }, + // LOADS_VARIATIONS displayed in a separated component, i.e., ParameterDndTableField +]; + +const loadsVariationsColumnsDefinition: DndColumn[] = [ + { + label: 'DynamicMarginCalculationLoadsFilter', + dataKey: LOAD_FILTERS, + initialValue: [], + editable: true, + type: DndColumnType.DIRECTORY_ITEMS, + equipmentTypes: [EquipmentType.LOAD], + elementType: ElementType.FILTER, + titleId: 'FiltersListsSelection', + }, + { + label: 'DynamicMarginCalculationLoadsVariation', + dataKey: VARIATION, + editable: true, + type: DndColumnType.NUMERIC, + textAlign: 'right', + }, + { + label: 'DynamicMarginCalculationLoadsActive', + initialValue: true, + dataKey: ACTIVE, + editable: true, + width: 100, + type: DndColumnType.SWITCH, + }, +]; + +export default function LoadsVariationsParameters({ path }: Readonly<{ path: string }>) { + const inlt = useIntl(); + const translatedColumnsDefinition = useMemo(() => { + return loadsVariationsColumnsDefinition.map((colDef) => ({ + ...colDef, + label: inlt.formatMessage({ id: colDef.label }), + })); + }, [inlt]); + return ( + + {params.map((param: SpecificParameterInfos) => { + const { name, type, ...otherParams } = param; + return ( + + ); + })} + + + ); +} diff --git a/src/components/parameters/dynamic-margin-calculation/time-delay-parameters.tsx b/src/components/parameters/dynamic-margin-calculation/time-delay-parameters.tsx new file mode 100644 index 000000000..d43dc9d8b --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/time-delay-parameters.tsx @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Grid } from '@mui/material'; +import yup from '../../../utils/yupConfig'; +import { + LOAD_INCREASE_START_TIME, + LOAD_INCREASE_STOP_TIME, + MARGIN_CALCULATION_START_TIME, + START_TIME, + STOP_TIME, +} from './constants'; +import { ParameterType, SpecificParameterInfos } from '../../../utils'; +import ParameterField from '../common/parameter-field'; + +export const formSchema = yup.object().shape({ + [START_TIME]: yup.number().required(), + [STOP_TIME]: yup.number().required(), + [MARGIN_CALCULATION_START_TIME]: yup.number().required(), + [LOAD_INCREASE_START_TIME]: yup.number().required(), + [LOAD_INCREASE_STOP_TIME]: yup.number().required(), +}); + +export const emptyFormData = { + [START_TIME]: 0, + [STOP_TIME]: 0, + [MARGIN_CALCULATION_START_TIME]: 0, + [LOAD_INCREASE_START_TIME]: 0, + [LOAD_INCREASE_STOP_TIME]: 0, +}; + +const params: SpecificParameterInfos[] = [ + { + name: START_TIME, + type: ParameterType.DOUBLE, + label: 'DynamicMarginCalculationStartTime', + }, + { + name: STOP_TIME, + type: ParameterType.DOUBLE, + label: 'DynamicMarginCalculationStopTime', + }, + { + name: MARGIN_CALCULATION_START_TIME, + type: ParameterType.DOUBLE, + label: 'DynamicMarginCalculationMarginCalculationStartTime', + }, + { + name: LOAD_INCREASE_START_TIME, + type: ParameterType.DOUBLE, + label: 'DynamicMarginCalculationLoadIncreaseStartTime', + }, + { + name: LOAD_INCREASE_STOP_TIME, + type: ParameterType.DOUBLE, + label: 'DynamicMarginCalculationLoadIncreaseStopTime', + }, +]; + +export default function TimeDelayParameters({ path }: Readonly<{ path: string }>) { + return ( + + {params.map((param: SpecificParameterInfos) => { + const { name, type, ...otherParams } = param; + return ( + + ); + })} + + ); +} diff --git a/src/components/parameters/dynamic-margin-calculation/use-dynamic-margin-calculation-parameters-form.ts b/src/components/parameters/dynamic-margin-calculation/use-dynamic-margin-calculation-parameters-form.ts new file mode 100644 index 000000000..12d7a3894 --- /dev/null +++ b/src/components/parameters/dynamic-margin-calculation/use-dynamic-margin-calculation-parameters-form.ts @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { FieldErrors, FieldValues, useForm, UseFormReturn } from 'react-hook-form'; +import { ObjectSchema } from 'yup'; +import { SyntheticEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import yup from '../../../utils/yupConfig'; +import { DynamicMarginCalculationParametersInfos } from '../../../utils/types/dynamic-margin-calculation.type'; +import { emptyFormData as timeDelayEmptyFormData, formSchema as timeDelayFormSchema } from './time-delay-parameters'; +import { + emptyFormData as loadsVariationsEmptyFormData, + formSchema as loadsVariationsFormSchema, +} from './loads-variations-parameters'; +import { ID, isObjectEmpty } from '../../../utils'; +import { PROVIDER } from '../common'; +import { getNameElementEditorEmptyFormData, getNameElementEditorSchema } from '../common/name-element-editor'; +import { + ACCURACY, + CALCULATION_TYPE, + LOAD_INCREASE_START_TIME, + LOAD_INCREASE_STOP_TIME, + LOAD_MODELS_RULE, + LOADS_VARIATIONS, + MARGIN_CALCULATION_START_TIME, + START_TIME, + STOP_TIME, +} from './constants'; +import { TabValues } from './dynamic-margin-calculation.type'; + +const formSchema = yup.object().shape({ + [PROVIDER]: yup.string().required(), + [TabValues.TAB_TIME_DELAY]: timeDelayFormSchema, + [TabValues.TAB_LOADS_VARIATIONS]: loadsVariationsFormSchema, +}); + +const emptyFormData = { + [PROVIDER]: '', + [TabValues.TAB_TIME_DELAY]: timeDelayEmptyFormData, + [TabValues.TAB_LOADS_VARIATIONS]: loadsVariationsEmptyFormData, +}; + +export const toFormValues = (_params: DynamicMarginCalculationParametersInfos): FieldValues => ({ + [ID]: _params.id, // not shown in form + [PROVIDER]: _params.provider, + [TabValues.TAB_TIME_DELAY]: { + [START_TIME]: _params.startTime, + [STOP_TIME]: _params.stopTime, + [MARGIN_CALCULATION_START_TIME]: _params.marginCalculationStartTime, + [LOAD_INCREASE_START_TIME]: _params.loadIncreaseStartTime, + [LOAD_INCREASE_STOP_TIME]: _params.loadIncreaseStopTime, + }, + [TabValues.TAB_LOADS_VARIATIONS]: { + [CALCULATION_TYPE]: _params.calculationType, + [ACCURACY]: _params.accuracy, + [LOAD_MODELS_RULE]: _params.loadModelsRule, + [LOADS_VARIATIONS]: _params.loadsVariations, + }, +}); + +export const toParamsInfos = (_formData: FieldValues): DynamicMarginCalculationParametersInfos => ({ + id: _formData[ID], + provider: _formData[PROVIDER], + startTime: _formData[TabValues.TAB_TIME_DELAY][START_TIME], + stopTime: _formData[TabValues.TAB_TIME_DELAY][STOP_TIME], + marginCalculationStartTime: _formData[TabValues.TAB_TIME_DELAY][MARGIN_CALCULATION_START_TIME], + loadIncreaseStartTime: _formData[TabValues.TAB_TIME_DELAY][LOAD_INCREASE_START_TIME], + loadIncreaseStopTime: _formData[TabValues.TAB_TIME_DELAY][LOAD_INCREASE_STOP_TIME], + calculationType: _formData[TabValues.TAB_LOADS_VARIATIONS][CALCULATION_TYPE], + accuracy: _formData[TabValues.TAB_LOADS_VARIATIONS][ACCURACY], + loadModelsRule: _formData[TabValues.TAB_LOADS_VARIATIONS][LOAD_MODELS_RULE], + loadsVariations: _formData[TabValues.TAB_LOADS_VARIATIONS][LOADS_VARIATIONS], +}); + +export type UseTabsReturn = { + selectedTab: TTabValue; + tabsWithError: TTabValue[]; + onTabChange: (event: SyntheticEvent, newValue: TTabValue) => void; + onError: (errors: FieldErrors) => void; +}; + +type UseTabsProps = { + defaultTab: TTabValue; + tabEnum: Record; +}; + +function useTabs({ + defaultTab, + tabEnum, +}: Readonly>): UseTabsReturn { + const [tabValue, setTabValue] = useState(defaultTab); + const [tabValuesWithError, setTabValuesWithError] = useState([]); + const handleTabChange = useCallback((event: SyntheticEvent, newValue: TTabValue) => { + setTabValue(newValue); + }, []); + + const onError = useCallback( + (errors: FieldErrors) => { + if (!errors || isObjectEmpty(errors)) { + return; + } + + const tabsInError: TTabValue[] = []; + // do not show error when being in the current tab + Object.values(tabEnum).forEach((tab) => { + if (errors?.[tab] && tab !== tabValue) { + tabsInError.push(tab); + } + }); + + if (tabsInError.includes(tabValue)) { + // error in current tab => do not change tab systematically but remove current tab in error list + setTabValuesWithError(tabsInError.filter((errorTab) => errorTab !== tabValue)); + } else if (tabsInError.length > 0) { + // switch to the first tab in the list then remove the tab in the error list + setTabValue(tabsInError[0]); + setTabValuesWithError(tabsInError.filter((errorTab, index, arr) => errorTab !== arr[0])); + } + }, + [tabValue, tabEnum] + ); + + return { + selectedTab: tabValue, + tabsWithError: tabValuesWithError, + onTabChange: handleTabChange, + onError, + }; +} + +export type UseComputationParametersFormReturn = UseTabsReturn & { + formMethods: UseFormReturn; + formSchema: ObjectSchema; + paramsLoaded: boolean; + formattedProviders: { id: string; label: string }[]; +}; + +export type UseDynamicMarginCalculationParametersFormReturn = UseComputationParametersFormReturn; +export type UseParametersFormProps = { + providers: Record; + params: DynamicMarginCalculationParametersInfos | null; + // default values fields managed in grid-explore via directory server + name: string | null; + description: string | null; +}; +export type UseDynamicMarginCalculationParametersFormProps = UseParametersFormProps; + +export function useDynamicMarginCalculationParametersForm({ + providers, + params, + name: initialName, + description: initialDescription, +}: Readonly): UseDynamicMarginCalculationParametersFormReturn { + const paramsLoaded = useMemo(() => !!params, [params]); + + const formattedProviders = useMemo( + () => + Object.entries(providers).map(([key, value]) => ({ + id: key, + label: value, + })), + [providers] + ); + + const returnFormSchema = useMemo(() => { + return initialName !== null ? formSchema.concat(getNameElementEditorSchema(initialName)) : formSchema; + }, [initialName]); + + const newEmptyFormData: any = useMemo(() => { + return { + ...(initialName !== null ? getNameElementEditorEmptyFormData(initialName, initialDescription) : {}), + ...emptyFormData, + }; + }, [initialName, initialDescription]); + + const returnFormMethods = useForm({ + defaultValues: newEmptyFormData, + resolver: yupResolver(returnFormSchema), + }); + + const { reset } = returnFormMethods; + + useEffect(() => { + if (params) { + console.log('xxx Resetting form with params:', params); + reset(toFormValues(params)); + } + }, [params, paramsLoaded, reset]); + + /* tab-related handling */ + const { selectedTab, tabsWithError, onTabChange, onError } = useTabs({ + defaultTab: TabValues.TAB_TIME_DELAY, + tabEnum: TabValues, + }); + + return { + formMethods: returnFormMethods, + formSchema: returnFormSchema, + paramsLoaded, + formattedProviders, + /* tab-related handling */ + selectedTab, + tabsWithError, + onTabChange, + onError, + }; +} diff --git a/src/components/parameters/index.ts b/src/components/parameters/index.ts index 985547ce1..e6c74063e 100644 --- a/src/components/parameters/index.ts +++ b/src/components/parameters/index.ts @@ -13,3 +13,4 @@ export * from './voltage-init'; export * from './pcc-min'; export * from './security-analysis'; export * from './sensi'; +export * from './dynamic-margin-calculation'; diff --git a/src/components/parameters/loadflow/load-flow-general-parameters.tsx b/src/components/parameters/loadflow/load-flow-general-parameters.tsx index a3df5b9ff..b8ef33311 100644 --- a/src/components/parameters/loadflow/load-flow-general-parameters.tsx +++ b/src/components/parameters/loadflow/load-flow-general-parameters.tsx @@ -6,7 +6,7 @@ */ import { memo } from 'react'; -import LoadFlowParameterField from './load-flow-parameter-field'; +import ParameterField from '../common/parameter-field'; import { BALANCE_TYPE, CONNECTED_MODE, @@ -151,7 +151,7 @@ function LoadFlowGeneralParameters({ provider, specificParams }: Readonly {basicParams.map((item) => ( - + ))} {showAdvancedLfParams && - advancedParams.map((item) => ( - - ))} + advancedParams.map((item) => )} {showSpecificLfParams && specificParams?.map((item) => ( - + ))} diff --git a/src/components/parameters/voltage-init/voltage-limits-parameters.tsx b/src/components/parameters/voltage-init/voltage-limits-parameters.tsx index 35263d39a..cba7c9168 100644 --- a/src/components/parameters/voltage-init/voltage-limits-parameters.tsx +++ b/src/components/parameters/voltage-init/voltage-limits-parameters.tsx @@ -113,7 +113,7 @@ export function VoltageLimitsParameters() { adornment: VoltageAdornment, textAlign: 'right', }, - ] satisfies (DndColumn & { initialValue: unknown[] | null })[] + ] satisfies DndColumn[] ).map((column) => ({ ...column, label: intl diff --git a/src/services/dynamic-margin-calculation.ts b/src/services/dynamic-margin-calculation.ts new file mode 100644 index 000000000..01b9be4fc --- /dev/null +++ b/src/services/dynamic-margin-calculation.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import type { UUID } from 'node:crypto'; +import { DynamicMarginCalculationParametersInfos } from '../utils/types/dynamic-margin-calculation.type'; +import { backendFetch, backendFetchJson } from './utils'; + +const PREFIX_DYNAMIC_MARGIN_CALCULATION_SERVER_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/dynamic-margin-calculation`; + +function getDynamicMarginCalculationUrl() { + return `${PREFIX_DYNAMIC_MARGIN_CALCULATION_SERVER_QUERIES}/v1/`; +} + +export function fetchDynamicMarginCalculationProviders() { + console.info('fetch dynamic margin calculation providers'); + const url = `${getDynamicMarginCalculationUrl()}providers`; + console.debug(url); + return backendFetchJson(url); +} + +export function fetchDynamicMarginCalculationParameters( + parameterUuid: UUID +): Promise { + console.info(`Fetching dynamic margin calculation parameters having uuid '${parameterUuid}' ...`); + const url = `${getDynamicMarginCalculationUrl()}parameters/${encodeURIComponent(parameterUuid)}`; + console.debug(url); + return backendFetchJson(url); +} + +export function updateDynamicMarginCalculationParameters( + parameterUuid: UUID, + newParams: DynamicMarginCalculationParametersInfos +): Promise { + console.info(`Setting dynamic margin calculation parameters having uuid '${parameterUuid}' ...`); + const url = `${getDynamicMarginCalculationUrl()}parameters/${parameterUuid}`; + console.debug(url); + return backendFetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newParams), + }); +} diff --git a/src/services/index.ts b/src/services/index.ts index 12f8d1be1..0d3c574fb 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -17,3 +17,4 @@ export * from './userAdmin'; export * from './utils'; export * from './voltage-init'; export * from './short-circuit-analysis'; +export * from './dynamic-margin-calculation'; diff --git a/src/translations/en/businessErrorsEn.ts b/src/translations/en/businessErrorsEn.ts index d8060c6ec..d14cee520 100644 --- a/src/translations/en/businessErrorsEn.ts +++ b/src/translations/en/businessErrorsEn.ts @@ -72,4 +72,6 @@ export const businessErrorsEn = { 'sensitivityAnalysis.tooManyFactors': 'Too many factors to run sensitivity analysis: {resultCount} results (limit: {resultCountLimit}) and {variableCount} variables (limit: {variableCountLimit}).', 'pccMin.missingFilter': 'The configuration contains one filter that has been deleted.', + 'dynamicMarginCalculation.providerNotFound': 'Dynamic margin calculation provider not found.', + 'dynamicMarginCalculation.loadFilterNotFound': 'Some load filters do not exist: {filterUuids}', }; diff --git a/src/translations/en/parameters.ts b/src/translations/en/parameters.ts index 0a9b41654..e1adb00dc 100644 --- a/src/translations/en/parameters.ts +++ b/src/translations/en/parameters.ts @@ -280,4 +280,27 @@ export const parametersEn = { PccMinParametersError: 'An error occurred while updating the pcc min parameters', updatePccMinParametersError: 'An error occurred while updating the pcc min parameters', pccMinParamFilter: 'Definition of contingencies on voltage levels', + // DynamicMarginCalculation + DynamicMarginCalculationParametersError: + 'An error occurred while updating the dynamic margin calculation parameters', + updateDynamicMarginCalculationParametersError: + 'An error occurred while updating the dynamic margin calculation parameters', + DynamicMarginCalculationTimeDelayTab: 'Time delay', + DynamicMarginCalculationLoadsVariationsTab: 'Load variations', + DynamicMarginCalculationStartTime: 'Start time', + DynamicMarginCalculationStopTime: 'Stop time', + DynamicMarginCalculationMarginCalculationStartTime: 'Margin calculation start time', + DynamicMarginCalculationLoadIncreaseStartTime: 'Load increase start time', + DynamicMarginCalculationLoadIncreaseStopTime: 'Load increase stop time', + DynamicMarginCalculationCalculationType: 'Calculation type', + DynamicMarginCalculationAccuracy: 'Accuracy', + DynamicMarginCalculationLoadModelsRule: 'Load models rule', + DynamicMarginCalculationLoadsVariations: 'Load variations', + DynamicMarginCalculationCalculationTypeGlobalMargin: 'Global margin', + DynamicMarginCalculationCalculationTypeLocalMargin: 'Local margin', + DynamicMarginCalculationLoadModelsRuleAllLoads: 'All loads', + DynamicMarginCalculationLoadModelsRuleTargetedLoads: 'Targeted loads', + DynamicMarginCalculationLoadsFilter: 'Loads filter', + DynamicMarginCalculationLoadsVariation: 'Load variation', + DynamicMarginCalculationLoadsActive: 'Active', }; diff --git a/src/translations/fr/businessErrorsFr.ts b/src/translations/fr/businessErrorsFr.ts index daa0231ca..d3051fff5 100644 --- a/src/translations/fr/businessErrorsFr.ts +++ b/src/translations/fr/businessErrorsFr.ts @@ -73,4 +73,6 @@ export const businessErrorsFr = { 'sensitivityAnalysis.tooManyFactors': 'Trop de facteurs pour exécuter l’analyse de sensibilité : {resultCount} résultats (limite : {resultCountLimit}) et {variableCount} variables (limite : {variableCountLimit}).', 'pccMin.missingFilter': 'La configuration contient un filtre qui a été supprimé.', + 'dynamicMarginCalculation.providerNotFound': 'Simulateur du calcul de marge dynamique non trouvé.', + 'dynamicMarginCalculation.loadFilterNotFound': "Certains filtres de consommations n'existent pas : {filterUuids}", }; diff --git a/src/translations/fr/parameters.ts b/src/translations/fr/parameters.ts index 5abde27e5..396c166e7 100644 --- a/src/translations/fr/parameters.ts +++ b/src/translations/fr/parameters.ts @@ -305,4 +305,26 @@ export const parametersFr = { PccMinParametersError: 'Erreur lors de la mise à jour des paramètres de pcc du min', updatePccMinParametersError: 'Une erreur est survenue lors de la mise a jour des paramètres de pcc min', pccMinParamFilter: 'Définition des postes en défaut', + // DynamicMarginCalculation + DynamicMarginCalculationParametersError: 'Erreur lors de la mise à jour des paramètres du calcul du marge', + updateDynamicMarginCalculationParametersError: + 'Une erreur est survenue lors de la mise a jour des paramètres du calcul du marge', + DynamicMarginCalculationTimeDelayTab: 'Temporisation', + DynamicMarginCalculationLoadsVariationsTab: 'Variations de charge', + DynamicMarginCalculationStartTime: 'Temps de début', + DynamicMarginCalculationStopTime: "Temps d'arrêt", + DynamicMarginCalculationMarginCalculationStartTime: 'Temps de début de du calcul de marge', + DynamicMarginCalculationLoadIncreaseStartTime: "Temps de début de l'augmentation de charge", + DynamicMarginCalculationLoadIncreaseStopTime: "Temps d'arrêt de l'augmentation de charge", + DynamicMarginCalculationCalculationType: 'Type de calcul', + DynamicMarginCalculationAccuracy: 'Précision', + DynamicMarginCalculationLoadModelsRule: 'Règle des modèles de charge', + DynamicMarginCalculationLoadsVariations: 'Variations de charge', + DynamicMarginCalculationCalculationTypeGlobalMargin: 'Margin global', + DynamicMarginCalculationCalculationTypeLocalMargin: 'Margin local', + DynamicMarginCalculationLoadModelsRuleAllLoads: 'Tous les charges', + DynamicMarginCalculationLoadModelsRuleTargetedLoads: 'Charges ciblées', + DynamicMarginCalculationLoadsFilter: 'Regroupement de charges', + DynamicMarginCalculationLoadsVariation: 'Variation de charge', + DynamicMarginCalculationLoadsActive: 'Actif', }; diff --git a/src/utils/types/dynamic-margin-calculation.type.ts b/src/utils/types/dynamic-margin-calculation.type.ts new file mode 100644 index 000000000..1786554dd --- /dev/null +++ b/src/utils/types/dynamic-margin-calculation.type.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import type { UUID } from 'node:crypto'; + +export enum CalculationType { + GLOBAL_MARGIN = 'GLOBAL_MARGIN', + LOCAL_MARGIN = 'LOCAL_MARGIN', +} + +export enum LoadModelsRule { + ALL_LOADS = 'ALL_LOADS', + TARGETED_LOADS = 'TARGETED_LOADS', +} + +export type IdNameInfos = { id: UUID; name?: string }; + +export type LoadsVariationInfos = { + id?: UUID; // persisted id of the info to be modified + loadFilters?: IdNameInfos[]; + variation: number; + active: boolean; +}; + +export type DynamicMarginCalculationParametersInfos = { + id?: UUID; + provider: string; + startTime: number; + stopTime: number; + marginCalculationStartTime: number; + loadIncreaseStartTime: number; + loadIncreaseStopTime: number; + calculationType: CalculationType; + accuracy: number; // integer + loadModelsRule: LoadModelsRule; + loadsVariations?: LoadsVariationInfos[]; +}; diff --git a/src/utils/types/elementType.ts b/src/utils/types/elementType.ts index 991c59b2f..b278a8991 100644 --- a/src/utils/types/elementType.ts +++ b/src/utils/types/elementType.ts @@ -21,6 +21,7 @@ export enum ElementType { LOADFLOW_PARAMETERS = 'LOADFLOW_PARAMETERS', SENSITIVITY_PARAMETERS = 'SENSITIVITY_PARAMETERS', SHORT_CIRCUIT_PARAMETERS = 'SHORT_CIRCUIT_PARAMETERS', + DYNAMIC_MARGIN_CALCULATION_PARAMETERS = 'DYNAMIC_MARGIN_CALCULATION_PARAMETERS', NETWORK_VISUALIZATIONS_PARAMETERS = 'NETWORK_VISUALIZATIONS_PARAMETERS', SPREADSHEET_CONFIG = 'SPREADSHEET_CONFIG', SPREADSHEET_CONFIG_COLLECTION = 'SPREADSHEET_CONFIG_COLLECTION', diff --git a/src/utils/types/index.ts b/src/utils/types/index.ts index ec7b3f2dd..b296e5cf6 100644 --- a/src/utils/types/index.ts +++ b/src/utils/types/index.ts @@ -19,3 +19,4 @@ export * from './dynamic-security-analysis.type'; export * from './dynamic-simulation.type'; export * from './loadflow.type'; export * from './sensitivity-analysis.type'; +export * from './dynamic-margin-calculation.type'; diff --git a/src/utils/types/parameters.type.ts b/src/utils/types/parameters.type.ts index 98c1036ab..8ff10d959 100644 --- a/src/utils/types/parameters.type.ts +++ b/src/utils/types/parameters.type.ts @@ -16,6 +16,7 @@ import type { import { DynamicSimulationParametersFetchReturn } from './dynamic-simulation.type'; import { SensitivityAnalysisParametersInfos } from './sensitivity-analysis.type'; import { type ShortCircuitParametersInfos } from '../../components/parameters/short-circuit/short-circuit-parameters.type'; +import { DynamicMarginCalculationParametersInfos } from './dynamic-margin-calculation.type'; export enum ParameterType { BOOLEAN = 'BOOLEAN', @@ -51,9 +52,11 @@ export type ParametersInfos = T extends ComputingType.S ? DynamicSimulationParametersFetchReturn : T extends ComputingType.DYNAMIC_SECURITY_ANALYSIS ? DynamicSecurityAnalysisParametersFetchReturn - : T extends ComputingType.SHORT_CIRCUIT - ? ShortCircuitParametersInfos - : Record; + : T extends ComputingType.DYNAMIC_MARGIN_CALCULATION + ? DynamicMarginCalculationParametersInfos + : T extends ComputingType.SHORT_CIRCUIT + ? ShortCircuitParametersInfos + : Record; export type UseParametersBackendReturnProps = [ Record,