Skip to content

Commit cd89cce

Browse files
committed
front: add power restriction column to new times & stops table
Add a power restriction column with a custom native select, wire up persistence via the API for both unique trains and paced train occurrences, and refactor cell update helpers to support the new field. Signed-off-by: nncluzu <ngamenichaka@yahoo.fr>
1 parent 63b97ce commit cd89cce

File tree

8 files changed

+260
-21
lines changed

8 files changed

+260
-21
lines changed

front/src/applications/operationalStudies/views/Scenario/components/SimulationResults/SimulationResults.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ const SimulationResults = ({
373373
upsertTimetableItems={upsertTimetableItems}
374374
isSimulationDataLoading={isSimulationDataLoading}
375375
operationalPointsOnPath={simulationResults.pathProperties?.operationalPoints}
376+
rollingStock={simulationResults.rollingStock}
376377
{...(simulationResults?.isValid &&
377378
simulationSummary?.isValid && {
378379
isValid: true,

front/src/modules/timesStops/TimesStopsOutput.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { PathPropertiesFormatted } from 'applications/operationalStudies/ty
77
import type {
88
CorePathfindingResultSuccess,
99
ReceptionSignal,
10+
RollingStock,
1011
SimulationResponseSuccess,
1112
} from 'common/api/osrdEditoastApi';
1213
import type { SimulationSummary, TimetableItemWithDetails } from 'modules/timetableItem/types';
@@ -35,6 +36,7 @@ type TimesStopsOutputProps = {
3536
simulatedPathItemRespect?: Extract<SimulationSummary, { isValid: true }>['pathItemRespect'];
3637
operationalPointsOnPath?: PathPropertiesFormatted['operationalPoints'];
3738
isSimulationDataLoading?: boolean;
39+
rollingStock?: RollingStock;
3840
};
3941

4042
const TimesStopsOutput = ({
@@ -48,6 +50,7 @@ const TimesStopsOutput = ({
4850
simulatedPathItemRespect,
4951
operationalPointsOnPath,
5052
isSimulationDataLoading = false,
53+
rollingStock,
5154
}: TimesStopsOutputProps) => {
5255
const useNewTimesStopsTable = useSelector(getUseNewTimesStopsTable);
5356

@@ -107,13 +110,23 @@ const TimesStopsOutput = ({
107110

108111
const startTime = useMemo(() => new Date(selectedTrain.start_time), [selectedTrain.start_time]);
109112

110-
const { updateArrival, updateStopDuration, updateDeparture, updateReceptionSignal } =
111-
useUpdateTimesStopsTable(
112-
selectedTrain,
113-
newRows,
114-
timetableItemsWithDetails,
115-
upsertTimetableItems
116-
);
113+
const availablePowerRestrictions = useMemo(
114+
() => Object.keys(rollingStock?.power_restrictions ?? {}),
115+
[rollingStock]
116+
);
117+
118+
const {
119+
updateArrival,
120+
updateStopDuration,
121+
updateDeparture,
122+
updateReceptionSignal,
123+
updatePowerRestrictions,
124+
} = useUpdateTimesStopsTable(
125+
selectedTrain,
126+
newRows,
127+
timetableItemsWithDetails,
128+
upsertTimetableItems
129+
);
117130

118131
// True if we are still waiting for fresh simulation data after a user edit.
119132
// Both conditions must be false before we clear the loading state:
@@ -176,17 +189,24 @@ const TimesStopsOutput = ({
176189
);
177190
};
178191

192+
const handlePowerRestrictionChange = (row: TimesStopsRowNew, value: string | null) =>
193+
commitEdit({ rowId: row.id, field: 'powerRestriction', value }, () =>
194+
updatePowerRestrictions(row, value)
195+
);
196+
179197
if (useNewTimesStopsTable) {
180198
return (
181199
<TimesStopsTable
182200
rows={optimisticRows}
183201
startTime={startTime}
184202
isValid={stableIsValid}
185203
isComputedDataPending={isAwaitingSimulation}
204+
availablePowerRestrictions={availablePowerRestrictions}
186205
onArrivalChange={handleArrivalChange}
187206
onStopDurationChange={handleStopDurationChange}
188207
onDepartureChange={handleDepartureChange}
189208
onReceptionSignalChange={handleReceptionSignalChange}
209+
onPowerRestrictionChange={handlePowerRestrictionChange}
190210
/>
191211
);
192212
}

front/src/modules/timesStops/TimesStopsTable.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, Fragment, useMemo, useRef } from 'react';
22

33
import { Checkbox } from '@osrd-project/ui-core';
4-
import { Moon } from '@osrd-project/ui-icons';
4+
import { Moon, TriangleDown } from '@osrd-project/ui-icons';
55
import {
66
createColumnHelper,
77
flexRender,
@@ -14,6 +14,7 @@ import cx from 'classnames';
1414
import { useTranslation } from 'react-i18next';
1515

1616
import type { ReceptionSignal } from 'common/api/osrdEditoastApi';
17+
import { NO_POWER_RESTRICTION } from 'modules/powerRestriction/consts';
1718
import { formatLocalTime, useDateTimeLocale } from 'utils/date';
1819
import { calculateTimeDifferenceInDays } from 'utils/timeManipulation';
1920

@@ -33,10 +34,12 @@ declare module '@tanstack/react-table' {
3334
interface TableMeta<TData extends RowData> {
3435
allRows: TimesStopsRowNew[];
3536
isComputedDataPending?: boolean;
37+
availablePowerRestrictions: string[];
3638
onArrivalChange: (row: TimesStopsRowNew, arrival: Date | null) => void;
3739
onStopDurationChange: (row: TimesStopsRowNew, durationSeconds: number | null) => void;
3840
onDepartureChange: (row: TimesStopsRowNew, departure: Date | null) => void;
3941
onReceptionSignalChange: (row: TimesStopsRowNew, signal: ReceptionSignal | undefined) => void;
42+
onPowerRestrictionChange: (row: TimesStopsRowNew, value: string | null) => void;
4043
}
4144
}
4245

@@ -80,10 +83,12 @@ type TimesStopsTableProps = {
8083
startTime: Date;
8184
isValid: boolean;
8285
isComputedDataPending?: boolean;
86+
availablePowerRestrictions: string[];
8387
onArrivalChange: (row: TimesStopsRowNew, arrival: Date | null) => void;
8488
onStopDurationChange: (row: TimesStopsRowNew, durationSeconds: number | null) => void;
8589
onDepartureChange: (row: TimesStopsRowNew, departure: Date | null) => void;
8690
onReceptionSignalChange: (row: TimesStopsRowNew, signal: ReceptionSignal | undefined) => void;
91+
onPowerRestrictionChange: (row: TimesStopsRowNew, value: string | null) => void;
8792
};
8893

8994
const columnHelper = createColumnHelper<TimesStopsRowNew>();
@@ -100,10 +105,12 @@ const TimesStopsTable = ({
100105
startTime,
101106
isValid,
102107
isComputedDataPending,
108+
availablePowerRestrictions,
103109
onArrivalChange,
104110
onStopDurationChange,
105111
onDepartureChange,
106112
onReceptionSignalChange,
113+
onPowerRestrictionChange,
107114
}: TimesStopsTableProps) => {
108115
const { t } = useTranslation('translation', { keyPrefix: 'timeStopTable' });
109116
const dateTimeLocale = useDateTimeLocale();
@@ -375,6 +382,34 @@ const TimesStopsTable = ({
375382
}),
376383
columnHelper.accessor('powerRestriction', {
377384
header: () => t('powerRestriction'),
385+
cell: (info) => {
386+
const {
387+
availablePowerRestrictions: codes,
388+
onPowerRestrictionChange: onRestrictionChange,
389+
} = info.table.options.meta!;
390+
const value = info.getValue();
391+
const row = info.row.original;
392+
return (
393+
<div className="power-restriction-select-wrapper">
394+
<select
395+
value={value ?? ''}
396+
onChange={(e) => {
397+
const v = e.target.value;
398+
onRestrictionChange(row, v === '' ? null : v);
399+
}}
400+
>
401+
<option value=""> </option>
402+
<option value={NO_POWER_RESTRICTION}>Ø</option>
403+
{codes.map((c) => (
404+
<option key={c} value={c}>
405+
{c}
406+
</option>
407+
))}
408+
</select>
409+
<TriangleDown className="power-restriction-arrow" />
410+
</div>
411+
);
412+
},
378413
meta: {
379414
className: 'col-power-restriction',
380415
},
@@ -427,10 +462,12 @@ const TimesStopsTable = ({
427462
meta: {
428463
allRows: rows,
429464
isComputedDataPending,
465+
availablePowerRestrictions,
430466
onArrivalChange,
431467
onStopDurationChange,
432468
onDepartureChange,
433469
onReceptionSignalChange,
470+
onPowerRestrictionChange,
434471
},
435472
});
436473

front/src/modules/timesStops/helpers/cellUpdate.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { v4 as uuidV4 } from 'uuid';
22

3-
import type { TrainSchedule, PathItem, ScheduleItem } from 'common/api/osrdEditoastApi';
3+
import type {
4+
TrainSchedule,
5+
PathItem,
6+
PowerRestrictionItem,
7+
ScheduleItem,
8+
} from 'common/api/osrdEditoastApi';
49
import type { Train } from 'reducers/osrdconf/types';
510
import { addElementAtIndex } from 'utils/array';
611
import { Duration } from 'utils/duration';
@@ -69,7 +74,7 @@ export type ScheduleState = {
6974
*/
7075
export const applyScheduleEdit = (
7176
current: ScheduleState,
72-
edit: OptimisticEdit
77+
edit: Exclude<OptimisticEdit, { field: 'powerRestriction' }>
7378
): ScheduleState & { departure: Date | null } => {
7479
const { arrival, stop } = current;
7580

@@ -115,7 +120,7 @@ export const applyScheduleEdit = (
115120
}
116121

117122
case 'receptionSignal': {
118-
// receptionSignal doesn't affect arrival/stop/departure values
123+
// TODO: receptionSignal doesn't belong in applyScheduleEdit (tech debt — to be addressed separately)
119124
return {
120125
arrival,
121126
stop,
@@ -172,8 +177,23 @@ export const computeOptimisticRow = (
172177
edit: OptimisticEdit
173178
): Pick<
174179
TimesStopsRowNew,
175-
'requestedArrival' | 'stopDuration' | 'requestedDeparture' | 'closedSignal' | 'shortSlipDistance'
180+
| 'requestedArrival'
181+
| 'stopDuration'
182+
| 'requestedDeparture'
183+
| 'closedSignal'
184+
| 'shortSlipDistance'
185+
| 'powerRestriction'
176186
> => {
187+
if (edit.field === 'powerRestriction') {
188+
return {
189+
requestedArrival: row.requestedArrival,
190+
stopDuration: row.stopDuration,
191+
requestedDeparture: row.requestedDeparture,
192+
closedSignal: row.closedSignal,
193+
shortSlipDistance: row.shortSlipDistance,
194+
powerRestriction: edit.value,
195+
};
196+
}
177197
const { arrival, stop, departure } = applyScheduleEdit(
178198
{ arrival: row.requestedArrival, stop: row.stopDuration },
179199
edit
@@ -187,20 +207,57 @@ export const computeOptimisticRow = (
187207
stopDuration: stop,
188208
requestedDeparture: departure,
189209
...checkboxes,
210+
powerRestriction: row.powerRestriction,
190211
};
191212
};
192213

214+
/**
215+
* Rebuilds the power_restrictions API array from path step rows.
216+
* Each non-null powerRestriction on a path step row marks the start of a range.
217+
* The range extends until the next path step with an explicit value (or the last path step).
218+
*
219+
* @example
220+
* // rows: [{ id: 'A', powerRestriction: 'C1' }, { id: 'B', powerRestriction: null }, { id: 'C', powerRestriction: '∅' }, { id: 'D', powerRestriction: null }]
221+
* // → [{ from: 'A', to: 'C', value: 'C1' }, { from: 'C', to: 'D', value: '∅' }]
222+
*
223+
* // rows: [{ id: 'A', powerRestriction: null }, { id: 'B', powerRestriction: 'C2' }, { id: 'C', powerRestriction: null }]
224+
* // → [{ from: 'B', to: 'C', value: 'C2' }]
225+
*/
226+
export const buildPowerRestrictionsFromRows = (
227+
rows: TimesStopsRowNew[]
228+
): PowerRestrictionItem[] => {
229+
const pathStepRows = rows.filter((r) => r.isPathStep);
230+
const result: PowerRestrictionItem[] = [];
231+
232+
for (let i = 0; i < pathStepRows.length - 1; i++) {
233+
const code = pathStepRows[i].powerRestriction;
234+
if (!code) continue;
235+
236+
const from = pathStepRows[i].id;
237+
let j = i + 1;
238+
while (j < pathStepRows.length - 1 && !pathStepRows[j].powerRestriction) {
239+
j++;
240+
}
241+
const to = pathStepRows[j].id;
242+
result.push({ from, to, value: code });
243+
}
244+
245+
return result;
246+
};
247+
193248
/** Build a TrainSchedule object from a Train with updated path and schedule. */
194249
export const buildUpdatedOccurrence = ({
195250
selectedTrain,
196251
updatedPath,
197252
updatedSchedule,
198253
trainName,
254+
powerRestrictions,
199255
}: {
200256
selectedTrain: Train;
201257
updatedPath: PathItem[];
202258
updatedSchedule: ScheduleItem[];
203259
trainName: string;
260+
powerRestrictions?: PowerRestrictionItem[];
204261
}): TrainSchedule => ({
205262
category: selectedTrain.category,
206263
comfort: selectedTrain.comfort,
@@ -210,7 +267,7 @@ export const buildUpdatedOccurrence = ({
210267
margins: selectedTrain.margins,
211268
options: selectedTrain.options,
212269
path: updatedPath,
213-
power_restrictions: selectedTrain.power_restrictions,
270+
power_restrictions: powerRestrictions ?? selectedTrain.power_restrictions,
214271
rolling_stock_name: selectedTrain.rolling_stock_name,
215272
schedule: updatedSchedule,
216273
speed_limit_tag: selectedTrain.speed_limit_tag,

0 commit comments

Comments
 (0)