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,
})}
>
-