diff --git a/src/components/dialogs/line-types-catalog/line-catalog.type.ts b/src/components/dialogs/line-types-catalog/line-catalog.type.ts index 2fd2a4c5cc..632e083539 100644 --- a/src/components/dialogs/line-types-catalog/line-catalog.type.ts +++ b/src/components/dialogs/line-types-catalog/line-catalog.type.ts @@ -37,16 +37,33 @@ export type ComputedLineCharacteristics = { finalCurrentLimits: CurrentLimitsInfo[]; }; -export type CurrentLimitsInfo = { +export type CurrentLimitHeader = { limitSetName: string; permanentLimit: number; - temporaryLimitValue: number; - temporaryLimitAcceptableDuration: number; - temporaryLimitName: string; +}; + +export type CurrentLimitsInfo = CurrentLimitHeader & { + temporaryLimits: TemporaryLimitsInfo[]; area: string; temperature: string; }; +export type LimitSelectedRowData = CurrentLimitHeader & TemporaryLimitSelectedRowData; + +export type TemporaryLimitSelectedRowData = Record; + +export type AreaTemperatureShapeFactorInfo = { + area: string | null; + temperature: string | null; + shapeFactor: number | null; +}; + +export type TemporaryLimitsInfo = { + limitValue: number; + acceptableDuration: number; + name: string; +}; + export const CATEGORIES_TABS = { AERIAL: { id: 0, name: 'AERIAL' }, UNDERGROUND: { id: 1, name: 'UNDERGROUND' }, diff --git a/src/components/dialogs/line-types-catalog/line-type-segment-form.tsx b/src/components/dialogs/line-types-catalog/line-type-segment-form.tsx index 0fff828477..160c2e528f 100644 --- a/src/components/dialogs/line-types-catalog/line-type-segment-form.tsx +++ b/src/components/dialogs/line-types-catalog/line-type-segment-form.tsx @@ -32,13 +32,19 @@ import { CustomAGGrid, DefaultCellRenderer, ExpandableInput, + fetchStudyMetadata, type MuiStyles, snackWithFallback, useSnackMessage, } from '@gridsuite/commons-ui'; -import { getLineTypesCatalog } from '../../../services/network-modification'; +import { getLineTypesCatalog, getLineTypeWithLimits } from '../../../services/network-modification'; import GridItem from '../commons/grid-item'; -import { CurrentLimitsInfo, LineTypeInfo } from './line-catalog.type'; +import { + AreaTemperatureShapeFactorInfo, + CurrentLimitsInfo, + LimitSelectedRowData, + LineTypeInfo, +} from './line-catalog.type'; import { emptyLineSegment, SegmentFormData } from './segment-utils'; import { ColDef } from 'ag-grid-community'; import GridSection from '../commons/grid-section'; @@ -65,6 +71,7 @@ export const LineTypeSegmentForm = () => { const { snackError } = useSnackMessage(); const intl = useIntl(); const [currentLimitResult, setCurrentLimitResult] = useState([]); + const [limitsColumnDefs, setLimitsColumnDefs] = useState([]); // Fetches the lineTypes catalog on startup useEffect(() => { @@ -104,12 +111,10 @@ export const LineTypeSegmentForm = () => { ); const updateSegmentLimitsValues = useCallback( - (index: number) => { - const typeId = getValues(`${SEGMENTS}.${index}.${SEGMENT_TYPE_ID}`); - const entryFromCatalog = lineTypesCatalog?.find((entry) => entry.id === typeId); - setValue(`${SEGMENTS}.${index}.${SEGMENT_CURRENT_LIMITS}`, entryFromCatalog?.limitsForLineType); + (index: number, limitInfo: CurrentLimitsInfo[]) => { + setValue(`${SEGMENTS}.${index}.${SEGMENT_CURRENT_LIMITS}`, limitInfo); }, - [getValues, setValue, lineTypesCatalog] + [setValue] ); const updateTotals = useCallback(() => { @@ -141,54 +146,65 @@ export const LineTypeSegmentForm = () => { const keepMostConstrainingLimits = useCallback(() => { const segments: SegmentFormData[] = getValues(SEGMENTS); - const computedLimits = new Map(); + const mostContrainingLimits = new Map(); segments.forEach((segment) => { segment[SEGMENT_CURRENT_LIMITS]?.forEach((limit: CurrentLimitsInfo) => { - if (computedLimits.has(limit.limitSetName)) { - let computedLimit: CurrentLimitsInfo | undefined = computedLimits.get(limit.limitSetName); - if (computedLimit !== undefined) { - if (limit?.temporaryLimitValue != null) { - if (computedLimit.temporaryLimitValue == null) { - computedLimit.temporaryLimitValue = limit.temporaryLimitValue; - computedLimit.temporaryLimitName = limit.temporaryLimitName; - computedLimit.temporaryLimitAcceptableDuration = limit.temporaryLimitAcceptableDuration; + if (mostContrainingLimits.has(limit.limitSetName)) { + let computedLimit: CurrentLimitsInfo | undefined = mostContrainingLimits.get(limit.limitSetName); + if (computedLimit !== undefined && computedLimit.temporaryLimits !== null) { + limit.temporaryLimits.forEach((temporaryLimit) => { + const foundTemporaryLimit = computedLimit?.temporaryLimits.find( + (temporaryLimitData) => temporaryLimitData.name === temporaryLimit.name + ); + if (foundTemporaryLimit === undefined) { + computedLimit?.temporaryLimits.push(temporaryLimit); + } else if (temporaryLimit.limitValue === null) { + foundTemporaryLimit.limitValue = temporaryLimit.limitValue; } else { - let temporaryLimitValue = Math.min( - computedLimit.temporaryLimitValue, - limit.temporaryLimitValue + foundTemporaryLimit.limitValue = Math.min( + foundTemporaryLimit.limitValue, + temporaryLimit.limitValue ); - if (temporaryLimitValue === limit.temporaryLimitValue) { - computedLimit.temporaryLimitValue = limit.temporaryLimitValue; - computedLimit.temporaryLimitAcceptableDuration = - limit.temporaryLimitAcceptableDuration; - computedLimit.temporaryLimitName = limit.temporaryLimitName; - } } - } + }); computedLimit.permanentLimit = Math.min(computedLimit.permanentLimit, limit.permanentLimit); } } else { - computedLimits.set(limit.limitSetName, limit); + // need deep copy else segment[SEGMENT_CURRENT_LIMITS] will be modified with computedLimit + mostContrainingLimits.set(limit.limitSetName, structuredClone(limit)); } }); }); - setCurrentLimitResult(Array.from(computedLimits.values())); - setValue(FINAL_CURRENT_LIMITS, Array.from(computedLimits.values())); + setCurrentLimitResult(Array.from(mostContrainingLimits.values())); + setValue(FINAL_CURRENT_LIMITS, Array.from(mostContrainingLimits.values())); }, [getValues, setValue, setCurrentLimitResult]); const onSelectCatalogLine = useCallback( - (selectedLine: LineTypeInfo) => { - if (selectedLine && openCatalogDialogIndex !== null) { - const selectedType = selectedLine.type ?? ''; - const selectedTypeId = selectedLine.id ?? ''; - setValue(`${SEGMENTS}.${openCatalogDialogIndex}.${SEGMENT_TYPE_VALUE}`, selectedType); - setValue(`${SEGMENTS}.${openCatalogDialogIndex}.${SEGMENT_TYPE_ID}`, selectedTypeId); - clearErrors(`${SEGMENTS}.${openCatalogDialogIndex}.${SEGMENT_TYPE_VALUE}`); - updateSegmentValues(openCatalogDialogIndex); - updateSegmentLimitsValues(openCatalogDialogIndex); - updateTotals(); - keepMostConstrainingLimits(); - } + (selectedLine: LineTypeInfo, selectedAreaAndTemperature2LineTypeData: AreaTemperatureShapeFactorInfo) => { + getLineTypeWithLimits( + selectedLine.id, + selectedAreaAndTemperature2LineTypeData?.area, + selectedAreaAndTemperature2LineTypeData?.temperature, + selectedAreaAndTemperature2LineTypeData?.shapeFactor + ) + .then((lineTypeWithLimits) => { + if (lineTypeWithLimits && openCatalogDialogIndex !== null) { + const selectedType = lineTypeWithLimits.type ?? ''; + const selectedTypeId = lineTypeWithLimits.id ?? ''; + setValue(`${SEGMENTS}.${openCatalogDialogIndex}.${SEGMENT_TYPE_VALUE}`, selectedType); + setValue(`${SEGMENTS}.${openCatalogDialogIndex}.${SEGMENT_TYPE_ID}`, selectedTypeId); + clearErrors(`${SEGMENTS}.${openCatalogDialogIndex}.${SEGMENT_TYPE_VALUE}`); + updateSegmentValues(openCatalogDialogIndex); + updateSegmentLimitsValues(openCatalogDialogIndex, lineTypeWithLimits.limitsForLineType); + updateTotals(); + keepMostConstrainingLimits(); + } + }) + .catch((error) => + snackWithFallback(snackError, error, { + headerId: 'LineTypesCatalogFetchingError', + }) + ); }, [ updateSegmentValues, @@ -198,6 +214,7 @@ export const LineTypeSegmentForm = () => { openCatalogDialogIndex, updateSegmentLimitsValues, keepMostConstrainingLimits, + snackError, ] ); @@ -277,8 +294,8 @@ export const LineTypeSegmentForm = () => { [] ); - const limitsColumnDefs = useMemo((): ColDef[] => { - return [ + useMemo((): void => { + let base: ColDef[] = [ { headerName: intl.formatMessage({ id: 'lineTypes.currentLimits.limitSet' }), field: 'limitSetName', @@ -290,13 +307,67 @@ export const LineTypeSegmentForm = () => { field: 'permanentLimit', cellRenderer: DefaultCellRenderer, }, - { - headerName: intl.formatMessage({ id: 'lineTypes.currentLimits.Temporary' }), - field: 'temporaryLimitValue', - cellRenderer: DefaultCellRenderer, - }, ]; - }, [intl]); + let limitNamesSet = new Set(); + currentLimitResult.forEach((limit) => { + limit.temporaryLimits?.forEach((temporaryLimit) => { + limitNamesSet.add(temporaryLimit.name); + }); + }); + fetchStudyMetadata().then((studyMetadata) => { + manageHeader(base, limitNamesSet, studyMetadata?.temporaryLimitsNamesForCatalog); + }); + }, [intl, currentLimitResult]); + + const manageHeader = (base: ColDef[], limitNamesSet: Set, temporaryLimitsNamesForCatalog?: string[]) => { + // metadata order makes order of temporary limits columns + if (temporaryLimitsNamesForCatalog) { + temporaryLimitsNamesForCatalog.forEach((limitName) => { + if (limitNamesSet.has(limitName)) { + base.push({ + headerName: `${limitName} [A]`, + field: limitName, + cellRenderer: DefaultCellRenderer, + }); + } + }); + // limits that are in catalog and not in metadata are added at the end + limitNamesSet.forEach((limitName) => { + if (!temporaryLimitsNamesForCatalog.includes(limitName)) { + base.push({ + headerName: `${limitName} [A]`, + field: limitName, + cellRenderer: DefaultCellRenderer, + }); + } + }); + } else { + // no metadata, all limits are added (no order) + limitNamesSet.forEach((limitName) => { + base.push({ + headerName: `${limitName} [A]`, + field: limitName, + cellRenderer: DefaultCellRenderer, + }); + }); + } + setLimitsColumnDefs(base); + }; + + const rowData = useMemo(() => { + const finalDataArray: LimitSelectedRowData[] = []; + currentLimitResult.forEach((currentLimit) => { + const limitData: LimitSelectedRowData = { + limitSetName: currentLimit.limitSetName, + permanentLimit: currentLimit.permanentLimit, + }; + currentLimit.temporaryLimits.forEach((temporaryLimit) => { + limitData[temporaryLimit.name] = temporaryLimit.limitValue; + }); + finalDataArray.push(limitData); + }); + return finalDataArray; + }, [currentLimitResult]); return ( <> @@ -331,7 +402,7 @@ export const LineTypeSegmentForm = () => { ; export type LineTypesCatalogSelectorDialogProps = { - onSelectLine: (selectedLine: LineTypeInfo) => void; + onSelectLine: (selectedLine: LineTypeInfo, selectedAreaAndTemperature: AreaTemperatureShapeFactorInfo) => void; preselectedRowId: string; rowData: LineTypeInfo[]; onClose: () => void; @@ -108,59 +108,29 @@ export default function LineTypesCatalogSelectorDialog({ }); const { setValue, getValues } = formMethods; - const handleSelectedAerial = useCallback( - (selectedAerialRow: LineTypeInfo) => { - const selectedArea = getValues(AERIAL_AREAS); - const selectedTemperature = getValues(AERIAL_TEMPERATURES); + const handleSelectedAerial = useCallback((): AreaTemperatureShapeFactorInfo => { + const selectedArea = getValues(AERIAL_AREAS); + const selectedTemperature = getValues(AERIAL_TEMPERATURES); + return { area: selectedArea?.id, temperature: selectedTemperature?.id } as AreaTemperatureShapeFactorInfo; + }, [getValues]); - if (areasOptions?.length > 0 && aerialTemperatures?.length > 0) { - const filteredLimits = selectedAerialRow?.limitsForLineType?.filter( - (limit) => limit?.area === selectedArea?.id && limit?.temperature === selectedTemperature?.id - ); - selectedAerialRow.limitsForLineType = filteredLimits ? filteredLimits : []; - } - }, - [getValues, areasOptions?.length, aerialTemperatures?.length] - ); - - const handleSelectedUnderground = useCallback( - (selectedUndergroundRow: LineTypeInfo) => { - const selectedArea = getValues(UNDERGROUND_AREAS); - const selectedShapeFactor = getValues(UNDERGROUND_SHAPE_FACTORS); - - const areaId = selectedArea?.id; - const shapeFactorId = selectedShapeFactor?.id; - - if (areasOptions.length > 0 && areaId && shapeFactorId) { - const filteredLimits = selectedUndergroundRow?.limitsForLineType?.filter( - (limit) => limit?.area === areaId - ); - - if (filteredLimits) { - const shapeFactorValue = parseFloat(shapeFactorId); - if (!isNaN(shapeFactorValue) && shapeFactorValue !== 0) { - filteredLimits.forEach((limit) => { - limit.permanentLimit = Math.floor(limit.permanentLimit / shapeFactorValue); - }); - selectedUndergroundRow.limitsForLineType = filteredLimits; - } - } else { - selectedUndergroundRow.limitsForLineType = []; - } - } - }, - [getValues, areasOptions] - ); + const handleSelectedUnderground = useCallback((): AreaTemperatureShapeFactorInfo => { + const selectedArea = getValues(UNDERGROUND_AREAS); + const selectedShapeFactor = getValues(UNDERGROUND_SHAPE_FACTORS); + const areaId = selectedArea?.id; + const shapeFactorId = selectedShapeFactor?.id; + return { area: areaId, shapeFactor: shapeFactorId } as AreaTemperatureShapeFactorInfo; + }, [getValues]); const onSubmit = useCallback(() => { + let selectedAreaAndTemperature = { area: null, temperature: null } as AreaTemperatureShapeFactorInfo; if (selectedRow?.category === CATEGORIES_TABS.AERIAL.name) { - handleSelectedAerial(selectedRow); + selectedAreaAndTemperature = handleSelectedAerial(); } else if (selectedRow?.category === CATEGORIES_TABS.UNDERGROUND.name) { - handleSelectedUnderground(selectedRow); + selectedAreaAndTemperature = handleSelectedUnderground(); } - - selectedRow && onSelectLine?.(selectedRow); - }, [selectedRow, handleSelectedAerial, handleSelectedUnderground, onSelectLine]); + selectedRow && onSelectLine?.(selectedRow, selectedAreaAndTemperature); + }, [selectedRow, handleSelectedUnderground, handleSelectedAerial, onSelectLine]); const createOptionsFromAreas = (limitsData?: CurrentLimitsInfo[]) => { if (!limitsData?.length) { @@ -191,7 +161,7 @@ export default function LineTypesCatalogSelectorDialog({ const handleSelectedRowData = useCallback( async (selectedData: LineTypeInfo) => { try { - const lineTypeWithLimits = await getLineTypeWithLimits(selectedData.id); + const lineTypeWithLimits = await getLineTypeWithAreaAndTemperature(selectedData.id); selectedData.limitsForLineType = lineTypeWithLimits.limitsForLineType; selectedData.shapeFactors = lineTypeWithLimits.shapeFactors; setSelectedRow(selectedData); diff --git a/src/components/dialogs/line-types-catalog/segment-utils.ts b/src/components/dialogs/line-types-catalog/segment-utils.ts index cf1e551986..87baa30ba6 100644 --- a/src/components/dialogs/line-types-catalog/segment-utils.ts +++ b/src/components/dialogs/line-types-catalog/segment-utils.ts @@ -7,6 +7,7 @@ import { LIMIT_SET_NAME, + LIMIT_VALUE, PERMANENT_LIMIT, SEGMENT_CURRENT_LIMITS, SEGMENT_DISTANCE_VALUE, @@ -15,7 +16,9 @@ import { SEGMENT_SUSCEPTANCE, SEGMENT_TYPE_ID, SEGMENT_TYPE_VALUE, - TEMPORARY_LIMIT, + TEMPORARY_LIMIT_DURATION, + TEMPORARY_LIMIT_NAME, + TEMPORARY_LIMITS, } from 'components/utils/field-constants'; import yup from '../../utils/yup-config'; @@ -36,7 +39,16 @@ export const SegmentSchema = yup.object().shape({ yup.object().shape({ [LIMIT_SET_NAME]: yup.string().required(), [PERMANENT_LIMIT]: yup.number().required(), - [TEMPORARY_LIMIT]: yup.number().nullable(), + [TEMPORARY_LIMITS]: yup + .array() + .of( + yup.object().shape({ + [LIMIT_VALUE]: yup.number().required(), + [TEMPORARY_LIMIT_DURATION]: yup.number().required(), + [TEMPORARY_LIMIT_NAME]: yup.string().required(), + }) + ) + .nullable(), }) ), }); diff --git a/src/components/dialogs/network-modifications/line/creation/line-creation-dialog.tsx b/src/components/dialogs/network-modifications/line/creation/line-creation-dialog.tsx index 446a0b6cca..5899dc71b1 100644 --- a/src/components/dialogs/network-modifications/line/creation/line-creation-dialog.tsx +++ b/src/components/dialogs/network-modifications/line/creation/line-creation-dialog.tsx @@ -92,7 +92,10 @@ import { LineCreationInfos } from '../../../../../services/network-modification- import { LineModificationFormSchema } from '../modification/line-modification-type'; import { ComputedLineCharacteristics, CurrentLimitsInfo } from '../../../line-types-catalog/line-catalog.type'; import { LineCreationFormSchema, LineFormInfos } from './line-creation-type'; -import { OperationalLimitsGroupFormSchema } from '../../../limits/operational-limits-groups-types'; +import { + OperationalLimitsGroupFormSchema, + TemporaryLimitFormSchema, +} from '../../../limits/operational-limits-groups-types'; import { NetworkModificationDialogProps } from '../../../../graph/menus/network-modifications/network-modification-menu.type'; const emptyFormData: any = { @@ -281,14 +284,14 @@ const LineCreationDialog = ({ }); const finalLimits: OperationalLimitsGroupFormSchema[] = []; data[FINAL_CURRENT_LIMITS].forEach((item: CurrentLimitsInfo) => { - const temporaryLimitsList = []; - if (item.temporaryLimitValue) { + const temporaryLimitsList: TemporaryLimitFormSchema[] = []; + item.temporaryLimits.forEach((temporaryLimit) => { temporaryLimitsList.push({ - name: item.temporaryLimitName, - acceptableDuration: item.temporaryLimitAcceptableDuration, - value: item.temporaryLimitValue, + name: temporaryLimit.name, + acceptableDuration: temporaryLimit.acceptableDuration, + value: temporaryLimit.limitValue, }); - } + }); finalLimits.push({ id: item.limitSetName + APPLICABILITY.EQUIPMENT.id, name: item.limitSetName, diff --git a/src/components/utils/field-constants.ts b/src/components/utils/field-constants.ts index 416f938fb5..aaf2d8fd4b 100644 --- a/src/components/utils/field-constants.ts +++ b/src/components/utils/field-constants.ts @@ -212,7 +212,7 @@ export const B2 = 'b2'; export const LIMITS = 'limits'; export const TAB_HEADER = 'tabHeader'; export const LIMIT_SET_NAME = 'limitSetName'; -export const TEMPORARY_LIMIT = 'temporaryLimit'; +export const LIMIT_VALUE = 'limitValue'; export const TEMPORARY_LIMITS = 'temporaryLimits'; export const TEMPORARY_LIMIT_NAME = 'name'; export const TEMPORARY_LIMIT_DURATION = 'acceptableDuration'; diff --git a/src/services/network-modification.ts b/src/services/network-modification.ts index cc8ba5c7cd..996b8c4c1d 100644 --- a/src/services/network-modification.ts +++ b/src/services/network-modification.ts @@ -16,7 +16,34 @@ export function getLineTypesCatalog(): Promise { return backendFetchJson(url); } -export function getLineTypeWithLimits(id: string): Promise { +export function getLineTypeWithAreaAndTemperature(id: string): Promise { const url = `${PREFIX_NETWORK_MODIFICATION_QUERIES}/v1/network-modifications/catalog/line_types/${id}`; return backendFetchJson(url); } + +export function getLineTypeWithLimits( + id: string, + area: string | null, + temperature: string | null, + shapeFactor: number | null +): Promise { + let urlSearchParams = new URLSearchParams(); + if (area != null) { + urlSearchParams.append('area', area); + } + if (temperature != null) { + urlSearchParams.append('temperature', temperature); + } + if (shapeFactor != null) { + urlSearchParams.append('shapeFactor', shapeFactor.toString()); + } + const url = + `${PREFIX_NETWORK_MODIFICATION_QUERIES}/v1/network-modifications/catalog/line_types/${id}/with-limits?` + + urlSearchParams.toString(); + return backendFetchJson(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); +}