Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

table, trains after midnight #8334

Merged
merged 4 commits into from
Sep 17, 2024
Merged
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
1 change: 1 addition & 0 deletions front/public/locales/en/timesStops.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"arrivalTime": "Requested arrival Time",
"calculatedArrivalTime": "Calculated arrival time",
"calculatedDepartureTime": "Calculated departure time",
"dayCounter": "D+{{count}}",
"departureTime": "Requested departure Time",
"diffMargins": "Margins diff.",
"name": "Name",
Expand Down
1 change: 1 addition & 0 deletions front/public/locales/fr/timesStops.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"arrivalTime": "Arrivée demandée",
"calculatedArrivalTime": "Arrivée calculée",
"calculatedDepartureTime": "Départ calculé",
"dayCounter": "J+{{count}}",
"departureTime": "Départ demandé",
"diffMargins": "Diff. marges",
"name": "Nom",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -70,9 +69,7 @@ const computeBasePathSteps = (trainSchedule: TrainScheduleResult) =>
...stepWithoutSecondaryCode,
ch: 'secondary_code' in step ? step.secondary_code : undefined,
name,
arrival: arrival
? addDurationToIsoDate(trainSchedule.start_time, arrival).substring(11, 19)
: arrival,
arrival, // ISODurationString
stopFor: stopFor ? ISO8601Duration2sec(stopFor).toString() : stopFor,
locked,
onStopSignal,
Expand Down
15 changes: 15 additions & 0 deletions front/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ 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;

/**
* A ISO 8601 duration string
* @example "PT3600S"
*/
export type IsoDurationString = string;
SharglutDev marked this conversation as resolved.
Show resolved Hide resolved

export type RangedValue = {
begin: number;
end: number;
Expand Down
27 changes: 17 additions & 10 deletions front/src/modules/pathfinding/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,18 @@ export const upsertPathStepsInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]):
return updatedOPs;
};

export const pathStepMatchesOp = (
pathStep: PathStep,
op: Pick<SuggestedOP, 'uic' | 'ch' | 'kp' | 'name' | 'opId'>,
withKP = false
) =>
('uic' in pathStep &&
'ch' in pathStep &&
pathStep.uic === op.uic &&
pathStep.ch === op.ch &&
(withKP ? pathStep.kp === op.kp : pathStep.name === op.name)) ||
pathStep.id === op.opId;

