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

front: add rolling stock categories #10790

Merged
merged 4 commits into from
Feb 26, 2025
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
30 changes: 23 additions & 7 deletions front/public/locales/en/rollingstock.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
"cancelAction": "Would you like to cancel this action?",
"cancelUpdateRollingStock": "Do you really want to cancel your changes?",
"chooseRollingStock": "Select a rolling stock to display informations",
"categoriesOptions": {
"choose": "Choose a category",
"HIGH_SPEED_TRAIN": "High-Speed Train",
"INTERCITY_TRAIN": "Intercity Train",
"REGIONAL_TRAIN": "Regional Train",
"NIGHT_TRAIN": "Night Train",
"COMMUTER_TRAIN": "Commuter Train",
"FREIGHT_TRAIN": "Freight Train",
"FAST_FREIGHT_TRAIN": "Fast Freight Train",
"TRAM_TRAIN": "Tram-Train",
"TOURISTIC_TRAIN": "Touristic Train",
"WORK_TRAIN": "Work Train"
},
"comfort": "Comfort",
"comfortLevels": "Comfort levels",
"comfortAcceleration": "Comfort acceleration",
Expand Down Expand Up @@ -59,21 +72,22 @@
"mass": "Mass",
"maxSpeed": "Maximum speed",
"messages": {
"success": "Group",
"failure": "Operation failed",
"invalidEffortCurves": "Invalid curves: {{invalidEffortCurves}}. Each curve must have at least 2 combinations of speed / effort values (with no duplication of speed values). Combinations with at least one empty field are ignored. A speed can't exceed 600km/h and an effort can't exceed 1000kN.",
"invalidForm": "Incomplete form",
"missingName": "Please fill out a name",
"missingEffortCurves": "Please enter a speed-effort curve",
"missingInformationAutomaticallyFilled_one": "The following filed was initialized by default: {{invalidFields}}.",
"missingInformationAutomaticallyFilled_other": "The following fileds were initialized by default: {{invalidFields}}.",
"missingInformationAutomaticallyFilled_one": "The following field was initialized by default: {{invalidFields}}.",
"missingInformationAutomaticallyFilled_other": "The following fields were initialized by default: {{invalidFields}}.",
"missingName": "Please fill out a name",
"missingPrimaryCategory": "Please fill out a primary category",
"rollingStockAdded": "Rolling stock added",
"rollingStockUpdated": "Rolling stock updated",
"rollingStockDeleted": "Rolling stock deleted",
"rollingStockDuplicateName": "Rolling stock of the same name already exists.",
"rollingStockNotAdded": "Rolling stock not added",
"rollingStockNotUpdated": "Rolling stock not updated",
"rollingStockNotDeleted": "Rolling stock not deleted",
"invalidEffortCurves": "Invalid curves: {{invalidEffortCurves}}. Each curve must have at least 2 combinations of speed / effort values (with no duplication of speed values). Combinations with at least one empty field are ignored. A speed can't exceed 600km/h and an effort can't exceed 1000kN."
"rollingStockNotUpdated": "Rolling stock not updated",
"rollingStockUpdated": "Rolling stock updated",
"success": "Group"
},
"missingPowerClass_one": "Missing power class",
"missingPowerClass_other": "Missing power classes",
Expand All @@ -85,9 +99,11 @@
"noRollingStock": "No selected rolling stock",
"notLocked": "Not locked",
"number": "Number",
"otherCategories": "Other categories",
"powerClass": "Power class",
"powerRestrictions": "Power restriction & power class",
"powerRestrictionsInfos": "Power restriction",
"primaryCategory": "Primary category",
"profilMode": "Profile",
"project": "Project {{projectName}}:",
"reference": "Reference",
Expand Down
26 changes: 21 additions & 5 deletions front/public/locales/fr/rollingstock.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
"basePowerClass": "Classe de puissance",
"cancelAction": "Souhaitez-vous annuler cette action ?",
"cancelUpdateRollingStock": "Annuler la mise à jour du matériel roulant?",
"categoriesOptions": {
"choose": "Choisissez une catégorie",
"HIGH_SPEED_TRAIN": "Train à grande vitesse",
"INTERCITY_TRAIN": "Train interurbain",
"REGIONAL_TRAIN": "Train régional",
"NIGHT_TRAIN": "Train de nuit",
"COMMUTER_TRAIN": "Train suburbain",
"FREIGHT_TRAIN": "Train de fret",
"FAST_FREIGHT_TRAIN": "Train de fret rapide",
"TRAM_TRAIN": "Tram-train",
"TOURISTIC_TRAIN": "Train touristique",
"WORK_TRAIN": "Train de travaux"
},
"chooseRollingStock": "Sélectionnez un matériel roulant pour en afficher les informations.",
"comfort": "Confort",
"comfortLevels": "Niveaux de confort",
Expand Down Expand Up @@ -60,21 +73,22 @@
"mass": "Masse",
"maxSpeed": "Vitesse maximale",
"messages": {
"success": "Opération réussie",
"failure": "Opération échouée",
"invalidEffortCurves": "Courbes invalides : {{invalidEffortCurves}}. Chaque courbe doit posséder au moins 2 combinaisons de valeurs vitesse / effort (sans doublon sur les valeurs de vitesse). Les combinaisons ayant au moins un champs vide sont ignorées. Une vitesse ne peut pas excéder 600km/h et un effort ne peut pas excéder 1000kN.",
"invalidForm": "Formulaire incomplet",
"missingName": "Veuillez renseigner un nom",
"missingEffortCurves": "Veuillez renseigner une courbe effort-vitesse",
"missingInformationAutomaticallyFilled_one": "Le champ suivant a été initialisé par défaut: {{invalidFields}}.",
"missingInformationAutomaticallyFilled_other": "Les champs suivants ont été initialisés par défaut: {{invalidFields}}.",
"missingName": "Veuillez renseigner un nom",
"missingPrimaryCategory": "Veuillez renseigner une catégorie principale",
"rollingStockAdded": "Le matériel roulant a été ajouté.",
"rollingStockUpdated": "Le matériel roulant a été mis à jour.",
"rollingStockDeleted": "Le matériel roulant a été supprimé.",
"rollingStockDuplicateName": "Un matériel roulant du même nom existe déjà.",
"rollingStockNotAdded": "Le matériel roulant n'a pas été ajouté.",
"rollingStockNotUpdated": "Le matériel roulant n'a pas été mis à jour.",
"rollingStockNotDeleted": "Le matériel roulant n'a pas été supprimé.",
"invalidEffortCurves": "Courbes invalides : {{invalidEffortCurves}}. Chaque courbe doit posséder au moins 2 combinaisons de valeurs vitesse / effort (sans doublon sur les valeurs de vitesse). Les combinaisons ayant au moins un champs vide sont ignorées. Une vitesse ne peut pas excéder 600km/h et un effort ne peut pas excéder 1000kN."
"rollingStockNotUpdated": "Le matériel roulant n'a pas été mis à jour.",
"rollingStockUpdated": "Le matériel roulant a été mis à jour.",
"success": "Opération réussie"
},
"missingPowerClass_one": "Classe de puissance manquante",
"missingPowerClass_other": "Classes de puissance manquantes",
Expand All @@ -86,9 +100,11 @@
"noRollingStock": "Pas de matériel sélectionné",
"notLocked": "Non verrouillé",
"number": "Numéro",
"otherCategories": "Autres catégories",
"powerClass": "Classe puissance",
"powerRestrictions": "Codes de restriction & classes de puissance",
"powerRestrictionsInfos": "Codes de restriction",
"primaryCategory": "Catégorie principale",
"profilMode": "Profil",
"project": "Projet {{projectName}} :",
"raisePantographTime": "Temps de lever panto",
Expand Down
3 changes: 3 additions & 0 deletions front/src/common/BootstrapSNCF/SelectSNCF.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface SelectProps<T> {
id: string;
label?: ReactNode;
name?: string;
'data-testid'?: string;
value?: T;
options: Array<T>;
onChange: (e?: T) => void;
Expand All @@ -31,6 +32,7 @@ function SelectSNCF<T extends string | SelectOptionObject>({
id,
label,
name,
'data-testid': data_testid,
options,
onChange,
className,
Expand All @@ -44,6 +46,7 @@ function SelectSNCF<T extends string | SelectOptionObject>({
<select
id={id}
name={name}
data-testid={data_testid}
onChange={(e) => {
const selected = e.target.value;
const item = options.find((o) => selected === (isString(o) ? o : o.id));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ export default function RollingStockCardDetail({
</td>
</tr>
)}
<tr>
<td className="text-primary text-nowrap pr-1">{t('primaryCategory')}</td>
<td> {t(`categoriesOptions.${rs.primary_category}`)} </td>
</tr>
{!isEmpty(rs.other_categories) && (
<tr>
<td className="text-primary text-nowrap pr-1">{t('otherCategories')}</td>
<td>
{rs.other_categories
.map((category) => t(`categoriesOptions.${category}`))
.toSorted()
.join(', ')}
</td>
</tr>
)}
</tbody>
</table>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { TabProps } from 'common/Tabs';
import RollingStock2Img from 'modules/rollingStock/components/RollingStock2Img';
import RollingStockEditorCurves from 'modules/rollingStock/components/RollingStockEditor/RollingStockEditorCurves';
import {
RollingStockEditorCategoryForm,
RollingStockEditorMetadataForm,
RollingStockEditorOnboardSystemEquipmentForm,
RollingStockEditorParameterForm,
Expand Down Expand Up @@ -157,6 +158,11 @@ const RollingStockEditorForm = ({
name: t('messages.invalidForm'),
message: t('messages.missingName'),
};
} else if (!data.primaryCategory) {
error = {
name: t('messages.invalidForm'),
message: t('messages.missingPrimaryCategory'),
};
} else if (!selectedTractionMode || !effortCurves) {
error = {
name: t('messages.invalidForm'),
Expand Down Expand Up @@ -247,6 +253,11 @@ const RollingStockEditorForm = ({
rsSignalingSystemsList={rollingStockValues.supportedSignalingSystems}
setRollingStockValues={setRollingStockValues}
/>

<RollingStockEditorCategoryForm
rollingStockValues={rollingStockValues}
setRollingStockValues={setRollingStockValues}
/>
</>
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import cx from 'classnames';
import { floor, isNil } from 'lodash';
import { useTranslation } from 'react-i18next';

import type { RollingStockCategory } from 'common/api/osrdEditoastApi';
import CheckboxRadioSNCF from 'common/BootstrapSNCF/CheckboxRadioSNCF';
import InputGroupSNCF, { type InputGroupSNCFValue } from 'common/BootstrapSNCF/InputGroupSNCF';
import InputSNCF from 'common/BootstrapSNCF/InputSNCF';
import SelectSNCF from 'common/BootstrapSNCF/SelectSNCF';
import {
DEFAULT_SIGNALING_SYSTEMS,
RollingStockCategoryDict,
RollingStockEditorMetadata,
RollingStockEditorParameter,
RS_REQUIRED_FIELDS,
Expand Down Expand Up @@ -302,8 +304,8 @@ export const RollingStockEditorParameterForm = ({
</div>
<div className="d-flex flex-column justify-content-between col-xl-4 pb-3">
<div className="d-flex flex-xl-column mb-2 mt-3 mt-xl-0">
<span className=" ml-xl-2 text-gray-dark">{t('rollingResistance')}</span>
<span className=" ml-4 text-muted">{t('rollingResistanceFormula')}</span>
<span className="ml-xl-2 text-gray-dark">{t('rollingResistance')}</span>
<span className="ml-4 text-muted">{t('rollingResistanceFormula')}</span>
</div>
<RollingStockEditorParameterFormColumn
rollingStockValues={rollingStockValues}
Expand Down Expand Up @@ -363,7 +365,7 @@ export const RollingStockEditorOnboardSystemEquipmentForm = ({

return (
<div className="d-lg-flex rollingstock-editor-input-container px-1 pb-3">
<div className={cx('d-flex', 'justify-content-space-around', 'mr-2')}>
<div className="d-flex justify-content-space-around mr-2">
<label className="signaling-systems-label col-xl-3" htmlFor="supportedSignalingSystems">
{t('supportedSignalingSystems')}
</label>
Expand All @@ -372,3 +374,92 @@ export const RollingStockEditorOnboardSystemEquipmentForm = ({
</div>
);
};

type CategoryOption = { id?: RollingStockCategory; label: string };

export const RollingStockEditorCategoryForm = ({
rollingStockValues,
setRollingStockValues,
}: RollingStockEditorParameterFormProps) => {
const { t } = useTranslation(['rollingstock']);

const categoryOptions: CategoryOption[] = [
{ label: t('categoriesOptions.choose') },
...Object.values(RollingStockCategoryDict).map((category) => ({
id: category,
label: t(`categoriesOptions.${category}`),
})),
];

const handlePrimaryCategoryChange = (selectedCategory?: CategoryOption) => {
setRollingStockValues((prevValues) => {
if (selectedCategory?.id) {
prevValues.categories.add(selectedCategory.id);
}
return {
...prevValues,
primaryCategory: selectedCategory?.id,
};
});
};

const handleOtherCategoryChange =
(category: RollingStockCategory) => (e: React.ChangeEvent<HTMLInputElement>) => {
setRollingStockValues((prevValues) => {
if (e.target.checked) {
prevValues.categories.add(category);
} else {
prevValues.categories.delete(category);
}
return { ...prevValues };
});
};

return (
<div className="rollingstock-editor-input-container px-1 pb-3">
{/* Primary Category Selection */}
<div className="d-flex align-items-center justify-content-between col rollingstock-editor-select mb-4">
<SelectSNCF
sm
id="primary-category-selector"
data-testid="primary-category-selector"
name="primary-category-selector"
label={t('primaryCategory')}
value={
rollingStockValues.primaryCategory
? {
id: rollingStockValues.primaryCategory,
label: t(rollingStockValues.primaryCategory),
}
: { label: t('categoriesOptions.choose') }
}
options={categoryOptions}
onChange={handlePrimaryCategoryChange}
/>
</div>

{/* Other Categories Selection */}
<div className="col">
<label className="form-label" htmlFor="rs_category_checkboxes">
{t('otherCategories')}
</label>
<div className="d-flex flex-wrap" id="rs_category_checkboxes">
{Object.values(RollingStockCategoryDict).map((category) => (
<div key={category} className={cx('col-12', 'col-sm-6', 'col-lg-4', 'mb-2')}>
<CheckboxRadioSNCF
type="checkbox"
id={`category-checkbox-${category}`}
data-testid={`category-checkbox-${category}`}
name={`category-checkbox-${category}`}
label={t(`categoriesOptions.${category}`)}
checked={rollingStockValues.categories.has(category)}
onChange={handleOtherCategoryChange(category)}
disabled={rollingStockValues.primaryCategory === category}
/>
</div>
))}
</div>
</div>
</div>
);
};
29 changes: 24 additions & 5 deletions front/src/modules/rollingStock/consts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Comfort } from 'common/api/osrdEditoastApi';
import type { Comfort, RollingStockCategory } from 'common/api/osrdEditoastApi';
import { isElectric } from 'modules/rollingStock/helpers/electric';
import type {
ElectricalProfileByMode,
MultiUnit,
RollingStockParametersValidValues,
RollingStockParametersValues,
SchemaProperty,
} from 'modules/rollingStock/types';
Expand Down Expand Up @@ -60,12 +61,14 @@ export const newRollingStockValues: RollingStockParametersValues = {
basePowerClass: null,
powerRestrictions: {},
supportedSignalingSystems: DEFAULT_SIGNALING_SYSTEMS,
primary_category: 'FREIGHT_TRAIN',
other_categories: [],
primaryCategory: undefined,
categories: new Set(),
};

export const RS_REQUIRED_FIELDS = Object.freeze({
name: '',
// This contains a list of required fields, as well as defaults values that are auto-filled in case they are missing
// However the form can not be user submitted at all if name or primaryCategory (or effort curve params) are missing
export const RS_REQUIRED_FIELDS: Partial<RollingStockParametersValidValues> = Object.freeze({
name: '', // Default value that should not end up being used
length: 1,
mass: newRollingStockValues.mass,
maxSpeed: newRollingStockValues.maxSpeed,
Expand All @@ -79,6 +82,7 @@ export const RS_REQUIRED_FIELDS = Object.freeze({
rollingResistanceC: newRollingStockValues.rollingResistanceC,
electricalPowerStartupTime: 0,
raisePantographTime: 15,
primaryCategory: 'FREIGHT_TRAIN', // Default value that should not end up being used
});

export enum RollingStockEditorMetadata {
Expand Down Expand Up @@ -339,3 +343,18 @@ export const EP_BY_MODE: ElectricalProfileByMode = {
other: [null],
thermal: [null],
};

// This dict is passthrough as we actually only need a list of categories, but using a dict lets typescript check
// that the keys perfectly corresponds to the API-provided keys or raise a type error, thus enforcing consistency
export const RollingStockCategoryDict: Record<RollingStockCategory, RollingStockCategory> = {
HIGH_SPEED_TRAIN: 'HIGH_SPEED_TRAIN',
INTERCITY_TRAIN: 'INTERCITY_TRAIN',
REGIONAL_TRAIN: 'REGIONAL_TRAIN',
COMMUTER_TRAIN: 'COMMUTER_TRAIN',
FREIGHT_TRAIN: 'FREIGHT_TRAIN',
FAST_FREIGHT_TRAIN: 'FAST_FREIGHT_TRAIN',
NIGHT_TRAIN: 'NIGHT_TRAIN',
TRAM_TRAIN: 'TRAM_TRAIN',
TOURISTIC_TRAIN: 'TOURISTIC_TRAIN',
WORK_TRAIN: 'WORK_TRAIN',
};
Loading
Loading