Skip to content

Commit 9c1092e

Browse files
Math-RUriel-Sautron
andcommitted
front: create occurrence item
- add test for computeOccurrenceName Co-authored-by: Uriel-Sautron <[email protected]> Signed-off-by: Math_R_ <[email protected]>
1 parent 913dd07 commit 9c1092e

File tree

9 files changed

+351
-20
lines changed

9 files changed

+351
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Dot, Moon } from '@osrd-project/ui-icons';
2+
import cx from 'classnames';
3+
import dayjs from 'dayjs';
4+
5+
import RollingStock2Img from 'modules/rollingStock/components/RollingStock2Img';
6+
7+
import type { Occurrence } from '../types';
8+
9+
const ConsecutiveDayDateDisplay = ({
10+
departureTime,
11+
nextDepartureTime,
12+
}: {
13+
departureTime: Date;
14+
nextDepartureTime: Date;
15+
}) => (
16+
<div className="consecutive-day-display">
17+
<div>
18+
<div className="date-display before-midnight">{dayjs(departureTime).format('DD')}</div>
19+
<div className="date-display after-midnight">{dayjs(nextDepartureTime).format('DD')}</div>
20+
</div>
21+
<div className="date-display">/{dayjs(nextDepartureTime).format('MM')}</div>
22+
</div>
23+
);
24+
25+
type OccurrenceItemProps = {
26+
occurrence: Occurrence;
27+
isSelected: boolean;
28+
nextOccurrence?: Occurrence;
29+
};
30+
31+
const OccurrenceItem = ({ occurrence, isSelected, nextOccurrence }: OccurrenceItemProps) => {
32+
const { trainName, rollingStock, startTime, arrivalTime } = occurrence;
33+
const isAfterMidnight = dayjs(occurrence.arrivalTime).isAfter(occurrence.startTime, 'day');
34+
const isNextAfterMidnight = nextOccurrence
35+
? dayjs(nextOccurrence.startTime).isAfter(occurrence.startTime, 'day')
36+
: false;
37+
38+
return (
39+
<div
40+
className={cx('occurrence-item', {
41+
'after-midnight': isAfterMidnight,
42+
'next-after-midnight': isNextAfterMidnight,
43+
selected: isSelected,
44+
})}
45+
>
46+
<div className="occurrence-item-dot">
47+
<Dot variant="fill" />
48+
</div>
49+
<div className="occurrence-item-name" title={trainName}>
50+
{trainName}
51+
</div>
52+
<div className="rolling-stock">
53+
{rollingStock && <RollingStock2Img rollingStock={rollingStock} />}
54+
</div>
55+
56+
<div className="occurrence-item-horaries">
57+
<div className="status-icon after-midnight">
58+
{isAfterMidnight && <Moon iconColor="rgba(33, 100, 130, 0.7)" />}
59+
</div>
60+
<div className="occurrence-item-time departure-time">
61+
{dayjs(startTime).format('HH:mm')}
62+
</div>
63+
<div className="occurrence-item-time arrival-time">
64+
{dayjs(arrivalTime).format('HH:mm')}
65+
</div>
66+
</div>
67+
{nextOccurrence && isNextAfterMidnight && (
68+
<ConsecutiveDayDateDisplay
69+
departureTime={startTime}
70+
nextDepartureTime={nextOccurrence?.startTime}
71+
/>
72+
)}
73+
</div>
74+
);
75+
};
76+
77+
export default OccurrenceItem;

front/src/modules/trainschedule/components/Timetable/PacedTrain/PacedTrainItem.tsx

+19-6
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import cx from 'classnames';
66
import dayjs from 'dayjs';
77
import { useTranslation } from 'react-i18next';
88

9-
import type { PacedTrainId } from 'reducers/osrdconf/types';
9+
import type { PacedTrainId, TrainId } from 'reducers/osrdconf/types';
1010
import { ms2min } from 'utils/timeManipulation';
1111

1212
import TimetableItemActions from '../TimetableItemActions';
1313
import type { PacedTrainWithResult } from '../types';
14+
import useOccurrences from './hooks/useOccurrences';
15+
import OccurrenceItem from './OccurrenceItem';
1416