/**
* Check if a suggested operational point is a via.
* Some OPs have same uic so we need to check also the ch (can be still not enough
Expand All @@ -165,13 +177,8 @@ export const upsertPathStepsInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]):
* @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) =>
('uic' in via &&
'ch' in via &&
via.uic === op.uic &&
via.ch === op.ch &&
(withKP ? via.kp === op.kp : via.name === op.name)) ||
via.id === op.opId
);
export const isVia = (
vias: PathStep[],
op: Pick<SuggestedOP, 'uic' | 'ch' | 'kp' | 'name' | 'opId'>,
{ withKP = false } = {}
) => vias.some((via) => pathStepMatchesOp(via, op, withKP));
23 changes: 23 additions & 0 deletions front/src/modules/timesStops/ReadOnlyTime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CellProps } from 'react-datasheet-grid/dist/types';
import { useTranslation } from 'react-i18next';

import { NO_BREAK_SPACE } from 'utils/strings';

import type { TimeExtraDays } from './types';

type ReadOnlyTimeProps = CellProps<TimeExtraDays | undefined, string>;

const ReadOnlyTime = ({ rowData }: ReadOnlyTimeProps) => {
const { time, daySinceDeparture, dayDisplayed } = rowData || {};
if (!time) {
return null;
}
const { t } = useTranslation('timesStops');
const fullString =
daySinceDeparture !== undefined && dayDisplayed
? `${time}${NO_BREAK_SPACE}${t('dayCounter', { count: daySinceDeparture })}`
: time;
return <div className="read-only-time">{fullString}</div>;
};

export default ReadOnlyTime;
41 changes: 29 additions & 12 deletions front/src/modules/timesStops/TimeInput.tsx
SharglutDev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useRef, useState, useEffect } from 'react';

import cx from 'classnames';
import type { CellProps } from 'react-datasheet-grid/dist/types';
import { useTranslation } from 'react-i18next';

const TimeInput = ({
focus,
rowData,
active,
setRowData,
}: CellProps<string | null | undefined, string>) => {
import type { TimeExtraDays } from './types';

type TimeInputProps = CellProps<TimeExtraDays | undefined, string>;

const TimeInput = ({ focus, rowData, active, setRowData }: TimeInputProps) => {
const { t } = useTranslation('timesStops');
const ref = useRef<HTMLInputElement>(null);
const [tempTimeValue, setTempTimeValue] = useState<string | null | undefined>(rowData);
const [tempTimeValue, setTempTimeValue] = useState<TimeExtraDays | undefined>(rowData);

useEffect(() => {
if (active) {
Expand All @@ -26,8 +28,9 @@ const TimeInput = ({
setTempTimeValue(rowData);
}, [rowData]);

return (
const input = (
<input
// className from react-datasheet-grid library
className="dsg-input"
SharglutDev marked this conversation as resolved.
Show resolved Hide resolved
type="time"
tabIndex={-1}
Expand All @@ -37,9 +40,9 @@ const TimeInput = ({
pointerEvents: focus ? 'auto' : 'none',
opacity: rowData || active ? undefined : 0,
}}
value={tempTimeValue ?? ''}
value={tempTimeValue?.time ?? ''}
onChange={(e) => {
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
Expand All @@ -49,8 +52,22 @@ const TimeInput = ({
}}
/>
);
};

TimeInput.displayName = 'TimeInput';
if (tempTimeValue?.daySinceDeparture && tempTimeValue.dayDisplayed) {
return (
<div className="time-input-container">
{input}
<span
className={cx('extra-text', {
'extra-text-firefox': navigator.userAgent.search('Firefox') !== -1,
})}
>
{t('dayCounter', { count: tempTimeValue.daySinceDeparture })}
</span>
</div>
);
}
return input;
};

export default TimeInput;
70 changes: 39 additions & 31 deletions front/src/modules/timesStops/TimesStops.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { useState, useEffect } from 'react';

import cx from 'classnames';
import { isEqual } from 'lodash';
import { DynamicDataSheetGrid, type DataSheetGridProps } from 'react-datasheet-grid';
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 } 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';
Expand All @@ -22,7 +27,7 @@ export const WITH_KP = true;
type TimesStopsProps = {
allWaypoints?: SuggestedOP[];
pathSteps?: PathStep[];
startTime?: string;
startTime?: IsoDateTimeString;
tableType: TableType;
cellClassName?: DataSheetGridProps['cellClassName'];
stickyRightColumn?: DataSheetGridProps['stickyRightColumn'];
Expand All @@ -42,7 +47,7 @@ const TimesStops = ({
const { t } = useTranslation('timesStops');

const dispatch = useAppDispatch();
const { upsertViaFromSuggestedOP } = useOsrdConfActions();
const { upsertSeveralViasFromSuggestedOP } = useOsrdConfActions();

const [rows, setRows] = useState<PathWaypointRow[]>([]);

Expand All @@ -55,7 +60,7 @@ const TimesStops = ({
startTime,
tableType
);
setRows(suggestedOPs);
setRows(updateDaySinceDeparture(suggestedOPs, startTime, true));
}
}, [allWaypoints, pathSteps, startTime]);

Expand All @@ -74,35 +79,38 @@ const TimesStops = ({
className="time-stops-datasheet"
columns={columns}
value={rows}
onChange={(row: PathWaypointRow[], [op]) => {
onChange={(newRows: PathWaypointRow[], [op]) => {
if (!isInputTable) {
return;
}
const rowData = { ...row[op.fromRowIndex] };
const previousRowData = rows[op.fromRowIndex];
if (
rowData.departure &&
rowData.arrival &&
(rowData.arrival !== previousRowData.arrival ||
rowData.departure !== previousRowData.departure)
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) {
newRows[op.fromRowIndex].isMarginValid = false;
setRows(newRows);
} else if (
!rows[op.fromRowIndex].isMarginValid &&
updatedRows[op.fromRowIndex].isMarginValid
) {
rowData.stopFor = String(time2sec(rowData.departure) - time2sec(rowData.arrival));
}
if (!rowData.stopFor && op.fromRowIndex !== allWaypoints.length - 1) {
rowData.onStopSignal = false;
}
if (rowData.theoreticalMargin && !marginRegExValidation.test(rowData.theoreticalMargin)) {
rowData.isMarginValid = false;
setRows(row);
newRows[op.fromRowIndex].isMarginValid = true;
setRows(newRows);
} else {
rowData.isMarginValid = true;
if (op.fromRowIndex === 0) {
rowData.arrival = null;
// As we put 0% by default for origin's margin, if the user removes a margin without
// replacing it to 0% (undefined), we change it to 0%
if (!rowData.theoreticalMargin) rowData.theoreticalMargin = '0%';
}
dispatch(upsertViaFromSuggestedOP(rowData as SuggestedOP));
const newVias = updatedRows
.filter((row, index) => !isEqual(row, rows[index]))
.map(
(row) =>
({
...row,
...(row.arrival && { arrival: durationSinceStartTime(startTime, row.arrival) }),
}) as SuggestedOP
);
dispatch(upsertSeveralViasFromSuggestedOP(newVias));
}
}}
stickyRightColumn={stickyRightColumn}
Expand All @@ -114,7 +122,7 @@ const TimesStops = ({
activeRow:
rowIndex === 0 ||
rowIndex === allWaypoints.length - 1 ||
isVia(pathSteps || [], rowData, WITH_KP),
isVia(pathSteps || [], rowData, { withKP: true }),
})
}
cellClassName={cellClassName}
Expand Down
11 changes: 9 additions & 2 deletions front/src/modules/timesStops/TimesStopsInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable react/jsx-no-useless-fragment */

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';
Expand All @@ -16,11 +17,18 @@ type ClearButtonProps = {
pathSteps: PathStep[];
};

