diff --git a/front/public/locales/en/timesStops.json b/front/public/locales/en/timesStops.json new file mode 100644 index 00000000000..92bfd94c122 --- /dev/null +++ b/front/public/locales/en/timesStops.json @@ -0,0 +1,9 @@ +{ + "name": "Name", + "stopTime": "Stopping Time (s)", + "arrivalTime": "Arrival Time", + "receptionOnClosedSignal": "Reception on Closed Signal", + "theoreticalMargin": "Theoretical Margin", + "theoreticalMarginPlaceholder": "% or min/100km", + "waypoint": "Waypoint {{id}}" +} diff --git a/front/public/locales/fr/timesStops.json b/front/public/locales/fr/timesStops.json new file mode 100644 index 00000000000..6117f215950 --- /dev/null +++ b/front/public/locales/fr/timesStops.json @@ -0,0 +1,9 @@ +{ + "name": "Nom", + "stopTime": "Temps d'arrêt (s)", + "arrivalTime": "Heure d'arrivée", + "receptionOnClosedSignal": "Réception sur signal fermé", + "theoreticalMargin": "Marge théorique", + "theoreticalMarginPlaceholder": "% ou min/100km", + "waypoint": "Via {{id}}" +} diff --git a/front/src/applications/operationalStudies/hooks.ts b/front/src/applications/operationalStudies/hooks.ts index 69afaa579cf..7c474524bf2 100644 --- a/front/src/applications/operationalStudies/hooks.ts +++ b/front/src/applications/operationalStudies/hooks.ts @@ -9,7 +9,7 @@ import { type PostV2InfraByInfraIdPathfindingBlocksApiArg, } from 'common/api/osrdEditoastApi'; import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; -import { formatSuggestedOperationalPoints, insertViasInOPs } from 'modules/pathfinding/utils'; +import { formatSuggestedOperationalPoints, upsertViasInOPs } from 'modules/pathfinding/utils'; import { getSupportedElectrification, isThermal } from 'modules/rollingStock/helpers/electric'; import { adjustConfWithTrainToModifyV2 } from 'modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; @@ -99,16 +99,14 @@ export const useSetupItineraryForTrainUpdate = ( geometry, pathfindingResult.length ); - // TODO TS2 : update operational_points property with vias added on map included in trainSchedule.path - const updatedSuggestedOPs = insertViasInOPs( - suggestedOperationalPoints, - formatedPathSteps - ); + + const allVias = upsertViasInOPs(suggestedOperationalPoints, formatedPathSteps); setPathProperties({ electrifications, geometry, - suggestedOperationalPoints: updatedSuggestedOPs, + suggestedOperationalPoints, + allVias, length: pathfindingResult.length, }); diff --git a/front/src/applications/operationalStudies/types.ts b/front/src/applications/operationalStudies/types.ts index 94f17b7c192..7ab8183b30a 100644 --- a/front/src/applications/operationalStudies/types.ts +++ b/front/src/applications/operationalStudies/types.ts @@ -80,7 +80,8 @@ export type TrainScheduleImportConfig = { export type ManageTrainSchedulePathProperties = { electrifications: NonNullable; geometry: NonNullable; - /** Operational points along the path and vias added by clicking on map */ suggestedOperationalPoints: SuggestedOP[]; + /** Operational points along the path and vias added by clicking on map */ + allVias: SuggestedOP[]; length: number; }; diff --git a/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx b/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx index 5c2a0ef5e1c..4fc0fed386f 100644 --- a/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx +++ b/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx @@ -1,5 +1,6 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { compact } from 'lodash'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -13,9 +14,11 @@ import SpeedLimitByTagSelector from 'common/SpeedLimitByTagSelector/SpeedLimitBy import { useStoreDataForSpeedLimitByTagSelector } from 'common/SpeedLimitByTagSelector/useStoreDataForSpeedLimitByTagSelector'; import Tabs from 'common/Tabs'; import ItineraryV2 from 'modules/pathfinding/components/Itinerary/ItineraryV2'; +import { upsertViasInOPs } from 'modules/pathfinding/utils'; import RollingStock2Img from 'modules/rollingStock/components/RollingStock2Img'; import { RollingStockSelector } from 'modules/rollingStock/components/RollingStockSelector'; import { useStoreDataForRollingStockSelector } from 'modules/rollingStock/components/RollingStockSelector/useStoreDataForRollingStockSelector'; +import TimesStops from 'modules/timesStops/TimesStops'; import { Map } from 'modules/trainschedule/components/ManageTrainSchedule'; import ElectricalProfiles from 'modules/trainschedule/components/ManageTrainSchedule/ElectricalProfiles'; import TrainSettings from 'modules/trainschedule/components/ManageTrainSchedule/TrainSettings'; @@ -23,9 +26,10 @@ import { formatKmValue } from 'utils/strings'; const ManageTrainScheduleV2 = () => { const { t } = useTranslation(['operationalStudies/manageTrainSchedule']); - const { getOriginV2, getDestinationV2 } = useOsrdConfSelectors(); + const { getOriginV2, getDestinationV2, getPathSteps } = useOsrdConfSelectors(); const origin = useSelector(getOriginV2); const destination = useSelector(getDestinationV2); + const pathSteps = useSelector(getPathSteps); const [pathProperties, setPathProperties] = useState(); @@ -35,6 +39,19 @@ const ManageTrainScheduleV2 = () => { const { rollingStockId, rollingStockComfort, rollingStock } = useStoreDataForRollingStockSelector(); + useEffect(() => { + if (pathProperties) { + const allVias = upsertViasInOPs( + pathProperties.suggestedOperationalPoints, + compact(pathSteps) + ); + setPathProperties({ + ...pathProperties, + allVias, + }); + } + }, [pathSteps]); + // TODO TS2 : test this hook in simulation results issue // useSetupItineraryForTrainUpdate(setPathProperties); @@ -105,7 +122,7 @@ const ManageTrainScheduleV2 = () => { ), label: t('tabs.timesStops'), - content: null, + content: pathProperties && , }; const tabSimulationSettings = { diff --git a/front/src/applications/stdcm/utils/formatStdcmConfV2.ts b/front/src/applications/stdcm/utils/formatStdcmConfV2.ts index dfd9889e1bb..dbbe6c57ce9 100644 --- a/front/src/applications/stdcm/utils/formatStdcmConfV2.ts +++ b/front/src/applications/stdcm/utils/formatStdcmConfV2.ts @@ -129,7 +129,7 @@ export const checkStdcmConf = ( id, arrival, locked, - stop_for, + stopFor, positionOnPath, coordinates, name, @@ -138,7 +138,7 @@ export const checkStdcmConf = ( ...path } = step; return { - duration: stop_for ? sec2ms(ISO8601Duration2sec(stop_for)) : 0, + duration: stopFor ? sec2ms(ISO8601Duration2sec(stopFor)) : 0, location: { ...path, secondary_code: ch }, }; }), diff --git a/front/src/applications/stdcm/views/StdcmView.tsx b/front/src/applications/stdcm/views/StdcmView.tsx index b6d2922d3cc..316363ff03d 100644 --- a/front/src/applications/stdcm/views/StdcmView.tsx +++ b/front/src/applications/stdcm/views/StdcmView.tsx @@ -16,7 +16,7 @@ import type { PostV2InfraByInfraIdPathPropertiesApiArg, } from 'common/api/osrdEditoastApi'; import { useInfraID, useOsrdConfSelectors } from 'common/osrdContext'; -import { formatSuggestedOperationalPoints, insertViasInOPs } from 'modules/pathfinding/utils'; +import { formatSuggestedOperationalPoints, upsertViasInOPs } from 'modules/pathfinding/utils'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; import { updateSelectedTrainId, updateSelectedProjection } from 'reducers/osrdsimulation/actions'; import { useAppDispatch } from 'store'; @@ -61,7 +61,7 @@ const StdcmView = () => { path.length ); - const updatedSuggestedOPs = insertViasInOPs( + const updatedSuggestedOPs = upsertViasInOPs( suggestedOperationalPoints, pathStepsWihPosition ); @@ -70,6 +70,7 @@ const StdcmView = () => { electrifications, geometry, suggestedOperationalPoints: updatedSuggestedOPs, + allVias: updatedSuggestedOPs, length: path.length, }); } diff --git a/front/src/modules/modules.scss b/front/src/modules/modules.scss index fc0777d98cc..a679de34c62 100644 --- a/front/src/modules/modules.scss +++ b/front/src/modules/modules.scss @@ -6,3 +6,4 @@ @use './scenario/styles/scenario.scss'; @use './study/styles/study.scss'; @use './trainschedule/styles/trainSchedule.scss'; +@use './timesStops/styles/timesStops.scss'; diff --git a/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViaStopDurationSelector.tsx b/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViaStopDurationSelector.tsx index 758b2ef1528..0b168552135 100644 --- a/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViaStopDurationSelector.tsx +++ b/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViaStopDurationSelector.tsx @@ -19,7 +19,7 @@ const ViaStopDurationSelector = ({ const dispatch = useAppDispatch(); const { updateViaStopTimeV2 } = useOsrdConfActions(); - const currentStopTime = via.stop_for ? ISO8601Duration2sec(via.stop_for) : 0; + const currentStopTime = via.stopFor ? ISO8601Duration2sec(via.stopFor) : 0; const [stopTime, setStopTime] = useState(currentStopTime); const debouncedStopTime = useDebounce(stopTime, 2000); diff --git a/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViasV2.tsx b/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViasV2.tsx index 4fbb15ab0e3..dd2f95e886b 100644 --- a/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViasV2.tsx +++ b/front/src/modules/pathfinding/components/Itinerary/DisplayItinerary/v2/ViasV2.tsx @@ -47,7 +47,7 @@ const ViasV2 = ({ zoomToFeaturePoint, shouldManageStopDuration }: DisplayViasV2P {...providedDraggable.draggableProps} {...providedDraggable.dragHandleProps} className={cx('place via', { - 'is-a-stop': via.arrival || via.stop_for, + 'is-a-stop': via.arrival || via.stopFor, })} >
diff --git a/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx b/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx index 58db819e96b..f80b5ded759 100644 --- a/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx +++ b/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx @@ -65,6 +65,7 @@ const ItineraryV2 = ({ }; const resetPathfinding = () => { + setPathProperties(undefined); dispatch(updatePathSteps([null, null])); }; @@ -111,9 +112,7 @@ const ItineraryV2 = ({ className="col my-1 text-white btn bg-info btn-sm" type="button" onClick={() => - openModal( - - ) + openModal() } > {t('addVias')} diff --git a/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx b/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx index 19d297b01da..3d08bbedcb1 100644 --- a/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx +++ b/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx @@ -16,10 +16,10 @@ import { useAppDispatch } from 'store'; import { formatUicToCi } from 'utils/strings'; type ModalSuggestedViasProps = { - suggestedOps: SuggestedOP[]; + suggestedVias: SuggestedOP[]; }; -const ModalSuggestedVias = ({ suggestedOps }: ModalSuggestedViasProps) => { +const ModalSuggestedVias = ({ suggestedVias }: ModalSuggestedViasProps) => { const { updatePathSteps, upsertViaFromSuggestedOP, clearViasV2 } = useOsrdConfActions(); const { getViasV2, getDestinationV2, getPathSteps } = useOsrdConfSelectors(); const dispatch = useAppDispatch(); @@ -97,11 +97,11 @@ const ModalSuggestedVias = ({ suggestedOps }: ModalSuggestedViasProps) => {
- {suggestedOps.map((op, idx) => { - if (!isOriginOrDestination(op)) { + {suggestedVias.map((via, idx) => { + if (!isOriginOrDestination(via)) { // If name is undefined, we know the op/via has been added by clicking on map - if (isVia(vias, op)) idxTrueVia += 1; - return formatOP(op, idx, idxTrueVia); + if (isVia(vias, via)) idxTrueVia += 1; + return formatOP(via, idx, idxTrueVia); } return null; })} diff --git a/front/src/modules/pathfinding/components/Pathfinding/PathfindingV2.tsx b/front/src/modules/pathfinding/components/Pathfinding/PathfindingV2.tsx index f3a4144960f..72af38b700b 100644 --- a/front/src/modules/pathfinding/components/Pathfinding/PathfindingV2.tsx +++ b/front/src/modules/pathfinding/components/Pathfinding/PathfindingV2.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useReducer } from 'react'; +import React, { useState, useEffect, useReducer, useMemo } from 'react'; import { Alert, CheckCircle, Stop } from '@osrd-project/ui-icons'; import cx from 'classnames'; @@ -23,7 +23,7 @@ import type { PathfindingActionV2, PathfindingState } from 'modules/pathfinding/ import { formatSuggestedOperationalPoints, getPathfindingQuery, - insertViasInOPs, + upsertViasInOPs, } from 'modules/pathfinding/utils'; import { useStoreDataForRollingStockSelector } from 'modules/rollingStock/components/RollingStockSelector/useStoreDataForRollingStockSelector'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; @@ -192,6 +192,8 @@ const Pathfinding = ({ pathProperties, setPathProperties }: PathfindingProps) => pollingInterval: !isInfraLoaded ? 1000 : undefined, } ); + const pathfindingAlReadyInitialized = useMemo(() => infra?.state === 'CACHED', []); + const [reloadInfra] = osrdEditoastApi.endpoints.postInfraByInfraIdLoad.useMutation(); const { @@ -225,14 +227,16 @@ const Pathfinding = ({ pathProperties, setPathProperties }: PathfindingProps) => case 'CACHED': { setIsInfraLoaded(true); if (isInfraError) setIsInfraError(false); - pathfindingDispatch({ - type: 'INFRA_CHANGED', - params: { - origin, - destination, - rollingStock, - }, - }); + if (!pathfindingAlReadyInitialized) { + pathfindingDispatch({ + type: 'INFRA_CHANGED', + params: { + origin, + destination, + rollingStock, + }, + }); + } break; } default: @@ -308,15 +312,13 @@ const Pathfinding = ({ pathProperties, setPathProperties }: PathfindingProps) => pathfindingResult.length ); - const updatedSuggestedOPs = insertViasInOPs( - suggestedOperationalPoints, - pathStepsWihPosition - ); + const allVias = upsertViasInOPs(suggestedOperationalPoints, pathStepsWihPosition); setPathProperties({ electrifications, geometry, - suggestedOperationalPoints: updatedSuggestedOPs, + suggestedOperationalPoints, + allVias, length: pathfindingResult.length, }); diff --git a/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx b/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx index 31c7a7162a0..86bacfdfeaf 100644 --- a/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx +++ b/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx @@ -221,6 +221,7 @@ const TypeAndPathV2 = ({ setPathProperties }: PathfindingProps) => { electrifications, geometry, suggestedOperationalPoints, + allVias: suggestedOperationalPoints, length: pathfindingResult.length, }); } diff --git a/front/src/modules/pathfinding/utils.ts b/front/src/modules/pathfinding/utils.ts index 52a02b66b97..829573c3e87 100644 --- a/front/src/modules/pathfinding/utils.ts +++ b/front/src/modules/pathfinding/utils.ts @@ -23,6 +23,7 @@ export const formatSuggestedOperationalPoints = ( name: op.extensions?.identifier?.name, uic: op.extensions?.identifier?.uic, ch: op.extensions?.sncf?.ch, + kp: op.part.extensions?.sncf?.kp, chLongLabel: op.extensions?.sncf?.ch_long_label, chShortLabel: op.extensions?.sncf?.ch_short_label, ci: op.extensions?.sncf?.ci, @@ -84,7 +85,7 @@ export const getPathfindingQuery = ({ return null; }; -export const insertViasInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]): SuggestedOP[] => { +export const upsertViasInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]): SuggestedOP[] => { let updatedOPs = [...ops]; const vias = pathSteps.slice(1, -1); if (vias.length > 0) { @@ -105,6 +106,19 @@ export const insertViasInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]): Sugg (op) => step.positionOnPath && op.positionOnPath >= step.positionOnPath ); updatedOPs = addElementAtIndex(updatedOPs, index, formattedStep); + } else if ('uic' in step) { + updatedOPs = updatedOPs.map((op) => { + if (op.uic === step.uic && op.ch === step.ch && op.kp === step.kp) { + return { + ...op, + stopFor: step.stopFor, + arrival: step.arrival, + onStopSignal: step.onStopSignal, + theoreticalMargin: step.theoreticalMargin, + }; + } + return op; + }); } }); } @@ -120,5 +134,6 @@ export const insertViasInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]): Sugg export const isVia = (vias: PathStep[], op: SuggestedOP) => vias.some( (via) => - ('uic' in via && 'ch' in via && via.uic === op.uic && via.ch === op.ch) || via.id === op.opId + ('uic' in via && 'ch' in via && via.uic === op.uic && via.ch === op.ch && via.kp === op.kp) || + via.id === op.opId ); diff --git a/front/src/modules/timesStops/TimeColumnComponent.tsx b/front/src/modules/timesStops/TimeColumnComponent.tsx new file mode 100644 index 00000000000..85f23bc1850 --- /dev/null +++ b/front/src/modules/timesStops/TimeColumnComponent.tsx @@ -0,0 +1,51 @@ +import React, { useLayoutEffect, useRef } from 'react'; + +import cx from 'classnames'; +import type { CellComponent, CellProps, Column } from 'react-datasheet-grid/dist/types'; + +const TimeComponent = ({ + focus, + rowData, + setRowData, +}: CellProps) => { + const ref = useRef(null); + + useLayoutEffect(() => { + if (focus) { + ref.current?.select(); + } else { + ref.current?.blur(); + } + }, [focus]); + + return ( + { + const time = e.target.value; + setRowData(time); + }} + /> + ); +}; + +TimeComponent.displayName = 'TimeComponent'; + +const timeColumn: Partial> = { + component: TimeComponent as CellComponent, + deleteValue: () => null, + copyValue: ({ rowData }) => rowData ?? null, + pasteValue: ({ value }) => value, + minWidth: 170, + isCellEmpty: ({ rowData }) => !rowData, +}; + +export default timeColumn; diff --git a/front/src/modules/timesStops/TimesStops.tsx b/front/src/modules/timesStops/TimesStops.tsx new file mode 100644 index 00000000000..669fd7f3c4f --- /dev/null +++ b/front/src/modules/timesStops/TimesStops.tsx @@ -0,0 +1,189 @@ +import React, { useMemo, useState, useEffect } from 'react'; + +import type { TFunction } from 'i18next'; +import { compact } from 'lodash'; +import { + keyColumn, + type Column, + checkboxColumn, + createTextColumn, + DynamicDataSheetGrid, +} from 'react-datasheet-grid'; +import { useTranslation } from 'react-i18next'; + +import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; +import { useOsrdConfActions } from 'common/osrdContext'; +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 { removeElementAtIndex } from 'utils/array'; + +import timeColumn from './TimeColumnComponent'; + +type TimesStopsProps = { + pathProperties: ManageTrainSchedulePathProperties; + pathSteps?: (PathStep | null)[]; +}; + +type PathWaypointColumn = SuggestedOP & { + isMarginValid: boolean; +}; + +const marginRegExValidation = /^\d+(\.\d+)?%$|^\d+(\.\d+)?min\/100km$/; + +const formatSuggestedViasToRowVias = ( + operationalPoints: SuggestedOP[], + t: TFunction<'timesStops', undefined> +): PathWaypointColumn[] => + operationalPoints?.map((op) => ({ + ...op, + name: op.name || t('waypoint', { id: op.opId }), + isMarginValid: op.theoreticalMargin ? marginRegExValidation.test(op.theoreticalMargin) : true, + onStopSignal: op.onStopSignal || false, + })); + +const createDeleteViaButton = ({ + removeVia, + isRowVia, +}: { + removeVia: () => void; + isRowVia: boolean; +}) => { + if (isRowVia) { + return ( + + ); + } + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>; +}; + +const TimesStops = ({ pathProperties, pathSteps = [] }: TimesStopsProps) => { + const { t } = useTranslation('timesStops'); + const dispatch = useAppDispatch(); + const { upsertViaFromSuggestedOP, updatePathSteps } = useOsrdConfActions(); + + const [timesStopsSteps, setTimesStopsSteps] = useState([]); + + useEffect(() => { + const suggestedOPs = formatSuggestedViasToRowVias(pathProperties.allVias, t); + setTimesStopsSteps(suggestedOPs); + }, [t, pathProperties.allVias]); + + const columns: Column[] = useMemo( + () => [ + { + ...keyColumn('name', createTextColumn()), + title: t('name'), + disabled: true, + }, + { + ...keyColumn('ch', createTextColumn()), + title: 'Ch', + disabled: true, + grow: 0.1, + }, + { + ...keyColumn('arrival', timeColumn), + title: t('arrivalTime'), + + // We should not be edit the arrival time of the origin + cellClassName: ({ rowIndex }) => (rowIndex === 0 ? 'dsg-hidden-cell' : ''), + grow: 0.6, + }, + { + ...keyColumn( + 'stopFor', + createTextColumn({ + continuousUpdates: false, + alignRight: true, + }) + ), + title: `${t('stopTime')}`, + grow: 0.6, + }, + { + ...keyColumn( + 'onStopSignal', + checkboxColumn as Partial> + ), + title: t('receptionOnClosedSignal'), + grow: 0.6, + }, + { + ...keyColumn( + 'theoreticalMargin', + createTextColumn({ + continuousUpdates: false, + alignRight: true, + placeholder: t('theoreticalMarginPlaceholder'), + formatBlurredInput: (value) => { + if (!value || value === 'none') return ''; + if (!marginRegExValidation.test(value)) { + return `${value}${t('theoreticalMarginPlaceholder')}`; + } + return value; + }, + }) + ), + cellClassName: ({ rowData }) => (rowData.isMarginValid ? '' : 'invalidCell'), + title: t('theoreticalMargin'), + disabled: ({ rowIndex }) => rowIndex === pathProperties.allVias.length - 1, + }, + ], + [t, pathProperties.allVias.length] + ); + + const removeVia = (rowData: PathWaypointColumn) => { + const index = compact(pathSteps).findIndex((step) => { + if ('uic' in step) { + return step.uic === rowData.uic && step.ch === rowData.ch && step.kp === rowData.kp; + } + if ('track' in step) { + return step.track === rowData.track; + } + return false; + }); + const updatedPathSteps = removeElementAtIndex(pathSteps, index); + + dispatch(updatePathSteps(updatedPathSteps)); + }; + + return ( + { + const rowData = row[`${op.fromRowIndex}`]; + if (rowData.theoreticalMargin && !marginRegExValidation.test(rowData.theoreticalMargin!)) { + rowData.isMarginValid = false; + setTimesStopsSteps(row); + } else { + rowData.isMarginValid = true; + dispatch(upsertViaFromSuggestedOP(rowData as SuggestedOP)); + } + }} + stickyRightColumn={{ + component: ({ rowData }) => + createDeleteViaButton({ + removeVia: () => removeVia(rowData), + isRowVia: isVia(compact(pathSteps), rowData), + }), + }} + lockRows + height={600} + rowClassName={({ rowData, rowIndex }) => + rowIndex === 0 || + rowIndex === pathProperties.allVias.length - 1 || + isVia(compact(pathSteps), rowData) + ? 'activeRow' + : '' + } + /> + ); +}; + +export default TimesStops; diff --git a/front/src/modules/timesStops/styles/_timesStopsDatasheet.scss b/front/src/modules/timesStops/styles/_timesStopsDatasheet.scss new file mode 100644 index 00000000000..8dca04a2df1 --- /dev/null +++ b/front/src/modules/timesStops/styles/_timesStopsDatasheet.scss @@ -0,0 +1,19 @@ +.activeRow { + font-weight: bold; + background-color: var(--coolgray1); + --dsg-cell-background-color: var(--coolgray1); + --dsg-cell-disabled-background-color: var(--coolgray1); + .dsg-cell { + font-weight: bold; + } +} + +.invalidCell { + color: red !important; +} + +.dsg-cell-sticky-right { + button { + margin: auto; + } +} diff --git a/front/src/modules/timesStops/styles/timesStops.scss b/front/src/modules/timesStops/styles/timesStops.scss new file mode 100644 index 00000000000..6eca88ff6c2 --- /dev/null +++ b/front/src/modules/timesStops/styles/timesStops.scss @@ -0,0 +1 @@ +@use './timesStopsDatasheet'; diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/ManageTrainScheduleMap/RenderPopup.tsx b/front/src/modules/trainschedule/components/ManageTrainSchedule/ManageTrainScheduleMap/RenderPopup.tsx index 8ecb626bc61..639b0470047 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/ManageTrainScheduleMap/RenderPopup.tsx +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/ManageTrainScheduleMap/RenderPopup.tsx @@ -102,6 +102,7 @@ function RenderPopup({ pathProperties }: RenderPopupProps) { coordinates, track: trackProperties.id, offset: Math.round(trackOffset), // offset needs to be an integer + kp: trackProperties.kp, metadata: { lineCode: trackProperties.extensions_sncf_line_code, lineName: trackProperties.extensions_sncf_line_name, diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts index d93a607736f..def4099d32b 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts @@ -2,11 +2,13 @@ import { compact } from 'lodash'; import type { Dispatch } from 'redux'; +import type { ValidConfig } from 'modules/trainschedule/components/ManageTrainSchedule/types'; import { setFailure } from 'reducers/main'; import type { OsrdConfState } from 'reducers/osrdconf/types'; import { kmhToMs } from 'utils/physics'; -import type { ValidConfig } from '../types'; +import formatMargin from './formatMargin'; +import formatSchedule from './formatSchedule'; const checkCurrentConfig = ( osrdconf: OsrdConfState, @@ -118,7 +120,6 @@ const checkCurrentConfig = ( } if (error) return null; - return { rollingStockName: rollingStockName as string, baseTrainName: trainName, @@ -134,19 +135,24 @@ const checkCurrentConfig = ( const { arrival, locked, - stop_for, + stopFor, positionOnPath, coordinates, name, ch, metadata, + kp, + onStopSignal, + theoreticalMargin, ...path } = step; return { ...path, secondary_code: ch }; }), - // TODO TS2 : adapt this for times and stops / power restrictions issues - // margins: formatMargin(pathSteps) - // schedule: formatSchedule(pathSteps) + + margins: formatMargin(compact(pathSteps)), + schedule: formatSchedule(compact(pathSteps), startTime), + + // TODO TS2 : adapt this for power restrictions issue // powerRestrictions: formatPowerRestrictions(pathSteps) firstStartTime: startTime, speedLimitByTag, diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatMargin.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatMargin.ts new file mode 100644 index 00000000000..c6558a1bf2b --- /dev/null +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatMargin.ts @@ -0,0 +1,21 @@ +import type { Margin } from 'modules/trainschedule/components/ManageTrainSchedule/types'; +import type { PathStep } from 'reducers/osrdconf/types'; + +const formatMargin = (pathSteps: PathStep[]): Margin => { + const margins: Margin = { + boundaries: [], + values: [], + }; + + pathSteps.forEach((step, index) => { + if (index === 0) { + margins.values.push(step.theoreticalMargin || 'none'); + } else if (step.theoreticalMargin !== pathSteps[index - 1].theoreticalMargin) { + margins.boundaries.push(step.id); + margins.values.push(step.theoreticalMargin || 'none'); + } + }); + return margins; +}; + +export default formatMargin; diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatSchedule.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatSchedule.ts new file mode 100644 index 00000000000..c8b9aa7199a --- /dev/null +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatSchedule.ts @@ -0,0 +1,45 @@ +import { compact, isNaN } from 'lodash'; + +import type { TrainScheduleBase } from 'common/api/osrdEditoastApi'; +import type { PathStep } from 'reducers/osrdconf/types'; +import { + datetime2sec, + durationInSeconds, + formatDurationAsISO8601, + time2sec, +} from 'utils/timeManipulation'; + +const formatSchedule = ( + pathSteps: PathStep[], + startTime: string +): 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); + } + + return { + at: step.id, + arrival: formatArrival ?? undefined, + locked: step.locked, + on_stop_signal: step.onStopSignal, + stop_for: isNaN(Number(step.stopFor)) + ? undefined + : formatDurationAsISO8601(Number(step.stopFor)), + }; + } + return undefined; + }); + return compact(schedules); +}; + +export default formatSchedule; diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts index 38fcc48d55a..9971dcc65a4 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts @@ -16,6 +16,7 @@ export default function formatTrainSchedulePayload( initialSpeed, usingElectricalProfiles, rollingStockComfort, + margins, } = validConfig; return { @@ -24,8 +25,7 @@ export default function formatTrainSchedulePayload( constraint_distribution: 'MARECO', initial_speed: initialSpeed, labels, - // TODO TS2 : handle margins - // margins: validConfig.margins, + margins, options: { use_electrical_profiles: usingElectricalProfiles, }, @@ -34,7 +34,7 @@ export default function formatTrainSchedulePayload( // power_restrictions: validConfig.powerRestrictions, rolling_stock_name: rollingStockName, // TODO TS2 : handle handle margins - // schedule: validConfig.pathSteps.*** + schedule: validConfig.schedule, speed_limit_tag: speedLimitByTag, start_time: startTime, train_name: trainName, diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts index d0382f83b68..cfc47c9819a 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts @@ -10,6 +10,7 @@ export type SuggestedOP = { chLongLabel?: string; chShortLabel?: string; ci?: number; + kp?: string; trigram?: string; offsetOnTrack: number; track: string; @@ -23,6 +24,8 @@ export type SuggestedOP = { arrival?: string | null; locked?: boolean; stopFor?: string | null; + theoreticalMargin?: string; + onStopSignal?: boolean; // Metadatas given by ManageTrainScheduleMap click event to add origin/destination/via metadata?: { lineCode: number; @@ -32,6 +35,11 @@ export type SuggestedOP = { }; }; +export type Margin = { + boundaries: string[]; + values: string[]; +}; + export type ValidConfig = { rollingStockName: string; baseTrainName: string; @@ -45,8 +53,8 @@ export type ValidConfig = { usingElectricalProfiles: boolean; path: TrainScheduleBase['path']; // TODO TS2 : adapt this for times and stops / power restrictions issues - // margins: TrainScheduleBase['margins'] - // schedule: TrainScheduleBase['schedule'] + margins: TrainScheduleBase['margins']; + schedule: TrainScheduleBase['schedule']; // powerRestrictions: TrainScheduleBase['power_restrictions'] firstStartTime: string; speedLimitByTag?: string; diff --git a/front/src/reducers/osrdconf/osrdConfCommon/__tests__/commonConfBuilder.ts b/front/src/reducers/osrdconf/osrdConfCommon/__tests__/commonConfBuilder.ts index 6e4d588ae7c..c2527526fdd 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/__tests__/commonConfBuilder.ts +++ b/front/src/reducers/osrdconf/osrdConfCommon/__tests__/commonConfBuilder.ts @@ -222,16 +222,8 @@ export default function commonConfBuilder() { [48.58505541984412, 7.73387081978364], ], }, - suggestedOperationalPoints: [ - { - opId: 'd9acb48e-6667-11e3-89ff-01f464e0362d', - - track: '69c04314-6667-11e3-81ff-01f464e0362d', - offsetOnTrack: 310, - positionOnPath: 0, - coordinates: [-4.4785379, 48.3878704], - }, - ], + suggestedOperationalPoints: [], + allVias: [], length: 1169926000, }), }; diff --git a/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts b/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts index 2f258bb7a63..e94b8acf4ff 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts +++ b/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts @@ -456,7 +456,7 @@ const testCommonConfReducers = (slice: OperationalStudiesConfSlice | StdcmConfSl store.dispatch(slice.actions.updateViaStopTimeV2({ via, duration: 'PT60S' })); const state = store.getState()[slice.name]; - expect(state.pathSteps[1]?.stop_for).toEqual('PT60S'); + expect(state.pathSteps[1]?.stopFor).toEqual('PT60S'); }); it('should handle permuteVias', () => { @@ -809,7 +809,7 @@ const testCommonConfReducers = (slice: OperationalStudiesConfSlice | StdcmConfSl positionOnPath: 200, track: '60ca8dda-6667-11e3-81ff-01f464e0362d', offset: 426.443, - stop_for: 'PT5M', + stopFor: 'PT5M', coordinates: [47.99542250806296, 0.1918181738752042], arrival: newVia.arrival, locked: newVia.locked, diff --git a/front/src/reducers/osrdconf/osrdConfCommon/index.ts b/front/src/reducers/osrdconf/osrdConfCommon/index.ts index 2fdde7603c8..6cca9aae67e 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/index.ts +++ b/front/src/reducers/osrdconf/osrdConfCommon/index.ts @@ -273,7 +273,7 @@ export function buildCommonConfReducers(): CommonConfRe } = action; state.pathSteps = state.pathSteps.map((pathStep) => { if (pathStep && pathStep.id === via.id) { - return { ...pathStep, stop_for: duration }; + return { ...pathStep, stopFor: duration }; } return pathStep; }); @@ -404,10 +404,13 @@ export function buildCommonConfReducers(): CommonConfRe positionOnPath: action.payload.positionOnPath, name: action.payload.name, ch: action.payload.ch, - stop_for: action.payload.stopFor, + kp: action.payload.kp, + stopFor: action.payload.stopFor, arrival: action.payload.arrival, locked: action.payload.locked, deleted: action.payload.deleted, + onStopSignal: action.payload.onStopSignal, + theoreticalMargin: action.payload.theoreticalMargin, ...(action.payload.uic ? { uic: action.payload.uic } : { diff --git a/front/src/reducers/osrdconf/types.ts b/front/src/reducers/osrdconf/types.ts index e3b3ecb0d01..a3334f8a757 100644 --- a/front/src/reducers/osrdconf/types.ts +++ b/front/src/reducers/osrdconf/types.ts @@ -87,7 +87,10 @@ export type PathStep = ( deleted?: boolean; arrival?: string | null; locked?: boolean; - stop_for?: string | null; + stopFor?: string | null; + theoreticalMargin?: string; + onStopSignal?: boolean; + kp?: string; /** Distance from the beginning of the path in mm */ positionOnPath?: number; coordinates: Position;