Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion front/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,8 @@
"timeFromPreviousOp": "time from above waypoint",
"totalTravelTime": "total travel time",
"trackName": "track",
"waypoint": "Waypoint {{id}}"
"waypoint": "Waypoint {{id}}",
"powerRestrictionIncompatibility_one": "{{count}} power restriction code isn't compatible with the electrification.",
"powerRestrictionIncompatibility_other": "{{count}} power restriction codes aren't compatible with the electrification."
}
}
4 changes: 3 additions & 1 deletion front/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,8 @@
"timeFromPreviousOp": "temps depuis le point précédent",
"totalTravelTime": "temps total du trajet",
"trackName": "voie",
"waypoint": "Via {{id}}"
"waypoint": "Via {{id}}",
"powerRestrictionIncompatibility_one": "{{count}} code de restriction de puissance n'est pas compatible avec l'électrification.",
"powerRestrictionIncompatibility_other": "{{count}} codes de restriction de puissance ne sont pas compatibles avec l'électrification."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ const SimulationResults = ({
upsertTimetableItems={upsertTimetableItems}
isSimulationDataLoading={isSimulationDataLoading}
operationalPointsOnPath={simulationResults.pathProperties?.operationalPoints}
voltages={simulationResults.pathProperties?.voltages}
rollingStock={simulationResults.rollingStock}
{...(simulationResults?.isValid &&
simulationSummary?.isValid && {
isValid: true,
Expand Down
102 changes: 94 additions & 8 deletions front/src/modules/timesStops/TimesStopsOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ import cx from 'classnames';
import { useSelector } from 'react-redux';

import type { PathPropertiesFormatted } from 'applications/operationalStudies/types';
import {
getPowerRestrictionsWarnings,
countWarnings,
} from 'applications/operationalStudies/views/Scenario/components/ManageTimetableItem/PowerRestrictionsSelector/helpers/powerRestrictionWarnings';
import type {
CorePathfindingResultSuccess,
ReceptionSignal,
RollingStock,
SimulationResponseSuccess,
} from 'common/api/osrdEditoastApi';
import { matchPathStepAndOp } from 'modules/pathfinding/utils';
import { NO_POWER_RESTRICTION } from 'modules/powerRestriction/consts';
import type { SimulationSummary, TimetableItemWithDetails } from 'modules/timetableItem/types';
import type { TimetableItem, Train } from 'reducers/osrdconf/types';
import { getUseNewTimesStopsTable } from 'reducers/user/userSelectors';
import { formatLocalTime } from 'utils/date';
import { Duration } from 'utils/duration';

import { computeOptimisticSchedule } from './helpers/cellUpdate';
import { buildPowerRestrictionsFromRows, computeOptimisticRow } from './helpers/cellUpdate';
import { buildOpMatchParams } from './helpers/utils';
import useOutputTableData from './hooks/useOutputTableData';
import useTimesStopsTableData from './hooks/useTimesStopsTableData';
import useUpdateTimesStopsTable from './hooks/useUpdateTimesStopsTable';
Expand All @@ -34,7 +42,9 @@ type TimesStopsOutputProps = {
simulatedPathItemTimes?: Extract<SimulationSummary, { isValid: true }>['pathItemTimes'];
simulatedPathItemRespect?: Extract<SimulationSummary, { isValid: true }>['pathItemRespect'];
operationalPointsOnPath?: PathPropertiesFormatted['operationalPoints'];
voltages?: PathPropertiesFormatted['voltages'];
isSimulationDataLoading?: boolean;
rollingStock?: RollingStock;
};

const TimesStopsOutput = ({
Expand All @@ -47,7 +57,9 @@ const TimesStopsOutput = ({
simulatedPathItemTimes,
simulatedPathItemRespect,
operationalPointsOnPath,
voltages,
isSimulationDataLoading = false,
rollingStock,
}: TimesStopsOutputProps) => {
const useNewTimesStopsTable = useSelector(getUseNewTimesStopsTable);

Expand Down Expand Up @@ -93,12 +105,15 @@ const TimesStopsOutput = ({
? pinnedState.edit
: null;

// The single source of truth for what the table displays. Any derived data fed to
// TimesStopsTable (warnings, styling, etc.) should be computed from optimisticRows,
// not from selectedTrain, to stay in sync with the displayed values during edits.
const optimisticRows = useMemo(
() =>
optimisticEdit
? newRows.map((row) =>
row.id === optimisticEdit.rowId
? { ...row, ...computeOptimisticSchedule(row, optimisticEdit) }
? { ...row, ...computeOptimisticRow(row, optimisticEdit) }
: row
)
: newRows,
Expand All @@ -107,14 +122,76 @@ const TimesStopsOutput = ({

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

const { updateArrival, updateStopDuration, updateDeparture, updateReceptionSignal } =
useUpdateTimesStopsTable(
selectedTrain,
newRows,
timetableItemsWithDetails,
upsertTimetableItems
const availablePowerRestrictions = useMemo(
() => Object.keys(rollingStock?.power_restrictions ?? {}),
[rollingStock]
);

const { powerRestrictionWarningCount, incompatiblePowerRestrictionIds } = useMemo(() => {
const empty = {
powerRestrictionWarningCount: 0,
incompatiblePowerRestrictionIds: new Set<string>(),
};
// Built from optimisticRows (not selectedTrain.power_restrictions) so warnings
// update immediately when the user edits a cell, before the API round-trip completes.
const powerRestrictions = buildPowerRestrictionsFromRows(optimisticRows);
if (!voltages?.length || !rollingStock || !powerRestrictions.length) return empty;

const pathStepPositions = new Map<string, number>();
selectedTrain.path.forEach((pathStep) => {
const matchingOp = operationalPointsOnPath?.find((op) =>
matchPathStepAndOp(pathStep.location, buildOpMatchParams(op))
);
if (matchingOp) pathStepPositions.set(pathStep.id, matchingOp.position);
});

const rangesWithId = powerRestrictions.flatMap((pr) => {
if (pr.value === NO_POWER_RESTRICTION) return [];
const begin = pathStepPositions.get(pr.from);
const end = pathStepPositions.get(pr.to);
if (begin === undefined || end === undefined) return [];
return [{ begin, end, value: pr.value, fromId: pr.from }];
});

if (!rangesWithId.length) return empty;

const warnings = getPowerRestrictionsWarnings(
rangesWithId,
voltages,
rollingStock.effort_curves.modes
);

const warningRanges = [
...warnings.invalidCombinationWarnings,
...warnings.modeNotSupportedWarnings,
...warnings.missingPowerRestrictionWarnings,
];

const incompatibleIds = new Set<string>(
rangesWithId
.filter((pr) => warningRanges.some((w) => w.end > pr.begin && w.begin < pr.end))
.map((pr) => pr.fromId)
);

return {
powerRestrictionWarningCount: countWarnings(warnings),
incompatiblePowerRestrictionIds: incompatibleIds,
};
}, [optimisticRows, voltages, selectedTrain.path, operationalPointsOnPath, rollingStock]);

const {
updateArrival,
updateStopDuration,
updateDeparture,
updateReceptionSignal,
updatePowerRestrictions,
} = useUpdateTimesStopsTable(
selectedTrain,
newRows,
timetableItemsWithDetails,
upsertTimetableItems
);

// True if we are still waiting for fresh simulation data after a user edit.
// Both conditions must be false before we clear the loading state:
// - Condition 1 (batch summary): simulatedPathItemTimes must get a new reference.
Expand Down Expand Up @@ -176,17 +253,26 @@ const TimesStopsOutput = ({
);
};

const handlePowerRestrictionChange = (row: TimesStopsRowNew, value: string | null) =>
commitEdit({ rowId: row.id, field: 'powerRestriction', value }, () =>
updatePowerRestrictions(row, value)
);

if (useNewTimesStopsTable) {
return (
<TimesStopsTable
rows={optimisticRows}
startTime={startTime}
isValid={stableIsValid}
isComputedDataPending={isAwaitingSimulation}
availablePowerRestrictions={availablePowerRestrictions}
powerRestrictionWarningCount={powerRestrictionWarningCount}
incompatiblePowerRestrictionIds={incompatiblePowerRestrictionIds}
onArrivalChange={handleArrivalChange}
onStopDurationChange={handleStopDurationChange}
onDepartureChange={handleDepartureChange}
onReceptionSignalChange={handleReceptionSignalChange}
onPowerRestrictionChange={handlePowerRestrictionChange}
/>
);
}
Expand Down
64 changes: 62 additions & 2 deletions front/src/modules/timesStops/TimesStopsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, Fragment, useMemo, useRef } from 'react';

import { Checkbox } from '@osrd-project/ui-core';
import { Moon } from '@osrd-project/ui-icons';
import { Alert, Moon, TriangleDown } from '@osrd-project/ui-icons';
import {
createColumnHelper,
flexRender,
Expand All @@ -14,6 +14,7 @@ import cx from 'classnames';
import { useTranslation } from 'react-i18next';

import type { ReceptionSignal } from 'common/api/osrdEditoastApi';
import { NO_POWER_RESTRICTION } from 'modules/powerRestriction/consts';
import { formatLocalTime, useDateTimeLocale } from 'utils/date';
import { calculateTimeDifferenceInDays } from 'utils/timeManipulation';

Expand All @@ -33,10 +34,14 @@ declare module '@tanstack/react-table' {
interface TableMeta<TData extends RowData> {
allRows: TimesStopsRowNew[];
isComputedDataPending?: boolean;
availablePowerRestrictions: string[];
powerRestrictionWarningCount: number;
incompatiblePowerRestrictionIds: Set<string>;
onArrivalChange: (row: TimesStopsRowNew, arrival: Date | null) => void;
onStopDurationChange: (row: TimesStopsRowNew, durationSeconds: number | null) => void;
onDepartureChange: (row: TimesStopsRowNew, departure: Date | null) => void;
onReceptionSignalChange: (row: TimesStopsRowNew, signal: ReceptionSignal | undefined) => void;
onPowerRestrictionChange: (row: TimesStopsRowNew, value: string | null) => void;
}
}

Expand Down Expand Up @@ -80,10 +85,14 @@ type TimesStopsTableProps = {
startTime: Date;
isValid: boolean;
isComputedDataPending?: boolean;
availablePowerRestrictions: string[];
powerRestrictionWarningCount?: number;
incompatiblePowerRestrictionIds?: Set<string>;
onArrivalChange: (row: TimesStopsRowNew, arrival: Date | null) => void;
onStopDurationChange: (row: TimesStopsRowNew, durationSeconds: number | null) => void;
onDepartureChange: (row: TimesStopsRowNew, departure: Date | null) => void;
onReceptionSignalChange: (row: TimesStopsRowNew, signal: ReceptionSignal | undefined) => void;
onPowerRestrictionChange: (row: TimesStopsRowNew, value: string | null) => void;
};

const columnHelper = createColumnHelper<TimesStopsRowNew>();
Expand All @@ -100,10 +109,14 @@ const TimesStopsTable = ({
startTime,
isValid,
isComputedDataPending,
availablePowerRestrictions,
powerRestrictionWarningCount = 0,
incompatiblePowerRestrictionIds,
onArrivalChange,
onStopDurationChange,
onDepartureChange,
onReceptionSignalChange,
onPowerRestrictionChange,
}: TimesStopsTableProps) => {
const { t } = useTranslation('translation', { keyPrefix: 'timeStopTable' });
const dateTimeLocale = useDateTimeLocale();
Expand Down Expand Up @@ -375,6 +388,34 @@ const TimesStopsTable = ({
}),
columnHelper.accessor('powerRestriction', {
header: () => t('powerRestriction'),
cell: (info) => {
const {
availablePowerRestrictions: codes,
onPowerRestrictionChange: onRestrictionChange,
} = info.table.options.meta!;
const value = info.getValue();
const row = info.row.original;
return (
<div className="power-restriction-select-wrapper">
<select
value={value ?? ''}
onChange={(e) => {
const v = e.target.value;
onRestrictionChange(row, v === '' ? null : v);
}}
>
<option value=""> </option>
<option value={NO_POWER_RESTRICTION}>Ø</option>
{codes.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
<TriangleDown className="power-restriction-arrow" />
</div>
);
},
meta: {
className: 'col-power-restriction',
},
Expand Down Expand Up @@ -427,10 +468,14 @@ const TimesStopsTable = ({
meta: {
allRows: rows,
isComputedDataPending,
availablePowerRestrictions,
powerRestrictionWarningCount,
incompatiblePowerRestrictionIds: incompatiblePowerRestrictionIds ?? new Set(),
onArrivalChange,
onStopDurationChange,
onDepartureChange,
onReceptionSignalChange,
onPowerRestrictionChange,
},
});

Expand Down Expand Up @@ -470,6 +515,14 @@ const TimesStopsTable = ({
<div
className={cx('times-stops-table-new', { 'computed-data-pending': isComputedDataPending })}
>
{powerRestrictionWarningCount > 0 && (
<div className="power-restriction-warning">
<Alert variant="fill" />
<span>
{t('powerRestrictionIncompatibility', { count: powerRestrictionWarningCount })}
</span>
</div>
)}
<table className="table-container">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
Expand Down Expand Up @@ -523,7 +576,14 @@ const TimesStopsTable = ({
})}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className={cell.column.columnDef.meta?.className}>
<td
key={cell.id}
className={cx(cell.column.columnDef.meta?.className, {
'power-restriction-incompatible':
cell.column.id === 'powerRestriction' &&
table.options.meta!.incompatiblePowerRestrictionIds.has(row.original.id),
})}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
Expand Down
Loading
Loading