diff --git a/front/public/locales/en/operational-studies.json b/front/public/locales/en/operational-studies.json index dd17bc0cbe6..725c48a9dac 100644 --- a/front/public/locales/en/operational-studies.json +++ b/front/public/locales/en/operational-studies.json @@ -426,6 +426,7 @@ "moveLocationOnMap": "Move location on the map", "movePointOnMap": "Move this waypoint using the map", "next": "Next", + "noComputation": "there will be no computation for this train", "opId": "Operational point identifier", "opName": "Operational point name", "opType": "Type", diff --git a/front/public/locales/fr/operational-studies.json b/front/public/locales/fr/operational-studies.json index f63f47f1d3b..4c086fe57ba 100644 --- a/front/public/locales/fr/operational-studies.json +++ b/front/public/locales/fr/operational-studies.json @@ -426,6 +426,7 @@ "moveLocationOnMap": "Déplacer le point remarquable sur la carte", "movePointOnMap": "Déplacez ce point en utilisant la carte", "next": "Suivant", + "noComputation": "il n'y aura pas de calcul pour ce train", "opId": "Identifiant de point", "opName": "Nom du point remarquable", "opType": "Type", diff --git a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModal.tsx b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModal.tsx index 54258d06365..2f90a04bec8 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModal.tsx +++ b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/ItineraryModal.tsx @@ -18,6 +18,7 @@ import type { PathItemLocation, TrainCategory, } from 'common/api/osrdEditoastApi'; +import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; import Banner from 'common/Banner'; import { computeBBoxViewport } from 'common/Map/WarpedMap/core/helpers'; import { useInfraID } from 'common/osrdContext'; @@ -35,7 +36,7 @@ import { getPathSteps, getRollingStockName, } from 'reducers/osrdconf/operationalStudiesConf/selectors'; -import type { PathStep, PathStepMetadata, PathStepV2 } from 'reducers/osrdconf/types'; +import type { PathStep, PathStepMetadata, PathStepV2, TrainScheduleToEditData } from 'reducers/osrdconf/types'; import { useAppDispatch } from 'store'; import { addElementAtIndex } from 'utils/array'; import { Duration } from 'utils/duration'; @@ -61,6 +62,7 @@ type ItineraryModalProps = { itineraryModalIsOpen: boolean; setItineraryModalIsOpen: (isOpen: boolean) => void; displayTrainScheduleManagement: string; + trainScheduleToEditData?: TrainScheduleToEditData; }; export type ItineraryModalFormState = { @@ -75,6 +77,7 @@ const ItineraryModal = ({ itineraryModalIsOpen, setItineraryModalIsOpen, displayTrainScheduleManagement, + trainScheduleToEditData, }: ItineraryModalProps) => { const { t } = useTranslation('operational-studies', { keyPrefix: 'manageTrainSchedule.itineraryModal', @@ -153,6 +156,42 @@ const ItineraryModal = ({ const { launchPathfindingV2, pathProperties, pathfindingError } = usePathfindingV2(); const { convertFeatureClickToLocation } = useMapTrackSelection(infraId); + /** + * When a custom track name is added (one that doesn't exist in the OP parts), + * immediately update the train schedule's path via API to persist the local_track_name. + */ + const updateTrainSchedulePathTrackName = useCallback( + async (stepId: string, trackName: string) => { + if (!trainScheduleToEditData) return; + + const { trainScheduleId } = trainScheduleToEditData; + + // GET the current train schedule fresh from the DB to avoid overwriting anything + const { id: _, train_schedule_set_id: __, ...currentData } = await dispatch( + osrdEditoastApi.endpoints.getTrainSchedulesById.initiate( + { id: trainScheduleId }, + { subscribe: false } + ) + ).unwrap(); + + // Only update the local_track_name on the matching path step + await dispatch( + osrdEditoastApi.endpoints.putTrainSchedulesById.initiate({ + id: trainScheduleId, + trainSchedule: { + ...currentData, + path: currentData.path.map((item) => + item.id === stepId && item.location.type !== 'track_offset' + ? { ...item, location: { ...item.location, local_track_name: trackName || undefined } } + : item + ), + }, + }) + ).unwrap(); + }, + [dispatch, trainScheduleToEditData] + ); + const applyOperationalPointToStep = ( stepId: string, suggestion: OperationalPointSuggestion, @@ -546,12 +585,12 @@ const ItineraryModal = ({ {rollingStockMessage && } {hasInvalidPathStepDisplay && (
- +
)} {!hasInvalidPathStepDisplay && pathfindingError && (
- +
)} {submitAttempted && @@ -678,6 +717,9 @@ const ItineraryModal = ({ }) ); }} + onAddCustomTrackName={(trackName) => { + updateTrainSchedulePathTrackName(pathStep.id, trackName); + }} onOpBlur={() => { // If the user focuses out on an input with a valid op, we display the last valid op of this input (or empty) const valueOnFocus = focusValueRef.current[pathStep.id]; diff --git a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/PathStepItem.tsx b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/PathStepItem.tsx index 49e6b4bd597..9bfdc941f28 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/PathStepItem.tsx +++ b/front/src/applications/operationalStudies/views/Scenario/components/ManageTrainSchedule/Itinerary/PathStepItem.tsx @@ -1,6 +1,6 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; -import { ComboBox, Select, SegmentedControl } from '@osrd-project/ui-core'; +import { ComboBox, SegmentedControl } from '@osrd-project/ui-core'; import { AddedLocation, AddLocation, @@ -38,6 +38,7 @@ type PathStepProps = { categoryColors: CategoryColors; onOpInputChange: (value: string) => void; onTrackNameChange: (trackName: string) => void; + onAddCustomTrackName?: (trackName: string) => void; onOpFocus: () => void; onOpBlur: () => void; inputValue: string | undefined; @@ -66,6 +67,7 @@ const PathStepItem = ({ categoryColors, onOpInputChange, onTrackNameChange, + onAddCustomTrackName, onOpFocus, onOpBlur, inputValue, @@ -110,11 +112,8 @@ const PathStepItem = ({ return (message += t('requestedPoint')); } - const trackInfo = - location && location.local_track_name ? `, ${t('track')} ${location.local_track_name}` : ''; - if (location?.operational_point.type === 'id') { - return (message += t('opId') + trackInfo); + return (message += t('opId')); } const secondaryCodeInfo = @@ -130,7 +129,7 @@ const PathStepItem = ({ message += t('uic') + ' ' + location.operational_point.uic; } - return message + secondaryCodeInfo + trackInfo; + return message + secondaryCodeInfo; }; const selectedSecondaryCodeOption = useMemo(() => { @@ -169,17 +168,29 @@ const PathStepItem = ({ return [{ label: '', id: '' }, ...sortedSuggestions]; }, [pathStepMetadata, selectedSecondaryCodeOption.id]); + const [filteredTrackSuggestions, setFilteredTrackSuggestions] = useState(trackNameSuggestions); + + useEffect(() => { + setFilteredTrackSuggestions(trackNameSuggestions); + }, [trackNameSuggestions]); + const selectedTrackNameOption = useMemo(() => { + // When OP is invalid but has a local_track_name, show it + if (pathStepMetadata?.isInvalid && pathStepMetadata.localTrackName) { + return { label: pathStepMetadata.localTrackName, id: pathStepMetadata.localTrackName }; + } + // No track should be selected if the path step is invalid or has no secondary code // or is a step added by map click - if (!isOpRefMetadata(pathStepMetadata) || !pathStepMetadata.trackName) { return EMPTY_OPTION; } return ( - trackNameSuggestions.find((track) => track.label === pathStepMetadata.trackName) || - EMPTY_OPTION + trackNameSuggestions.find((track) => track.label === pathStepMetadata.trackName) || { + label: pathStepMetadata.trackName, + id: pathStepMetadata.trackName, + } ); }, [pathStepMetadata, trackNameSuggestions]); @@ -293,6 +304,7 @@ const PathStepItem = ({ className={cx('path-step-wrapper', { 'is-placeholder': isTrailingPlaceHolder, 'map-selection-active': isMapSelectionMode, + 'is-invalid': isInvalidAndIsEditing, })} >
-