const createClearViaButton = ({ removeVia, rowIndex, rowData, allWaypoints }: ClearButtonProps) => {
const createClearViaButton = ({
removeVia,
rowIndex,
rowData,
allWaypoints,
pathSteps,
}: ClearButtonProps) => {
const isClearBtnShown =
allWaypoints &&
rowIndex > 0 &&
rowIndex < allWaypoints.length - 1 &&
isVia(pathSteps || [], rowData, { withKP: true }) &&
(rowData.stopFor !== undefined ||
rowData.theoreticalMargin !== undefined ||
rowData.arrival !== undefined ||
Expand Down Expand Up @@ -79,7 +87,6 @@ const TimesStopsinput = ({ allWaypoints, startTime, pathSteps }: TimesStopsInput
});
dispatch(updatePathSteps({ pathSteps: updatedPathSteps }));
};

return (
<TimesStops
allWaypoints={allWaypoints}
Expand Down
4 changes: 2 additions & 2 deletions front/src/modules/timesStops/TimesStopsOutput.tsx
SharglutDev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ const TimesStopsOutput = ({
tableType={TableType.Output}
cellClassName={({ rowData: rowData_ }) => {
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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ 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,
};

expect(computeScheduleData(schedule, startTime)).toEqual({
arrival: 3600,
departure: 3700,
stopFor: 100,
expect(computeScheduleData(schedule)).toEqual({
arrival: 3600,
departure: 3700,
stopFor: 100,
});
});
});
});
Loading
Loading