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={ - } @@ -98,18 +103,17 @@ const StdcmLinkedPathSearch = ({ {t('find')} )} - {!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();