1517
type PacedTrainItemProps = {
1618
isInSelection: boolean;
@@ -19,6 +21,7 @@ type PacedTrainItemProps = {
1921
isOnEdit: boolean;
2022
isProjectionPathUsed: boolean;
2123
selectPacedTrainToEdit: (pacedTrain: PacedTrainWithResult) => void;
24+
selectedTimeTableItemId: TrainId | undefined;
2225
};
2326

2427
const PacedTrainItem = ({
@@ -28,19 +31,18 @@ const PacedTrainItem = ({
2831
isOnEdit,
2932
isProjectionPathUsed,
3033
selectPacedTrainToEdit,
34+
selectedTimeTableItemId,
3135
}: PacedTrainItemProps) => {
3236
const { t } = useTranslation(['operationalStudies/scenario']);
3337

3438
const [isOccurrencesListOpen, setIsOccurrencesListOpen] = useState(false);
39+
const { occurrences, occurrencesCount } = useOccurrences(pacedTrain);
3540

3641
const toggleOccurrencesList = () => setIsOccurrencesListOpen((open) => !open);
3742
const selectPathProjection = async () => {};
3843
const duplicatePacedTrain = async () => {};
3944
const deletePacedTrain = async () => {};
4045

41-
const pacedTrainCadence = pacedTrain.paced.step;
42-
43-
const occurrencesCount = Math.ceil(pacedTrain.paced.duration.ms / pacedTrain.paced.step.ms);
4446
return (
4547
<div
4648
data-testid="scenario-timetable-train"
@@ -92,7 +94,9 @@ const PacedTrainItem = ({
9294

9395
{!pacedTrain.invalidReason ? (
9496
<div className="paced-train-right-zone">
95-
{pacedTrain.isValid && <div>&mdash;&nbsp;{`${ms2min(pacedTrainCadence.ms)}min`}</div>}
97+
{pacedTrain.isValid && (
98+
<div>&mdash;&nbsp;{`${ms2min(pacedTrain.paced.step.ms)}min`}</div>
99+
)}
96100
<div
97101
className={cx('status-icon', {
98102
'not-honored-or-too-fast': pacedTrain.notHonoredReason,
@@ -120,7 +124,16 @@ const PacedTrainItem = ({
120124
editTimetableItem={() => selectPacedTrainToEdit(pacedTrain)}
121125
deleteTimetableItem={deletePacedTrain}
122126
/>
123-
<div className="occurrences" />
127+
<div className="occurrences">
128+
{occurrences.map((occurrence, index) => (
129+
<OccurrenceItem
130+
occurrence={occurrence}
131+
key={occurrence.id}
132+
isSelected={selectedTimeTableItemId === occurrence.id}
133+
nextOccurrence={occurrences[index + 1]}
134+
/>
135+
))}
136+
</div>
124137
{pacedTrain.isValid && (
125138
<div className="more-info">
126139
<div className="more-info-left">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import { computeOccurrenceName } from './useOccurrences';
4+
5+
describe('computeOccurrenceName', () => {
6+
it('should properly compute occurrence name', () => {
7+
expect(computeOccurrenceName('trainName', 0)).toEqual('trainName 1');
8+
expect(computeOccurrenceName('trainName 1', 1)).toEqual('trainName 3');
9+
expect(computeOccurrenceName('trainName-2 3', 3)).toEqual('trainName-2 9');
10+
expect(computeOccurrenceName('trainName-2', 3)).toEqual('trainName-2 7');
11+
expect(computeOccurrenceName('12345', 2)).toEqual('12349');
12+
expect(computeOccurrenceName('12345 1', 2)).toEqual('12345 5');
13+
expect(computeOccurrenceName('1', 2)).toEqual('5');
14+
});
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useMemo } from 'react';
2+
3+
import dayjs from 'dayjs';
4+
5+
import type { OccurrenceId } from 'reducers/osrdconf/types';
6+
7+
import type { Occurrence, PacedTrainWithResult } from '../../types';
8+
9+
export const computeOccurrenceName = (pacedTrainName: string, index: number): string => {
10+
const endByNumber = /\b\w+\s\d+$/;
11+
12+
if (endByNumber.test(pacedTrainName)) {
13+
const endOfPacedTrainName = Number(pacedTrainName.split(' ').pop());
14+
return `${pacedTrainName.replace(/\s\d+$/, '')} ${endOfPacedTrainName + 2 * index}`;
15+
}
16+
if (!Number.isNaN(+pacedTrainName)) {
17+
return `${+pacedTrainName + 2 * index}`;
18+
}
19+
return `${pacedTrainName} ${2 * index + 1}`;
20+
};
21+
22+
type OccurrencesState = {
23+
occurrences: Occurrence[];
24+
occurrencesCount: number;
25+
};
26+
27+
const useOccurrences = ({
28+
id,
29+
paced,
30+
startTime,
31+
arrivalTime,
32+
trainName,
33+
rollingStock,
34+
}: PacedTrainWithResult) => {
35+
const occurrencesState = useMemo<OccurrencesState>(() => {
36+
const occurrencesCount = Math.ceil(paced.duration.ms / paced.step.ms);
37+
const computedOccurrences: Occurrence[] = [];
38+
39+
for (let i = 0; i < occurrencesCount; i += 1) {
40+
const occurrenceStartTime = dayjs(startTime)
41+
.add(i * paced.step.ms, 'ms')
42+
.toDate();
43+
const occurrenceArrivalTime = dayjs(arrivalTime)
44+
.add(i * paced.step.ms, 'ms')
45+
.toDate();
46+
computedOccurrences.push({
47+
id: `occurrence-${i}-${id}` as OccurrenceId,
48+
trainName: computeOccurrenceName(trainName, i),
49+
rollingStock,
50+
startTime: occurrenceStartTime,
51+
arrivalTime: occurrenceArrivalTime,
52+
});
53+
}
54+
return { occurrencesCount, occurrences: computedOccurrences };
55+
}, [paced.duration, paced.step, startTime, arrivalTime, trainName, id, rollingStock]);
56+
57+
return occurrencesState;
58+
};
59+
60+
export default useOccurrences;

front/src/modules/trainschedule/components/Timetable/Timetable.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ const Timetable = ({
143143
...trainSchedulesWithDetails[0],
144144
id: formatEditoastTrainIdToPacedTrainId(12345),
145145
paced: {
146-
duration: Duration.parse('PT2H'),
147-
step: Duration.parse('PT30M'),
146+
duration: Duration.parse('PT72H'),
147+
step: Duration.parse('PT7H'),
148148
},
149149
},
150150
]
@@ -223,6 +223,7 @@ const Timetable = ({
223223
handleSelectPacedTrain={handleSelectTimetableItem}
224224
isOnEdit={timetableItem.id === itemIdToEdit}
225225
isProjectionPathUsed={false}
226+
selectedTimeTableItemId={selectedTrainId}
226227
/>
227228
)}
228229
</div>

front/src/modules/trainschedule/components/Timetable/types.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
SimulationSummaryResult,
66
TrainScheduleResult,
77
} from 'common/api/osrdEditoastApi';
8-
import type { PacedTrainId, TrainScheduleId } from 'reducers/osrdconf/types';
8+
import type { OccurrenceId, PacedTrainId, TrainScheduleId } from 'reducers/osrdconf/types';
99
import type { Duration } from 'utils/duration';
1010

1111
export type ValidityFilter = 'both' | 'valid' | 'invalid';
@@ -73,3 +73,11 @@ export type TimetableFilters = {
7373
selectedTags: Set<string | null>;
7474
setSelectedTags: React.Dispatch<React.SetStateAction<Set<string | null>>>;
7575
};
76+
77+
export type Occurrence = {
78+
id: OccurrenceId;
79+
trainName: string;
80+
rollingStock?: LightRollingStockWithLiveries;
81+
startTime: Date;
82+
arrivalTime: Date;
83+
};

front/src/styles/scss/_variables.scss

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ $colors: (
4242
'black5': rgba(0, 0, 0, 0.05),
4343
'black10': rgba(0, 0, 0, 0.1),
4444
'black25': rgba(0, 0, 0, 0.25),
45+
'black50': rgba(0, 0, 0, 0.5),
4546
'black100': #000000,
4647
'error5': #ffeeed,
4748
'error30': #ff6868,

0 commit comments

Comments
 (0)