diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmLinkedPathSearch.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmLinkedPathSearch.tsx
index 30cde1c5d2f..ac38f698c1a 100644
--- a/front/src/applications/stdcm/components/StdcmForm/StdcmLinkedPathSearch.tsx
+++ b/front/src/applications/stdcm/components/StdcmForm/StdcmLinkedPathSearch.tsx
@@ -33,10 +33,10 @@ const StdcmLinkedPathSearch = ({
const {
displaySearchButton,
- hasSearchBeenLaunched,
launchTrainScheduleSearch,
linkedPathDate,
linkedPathResults,
+ resetLinkedPathSearch,
selectableSlot,
setDisplaySearchButton,
setLinkedPathDate,
@@ -44,6 +44,11 @@ const StdcmLinkedPathSearch = ({
trainNameInput,
} = useLinkedPathSearch();
+ const removeLinkedPathCard = () => {
+ setShowLinkedPathSearch(false);
+ resetLinkedPathSearch();
+ };
+
return (
{!displayLinkedPathSearch ? (
@@ -59,7 +64,7 @@ const StdcmLinkedPathSearch = ({
disabled={disabled}
name={cardName}
title={
-
)}
- {!displaySearchButton && !linkedPathResults.length && (
+ {!displaySearchButton && !linkedPathResults && (
)}
- {linkedPathResults.length > 0 ? (
-
- ) : (
- hasSearchBeenLaunched && (
+ {linkedPathResults &&
+ (linkedPathResults.length > 0 ? (
+
+ ) : (
{t('noCorrespondingResults')}
- )
- )}
+ ))}
)}
diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmOpSchedule.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmOpSchedule.tsx
index ea584cc079e..1d377a4b519 100644
--- a/front/src/applications/stdcm/components/StdcmForm/StdcmOpSchedule.tsx
+++ b/front/src/applications/stdcm/components/StdcmForm/StdcmOpSchedule.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useEffect, useMemo } from 'react';
import { DatePicker, Select, TimePicker, TolerancePicker } from '@osrd-project/ui-core';
import { useTranslation } from 'react-i18next';
@@ -38,9 +38,7 @@ type StdcmOpScheduleProps = {
const defaultDate = (date?: Date) => {
const newDate = date ? new Date(date) : new Date();
- newDate.setHours(0);
- newDate.setMinutes(0);
- newDate.setSeconds(0);
+ newDate.setHours(0, 0, 0);
return newDate;
};
@@ -63,13 +61,13 @@ const StdcmOpSchedule = ({
useMemo(() => {
const isArrivalDateValid =
opTimingData?.arrivalDate &&
- isArrivalDateInSearchTimeWindow(opTimingData.arrivalDate, searchDatetimeWindow);
+ isArrivalDateInSearchTimeWindow(new Date(opTimingData.arrivalDate), searchDatetimeWindow);
return {
arrivalDate:
opTimingData && isArrivalDateValid
? new Date(opTimingData.arrivalDate)
: defaultDate(searchDatetimeWindow?.begin),
- arrivalTime: opTimingData?.arrivalTime || '--:--',
+ arrivalTime: opTimingData?.arrivalTime,
arrivalTimeHours: opTimingData?.arrivalTimehours,
arrivalTimeMinutes: opTimingData?.arrivalTimeMinutes,
arrivalToleranceValues: {
@@ -101,6 +99,20 @@ const StdcmOpSchedule = ({
[t, searchDatetimeWindow]
);
+ useEffect(() => {
+ if (
+ (!isArrivalDateInSearchTimeWindow(arrivalDate, searchDatetimeWindow) ||
+ !opTimingData?.arrivalDate) &&
+ opScheduleTimeType === 'preciseTime'
+ ) {
+ onArrivalChange({
+ date: defaultDate(searchDatetimeWindow?.begin),
+ hours: arrivalTimeHours || 0,
+ minutes: arrivalTimeMinutes || 0,
+ });
+ }
+ }, [searchDatetimeWindow, opScheduleTimeType]);
+
return (
<>
diff --git a/front/src/applications/stdcm/hooks/useLinkedPathSearch.ts b/front/src/applications/stdcm/hooks/useLinkedPathSearch.ts
index 0f35d41ccca..355b1734aa4 100644
--- a/front/src/applications/stdcm/hooks/useLinkedPathSearch.ts
+++ b/front/src/applications/stdcm/hooks/useLinkedPathSearch.ts
@@ -1,21 +1,29 @@
-import { useMemo, useState, useCallback } from 'react';
+import { useMemo, useState, useCallback, useEffect } from 'react';
-import { compact, groupBy } from 'lodash';
+import { compact } from 'lodash';
import { useSelector } from 'react-redux';
-import type { PathItem, SearchResultItemTrainSchedule } from 'common/api/osrdEditoastApi';
+import type {
+ PathItem,
+ SearchQuery,
+ SearchResultItemOperationalPoint,
+ SearchResultItemTrainSchedule,
+} from 'common/api/osrdEditoastApi';
import { osrdEditoastApi } from 'common/api/osrdEditoastApi';
-import { useOsrdConfSelectors } from 'common/osrdContext';
-import { isEqualDate } from 'utils/date';
+import { useInfraID, useOsrdConfSelectors } from 'common/osrdContext';
+import { isArrivalDateInSearchTimeWindow, isEqualDate } from 'utils/date';
-import type { StdcmLinkedPathResult, StdcmLinkedPathStep } from '../types';
+import type { StdcmLinkedPathResult } from '../types';
import computeOpSchedules from '../utils/computeOpSchedules';
const useLinkedPathSearch = () => {
const [postSearch] = osrdEditoastApi.endpoints.postSearch.useMutation();
+ const [postTrainScheduleSimulationSummary] =
+ osrdEditoastApi.endpoints.postTrainScheduleSimulationSummary.useLazyQuery();
const { getTimetableID, getSearchDatetimeWindow } = useOsrdConfSelectors();
+ const infraId = useInfraID();
const timetableId = useSelector(getTimetableID);
const searchDatetimeWindow = useSelector(getSearchDatetimeWindow);
@@ -29,42 +37,60 @@ const useLinkedPathSearch = () => {
}, [searchDatetimeWindow]);
const [displaySearchButton, setDisplaySearchButton] = useState(true);
- const [hasSearchBeenLaunched, setHasSearchBeenLaunched] = useState(false);
const [trainNameInput, setTrainNameInput] = useState('');
const [linkedPathDate, setLinkedPathDate] = useState(selectableSlot.start);
- const [linkedPathResults, setLinkedPathResults] = useState([]);
+ const [linkedPathResults, setLinkedPathResults] = useState();
- const getExtremitiesDetails = useCallback(
- async (pathItemList: PathItem[]) => {
- const origin = pathItemList.at(0)!;
- const destination = pathItemList.at(-1)!;
- if (!('operational_point' in origin) || !('operational_point' in destination))
- return undefined;
- const originId = origin.operational_point;
- const destinationId = destination.operational_point;
+ const getExtremityDetails = useCallback(
+ async (pathItem: PathItem) => {
+ if (!('operational_point' in pathItem) && !('uic' in pathItem)) return undefined;
+
+ const pathItemQuery =
+ 'operational_point' in pathItem
+ ? ['=', ['obj_id'], pathItem.operational_point]
+ : ([
+ 'and',
+ ['=', ['uic'], pathItem.uic],
+ ['=', ['ch'], pathItem.secondary_code],
+ ] as SearchQuery);
try {
const payloadOP = {
object: 'operationalpoint',
- query: ['or', ['=', ['obj_id'], originId], ['=', ['obj_id'], destinationId]],
- };
- const resultsOP = await postSearch({ searchPayload: payloadOP, pageSize: 25 }).unwrap();
- const groupedResults = groupBy(resultsOP, 'obj_id');
- return {
- origin: groupedResults[originId][0],
- destination: groupedResults[destinationId][0],
+ query: pathItemQuery,
};
+ const opDetails = (await postSearch({
+ searchPayload: payloadOP,
+ pageSize: 25,
+ }).unwrap()) as SearchResultItemOperationalPoint[];
+ return opDetails[0];
} catch (error) {
- console.error('Failed to fetch operational points:', error);
+ console.error('Failed to fetch operational point:', error);
return undefined;
}
},
[postSearch]
);
+ const getTrainsSummaries = useCallback(
+ async (trainsIds: number[]) => {
+ if (!infraId) return undefined;
+ const trainsSummaries = await postTrainScheduleSimulationSummary({
+ body: {
+ infra_id: infraId,
+ ids: trainsIds,
+ },
+ }).unwrap();
+ return trainsSummaries;
+ },
+ [postTrainScheduleSimulationSummary, infraId]
+ );
+
const launchTrainScheduleSearch = useCallback(async () => {
+ setLinkedPathResults(undefined);
+ if (!trainNameInput) return;
+
setDisplaySearchButton(false);
- setLinkedPathResults([]);
try {
const results = (await postSearch({
searchPayload: {
@@ -86,41 +112,59 @@ const useLinkedPathSearch = () => {
return;
}
+ const filteredResultsSummaries = await getTrainsSummaries(filteredResults.map((r) => r.id));
+
const newLinkedPathResults = await Promise.all(
filteredResults.map(async (result) => {
- const opDetails = await getExtremitiesDetails(result.path);
- const computedOpSchedules = computeOpSchedules(
- result.start_time,
- result.schedule.at(-1)!.arrival!
- );
- if (opDetails === undefined) return undefined;
+ const resultSummary = filteredResultsSummaries && filteredResultsSummaries[result.id];
+ if (!resultSummary || resultSummary.status !== 'success') return undefined;
+ const msFromStartTime = resultSummary.path_item_times_final.at(-1)!;
+
+ const originDetails = await getExtremityDetails(result.path.at(0)!);
+ const destinationDetails = await getExtremityDetails(result.path.at(-1)!);
+ const computedOpSchedules = computeOpSchedules(result.start_time, msFromStartTime);
+
+ if (!originDetails || !destinationDetails) return undefined;
return {
trainName: result.train_name,
- origin: { ...opDetails.origin, ...computedOpSchedules.origin } as StdcmLinkedPathStep,
+ origin: { ...originDetails, ...computedOpSchedules.origin },
destination: {
- ...opDetails.destination,
+ ...destinationDetails,
...computedOpSchedules.destination,
- } as StdcmLinkedPathStep,
+ },
};
})
);
setLinkedPathResults(compact(newLinkedPathResults));
- setHasSearchBeenLaunched(true);
} catch (error) {
console.error('Train schedule search failed:', error);
setDisplaySearchButton(true);
}
- }, [postSearch, trainNameInput, timetableId, linkedPathDate, getExtremitiesDetails]);
+ }, [postSearch, trainNameInput, timetableId, linkedPathDate, getExtremityDetails]);
+
+ const resetLinkedPathSearch = () => {
+ setDisplaySearchButton(true);
+ setLinkedPathResults(undefined);
+ setTrainNameInput('');
+ };
+
+ useEffect(() => {
+ if (!isArrivalDateInSearchTimeWindow(linkedPathDate, searchDatetimeWindow)) {
+ setLinkedPathDate(selectableSlot.start);
+ resetLinkedPathSearch();
+ }
+ }, [selectableSlot]);
return {
displaySearchButton,
- hasSearchBeenLaunched,
launchTrainScheduleSearch,
linkedPathDate,
linkedPathResults,
+ resetLinkedPathSearch,
selectableSlot,
setDisplaySearchButton,
setLinkedPathDate,
+ setLinkedPathResults,
setTrainNameInput,
trainNameInput,
};
diff --git a/front/src/applications/stdcm/utils/computeOpSchedules.ts b/front/src/applications/stdcm/utils/computeOpSchedules.ts
index 47566fade38..cb40269d796 100644
--- a/front/src/applications/stdcm/utils/computeOpSchedules.ts
+++ b/front/src/applications/stdcm/utils/computeOpSchedules.ts
@@ -4,12 +4,26 @@ import {
substractDurationToIsoDate,
} from 'utils/date';
-const computeOpSchedules = (startTime: string, secondsFromStartTime: string) => {
+/**
+ * Computes the operation schedules for a given start time and duration.
+ *
+ * @param startTime - The ISO string representing the start time.
+ * @param msFromStartTime - The duration in milliseconds from the start time.
+ * @returns An object containing the origin and destination schedules.
+ *
+ * The function extracts the date and time from the provided ISO start time and calculates the destination arrival time
+ * by adding the specified duration. It then returns an object with the origin and destination schedules, including
+ * the date, time, and ISO arrival times.
+ *
+ * Note: A margin of 1800 seconds (30 minutes) is applied to the departure and arrival times to allow for necessary
+ * activities such as preparation for the next departure.
+ */
+const computeOpSchedules = (startTime: string, msFromStartTime: number) => {
const { arrivalDate: originDate, arrivalTime: originTime } = extractDateAndTimefromISO(
startTime,
'DD/MM/YY'
);
- const destinationArrivalTime = addDurationToIsoDate(startTime, secondsFromStartTime);
+ const destinationArrivalTime = addDurationToIsoDate(startTime, msFromStartTime, 'millisecond');
const { arrivalDate: destinationDate, arrivalTime: destinationTime } = extractDateAndTimefromISO(
destinationArrivalTime,
'DD/MM/YY'
@@ -19,12 +33,12 @@ const computeOpSchedules = (startTime: string, secondsFromStartTime: string) =>
origin: {
date: originDate,
time: originTime,
- isoArrivalTime: substractDurationToIsoDate(startTime, 'PT1800S'),
+ isoArrivalTime: substractDurationToIsoDate(startTime, 1800),
},
destination: {
date: destinationDate,
time: destinationTime,
- isoArrivalTime: addDurationToIsoDate(destinationArrivalTime, 'PT1800S'),
+ isoArrivalTime: addDurationToIsoDate(destinationArrivalTime, 1800),
},
};
};
diff --git a/front/src/utils/__tests__/date.spec.ts b/front/src/utils/__tests__/date.spec.ts
index 99ff950ccb2..3a04f0fde14 100644
--- a/front/src/utils/__tests__/date.spec.ts
+++ b/front/src/utils/__tests__/date.spec.ts
@@ -95,7 +95,7 @@ describe('extractDateAndTimefromISO', () => {
describe('isArrivalDateInSearchTimeWindow', () => {
it('should return true if searchDatetimeWindow is undefined', () => {
- const result = isArrivalDateInSearchTimeWindow('2024-08-01T10:00:00Z', undefined);
+ const result = isArrivalDateInSearchTimeWindow(new Date('2024-08-01T10:00:00Z'), undefined);
expect(result).toBe(true);
});
@@ -104,7 +104,10 @@ describe('isArrivalDateInSearchTimeWindow', () => {
begin: new Date('2024-08-01T00:00:00Z'),
end: new Date('2024-08-02T00:00:00Z'),
};
- const result = isArrivalDateInSearchTimeWindow('2024-08-01T10:00:00Z', searchDatetimeWindow);
+ const result = isArrivalDateInSearchTimeWindow(
+ new Date('2024-08-01T10:00:00Z'),
+ searchDatetimeWindow
+ );
expect(result).toBe(true);
});
@@ -113,7 +116,10 @@ describe('isArrivalDateInSearchTimeWindow', () => {
begin: new Date('2024-08-01T00:00:00Z'),
end: new Date('2024-08-02T00:00:00Z'),
};
- const result = isArrivalDateInSearchTimeWindow('2024-07-30T23:59:59Z', searchDatetimeWindow);
+ const result = isArrivalDateInSearchTimeWindow(
+ new Date('2024-07-30T23:59:59Z'),
+ searchDatetimeWindow
+ );
expect(result).toBe(false);
});
});
diff --git a/front/src/utils/date.ts b/front/src/utils/date.ts
index 501eec11de4..824f7e5ae5f 100644
--- a/front/src/utils/date.ts
+++ b/front/src/utils/date.ts
@@ -1,4 +1,4 @@
-import dayjs from 'dayjs';
+import dayjs, { type ManipulateType } from 'dayjs';
import 'dayjs/locale/fr';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import timezone from 'dayjs/plugin/timezone';
@@ -6,11 +6,9 @@ import utc from 'dayjs/plugin/utc';
import i18next from 'i18next';
import type { ScheduleConstraint } from 'applications/stdcm/types';
-import type { IsoDateTimeString, IsoDurationString } from 'common/types';
+import type { IsoDateTimeString } from 'common/types';
import i18n from 'i18n';
-import { ISO8601Duration2sec } from './timeManipulation';
-
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(customParseFormat);
@@ -151,16 +149,18 @@ export function convertIsoUtcToLocalTime(isoUtcString: IsoDateTimeString): strin
export function addDurationToIsoDate(
startTime: IsoDateTimeString,
- duration: IsoDurationString
+ duration: number,
+ durationUnit: ManipulateType = 'second'
): IsoDateTimeString {
- return dayjs(startTime).add(ISO8601Duration2sec(duration), 'second').format();
+ return dayjs(startTime).add(duration, durationUnit).format();
}
export function substractDurationToIsoDate(
startTime: IsoDateTimeString,
- duration: IsoDurationString
+ duration: number,
+ durationUnit: ManipulateType = 'second'
): IsoDateTimeString {
- return dayjs(startTime).subtract(ISO8601Duration2sec(duration), 'second').format();
+ return dayjs(startTime).subtract(duration, durationUnit).format();
}
/**
@@ -191,18 +191,17 @@ export function extractDateAndTimefromISO(arrivalTime: string, dateFormat: strin
/**
* Checks if the given arrival date falls within the specified search time window.
*
- * @param {string} arrivalTime - The arrival time as a string, which will be parsed into a Date object.
+ * @param {Date} arrivalDate - The arrival time, which is a Date object.
* @param {{ begin: Date; end: Date } | undefined} searchDatetimeWindow - An object containing the start and end dates of the search window. If undefined, the function will return true.
* @returns {boolean} - Returns true if the arrival date is within the search time window, or if the search time window is undefined. Returns false otherwise.
*/
export function isArrivalDateInSearchTimeWindow(
- arrivalTime: string,
+ arrivalDate: Date,
searchDatetimeWindow?: { begin: Date; end: Date }
) {
if (!searchDatetimeWindow) {
return true;
}
- const arrivalDate = new Date(arrivalTime);
return arrivalDate >= searchDatetimeWindow.begin && arrivalDate <= searchDatetimeWindow.end;
}
diff --git a/front/tests/pages/stdcm-page-model.ts b/front/tests/pages/stdcm-page-model.ts
index aff03785e20..ae363380172 100644
--- a/front/tests/pages/stdcm-page-model.ts
+++ b/front/tests/pages/stdcm-page-model.ts
@@ -315,7 +315,7 @@ class STDCMPage {
await expect(this.dynamicOriginCh).toHaveValue('BV');
await expect(this.originArrival).toHaveValue('preciseTime');
await expect(this.dateOriginArrival).toHaveValue('17/10/24');
- await expect(this.timeOriginArrival).toHaveValue('');
+ await expect(this.timeOriginArrival).toHaveValue('00:00');
await expect(this.toleranceOriginArrival).toHaveValue('-30/+30');
await this.dynamicOriginCh.selectOption('BC');
await this.originArrival.selectOption('respectDestinationSchedule');
@@ -340,7 +340,7 @@ class STDCMPage {
await expect(this.toleranceDestinationArrival).not.toBeVisible();
await this.destinationArrival.selectOption('preciseTime');
await expect(this.dateDestinationArrival).toHaveValue('17/10/24');
- await expect(this.timeDestinationArrival).toHaveValue('');
+ await expect(this.timeDestinationArrival).toHaveValue('00:00');
await expect(this.toleranceDestinationArrival).toHaveValue('-30/+30');
await this.dateDestinationArrival.fill('18/10/24');
await this.timeDestinationArrival.click();