From cbdf62e106b520b0e8733b04bf880c529a727843 Mon Sep 17 00:00:00 2001 From: Egor Berezovskiy Date: Tue, 7 Jan 2025 11:40:55 +0100 Subject: [PATCH] front: stdcm consist form validation Signed-off-by: Egor Berezovskiy --- front/public/locales/en/stdcm.json | 16 +++- front/public/locales/fr/stdcm.json | 16 +++- .../components/StdcmForm/StdcmConfig.tsx | 44 ++++++++++- .../components/StdcmForm/StdcmConsist.tsx | 48 +++++++++++- front/src/applications/stdcm/consts.ts | 14 +--- front/src/applications/stdcm/types.ts | 7 ++ .../stdcm/utils/consistValidation.ts | 78 +++++++++++++++++++ .../styles/scss/applications/stdcm/_card.scss | 13 +++- 8 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 front/src/applications/stdcm/utils/consistValidation.ts diff --git a/front/public/locales/en/stdcm.json b/front/public/locales/en/stdcm.json index 31172b49e02..c2b36708ed4 100644 --- a/front/public/locales/en/stdcm.json +++ b/front/public/locales/en/stdcm.json @@ -8,7 +8,21 @@ "tonnage": "Tonnage", "maxSpeed": "Max speed", "tractionEngine": "Traction engine", - "towedRollingStock": "Towed rolling stock" + "towedRollingStock": "Towed rolling stock", + "errors": { + "totalMass": { + "negative": "Must be positive.", + "range": "The total weight must be between {{low}} and {{high}}t" + }, + "totalLength": { + "negative": "Must be positive.", + "range": "The total length must be between {{low}} and {{high}}m" + }, + "maxSpeed": { + "negative": "Must be positive.", + "range": "The max speed must be between {{low}} and {{high}}km/h" + } + } }, "datetimeOutsideWindow": "Date must be between {{low}} and {{high}}", "debug": { diff --git a/front/public/locales/fr/stdcm.json b/front/public/locales/fr/stdcm.json index 30a085dd736..4a42fd5125e 100644 --- a/front/public/locales/fr/stdcm.json +++ b/front/public/locales/fr/stdcm.json @@ -8,7 +8,21 @@ "tonnage": "Tonnage", "maxSpeed": "Vitesse max.", "tractionEngine": "Engin de traction", - "towedRollingStock": "Matériel remorqué" + "towedRollingStock": "Matériel remorqué", + "errors": { + "totalMass": { + "negative": "Doit être positif.", + "range": "Le tonnage total doit être compris entre {{low}} et {{high}}t" + }, + "totalLength": { + "negative": "Doit être positif.", + "range": "La longueur totale doit être comprise entre {{low}} et {{high}}m" + }, + "maxSpeed": { + "negative": "Doit être positif.", + "range": "La vitesse max. doit être comprise entre {{low}} et {{high}}km/h" + } + } }, "datetimeOutsideWindow": "La date et l'heure doivent être comprises entre le {{low}} et le {{high}}", "debug": { diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx index b330b391b03..f607811db87 100644 --- a/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx +++ b/front/src/applications/stdcm/components/StdcmForm/StdcmConfig.tsx @@ -5,9 +5,16 @@ import cx from 'classnames'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import useStdcmTowedRollingStock from 'applications/stdcm/hooks/useStdcmTowedRollingStock'; import { extractMarkersInfo } from 'applications/stdcm/utils'; +import { + validateMaxSpeed, + validateTotalLength, + validateTotalMass, +} from 'applications/stdcm/utils/consistValidation'; import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; import useInfraStatus from 'modules/pathfinding/hooks/useInfraStatus'; +import { useStoreDataForRollingStockSelector } from 'modules/rollingStock/components/RollingStockSelector/useStoreDataForRollingStockSelector'; import { Map } from 'modules/trainschedule/components/ManageTrainSchedule'; import { type StdcmConfSliceActions, resetMargins } from 'reducers/osrdconf/stdcmConf'; import type { StdcmConfSelectors } from 'reducers/osrdconf/stdcmConf/selectors'; @@ -61,6 +68,9 @@ const StdcmConfig = ({ getProjectID, getScenarioID, getStudyID, + getTotalMass, + getTotalLength, + getMaxSpeed, } = useOsrdConfSelectors() as StdcmConfSelectors; const origin = useSelector(getStdcmOrigin); const pathSteps = useSelector(getStdcmPathSteps); @@ -69,11 +79,38 @@ const StdcmConfig = ({ const studyID = useSelector(getStudyID); const scenarioID = useSelector(getScenarioID); + const totalMass = useSelector(getTotalMass); + const totalLength = useSelector(getTotalLength); + const maxSpeed = useSelector(getMaxSpeed); + const pathfinding = useStaticPathfinding(infra); const formRef = useRef(null); const [formErrors, setFormErrors] = useState(); + const { rollingStock } = useStoreDataForRollingStockSelector(); + const towedRollingStock = useStdcmTowedRollingStock(); + + const consistErrors = useMemo(() => { + const totalMassError = validateTotalMass({ + tractionEngineMass: rollingStock?.mass, + towedMass: towedRollingStock?.mass, + totalMass, + }); + + const totalLengthError = validateTotalLength({ + tractionEngineLength: rollingStock?.length, + towedLength: towedRollingStock?.length, + totalLength, + }); + + return { + totalMass: totalMassError, + totalLength: totalLengthError, + maxSpeed: validateMaxSpeed(maxSpeed, rollingStock?.max_speed), + }; + }, [rollingStock, towedRollingStock, totalMass, totalLength, maxSpeed]); + const disabled = isPending || retainedSimulationIndex > -1; const markersInfo = useMemo(() => extractMarkersInfo(pathSteps), [pathSteps]); @@ -146,7 +183,7 @@ const StdcmConfig = ({ />
- +
@@ -175,7 +212,10 @@ const StdcmConfig = ({ isDisabled={ disabled || !showBtnToLaunchSimulation || - formErrors?.errorType === StdcmConfigErrorTypes.INFRA_NOT_LOADED + formErrors?.errorType === StdcmConfigErrorTypes.INFRA_NOT_LOADED || + !!consistErrors.totalMass || + !!consistErrors.totalLength || + !!consistErrors.maxSpeed } /> {formErrors && ( diff --git a/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx b/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx index ec647c58123..7f0000e3732 100644 --- a/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx +++ b/front/src/applications/stdcm/components/StdcmForm/StdcmConsist.tsx @@ -4,6 +4,11 @@ import { Input, ComboBox } from '@osrd-project/ui-core'; import { useTranslation } from 'react-i18next'; import useStdcmTowedRollingStock from 'applications/stdcm/hooks/useStdcmTowedRollingStock'; +import { + CONSIST_MAX_SPEED_MIN, + CONSIST_TOTAL_LENGTH_MAX, + CONSIST_TOTAL_MASS_MAX, +} from 'applications/stdcm/utils/consistValidation'; import type { LightRollingStockWithLiveries, TowedRollingStock } from 'common/api/osrdEditoastApi'; import { useOsrdConfActions } from 'common/osrdContext'; import SpeedLimitByTagSelector from 'common/SpeedLimitByTagSelector/SpeedLimitByTagSelector'; @@ -14,6 +19,7 @@ import useFilterRollingStock from 'modules/rollingStock/hooks/useFilterRollingSt import useFilterTowedRollingStock from 'modules/towedRollingStock/hooks/useFilterTowedRollingStock'; import { type StdcmConfSliceActions } from 'reducers/osrdconf/stdcmConf'; import { useAppDispatch } from 'store'; +import { kgToT, kmhToMs, msToKmh } from 'utils/physics'; import StdcmCard from './StdcmCard'; import useStdcmConsist from '../../hooks/useStdcmConsist'; @@ -33,7 +39,7 @@ const ConsistCardTitle = ({ ); }; -const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { +const StdcmConsist = ({ consistErrors = {}, disabled = false }: StdcmConfigCardProps) => { const { t } = useTranslation('stdcm'); const { speedLimitByTag, speedLimitsByTags, dispatchUpdateSpeedLimitByTag } = useStoreDataForSpeedLimitByTagSelector({ isStdcm: true }); @@ -166,6 +172,20 @@ const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { value={totalMass ?? ''} onChange={onTotalMassChange} disabled={disabled} + statusWithMessage={ + consistErrors?.totalMass + ? { + status: 'error', + tooltip: 'left', + message: t(consistErrors.totalMass, { + low: Math.floor( + kgToT((rollingStock?.mass ?? 0) + (towedRollingStock?.mass ?? 0)) + ), + high: CONSIST_TOTAL_MASS_MAX, + }), + } + : undefined + } /> { value={totalLength ?? ''} onChange={onTotalLengthChange} disabled={disabled} + statusWithMessage={ + consistErrors?.totalLength + ? { + status: 'error', + tooltip: 'right', + message: t(consistErrors.totalLength, { + low: Math.floor((rollingStock?.length ?? 0) + (towedRollingStock?.length ?? 0)), + high: CONSIST_TOTAL_LENGTH_MAX, + }), + } + : undefined + } />
@@ -194,6 +226,20 @@ const StdcmConsist = ({ disabled = false }: StdcmConfigCardProps) => { value={maxSpeed ?? ''} onChange={onMaxSpeedChange} disabled={disabled} + statusWithMessage={ + consistErrors?.maxSpeed + ? { + status: 'error', + tooltip: 'right', + message: t(consistErrors.maxSpeed, { + low: CONSIST_MAX_SPEED_MIN, + high: Math.floor( + msToKmh(Math.min(rollingStock?.max_speed ?? kmhToMs(CONSIST_MAX_SPEED_MIN))) + ), + }), + } + : undefined + } />
diff --git a/front/src/applications/stdcm/consts.ts b/front/src/applications/stdcm/consts.ts index 76f50bdfa08..a1bd3625a42 100644 --- a/front/src/applications/stdcm/consts.ts +++ b/front/src/applications/stdcm/consts.ts @@ -9,18 +9,6 @@ export const STDCM_REQUEST_STATUS = Object.freeze({ export const STDCM_TRAIN_ID = -10; -export const COMPOSITION_CODES = [ - 'HLP', - 'MA100', - 'MA80', - 'MA90', - 'ME100', - 'ME120', - 'ME140', - 'MV160', - 'MVGV', -]; - export const COMPOSITION_CODES_MAX_SPEEDS: Record = { MA80: 80, MA90: 90, @@ -32,3 +20,5 @@ export const COMPOSITION_CODES_MAX_SPEEDS: Record = HLP: 100, MVGV: 200, }; + +export const COMPOSITION_CODES = Object.keys(COMPOSITION_CODES_MAX_SPEEDS); diff --git a/front/src/applications/stdcm/types.ts b/front/src/applications/stdcm/types.ts index 746ce92658d..3c24e877877 100644 --- a/front/src/applications/stdcm/types.ts +++ b/front/src/applications/stdcm/types.ts @@ -72,6 +72,12 @@ export type StdcmResultsOperationalPoint = { trackName?: string; }; +export type ConsistErrors = { + totalMass?: string; + totalLength?: string; + maxSpeed?: string; +}; + export type StdcmResults = { stdcmResponse: StdcmSuccessResponse; speedSpaceChartData: SpeedSpaceChartData | null; @@ -146,6 +152,7 @@ export type StdcmSimulation = { /** This type is used for StdcmConsist, StdcmOrigin, StdcmDestination and StdcmVias components */ export type StdcmConfigCardProps = { disabled?: boolean; + consistErrors?: ConsistErrors; }; export enum ArrivalTimeTypes { diff --git a/front/src/applications/stdcm/utils/consistValidation.ts b/front/src/applications/stdcm/utils/consistValidation.ts new file mode 100644 index 00000000000..08f94aef904 --- /dev/null +++ b/front/src/applications/stdcm/utils/consistValidation.ts @@ -0,0 +1,78 @@ +import { kgToT, msToKmh } from 'utils/physics'; + +export const CONSIST_TOTAL_MASS_MAX = 10000; // ton +export const CONSIST_TOTAL_LENGTH_MAX = 750; // m +export const CONSIST_MAX_SPEED_MIN = 30; // km/h + +export const validateTotalMass = ({ + tractionEngineMass = 0, + towedMass = 0, + totalMass, +}: { + tractionEngineMass?: number; + towedMass?: number; + totalMass?: number; +}) => { + if (!totalMass) { + return undefined; + } + + if (totalMass <= 0) { + return 'consist.errors.totalMass.negative'; + } + + const tractionMassInTons = kgToT(tractionEngineMass); + const consistMassInTons = kgToT(tractionEngineMass + towedMass); + const massLimit = towedMass ? consistMassInTons : tractionMassInTons; + + if (totalMass < massLimit || totalMass >= CONSIST_TOTAL_MASS_MAX) { + return 'consist.errors.totalMass.range'; + } + + return undefined; +}; + +export const validateTotalLength = ({ + tractionEngineLength = 0, + towedLength = 0, + totalLength, +}: { + tractionEngineLength?: number; + towedLength?: number; + totalLength?: number; +}) => { + if (!totalLength) { + return undefined; + } + + if (totalLength <= 0) { + return 'consist.errors.totalLength.negative'; + } + + const consistLength = Math.floor(tractionEngineLength + towedLength); + + if (totalLength < consistLength || totalLength >= CONSIST_TOTAL_LENGTH_MAX) { + return 'consist.errors.totalLength.range'; + } + + return undefined; +}; + +export const validateMaxSpeed = (maxSpeed?: number, tractionEngineMaxSpeed?: number) => { + if (!maxSpeed) { + return undefined; + } + + if (maxSpeed <= 0) { + return 'consist.errors.maxSpeed.negative'; + } + + if ( + maxSpeed < CONSIST_MAX_SPEED_MIN || + (tractionEngineMaxSpeed && maxSpeed > Math.floor(msToKmh(tractionEngineMaxSpeed))) + ) { + return 'consist.errors.maxSpeed.range'; + } + + return undefined; +}; diff --git a/front/src/styles/scss/applications/stdcm/_card.scss b/front/src/styles/scss/applications/stdcm/_card.scss index fb25345aa26..9b48b12f5cd 100644 --- a/front/src/styles/scss/applications/stdcm/_card.scss +++ b/front/src/styles/scss/applications/stdcm/_card.scss @@ -7,6 +7,9 @@ background-color: rgba(255, 255, 255, 1); height: fit-content; position: relative; + display: flex; + flex-direction: column; + gap: 5px; .stdcm-default-card-icon { color: #1844ef; @@ -89,7 +92,9 @@ } .feed-back { - padding: 0; + --field-wrapper-padding-bottom: 9px; + padding-top: 3px; + padding-bottom: 3px; } .date-picker { @@ -121,7 +126,8 @@ // cards styles &.consist { - padding-left: 24px; + padding-left: 11px; + padding-right: 11px; padding-bottom: 15px; .towed-rolling-stock { @@ -130,9 +136,8 @@ .stdcm-consist__properties { display: grid; - grid-template-columns: 119px 127px; + grid-template-columns: 139px 147px; justify-content: space-between; - padding-block: 8px; .osrd-config-item { padding-right: 3px;