From 4fef754fc7d3ad510cc5f6d5f20a45c51181b796 Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Tue, 3 Sep 2024 09:11:56 +0200 Subject: [PATCH] save --- .../hooks/useSetupItineraryForTrainUpdate.ts | 8 +- front/src/common/types.ts | 15 + front/src/modules/pathfinding/utils.ts | 13 +- front/src/modules/timesStops/ReadOnlyTime.tsx | 21 + front/src/modules/timesStops/TimeInput.tsx | 29 +- front/src/modules/timesStops/TimesStops.tsx | 44 +- .../modules/timesStops/TimesStopsInput.tsx | 2 - .../modules/timesStops/TimesStopsOutput.tsx | 6 +- .../helpers/__tests__/scheduleData.spec.ts | 62 ++- .../helpers/__tests__/utils.spec.ts | 413 +++++++++++++++++- .../timesStops/helpers/scheduleData.ts | 26 +- front/src/modules/timesStops/helpers/utils.ts | 187 ++++++-- .../timesStops/hooks/useOutputTableData.ts | 5 +- .../timesStops/hooks/useTimeStopsColumns.ts | 32 +- .../timesStops/styles/_readOnlyTime.scss | 3 + .../modules/timesStops/styles/_timeInput.scss | 15 + .../modules/timesStops/styles/timesStops.scss | 2 + front/src/modules/timesStops/types.ts | 10 +- .../helpers/checkCurrentConfig.ts | 5 +- .../helpers/formatSchedule.ts | 28 +- .../hooks/useUpdateTrainSchedule.ts | 2 +- .../components/ManageTrainSchedule/types.ts | 10 +- .../reducers/osrdconf/osrdConfCommon/index.ts | 6 + front/src/reducers/osrdconf/types.ts | 3 +- front/src/utils/date.ts | 8 +- 25 files changed, 803 insertions(+), 152 deletions(-) create mode 100644 front/src/modules/timesStops/ReadOnlyTime.tsx create mode 100644 front/src/modules/timesStops/styles/_readOnlyTime.scss create mode 100644 front/src/modules/timesStops/styles/_timeInput.scss diff --git a/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts b/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts index 69ad7e80d2f..e6771eda3c4 100644 --- a/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts +++ b/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts @@ -22,7 +22,6 @@ import { setFailure } from 'reducers/main'; import type { OperationalStudiesConfSliceActions } from 'reducers/osrdconf/operationalStudiesConf'; import type { PathStep } from 'reducers/osrdconf/types'; import { useAppDispatch } from 'store'; -import { addDurationToIsoDate } from 'utils/date'; import { castErrorToFailure } from 'utils/error'; import { getPointCoordinates } from 'utils/geometry'; import { mmToM } from 'utils/physics'; @@ -67,13 +66,13 @@ const computeBasePathSteps = (trainSchedule: TrainScheduleResult) => name = step.operational_point; } + // NOTE: arrival was the time "HH:MM:SS" + // addDurationToIsoDate(trainSchedule.start_time, arrival).substring(11, 19) return { ...stepWithoutSecondaryCode, ch: 'secondary_code' in step ? step.secondary_code : undefined, name, - arrival: arrival - ? addDurationToIsoDate(trainSchedule.start_time, arrival).substring(11, 19) - : arrival, + arrival, stopFor: stopFor ? ISO8601Duration2sec(stopFor).toString() : stopFor, locked, onStopSignal, @@ -258,7 +257,6 @@ const useSetupItineraryForTrainUpdate = ( dispatch(setFailure(castErrorToFailure(e))); } } - adjustConfWithTrainToModifyV2( trainSchedule, pathSteps || computeBasePathSteps(trainSchedule), diff --git a/front/src/common/types.ts b/front/src/common/types.ts index 371c2aadae8..5b83537346f 100644 --- a/front/src/common/types.ts +++ b/front/src/common/types.ts @@ -15,8 +15,23 @@ export const DATA_TYPES = { */ export type TimeString = string; +/** + * A string with the complete iso format + * + * @example "2024-08-08T10:12:46.209Z" + * @example "2024-08-08T10:12:46Z" + * @example "2024-08-08T10:12:46+02:00" + */ +export type IsoDateTimeString = string; + export type RangedValue = { begin: number; end: number; value: string; }; + +/** + * A ISO 8601 duration string + * @example "PT3600S" + */ +export type IsoDurationString = string; diff --git a/front/src/modules/pathfinding/utils.ts b/front/src/modules/pathfinding/utils.ts index 34f0c4251f1..9dd008f838d 100644 --- a/front/src/modules/pathfinding/utils.ts +++ b/front/src/modules/pathfinding/utils.ts @@ -156,7 +156,11 @@ export const upsertPathStepsInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]): return updatedOPs; }; -export const pathStepMatchesOp = (pathStep: PathStep, op: SuggestedOP, withKP = false) => +export const pathStepMatchesOp = ( + pathStep: PathStep, + op: Pick, + withKP = false +) => ('uic' in pathStep && 'ch' in pathStep && pathStep.uic === op.uic && @@ -172,5 +176,8 @@ export const pathStepMatchesOp = (pathStep: PathStep, op: SuggestedOP, withKP = * @param withKP - If true, we check the kp compatibility instead of the name. * It is used in the times and stops table to check if an operational point is a via. */ -export const isVia = (vias: PathStep[], op: SuggestedOP, withKP = false) => - vias.some((via) => pathStepMatchesOp(via, op, withKP)); +export const isVia = ( + vias: PathStep[], + op: Pick, + withKP = false +) => vias.some((via) => pathStepMatchesOp(via, op, withKP)); diff --git a/front/src/modules/timesStops/ReadOnlyTime.tsx b/front/src/modules/timesStops/ReadOnlyTime.tsx new file mode 100644 index 00000000000..350cb56c818 --- /dev/null +++ b/front/src/modules/timesStops/ReadOnlyTime.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import type { CellProps } from 'react-datasheet-grid/dist/types'; + +import type { TimeExtraDays } from './types'; + +type ReadOnlyTimeProps = CellProps; + +const ReadOnlyTime = ({ rowData }: ReadOnlyTimeProps) => { + const { time, daySinceDeparture, dayDisplayed } = rowData || {}; + if (time) { + const fullString = + daySinceDeparture !== undefined && dayDisplayed ? `${time} J+${daySinceDeparture}` : time; + return
{fullString}
; + } + return null; +}; + +ReadOnlyTime.displayName = 'ReadOnlyTime'; + +export default ReadOnlyTime; diff --git a/front/src/modules/timesStops/TimeInput.tsx b/front/src/modules/timesStops/TimeInput.tsx index 0e6cc4b5f9a..23bc6868f58 100644 --- a/front/src/modules/timesStops/TimeInput.tsx +++ b/front/src/modules/timesStops/TimeInput.tsx @@ -2,14 +2,13 @@ import React, { useRef, useState, useEffect } from 'react'; import type { CellProps } from 'react-datasheet-grid/dist/types'; -const TimeInput = ({ - focus, - rowData, - active, - setRowData, -}: CellProps) => { +import type { TimeExtraDays } from './types'; + +type TimeInputProps = CellProps; + +const TimeInput = ({ focus, rowData, active, setRowData }: TimeInputProps) => { const ref = useRef(null); - const [tempTimeValue, setTempTimeValue] = useState(rowData); + const [tempTimeValue, setTempTimeValue] = useState(rowData); useEffect(() => { if (active) { @@ -26,7 +25,7 @@ const TimeInput = ({ setTempTimeValue(rowData); }, [rowData]); - return ( + const input = ( { - setTempTimeValue(e.target.value); + setTempTimeValue((prev) => ({ ...prev, time: e.target.value })); }} onBlur={() => { // To prevent the operational point to be transformed into a via if we leave the cell empty after focusing it @@ -49,6 +48,16 @@ const TimeInput = ({ }} /> ); + + if (tempTimeValue?.daySinceDeparture && tempTimeValue.dayDisplayed) { + return ( +
+ {input} + J+{tempTimeValue.daySinceDeparture} +
+ ); + } + return input; }; TimeInput.displayName = 'TimeInput'; diff --git a/front/src/modules/timesStops/TimesStops.tsx b/front/src/modules/timesStops/TimesStops.tsx index 89cdbda94bc..39180fdd747 100644 --- a/front/src/modules/timesStops/TimesStops.tsx +++ b/front/src/modules/timesStops/TimesStops.tsx @@ -5,14 +5,18 @@ import { DynamicDataSheetGrid, type DataSheetGridProps } from 'react-datasheet-g import { useTranslation } from 'react-i18next'; import { useOsrdConfActions } from 'common/osrdContext'; +import type { IsoDateTimeString } from 'common/types'; import { isVia } from 'modules/pathfinding/utils'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; import type { PathStep } from 'reducers/osrdconf/types'; import { useAppDispatch } from 'store'; -import { time2sec } from 'utils/timeManipulation'; -import { marginRegExValidation } from './consts'; -import { formatSuggestedViasToRowVias, transformRowDataOnChange } from './helpers/utils'; +import { + formatSuggestedViasToRowVias, + updateRowTimesAndMargin, + updateDaySinceDeparture, + durationSinceStartTime, +} from './helpers/utils'; import { useTimeStopsColumns } from './hooks/useTimeStopsColumns'; import { TableType } from './types'; import type { PathWaypointRow } from './types'; @@ -22,7 +26,7 @@ export const WITH_KP = true; type TimesStopsProps = { allWaypoints?: SuggestedOP[]; pathSteps?: PathStep[]; - startTime?: string; + startTime?: IsoDateTimeString; tableType: TableType; cellClassName?: DataSheetGridProps['cellClassName']; stickyRightColumn?: DataSheetGridProps['stickyRightColumn']; @@ -42,7 +46,7 @@ const TimesStops = ({ const { t } = useTranslation('timesStops'); const dispatch = useAppDispatch(); - const { upsertViaFromSuggestedOP } = useOsrdConfActions(); + const { upsertSeveralViasFromSuggestedOP } = useOsrdConfActions(); const [rows, setRows] = useState([]); @@ -55,7 +59,7 @@ const TimesStops = ({ startTime, tableType ); - setRows(suggestedOPs); + setRows(updateDaySinceDeparture(suggestedOPs, startTime, true, true)); } }, [allWaypoints, pathSteps, startTime]); @@ -74,15 +78,33 @@ const TimesStops = ({ className="time-stops-datasheet" columns={columns} value={rows} - onChange={(row: PathWaypointRow[], [op]) => { + onChange={(newRows: PathWaypointRow[], [op]) => { if (!isInputTable) { return; } - const newRowData = transformRowDataOnChange(row[op.fromRowIndex], rows[op.fromRowIndex], op, allWaypoints.length); - if (!newRowData.isMarginValid) { - setRows(row); + let updatedRows = [...newRows]; + updatedRows[op.fromRowIndex] = updateRowTimesAndMargin( + newRows[op.fromRowIndex], + rows[op.fromRowIndex], + op, + allWaypoints.length + ); + updatedRows = updateDaySinceDeparture(updatedRows, startTime!); + if (!updatedRows[op.fromRowIndex].isMarginValid) { + setRows(newRows); } else { - dispatch(upsertViaFromSuggestedOP(newRowData as SuggestedOP)); + const newVias = updatedRows + .filter((row) => row.arrival) + .map((row) => { + const arrival = durationSinceStartTime(startTime, row.arrival); + const departure = durationSinceStartTime(startTime, row.departure); + return { + ...row, + arrival, + departure, + }; + }); + dispatch(upsertSeveralViasFromSuggestedOP(newVias)); } }} stickyRightColumn={stickyRightColumn} diff --git a/front/src/modules/timesStops/TimesStopsInput.tsx b/front/src/modules/timesStops/TimesStopsInput.tsx index 887b4b4bc54..bf24001bcf7 100644 --- a/front/src/modules/timesStops/TimesStopsInput.tsx +++ b/front/src/modules/timesStops/TimesStopsInput.tsx @@ -41,7 +41,6 @@ type TimesStopsInputProps = { startTime: string; pathSteps: PathStep[]; }; - const TimesStopsinput = ({ allWaypoints, startTime, pathSteps }: TimesStopsInputProps) => { const dispatch = useAppDispatch(); const { updatePathSteps } = useOsrdConfActions(); @@ -80,7 +79,6 @@ const TimesStopsinput = ({ allWaypoints, startTime, pathSteps }: TimesStopsInput }); dispatch(updatePathSteps({ pathSteps: updatedPathSteps })); }; - return ( { const rowData = rowData_ as PathWaypointRow; - const arrivalScheduleNotRespected = rowData.arrival - ? rowData.calculatedArrival !== rowData.arrival + const arrivalScheduleNotRespected = rowData?.arrival?.time + ? rowData.calculatedArrival !== rowData.arrival.time : false; const negativeDiffMargins = Number(rowData.diffMargins?.split(NO_BREAK_SPACE)[0]) < 0; return cx({ diff --git a/front/src/modules/timesStops/helpers/__tests__/scheduleData.spec.ts b/front/src/modules/timesStops/helpers/__tests__/scheduleData.spec.ts index b208a18e86d..e9bb07b2553 100644 --- a/front/src/modules/timesStops/helpers/__tests__/scheduleData.spec.ts +++ b/front/src/modules/timesStops/helpers/__tests__/scheduleData.spec.ts @@ -3,20 +3,56 @@ import { describe, it, expect } from 'vitest'; import { computeScheduleData } from '../scheduleData'; describe('computeScheduleData', () => { - it('should compute simple arrival time in the correct timezone', () => { - const schedule = { - at: 'id325', - arrival: 'PT3600S', - stop_for: 'PT100S', - on_stop_signal: false, - locked: false, - }; - const startTime = '2024-05-14T00:00:00Z'; + describe('same day', () => { + it('should compute simple arrival time in the correct timezone', () => { + const schedule = { + at: 'id325', + arrival: 'PT3600S', + stop_for: 'PT100S', + on_stop_signal: false, + locked: false, + }; + const startTime = '2024-05-14T00:00:00Z'; - expect(computeScheduleData(schedule, startTime)).toEqual({ - arrival: 3600, - departure: 3700, - stopFor: 100, + expect(computeScheduleData(schedule, startTime)).toEqual({ + arrival: 3600, + departure: 3700, + stopFor: 100, + }); + }); + it('should compute simple arrival time in the correct timezone 2', () => { + const schedule = { + at: 'id325', + arrival: 'PT3600S', + stop_for: 'PT100S', + on_stop_signal: false, + locked: false, + }; + const startTime = '2024-05-14T01:00:00Z'; + + expect(computeScheduleData(schedule, startTime)).toEqual({ + arrival: 7200, + departure: 7300, + stopFor: 100, + }); + }); + }); + describe('after midnight', () => { + it('should compute simple arrival time in the correct timezone', () => { + const schedule = { + at: 'id325', + arrival: 'PT3600S', + stop_for: 'PT100S', + on_stop_signal: false, + locked: false, + }; + const startTime = '2024-05-14T23:50:00Z'; + + expect(computeScheduleData(schedule, startTime)).toEqual({ + arrival: 3000, + departure: 3100, + stopFor: 100, + }); }); }); }); diff --git a/front/src/modules/timesStops/helpers/__tests__/utils.spec.ts b/front/src/modules/timesStops/helpers/__tests__/utils.spec.ts index 37a8d26068b..f84432c411a 100644 --- a/front/src/modules/timesStops/helpers/__tests__/utils.spec.ts +++ b/front/src/modules/timesStops/helpers/__tests__/utils.spec.ts @@ -1,32 +1,39 @@ -import type { PathWaypointRow } from 'modules/timesStops/types'; -import { transformRowDataOnChange } from '../utils'; +import { describe, it, expect } from 'vitest'; +import { type PathWaypointRow } from 'modules/timesStops/types'; -describe('transformRowDataOnChange', () => { +import { + updateRowTimesAndMargin, + updateDaySinceDeparture, + durationSinceStartTime, + calculateStepTimeAndDays, +} from '../utils'; + +describe('updateRowTimesAndMargin', () => { const whateverOperation = { fromRowIndex: 2 }; describe('arrival is set, departure just changed', () => { it('should update stop duration from the arrival and departure', () => { const rowData = { - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', - arrival: '23:40:00', - departure: '23:50:00', + arrival: { time: '23:40:00' }, + departure: { time: '23:50:00' }, stopFor: '300', // no longer correct, not yet updated by the function } as PathWaypointRow; const previousRowData = { - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', - arrival: '23:40:00', - departure: '23:45:00', + arrival: { time: '23:40:00' }, + departure: { time: '23:45:00' }, stopFor: '300', } as PathWaypointRow; - const result = transformRowDataOnChange(rowData, previousRowData, whateverOperation, 4); + const result = updateRowTimesAndMargin(rowData, previousRowData, whateverOperation, 4); expect(result).toEqual({ - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', - arrival: '23:40:00', - departure: '23:50:00', + arrival: { time: '23:40:00' }, + departure: { time: '23:50:00' }, stopFor: '600', // now correct with the new arrival and departure isMarginValid: true, }); @@ -35,35 +42,35 @@ describe('transformRowDataOnChange', () => { describe('theoritical margin is incorrect', () => { it('should set isMarginValid flag to false', () => { const rowData = { - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', theoreticalMargin: '10', } as PathWaypointRow; const previousRowData = { - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', } as PathWaypointRow; - const result = transformRowDataOnChange(rowData, previousRowData, whateverOperation, 4); + const result = updateRowTimesAndMargin(rowData, previousRowData, whateverOperation, 4); expect(result.isMarginValid).toBe(false); }); }); describe('user removed first row theoritical margin', () => { it('should set the theoritical margin back to 0%', () => { const rowData = { - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', } as PathWaypointRow; const previousRowData = { - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', theoreticalMargin: '10%', } as PathWaypointRow; const operation = { fromRowIndex: 0, }; - const result = transformRowDataOnChange(rowData, previousRowData, operation, 4); + const result = updateRowTimesAndMargin(rowData, previousRowData, operation, 4); expect(result).toEqual({ - opId: 'd94a2af4-6667-11e3-89ff-01f464e0362d', + opId: 'd94a2af4', name: 'Gr', arrival: null, isMarginValid: true, @@ -72,4 +79,370 @@ describe('transformRowDataOnChange', () => { }); }); }); + describe('arrival is before midnight, departure after midnight', () => { + it('should compute the stopFor correctly', () => { + const rowData = { + opId: 'd94a2af4', + name: 'Gr', + arrival: { time: '23:40:00' }, + departure: { time: '00:20:00' }, + stopFor: '300', + } as PathWaypointRow; + const previousRowData = { + opId: 'd94a2af4', + name: 'Gr', + arrival: { time: '23:40:00' }, + departure: { time: '23:45:00' }, + stopFor: '300', + } as PathWaypointRow; + const result = updateRowTimesAndMargin(rowData, previousRowData, whateverOperation, 4); + expect(result).toEqual({ + opId: 'd94a2af4', + name: 'Gr', + arrival: { time: '23:40:00' }, + departure: { time: '00:20:00' }, + stopFor: '2400', + isMarginValid: true, + }); + }); + }); +}); + +describe('updateTimeAndDays', () => { + describe('1 day span', () => { + it('should add the day since departure', () => { + const pathWaypointRows = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '10:00:00' }, + }, + ] as PathWaypointRow[]; + const startTime = '2024-08-13T10:00:00'; + const result = updateDaySinceDeparture(pathWaypointRows, startTime, true, true); + const expected = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '10:00:00', daySinceDeparture: 0 }, + departure: null, + }, + ]; + expect(result).toEqual(expected); + }); + it('should format departure', () => { + const pathWaypointRows = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '10:00:00' }, + }, + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BX', + arrival: { time: '11:00:00' }, + stopFor: '1800', + }, + ] as PathWaypointRow[]; + const startTime = '2024-08-13T10:00:00'; + const result = updateDaySinceDeparture(pathWaypointRows, startTime, true, true); + const expected = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '10:00:00', daySinceDeparture: 0 }, + departure: null, + stopFor: undefined, + }, + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BX', + arrival: { time: '11:00:00', daySinceDeparture: 0 }, + departure: { time: '11:30:00', daySinceDeparture: 0 }, + stopFor: '1800', + }, + ]; + expect(result).toEqual(expected); + }); + }); + describe('2 day span', () => { + it('should add day 1 field', () => { + const pathWaypointRows = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '23:50:00' }, + }, + { + opId: 'd9b38600', + name: 'Ge', + uic: 86, + ch: 'BX', + arrival: { time: '00:30:00' }, + }, + ] as PathWaypointRow[]; + const startTime = '2024-08-13T23:50:00'; + const result = updateDaySinceDeparture(pathWaypointRows, startTime, true, true); + const expected = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '23:50:00', daySinceDeparture: 0 }, + departure: null, + }, + { + opId: 'd9b38600', + name: 'Ge', + uic: 86, + ch: 'BX', + arrival: { time: '00:30:00', daySinceDeparture: 1, dayDisplayed: true }, + departure: null, + }, + ]; + expect(result).toEqual(expected); + }); + it('should add display flag for the first time in the new day', () => { + const pathWaypointRows = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '23:50:00' }, + }, + { + opId: 'd9b38600', + name: 'Ge', + uic: 84, + ch: 'BX', + arrival: { time: '00:30:00' }, + }, + { + opId: 'd982df3e', + name: 'St', + uic: 82, + ch: 'BV', + arrival: null, + }, + { + opId: 'd982df3e', + name: 'Vp', + uic: 78, + ch: 'BV', + arrival: { time: '00:50:00' }, + }, + ] as PathWaypointRow[]; + const startTime = '2024-08-13T23:50:00'; + const result = updateDaySinceDeparture(pathWaypointRows, startTime, true, true); + const expected = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '23:50:00', daySinceDeparture: 0 }, + departure: null, + }, + { + opId: 'd9b38600', + name: 'Ge', + uic: 84, + ch: 'BX', + arrival: { time: '00:30:00', daySinceDeparture: 1, dayDisplayed: true }, + departure: null, + }, + { + opId: 'd982df3e', + name: 'St', + uic: 82, + ch: 'BV', + arrival: null, + departure: null, + }, + { + opId: 'd982df3e', + name: 'Vp', + uic: 78, + ch: 'BV', + arrival: { time: '00:50:00', daySinceDeparture: 1 }, + departure: null, + }, + ]; + expect(result).toEqual(expected); + }); + }); + describe('3 day span', () => { + it('should add display flag for the first time in the new day', () => { + const pathWaypointRows = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '23:50:00' }, + }, + { + opId: 'd9b38600', + name: 'Ge', + uic: 84, + ch: 'BX', + arrival: { time: '00:30:00' }, + }, + { + opId: 'd982df3e', + name: 'St', + uic: 82, + ch: 'BV', + }, + { + opId: 'auie', + name: 'Vp', + uic: 78, + ch: 'BV', + arrival: { time: '00:50:00' }, + }, + { + opId: 'bépo', + name: 'Uj', + uic: 76, + ch: 'BV', + arrival: { time: '18:50:00' }, + }, + { + opId: 'àyx.', + name: 'Vs', + uic: 72, + ch: 'BV', + arrival: { time: '23:30:00' }, + stopFor: '3600', + }, + ] as PathWaypointRow[]; + const startTime = '2024-08-13T23:50:00'; + const result = updateDaySinceDeparture(pathWaypointRows, startTime, true, true); + const expected = [ + { + opId: 'd9c92cb4', + name: 'Ge', + uic: 86, + ch: 'BV', + arrival: { time: '23:50:00', daySinceDeparture: 0 }, + departure: null, + }, + { + opId: 'd9b38600', + name: 'Ge', + uic: 84, + ch: 'BX', + arrival: { time: '00:30:00', daySinceDeparture: 1, dayDisplayed: true }, + departure: null, + }, + { + opId: 'd982df3e', + name: 'St', + uic: 82, + ch: 'BV', + arrival: null, + departure: null, + }, + { + opId: 'auie', + name: 'Vp', + uic: 78, + ch: 'BV', + arrival: { time: '00:50:00', daySinceDeparture: 1 }, + departure: null, + }, + { + opId: 'bépo', + name: 'Uj', + uic: 76, + ch: 'BV', + arrival: { time: '18:50:00', daySinceDeparture: 1 }, + departure: null, + }, + { + opId: 'àyx.', + name: 'Vs', + uic: 72, + ch: 'BV', + arrival: { time: '23:30:00', daySinceDeparture: 1 }, + departure: { + time: '00:30:00', + daySinceDeparture: 2, + dayDisplayed: true, + }, + stopFor: '3600', + }, + ]; + expect(result).toEqual(expected); + }); + }); +}); + +describe('durationSinceStartTime', () => { + it('should return the correct duration', () => { + const startTime = '2023-09-01T10:00:00Z'; + const stepTimeDays = { + time: '20:00:00', + daySinceDeparture: 0, + }; + + const result = durationSinceStartTime(startTime, stepTimeDays); + + expect(result).toBe('PT36000S'); + }); + + it('should return the correct duration. daySinceDeparture 1', () => { + const startTime = '2023-09-01T10:00:00Z'; + const stepTimeDays = { + time: '11:00:00', + daySinceDeparture: 1, + }; + + const result = durationSinceStartTime(startTime, stepTimeDays); + + expect(result).toBe('PT90000S'); + }); +}); + +describe('calculateStepTimeDays', () => { + it('should return correct time and daySinceDeparture', () => { + const startTime = '2023-09-01T10:00:00Z'; + const isoDuration = 'PT36000S'; + + const result = calculateStepTimeAndDays(startTime, isoDuration); + + expect(result).toEqual({ + time: '20:00:00', + daySinceDeparture: 0, + }); + }); + + it('should return correct time and daySinceDeparture, daySinceDeparture 1', () => { + const startTime = '2023-09-01T10:00:00Z'; + const isoDuration = 'PT122400S'; + + const result = calculateStepTimeAndDays(startTime, isoDuration); + + expect(result).toEqual({ + time: '20:00:00', + daySinceDeparture: 1, + }); + }); }); diff --git a/front/src/modules/timesStops/helpers/scheduleData.ts b/front/src/modules/timesStops/helpers/scheduleData.ts index e4d216a26a8..c542419df5c 100644 --- a/front/src/modules/timesStops/helpers/scheduleData.ts +++ b/front/src/modules/timesStops/helpers/scheduleData.ts @@ -1,4 +1,10 @@ -import { ISO8601Duration2sec, datetime2sec, secToHoursString } from 'utils/timeManipulation'; +import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; +import { + ISO8601Duration2sec, + SECONDS_IN_A_DAY, + datetime2sec, + formatDurationAsISO8601, +} from 'utils/timeManipulation'; import type { ComputedScheduleEntry, ScheduleEntry } from '../types'; @@ -14,13 +20,14 @@ export function computeScheduleData(schedule: ScheduleEntry, startTime: string) const startTimeSeconds = datetime2sec(new Date(startTime)); // relative value, number of seconds since startTime const arrivalSeconds = schedule.arrival - ? startTimeSeconds + ISO8601Duration2sec(schedule.arrival) + ? (startTimeSeconds + ISO8601Duration2sec(schedule.arrival)) % SECONDS_IN_A_DAY : null; const stopForSeconds = schedule.stop_for ? ISO8601Duration2sec(schedule.stop_for) : null; const departure = - arrivalSeconds && stopForSeconds ? startTimeSeconds + arrivalSeconds + stopForSeconds : null; + arrivalSeconds && stopForSeconds ? (arrivalSeconds + stopForSeconds) % SECONDS_IN_A_DAY : null; + console.log(schedule, startTime, arrivalSeconds, departure, stopForSeconds); return { arrival: arrivalSeconds, departure, @@ -28,10 +35,15 @@ export function computeScheduleData(schedule: ScheduleEntry, startTime: string) }; } -export function formatScheduleData(scheduleData: ComputedScheduleEntry) { +export function formatScheduleData( + scheduleData: ComputedScheduleEntry +): Pick { + const arrival = scheduleData.arrival ? formatDurationAsISO8601(scheduleData.arrival) : null; + const departure = scheduleData.departure ? formatDurationAsISO8601(scheduleData.departure) : null; + const stopFor = scheduleData.stopFor !== null ? String(scheduleData.stopFor) : ''; return { - arrival: scheduleData.arrival ? secToHoursString(scheduleData.arrival, true) : '', - departure: scheduleData.departure ? secToHoursString(scheduleData.departure, true) : '', - stopFor: scheduleData.stopFor !== null ? String(scheduleData.stopFor) : '', + arrival, + departure, + stopFor, }; } diff --git a/front/src/modules/timesStops/helpers/utils.ts b/front/src/modules/timesStops/helpers/utils.ts index 5c2b47c8a41..2ef20f08b59 100644 --- a/front/src/modules/timesStops/helpers/utils.ts +++ b/front/src/modules/timesStops/helpers/utils.ts @@ -1,26 +1,40 @@ +import dayjs from 'dayjs'; import type { TFunction } from 'i18next'; -import { round } from 'lodash'; +import { round, isEqual } from 'lodash'; import { keyColumn, createTextColumn } from 'react-datasheet-grid'; +import type { IsoDateTimeString, IsoDurationString } from 'common/types'; import { matchPathStepAndOp } from 'modules/pathfinding/utils'; import type { OperationalPointWithTimeAndSpeed } from 'modules/trainschedule/components/DriverTrainScheduleV2/types'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; import type { PathStep } from 'reducers/osrdconf/types'; -import { extractHHMMSS } from 'utils/date'; import { NO_BREAK_SPACE } from 'utils/strings'; -import { datetime2sec, secToHoursString, time2sec } from 'utils/timeManipulation'; +import { + calculateTimeDifferenceInSeconds, + datetime2sec, + durationInSeconds, + formatDurationAsISO8601, + SECONDS_IN_A_DAY, + secToHoursString, + time2sec, +} from 'utils/timeManipulation'; import { marginRegExValidation, MarginUnit } from '../consts'; -import { TableType } from '../types'; -import type { PathStepOpPointCorrespondance, PathWaypointRow } from '../types'; +import { + TableType, + type TimeExtraDays, + type PathStepOpPointCorrespondance, + type PathWaypointRow, +} from '../types'; export const formatSuggestedViasToRowVias = ( operationalPoints: SuggestedOP[], pathSteps: PathStep[], t: TFunction<'timesStops', undefined>, - startTime?: string, + startTime?: IsoDateTimeString, tableType?: TableType ): PathWaypointRow[] => { + console.log(operationalPoints); const formattedOps = [...operationalPoints]; // If the origin is in the ops and isn't the first operational point, we need @@ -56,21 +70,21 @@ export const formatSuggestedViasToRowVias = ( const { arrival, onStopSignal, stopFor, theoreticalMargin } = objectToUse || {}; const isMarginValid = theoreticalMargin ? marginRegExValidation.test(theoreticalMargin) : true; - let departure: string | undefined; - if (stopFor) { - if (i === 0) { - departure = startTime - ? secToHoursString(datetime2sec(new Date(startTime)) + Number(stopFor), true) - : undefined; - } else if (arrival) { - departure = secToHoursString(time2sec(arrival) + Number(stopFor), true); - } - } + const roundedArrivalTime = i === 0 ? 'PT0S' : arrival; + const arrivalInSeconds = roundedArrivalTime ? time2sec(roundedArrivalTime) : null; + + const formattedArrival = calculateStepTimeAndDays(startTime, roundedArrivalTime); + + const departureTime = + stopFor && arrivalInSeconds + ? secToHoursString(arrivalInSeconds + Number(stopFor), true) + : undefined; + const formattedDeparture = departureTime ? { time: departureTime } : null; return { ...op, isMarginValid, - arrival: i === 0 ? extractHHMMSS(startTime) : arrival, - departure, + arrival: formattedArrival, + departure: formattedDeparture, onStopSignal: onStopSignal || false, name: name || t('waypoint', { id: op.opId }), stopFor, @@ -126,25 +140,37 @@ export function disabledTextColumn( }; } -export function transformRowDataOnChange( - rowData: PathWaypointRow, - previousRowData: PathWaypointRow, - op: { fromRowIndex: number }, - allWaypointsLength: number, - ) { +/** + * Synchronizes arrival, departure and stop times. + * updates onStopSignal + * updates isMarginValid and theoreticalMargin + */ +export function updateRowTimesAndMargin( + rowData: PathWaypointRow, + previousRowData: PathWaypointRow, + op: { fromRowIndex: number }, + allWaypointsLength: number +): PathWaypointRow { const newRowData = { ...rowData }; if ( newRowData.departure && newRowData.arrival && - (newRowData.arrival !== previousRowData.arrival || - newRowData.departure !== previousRowData.departure) + (!isEqual(newRowData.arrival, previousRowData.arrival) || + !isEqual(newRowData.departure, previousRowData.departure)) ) { - newRowData.stopFor = String(time2sec(newRowData.departure) - time2sec(newRowData.arrival)); + newRowData.stopFor = String( + durationInSeconds( + time2sec(newRowData.arrival.time || ''), + time2sec(newRowData.departure.time || '') + ) + ); } if (!newRowData.stopFor && op.fromRowIndex !== allWaypointsLength - 1) { newRowData.onStopSignal = false; } - newRowData.isMarginValid = !(newRowData.theoreticalMargin && !marginRegExValidation.test(newRowData.theoreticalMargin!)); + newRowData.isMarginValid = !( + newRowData.theoreticalMargin && !marginRegExValidation.test(newRowData.theoreticalMargin!) + ); if (newRowData.isMarginValid && op.fromRowIndex === 0) { newRowData.arrival = null; // As we put 0% by default for origin's margin, if the user removes a margin without @@ -152,6 +178,107 @@ export function transformRowDataOnChange( if (!newRowData.theoreticalMargin) { newRowData.theoreticalMargin = '0%'; } - } + } return newRowData; -} \ No newline at end of file +} + +/** + * This function goes through the whole array of path waypoints + * and updates the number of days since departure. + */ +export function updateDaySinceDeparture>( + pathWaypointRows: T[], + startTime?: IsoDateTimeString, + keepFirstIndexArrival?: boolean, + withDeparture?: boolean +): T[] { + let currentDaySinceDeparture = 0; + let previousTime = startTime ? datetime2sec(new Date(startTime)) : Number.NEGATIVE_INFINITY; + + return pathWaypointRows.map((pathWaypoint, index) => { + const { arrival, stopFor } = pathWaypoint; + + const arrivalInSeconds = arrival?.time ? time2sec(arrival.time) : null; + let formattedArrival: TimeExtraDays | undefined; + if (arrivalInSeconds) { + if (arrivalInSeconds < previousTime) { + currentDaySinceDeparture += 1; + formattedArrival = { + time: arrival!.time, + daySinceDeparture: currentDaySinceDeparture, + dayDisplayed: true, + }; + } else { + formattedArrival = { + time: arrival!.time, + daySinceDeparture: currentDaySinceDeparture, + }; + } + previousTime = arrivalInSeconds; + } + + let formattedDeparture: TimeExtraDays | undefined; + if (withDeparture && stopFor && arrivalInSeconds) { + const departureInSeconds = (arrivalInSeconds + Number(stopFor)) % SECONDS_IN_A_DAY; + const isAfterMidnight = departureInSeconds < previousTime; + if (isAfterMidnight) { + currentDaySinceDeparture += 1; + formattedDeparture = { + time: secToHoursString(departureInSeconds, true), + daySinceDeparture: currentDaySinceDeparture, + dayDisplayed: true, + }; + } else { + formattedDeparture = { + time: secToHoursString(departureInSeconds, true), + daySinceDeparture: currentDaySinceDeparture, + }; + } + previousTime = arrivalInSeconds; + } + + return { + ...pathWaypoint, + arrival: keepFirstIndexArrival || index > 0 ? formattedArrival : undefined, + ...(withDeparture && { departure: formattedDeparture }), + }; + }); +} + +export function durationSinceStartTime( + startTime?: IsoDateTimeString, + stepTimeDays?: TimeExtraDays | null +): IsoDurationString | null { + if (!startTime || !stepTimeDays?.time || stepTimeDays?.daySinceDeparture === undefined) { + return null; + } + const start = dayjs(startTime); + const step = dayjs(`${startTime.split('T')[0]}T${stepTimeDays.time}`).add( + stepTimeDays.daySinceDeparture, + 'day' + ); + return formatDurationAsISO8601( + calculateTimeDifferenceInSeconds(start.toISOString(), step.toISOString()) + ); +} + +export function calculateStepTimeAndDays( + startTime?: IsoDateTimeString | null, + isoDuration?: IsoDurationString | null +): TimeExtraDays | null { + if (!startTime || !isoDuration) { + return null; + } + + const start = dayjs(startTime); + const duration = dayjs.duration(isoDuration); + + const endTime = start.add(duration); + const daySinceDeparture = endTime.diff(start, 'day'); + const time = endTime.format('HH:mm:ss'); + + return { + time, + daySinceDeparture, + }; +} diff --git a/front/src/modules/timesStops/hooks/useOutputTableData.ts b/front/src/modules/timesStops/hooks/useOutputTableData.ts index 5bc778341da..84756f4517e 100644 --- a/front/src/modules/timesStops/hooks/useOutputTableData.ts +++ b/front/src/modules/timesStops/hooks/useOutputTableData.ts @@ -106,7 +106,10 @@ function useOutputTableData( } as SuggestedOP; } - return { ...sugOpPoint, calculatedArrival: secToHoursString(opPoint.time, true) }; + return { + ...sugOpPoint, + calculatedArrival: secToHoursString(opPoint.time, true), + }; }), [simulatedTrain, pathProperties, operationalPoints, selectedTrainSchedule, pathLength] ); diff --git a/front/src/modules/timesStops/hooks/useTimeStopsColumns.ts b/front/src/modules/timesStops/hooks/useTimeStopsColumns.ts index 9250714ce55..0ade27c8019 100644 --- a/front/src/modules/timesStops/hooks/useTimeStopsColumns.ts +++ b/front/src/modules/timesStops/hooks/useTimeStopsColumns.ts @@ -9,17 +9,22 @@ import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSc import { marginRegExValidation } from '../consts'; import { disabledTextColumn } from '../helpers/utils'; +import ReadOnlyTime from '../ReadOnlyTime'; import TimeInput from '../TimeInput'; -import { TableType, type PathWaypointRow } from '../types'; +import { TableType, type PathWaypointRow, type TimeExtraDays } from '../types'; -const timeColumn: Partial> = { - component: TimeInput as CellComponent, - deleteValue: () => null, - copyValue: ({ rowData }) => rowData ?? null, - pasteValue: ({ value }) => value, - minWidth: 170, - isCellEmpty: ({ rowData }) => !rowData, -}; +const timeColumn = (isOutputTable: boolean) => + ({ + component: (isOutputTable ? ReadOnlyTime : TimeInput) as CellComponent< + TimeExtraDays | null | undefined, + string + >, + deleteValue: () => null, + copyValue: ({ rowData }) => rowData?.time ?? null, + pasteValue: ({ value }) => ({ time: value }), + minWidth: isOutputTable ? 110 : 170, + isCellEmpty: ({ rowData }) => !rowData, + }) as Partial>; const fixedWidth = (width: number) => ({ minWidth: width, maxWidth: width }); @@ -71,20 +76,19 @@ export const useTimeStopsColumns = (tableType: TableType, allWaypoints: Suggeste maxWidth: 45, }, { - ...keyColumn('arrival', isOutputTable ? createTextColumn() : timeColumn), + ...keyColumn('arrival', timeColumn(isOutputTable)), + alignRight: true, title: t('arrivalTime'), // We should not be able to edit the arrival time of the origin disabled: ({ rowIndex }) => isOutputTable || rowIndex === 0, - maxWidth: isOutputTable ? 90 : undefined, }, { - ...keyColumn('departure', isOutputTable ? createTextColumn() : timeColumn), + ...keyColumn('departure', timeColumn(isOutputTable)), title: t('departureTime'), // We should not be able to edit the departure time of the origin disabled: ({ rowIndex }) => isOutputTable || rowIndex === 0, - maxWidth: isOutputTable ? 90 : undefined, }, { ...keyColumn( @@ -105,7 +109,7 @@ export const useTimeStopsColumns = (tableType: TableType, allWaypoints: Suggeste // We should not be able to edit the reception on close signal if stopFor is not filled // except for the destination - ...fixedWidth(120), + ...fixedWidth(94), disabled: ({ rowData, rowIndex }) => isOutputTable || (rowIndex !== allWaypoints?.length - 1 && !rowData.stopFor), }, diff --git a/front/src/modules/timesStops/styles/_readOnlyTime.scss b/front/src/modules/timesStops/styles/_readOnlyTime.scss new file mode 100644 index 00000000000..dde4c3cdf0d --- /dev/null +++ b/front/src/modules/timesStops/styles/_readOnlyTime.scss @@ -0,0 +1,3 @@ +.read-only-time { + padding: 0 10px; +} diff --git a/front/src/modules/timesStops/styles/_timeInput.scss b/front/src/modules/timesStops/styles/_timeInput.scss new file mode 100644 index 00000000000..2e805f39984 --- /dev/null +++ b/front/src/modules/timesStops/styles/_timeInput.scss @@ -0,0 +1,15 @@ +.time-input-container { + position: relative; + width: 100%; + + input.dsg-input { + width: 100%; + } + + span.extra-text { + position: absolute; + left: 5.5rem; + top: 0.075rem; + pointer-events: none; + } +} \ No newline at end of file diff --git a/front/src/modules/timesStops/styles/timesStops.scss b/front/src/modules/timesStops/styles/timesStops.scss index 6eca88ff6c2..35b95184a6d 100644 --- a/front/src/modules/timesStops/styles/timesStops.scss +++ b/front/src/modules/timesStops/styles/timesStops.scss @@ -1 +1,3 @@ @use './timesStopsDatasheet'; +@use './timeInput'; +@use './readOnlyTime'; diff --git a/front/src/modules/timesStops/types.ts b/front/src/modules/timesStops/types.ts index ddc8d96c66e..4fdb3a657f5 100644 --- a/front/src/modules/timesStops/types.ts +++ b/front/src/modules/timesStops/types.ts @@ -2,8 +2,16 @@ import type { TrainScheduleBase, TrainScheduleResult } from 'common/api/osrdEdit import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; import type { ArrayElement } from 'utils/types'; -export type PathWaypointRow = SuggestedOP & { +export type TimeExtraDays = { + time: string | null | undefined; + daySinceDeparture?: number; + dayDisplayed?: boolean; +}; + +export type PathWaypointRow = Omit & { isMarginValid: boolean; + arrival?: TimeExtraDays | null; // value asked by user + departure?: TimeExtraDays | null; // value asked by user }; export enum TableType { diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts index 3bcde5b44f4..07c72617f3d 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts @@ -145,7 +145,8 @@ const checkCurrentConfig = ( rollingStockComfort: rollingStockComfortV2, initialSpeed: initialSpeed ? kmhToMs(initialSpeed) : 0, usingElectricalProfiles, - path: compact(osrdconf.pathSteps).map((step) => { + path: compact(pathSteps).map((step) => { + // TODO use lodash pick const { arrival, locked, @@ -172,7 +173,7 @@ const checkCurrentConfig = ( }), margins: formatMargin(compact(pathSteps)), - schedule: formatSchedule(compact(pathSteps), startTime), + schedule: formatSchedule(compact(pathSteps)), powerRestrictions: powerRestrictionV2, firstStartTime: startTime, speedLimitByTag, diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatSchedule.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatSchedule.ts index 11b4bdd4c03..5d9990d0213 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatSchedule.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatSchedule.ts @@ -2,34 +2,14 @@ import { compact, isNaN, isNil } from 'lodash'; import type { TrainScheduleBase } from 'common/api/osrdEditoastApi'; import type { PathStep } from 'reducers/osrdconf/types'; -import { - datetime2sec, - durationInSeconds, - formatDurationAsISO8601, - time2sec, -} from 'utils/timeManipulation'; +import { formatDurationAsISO8601 } from 'utils/timeManipulation'; -const formatSchedule = ( - pathSteps: PathStep[], - startTime: string -): TrainScheduleBase['schedule'] => { +const formatSchedule = (pathSteps: PathStep[]): TrainScheduleBase['schedule'] => { const schedules = pathSteps.map((step) => { - let formatArrival; - if (step.arrival || step.stopFor) { - if (step.arrival) { - // Duration in seconds between startTime and step.arrival - const durationStartTimeArrival = durationInSeconds( - datetime2sec(new Date(startTime)), - time2sec(step.arrival) - ); - - // Format duration in ISO8601 - formatArrival = formatDurationAsISO8601(durationStartTimeArrival); - } - + if (step?.arrival || step.stopFor) { return { at: step.id, - arrival: formatArrival ?? undefined, + arrival: step.arrival ?? undefined, locked: step.locked, on_stop_signal: step.onStopSignal, stop_for: diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/hooks/useUpdateTrainSchedule.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/hooks/useUpdateTrainSchedule.ts index d4b0533b632..ba064b78b8d 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/hooks/useUpdateTrainSchedule.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/hooks/useUpdateTrainSchedule.ts @@ -27,7 +27,7 @@ const useUpdateTrainSchedule = ( const dispatch = useAppDispatch(); const { getConf, getName, getStartTime } = useOsrdConfSelectors(); const confName = useSelector(getName); - const simulationConf = useSelector(getConf); + const simulationConf = useSelector(getConf); // operational studies conf const startTime = useSelector(getStartTime); const { rollingStock } = useStoreDataForRollingStockSelector(); diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts index 4ff56ba52d2..264dcaa15fb 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts @@ -1,6 +1,12 @@ import type { Position } from 'geojson'; import type { TrainScheduleBase } from 'common/api/osrdEditoastApi'; +import type { IsoDurationString } from 'common/types'; + +export type TimePlusDay = { + time: string; + day: string | undefined; +}; export type SuggestedOP = { opId: string; @@ -21,8 +27,8 @@ export type SuggestedOP = { It's useful for soft deleting the point (waiting to fix / remove all references) If true, the train schedule is consider as invalid and must be edited */ deleted?: boolean; - arrival?: string | null; // value asked by user - departure?: string | null; // value asked by user + arrival?: IsoDurationString | null; // value asked by user + departure?: IsoDurationString | null; // value asked by user locked?: boolean; stopFor?: string | null; // value asked by user theoreticalMargin?: string; // value asked by user diff --git a/front/src/reducers/osrdconf/osrdConfCommon/index.ts b/front/src/reducers/osrdconf/osrdConfCommon/index.ts index 7faa941ee8f..a4b2e1768f7 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/index.ts +++ b/front/src/reducers/osrdconf/osrdConfCommon/index.ts @@ -98,6 +98,7 @@ interface CommonConfReducers extends InfraStateReducers prepare: PrepareAction; }; ['upsertViaFromSuggestedOP']: CaseReducer>; + ['upsertSeveralViasFromSuggestedOP']: CaseReducer>; ['updateRollingStockComfortV2']: CaseReducer>; ['updateStartTime']: CaseReducer>; ['updateOriginV2']: CaseReducer>>; @@ -292,6 +293,11 @@ export function buildCommonConfReducers(): CommonConfRe upsertViaFromSuggestedOP(state: Draft, action: PayloadAction) { upsertPathStep(state.pathSteps, action.payload); }, + upsertSeveralViasFromSuggestedOP(state: Draft, action: PayloadAction) { + action.payload.forEach((suggestedOp) => { + upsertPathStep(state.pathSteps, suggestedOp); + }); + }, updateRollingStockComfortV2( state: Draft, action: PayloadAction diff --git a/front/src/reducers/osrdconf/types.ts b/front/src/reducers/osrdconf/types.ts index 0e9cca40898..f16010e3295 100644 --- a/front/src/reducers/osrdconf/types.ts +++ b/front/src/reducers/osrdconf/types.ts @@ -4,6 +4,7 @@ import type { PowerRestrictionV2 } from 'applications/operationalStudies/types'; import type { AllowanceValue } from 'applications/stdcm/types'; import type { ArrivalTimeTypes } from 'applications/stdcmV2/types'; import type { Comfort, Distribution, PathItemLocation } from 'common/api/osrdEditoastApi'; +import type { IsoDurationString } from 'common/types'; import type { InfraState } from 'reducers/infra'; export interface OsrdConfState extends InfraState { @@ -55,7 +56,7 @@ export type PathStep = PathItemLocation & { It's useful for soft deleting the point (waiting to fix / remove all references) If true, the train schedule is consider as invalid and must be edited */ deleted?: boolean; - arrival?: string | null; + arrival?: IsoDurationString | null; arrivalType?: ArrivalTimeTypes; arrivalToleranceBefore?: number; arrivalToleranceAfter?: number; diff --git a/front/src/utils/date.ts b/front/src/utils/date.ts index 5770a6762a3..4727d8c28ba 100644 --- a/front/src/utils/date.ts +++ b/front/src/utils/date.ts @@ -4,6 +4,7 @@ import customParseFormat from 'dayjs/plugin/customParseFormat'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; +import type { IsoDateTimeString } from 'common/types'; import i18n from 'i18n'; import { ISO8601Duration2sec } from './timeManipulation'; @@ -176,11 +177,14 @@ export function convertUTCDateToLocalDate(date: number) { return Math.abs(timeDifferenceMinutes) * 60 + date; } -export function convertIsoUtcToLocalTime(isoUtcString: string): string { +export function convertIsoUtcToLocalTime(isoUtcString: IsoDateTimeString): string { return dayjs(isoUtcString).local().format(); } -export function addDurationToIsoDate(startTime: string, duration: string) { +export function addDurationToIsoDate( + startTime: IsoDateTimeString, + duration: string +): IsoDateTimeString { return dayjs(startTime).add(ISO8601Duration2sec(duration), 'second').format(); }