Skip to content
Open
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
20 changes: 20 additions & 0 deletions front/public/locales/en/operational-studies.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,26 @@
},
"toggleTrigramSearch": "Toggle trigram search",
"trainAdded": "Train added",
"trainHeader": {
"form": {
"comfort": "Comfort",
"compositionCode": "Composition code",
"departureDate": "Departure date",
"electricProfiles": "Electric profiles",
"initialVelocity": "Initial velocity",
"manageExtraOccurrences": "Manage extra occurrences",
"recoveryMargin": "Recovery margin",
"rollingStock": "Rolling stock",
"serviceCadence": "Service cadence",
"serviceWindow": "Service window",
"tags": "Tags",
"trainCategory": "Train category",
"trainName": "Train name"
},
"itinerary": "Itinerary",
"serviceModelTrain": "Service model train",
"serviceOccurrence": "Service occurrence"
},
"trainLabels": "Tags",
"trainScheduleDepartureTime": "Departure time",
"trainScheduleInitialSpeed": "Initial velocity",
Expand Down
20 changes: 20 additions & 0 deletions front/public/locales/fr/operational-studies.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,26 @@
},
"toggleTrigramSearch": "Basculer l'affichage de la recherche par trigramme",
"trainAdded": "Train ajouté",
"trainHeader": {
"form": {
"comfort": "Confort",
"compositionCode": "Code composition",
"departureDate": "Date de départ",
"electricProfiles": "Profils électriques",
"initialVelocity": "Vitesse initiale",
"manageExtraOccurrences": "Gérer les occurrences hors cadence",
"recoveryMargin": "Marge de régularité",
"rollingStock": "Matériel roulant",
"serviceCadence": "Cadence de la mission",
"serviceWindow": "Durée de la plage de répétition",
"tags": "Étiquettes",
"trainCategory": "Catégorie de train",
"trainName": "Nom du train"
},
"itinerary": "Itinéraire",
"serviceModelTrain": "Train modèle de mission",
"serviceOccurrence": "Occurrence de mission"
},
"trainLabels": "Étiquettes",
"trainScheduleDepartureTime": "Heure de départ",
"trainScheduleInitialSpeed": "Vitesse initiale",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type ResizableProps = {

type BoardWrapperProps = {
children: React.ReactNode;
customHeader?: React.ReactNode;
customFooter?: React.ReactNode;
hidden?: boolean;
name: string;
Expand All @@ -34,6 +35,7 @@ const BoardWrapper = ({
withFooter = false,
footerClass,
dataTestId,
customHeader,
customFooter,
resizable,
}: BoardWrapperProps) => {
Expand All @@ -55,6 +57,7 @@ const BoardWrapper = ({
menuProps={{ items }}
/>
</div>
{customHeader}
<div
className={cx('board-body', {
'with-rounded-corners': !withFooter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import useTrackOccupancy from 'modules/simulationResult/components/SpaceTimeChar
import SpeedDistanceDiagramWrapper from 'modules/simulationResult/components/SpeedDistanceDiagram/SpeedDistanceDiagramWrapper';
import type { ProjectionData, TrainSpaceTimeData } from 'modules/simulationResult/types';
import TimesStopsOutput from 'modules/timesStops/TimesStopsOutput';
import TrainHeader from 'modules/trainHeader/TrainHeader';
import { findExceptionWithOccurrenceId } from 'modules/trainSchedule/helpers/pacedTrain';
import type { TrainScheduleWithDetails } from 'modules/trainSchedule/types';
import { toggleDisplayOnlyPathSteps, updateSelectedTrain } from 'reducers/simulationResults';
Expand Down Expand Up @@ -351,6 +352,9 @@ const SimulationResults = ({
},
},
]}
customHeader={
simulationResults?.train && <TrainHeader train={simulationResults?.train} />
}
customFooter={
simulationResults?.isValid && (
<div className="time-stop-outputs">
Expand Down
1 change: 1 addition & 0 deletions front/src/modules/modules.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
@use './scenario/styles/scenario.scss';
@use './trainSchedule/styles/trainSchedule.scss';
@use './timesStops/styles/timesStops.scss';
@use './trainHeader/styles/trainHeader.scss';
77 changes: 77 additions & 0 deletions front/src/modules/trainHeader/CollapsedTrainOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Button } from '@osrd-project/ui-core';
import { ChevronDown } from '@osrd-project/ui-icons';
import { useTranslation } from 'react-i18next';

import type { PacedTrainWithPaced } from 'applications/operationalStudies/types';
import type { Train } from 'reducers/osrdconf/types';
import { isOccurrenceId } from 'utils/trainId';

import {
getCategoryName,
getServiceInterval,
getServiceWindow,
getShortDepartureDate,
} from './utils/trainProperties';

export type CollapsedTrainOverviewProps = {
train: Train;
onExpand: () => void;
};

/**
* A simple line that shows an overview of the key properties of a train.
*/
const CollapsedTrainOverview = ({ train, onExpand }: CollapsedTrainOverviewProps) => {
const { t } = useTranslation(['operational-studies', 'translation']);

const pacedTrain = train.paced ? (train as PacedTrainWithPaced) : null;
const isOccurrence = isOccurrenceId(train.id);
const isException = isOccurrence && 'exception' in train;

return (
<div className="train-header collapsed-train-summary">
{pacedTrain && (
<div className="train-kind-header">
{isOccurrence
? t('manageTrainSchedule.trainHeader.serviceOccurrence')
: t('manageTrainSchedule.trainHeader.serviceModelTrain')}
{isException && '≠'}
</div>
)}
<div className="train-metadata">
{pacedTrain && !isOccurrence && (
<div className="train-service-cadence">
{getServiceInterval(pacedTrain)}’ — {getServiceWindow(pacedTrain)}’
</div>
)}
<div className="train-departure-date">{getShortDepartureDate(train)}</div>
<div className="train-category">{getCategoryName(train, t)}</div>
{train.rolling_stock_name && (
<div className="train-rolling-stock">{train.rolling_stock_name}</div>
)}
{train.speed_limit_tag && (
<div className="train-composition-code">{train.speed_limit_tag}</div>
)}
<div className="train-recovery-margin">
{train.constraint_distribution === 'MARECO'
? t('manageTrainSchedule.allowances.distribution-mareco')
: t('manageTrainSchedule.allowances.distribution-linear')}
</div>
</div>
<div className="actions">
<Button
label={t('manageTrainSchedule.trainHeader.itinerary')}
variant="Quiet"
onClick={() => {}}
size="small"
isDisabled
/>
</div>
<button className="header-toggle" onClick={() => onExpand()}>
<ChevronDown />
</button>
</div>
);
};

export default CollapsedTrainOverview;
199 changes: 199 additions & 0 deletions front/src/modules/trainHeader/ExpandedTrainForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { Button, Checkbox, Input } from '@osrd-project/ui-core';
import { ChevronUp } from '@osrd-project/ui-icons';
import { useTranslation } from 'react-i18next';

import type { Train } from 'reducers/osrdconf/types';
import { Duration } from 'utils/duration';
import { isOccurrenceId } from 'utils/trainId';

import { getCategoryName, getComfortType, getShortDepartureDate } from './utils/trainProperties';

export type ExpandedTrainFormProps = {
train: Train;
onCollapse: () => void;
};

/**
* A header-shaped form that allow users to set most of the properties of a train, beside the itinerary itself.
*/
const ExpandedTrainForm = ({ train, onCollapse }: ExpandedTrainFormProps) => {
const { t } = useTranslation(['operational-studies', 'translation']);

const isPaced = !!train.paced;
const isOccurrence = isOccurrenceId(train.id);

const toggleBand = (
<div className="toggle-band">
<button className="header-toggle" onClick={() => onCollapse()}>
<ChevronUp />
</button>
</div>
);

return (
<div className="train-header expanded-train-form">
{isPaced && (
<div className="train-service">
{toggleBand}
{isOccurrence ? (
<div className="train-occurrence">
<div className="train-paced-kind">
{t('manageTrainSchedule.trainHeader.serviceOccurrence')}
</div>
</div>
) : (
<div className="train-service-form">
<div className="train-paced-kind">
{t('manageTrainSchedule.trainHeader.serviceModelTrain')}
</div>
<div className="train-service-cadence">
<Input
id="train-header-service-cadence-input"
small
label={t('manageTrainSchedule.trainHeader.form.serviceCadence')}
value={
train?.paced?.interval
? `${Duration.parse(train.paced.interval).total('minute')} min`
: ''
}
disabled
/>
</div>
<div className="train-service-window">
<Input
id="train-header-service-window-input"
small
label={t('manageTrainSchedule.trainHeader.form.serviceWindow')}
value={
train?.paced?.time_window
? `${Duration.parse(train.paced.time_window).total('minute')} min`
: ''
}
disabled
/>
</div>
<div className="actions">
<Button
label={t('manageTrainSchedule.trainHeader.form.manageExtraOccurrences')}
variant="Quiet"
onClick={() => {}}
size="small"
isDisabled={true}
/>
</div>
</div>
)}
</div>
)}
{!isPaced && toggleBand}
<div className="train-form">
<div className="train-name">
<Input
id="train-header-name-input"
small
label={t('manageTrainSchedule.trainHeader.form.trainName')}
value={train.train_name ?? ''}
disabled
/>
</div>
<div className="train-departure-date">
<Input
id="train-header-departure-date-input"
small
label={t('manageTrainSchedule.trainHeader.form.departureDate')}
value={getShortDepartureDate(train) ?? ''}
disabled
/>
</div>
<div className="train-initial-velocity">
<Input
id="train-header-initial-velocity-input"
small
label={t('manageTrainSchedule.trainHeader.form.initialVelocity')}
value={train.initial_speed ?? ''}
disabled
/>
</div>
<div className="train-category">
<Input
id="train-header-category-input"
small
label={t('manageTrainSchedule.trainHeader.form.trainCategory')}
value={getCategoryName(train, t) ?? ''}
disabled
/>
</div>
<div className="train-rolling-stock">
<Input
id="train-header-rolling-stock-input"
small
label={t('manageTrainSchedule.trainHeader.form.rollingStock')}
value={train.rolling_stock_name ?? ''}
disabled
/>
</div>
<div className="train-composition-code">
<Input
id="train-header-composition-code-input"
small
label={t('manageTrainSchedule.trainHeader.form.compositionCode')}
value={train.speed_limit_tag ?? ''}
disabled
/>
</div>
<div className="train-recovery-margin">
<Input
id="train-header-recovery-margin-input"
small
label={t('manageTrainSchedule.trainHeader.form.recoveryMargin')}
value={
train.constraint_distribution === 'MARECO'
? t('manageTrainSchedule.allowances.distribution-mareco')
: t('manageTrainSchedule.allowances.distribution-linear')
}
disabled
/>
</div>
<div className="train-comfort">
<Input
id="train-header-comfort-input"
small
label={t('manageTrainSchedule.trainHeader.form.comfort')}
value={getComfortType(train, t) ?? ''}
disabled
/>
</div>
<div className="train-electric-profile loose-field">
<Checkbox
id="train-header-electric-profile-input"
checked={train?.options?.use_electrical_profiles}
disabled
>
{t('manageTrainSchedule.trainHeader.form.electricProfiles')}
</Checkbox>
</div>
<div className="train-tags">
<Input
id="train-header-tags-input"
small
label={t('manageTrainSchedule.trainHeader.form.tags')}
value={train?.labels?.join(', ')}
disabled
/>
</div>

<div className="actions">
<Button
label={t('manageTrainSchedule.trainHeader.itinerary')}
variant="Quiet"
onClick={() => {}}
size="small"
isDisabled
/>
</div>
</div>
</div>
);
};

export default ExpandedTrainForm;
Loading