Skip to content

Commit

Permalink
front: lmr: linked train search improvements
Browse files Browse the repository at this point in the history
Signed-off-by: SarahBellaha <[email protected]>

front: stdcm: fix app crash when using the date picker in debug mode

front: add trigram of new selected op

front: stdcm: fix op search payload

front: lmr: be able to reset linked train search

front: lmr: fix date picker issue

front: change arrivalDate type to Date in isArrivalDateInSearchTimeWindow

front: lmr: ensure correct date handling in StdcmOpSchedule
  • Loading branch information
SarahBellaha committed Dec 16, 2024
1 parent 19e6bec commit e815e4f
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,22 @@ const StdcmLinkedPathSearch = ({

const {
displaySearchButton,
hasSearchBeenLaunched,
launchTrainScheduleSearch,
linkedPathDate,
linkedPathResults,
resetLinkedPathSearch,
selectableSlot,
setDisplaySearchButton,
setLinkedPathDate,
setTrainNameInput,
trainNameInput,
} = useLinkedPathSearch();

const removeLinkedPathCard = () => {
setShowLinkedPathSearch(false);
resetLinkedPathSearch();
};

return (
<div className={`stdcm-linked-path-search-container ${className}`}>
{!displayLinkedPathSearch ? (
Expand All @@ -59,7 +64,7 @@ const StdcmLinkedPathSearch = ({
disabled={disabled}
name={cardName}
title={
<button type="button" onClick={() => setShowLinkedPathSearch(false)}>
<button type="button" onClick={removeLinkedPathCard}>
{t('translation:common.delete').toLowerCase()}
</button>
}
Expand Down Expand Up @@ -98,18 +103,17 @@ const StdcmLinkedPathSearch = ({
{t('find')}
</button>
)}
{!displaySearchButton && !linkedPathResults.length && (
{!displaySearchButton && !linkedPathResults && (
<div className="stdcm-linked-path-button white">
<Gear size="lg" className="stdcm-linked-path-loading" />
</div>
)}
{linkedPathResults.length > 0 ? (
<StdcmLinkedPathResults linkedPathResults={linkedPathResults} linkedOp={linkedOp} />
) : (
hasSearchBeenLaunched && (
{linkedPathResults &&
(linkedPathResults.length > 0 ? (
<StdcmLinkedPathResults linkedPathResults={linkedPathResults} linkedOp={linkedOp} />
) : (
<p className="text-center mb-0">{t('noCorrespondingResults')}</p>
)
)}
))}
</StdcmCard>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
};

Expand All @@ -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: {
Expand Down Expand Up @@ -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 (
<>
<div className="arrival-type-select">
Expand Down
118 changes: 81 additions & 37 deletions front/src/applications/stdcm/hooks/useLinkedPathSearch.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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<StdcmLinkedPathResult[]>([]);
const [linkedPathResults, setLinkedPathResults] = useState<StdcmLinkedPathResult[]>();

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: {
Expand All @@ -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,
};
Expand Down
22 changes: 18 additions & 4 deletions front/src/applications/stdcm/utils/computeOpSchedules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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),
},
};
};
Expand Down
12 changes: 9 additions & 3 deletions front/src/utils/__tests__/date.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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);
});

Expand All @@ -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);
});
});
Expand Down
Loading

0 comments on commit e815e4f

Please sign in to comment.