diff --git a/front/package.json b/front/package.json index 25c31cfc68a..70174d63205 100644 --- a/front/package.json +++ b/front/package.json @@ -10,8 +10,8 @@ "@nivo/line": "^0.80.0", "@nivo/tooltip": "^0.80.0", "@openapi-contrib/openapi-schema-to-json-schema": "^5.1.0", - "@osrd-project/ui-core": "^0.0.21", - "@osrd-project/ui-icons": "^0.0.21", + "@osrd-project/ui-core": "^0.0.24", + "@osrd-project/ui-icons": "^0.0.24", "@react-pdf/renderer": "^3.4.2", "@redux-devtools/extension": "^3.3.0", "@reduxjs/toolkit": "^2.1.0", diff --git a/front/public/locales/en/home/navbar.json b/front/public/locales/en/home/navbar.json index d7ede19a4b2..75b243e3ba6 100644 --- a/front/public/locales/en/home/navbar.json +++ b/front/public/locales/en/home/navbar.json @@ -20,6 +20,7 @@ }, "safeWord": "Safety keyword", "safeWordHelp": "The \"security keyword\" allows you to transparently filter the list of projects with the word you enter, used as a label. Choose a complicated word so that no-one uses it inadvertently. Add it as a label to a project; it will be automatically added when the project is created if the word is entered here.", + "stdcmToggle": "STDCM New interface", "userSettings": "User settings", "yourSafeWord": "Type in your word" } diff --git a/front/public/locales/en/stdcm.json b/front/public/locales/en/stdcm.json index 8dfc64fd413..5d7b99b30c8 100644 --- a/front/public/locales/en/stdcm.json +++ b/front/public/locales/en/stdcm.json @@ -1,7 +1,20 @@ { "apply": "Apply", "cancelRequest": "Cancel request", + "consist": { + "consist": "Consist", + "tractionEngine": "Traction engine" + }, + "loaderImageLegend": "The TGV Nord line", + "notificationTitle": "Phase 1: from D-7 to D-1 5pm, on the Perrigny-Miramas axis.", "pleaseWait": "Please wait…", + "simulation":{ + "averageRequestTime": "For your request, the time required is generally 90 seconds.", + "calculatingSimulation": "Calculation in progress...", + "getSimulation": "Get the simulation", + "pendingSimulation": "Simulation in progress", + "stopCalculation" : "Stop calculation" + }, "spaceSpeedGraphic": "Space-Velocity graph", "spaceTimeGraphic": "Space-Time graph", "stdcmComputation": "Search for a train path", @@ -9,5 +22,14 @@ "stdcmErrorNoPaths": "Incompatibility with other train paths.", "stdcmNoResults": "No path found", "stdcmResults": "Results", - "stdcmSimulationReport": "Path simulation report" + "stdcmSimulationReport": "Path simulation report", + "trainPath": { + "asSoonAsPossible": "As soon as possible", + "ch": "CH", + "ci": "CI", + "date": "Date", + "destination": "Destination", + "origin": "Origin", + "time": "Time" + } } diff --git a/front/public/locales/fr/home/navbar.json b/front/public/locales/fr/home/navbar.json index 85b695334e5..6161fab7f5a 100644 --- a/front/public/locales/fr/home/navbar.json +++ b/front/public/locales/fr/home/navbar.json @@ -20,6 +20,7 @@ }, "safeWord": "Mot clé de sécurité", "safeWordHelp": "Le « mot-clé de sécurité » permet de filtrer de manière transparente la liste des projets avec le mot renseigné, utilisé comme une étiquette. Préférez un mot compliqué dans l'idée que personne ne l'utilise par inadvertance. Ajoutez-le comme une étiquette à un projet ; il sera automatiquement ajouté à la création de projet si le mot est renseigné ici.", + "stdcmToggle": "STDCM Nouvelle interface", "userSettings": "Paramètres utilisateur", "yourSafeWord": "Tapez votre mot" } diff --git a/front/public/locales/fr/stdcm.json b/front/public/locales/fr/stdcm.json index 5b84a35a5df..e5382a517f2 100644 --- a/front/public/locales/fr/stdcm.json +++ b/front/public/locales/fr/stdcm.json @@ -1,7 +1,20 @@ { "apply": "Appliquer", "cancelRequest": "Annuler la requête", + "consist": { + "consist": "Convoi", + "tractionEngine": "Engin de traction" + }, + "loaderImageLegend": "La ligne TGV Nord", + "notificationTitle": "Phase 1 : de J-7 à J-1 17h, sur l’axe Perrigny—Miramas.", "pleaseWait": "Veuillez patientez…", + "simulation":{ + "averageRequestTime": "Pour votre demande, le temps nécessaire est généralement de 90 secondes.", + "calculatingSimulation": "Calcul en cours...", + "getSimulation": "Obtenir la simulation", + "pendingSimulation": "simulation en cours", + "stopCalculation" : "Arrêter le calcul" + }, "spaceSpeedGraphic": "Graphique Espace-Vitesse", "spaceTimeGraphic": "Graphique Espace-Temps", "stdcmComputation": "Recherche de sillon", @@ -9,5 +22,14 @@ "stdcmErrorNoPaths": "Incompatibilité avec d'autres sillons.", "stdcmNoResults": "Aucun sillon trouvé", "stdcmResults": "Résultats", - "stdcmSimulationReport": "Fiche simulation" + "stdcmSimulationReport": "Fiche simulation", + "trainPath": { + "asSoonAsPossible": "Dès que possible", + "ch": "CH", + "ci": "CI", + "date": "Date", + "destination": "Destination", + "origin": "Origine", + "time": "Heure" + } } diff --git a/front/src/applications/operationalStudies/consts.ts b/front/src/applications/operationalStudies/consts.ts index 8c41212b0c4..4eebfc4f3df 100644 --- a/front/src/applications/operationalStudies/consts.ts +++ b/front/src/applications/operationalStudies/consts.ts @@ -94,6 +94,7 @@ export interface PointOnMap { path_offset?: number; uic?: number | null; ch?: string | null; + ci?: number | null; location?: { track_section?: string; offset?: number; diff --git a/front/src/applications/operationalStudies/views/Scenario.tsx b/front/src/applications/operationalStudies/views/Scenario.tsx index be5c9f47635..5cd8ed1fa14 100644 --- a/front/src/applications/operationalStudies/views/Scenario.tsx +++ b/front/src/applications/operationalStudies/views/Scenario.tsx @@ -2,12 +2,14 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { getTrainScheduleV2Activated } from 'reducers/user/userSelectors'; +import { getStdcmV2Activated, getTrainScheduleV2Activated } from 'reducers/user/userSelectors'; import ScenarioV1 from './ScenarioV1'; import ScenarioV2 from './v2/ScenarioV2'; export default function Scenario() { const trainScheduleV2Activated = useSelector(getTrainScheduleV2Activated); - return trainScheduleV2Activated ? : ; + const stdcmV2Activated = useSelector(getStdcmV2Activated); + const useTrainScheduleV2 = trainScheduleV2Activated || stdcmV2Activated; + return useTrainScheduleV2 ? : ; } diff --git a/front/src/applications/operationalStudies/views/Study.tsx b/front/src/applications/operationalStudies/views/Study.tsx index 98f06f0f13b..a9ed7c30cb6 100644 --- a/front/src/applications/operationalStudies/views/Study.tsx +++ b/front/src/applications/operationalStudies/views/Study.tsx @@ -23,7 +23,7 @@ import { Loader, Spinner } from 'common/Loaders'; import ScenarioCard from 'modules/scenario/components/ScenarioCard'; import ScenarioCardEmpty from 'modules/scenario/components/ScenarioCardEmpty'; import AddOrEditStudyModal from 'modules/study/components/AddOrEditStudyModal'; -import { getTrainScheduleV2Activated } from 'reducers/user/userSelectors'; +import { getStdcmV2Activated, getTrainScheduleV2Activated } from 'reducers/user/userSelectors'; import { budgetFormat } from 'utils/numbers'; type SortOptions = @@ -44,6 +44,8 @@ export default function Study() { const { openModal } = useModal(); const { projectId: urlProjectId, studyId: urlStudyId } = useParams() as studyParams; const trainScheduleV2Activated = useSelector(getTrainScheduleV2Activated); + const stdcmV2Activated = useSelector(getStdcmV2Activated); + const useTrainScheduleV2 = trainScheduleV2Activated || stdcmV2Activated; const [scenariosList, setScenariosList] = useState([]); const [filter, setFilter] = useState(''); @@ -154,7 +156,7 @@ export default function Study() { console.error(error); } } else { - const scenarios = trainScheduleV2Activated ? scenariosV2?.results : scenariosV1?.results; + const scenarios = useTrainScheduleV2 ? scenariosV2?.results : scenariosV1?.results; setScenariosList(scenarios || []); } setIsLoading(false); @@ -186,7 +188,7 @@ export default function Study() { useEffect(() => { getScenarioList(); - }, [sortOption, filter, scenariosV1, scenariosV2, trainScheduleV2Activated]); + }, [sortOption, filter, scenariosV1, scenariosV2, useTrainScheduleV2]); return ( <> diff --git a/front/src/applications/rollingStockEditor/Home.tsx b/front/src/applications/rollingStockEditor/Home.tsx index 60ea1c03704..f3d71c3b07b 100644 --- a/front/src/applications/rollingStockEditor/Home.tsx +++ b/front/src/applications/rollingStockEditor/Home.tsx @@ -2,7 +2,6 @@ import React, { type FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { enhancedEditoastApi } from 'common/api/enhancedEditoastApi'; import { ModalProvider } from 'common/BootstrapSNCF/ModalSNCF/ModalProvider'; import NavBarSNCF from 'common/BootstrapSNCF/NavBarSNCF'; @@ -11,15 +10,10 @@ import RollingStockEditor from './views/RollingStockEditor'; const HomeRollingStockEditor: FC = () => { const { t } = useTranslation(['home/home', 'referenceMap']); - const { data: { results: rollingStocks } = { results: [] } } = - enhancedEditoastApi.endpoints.getLightRollingStock.useQuery({ - pageSize: 1000, - }); - return ( {t('rollingStockEditor')}} /> - + ); }; diff --git a/front/src/applications/rollingStockEditor/views/RollingStockEditor.tsx b/front/src/applications/rollingStockEditor/views/RollingStockEditor.tsx index be406682353..52c09c9e07b 100644 --- a/front/src/applications/rollingStockEditor/views/RollingStockEditor.tsx +++ b/front/src/applications/rollingStockEditor/views/RollingStockEditor.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; -import type { LightRollingStockWithLiveries } from 'common/api/osrdEditoastApi'; import { useModal } from 'common/BootstrapSNCF/ModalSNCF'; import { Loader } from 'common/Loaders/Loader'; import { RollingStockCard } from 'modules/rollingStock/components/RollingStockCard'; @@ -12,19 +11,13 @@ import RollingStockEditorButtons from 'modules/rollingStock/components/RollingSt import RollingStockEditorFormModal from 'modules/rollingStock/components/RollingStockEditor/RollingStockEditorFormModal'; import RollingStockInformationPanel from 'modules/rollingStock/components/RollingStockEditor/RollingStockInformationPanel'; import { SearchRollingStock } from 'modules/rollingStock/components/RollingStockSelector'; +import useFilterRollingStock from 'modules/rollingStock/hooks/useFilterRollingStock'; -type RollingStockEditorProps = { - rollingStocks: LightRollingStockWithLiveries[]; -}; - -export default function RollingStockEditor({ rollingStocks }: RollingStockEditorProps) { +const RollingStockEditor = () => { const { t } = useTranslation('rollingstock'); const ref2scroll: React.RefObject = useRef(null); - const [filteredRollingStockList, setFilteredRollingStockList] = useState(rollingStocks); - const [isLoading, setIsLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); const [isAdding, setIsAdding] = useState(false); - const [isDuplicating, setIsDuplicating] = useState(false); const { openModal } = useModal(); const [openedRollingStockCardId, setOpenedRollingStockCardId] = useState(); @@ -39,7 +32,14 @@ export default function RollingStockEditor({ rollingStocks }: RollingStockEditor } ); - useEffect(() => setFilteredRollingStockList(rollingStocks), []); + const { + filteredRollingStockList, + filters, + searchMateriel, + toggleFilter, + searchIsLoading, + resetFilters, + } = useFilterRollingStock(); const rollingStocksList = (
@@ -72,7 +72,7 @@ export default function RollingStockEditor({ rollingStocks }: RollingStockEditor isCondensed rollingStock={selectedRollingStock} setIsEditing={setIsEditing} - setIsDuplicating={setIsDuplicating} + resetFilters={resetFilters} isRollingStockLocked={selectedRollingStock.locked as boolean} />
@@ -102,7 +102,7 @@ export default function RollingStockEditor({ rollingStocks }: RollingStockEditor ); function displayList() { - if (isLoading) { + if (searchIsLoading) { return ; } if (filteredRollingStockList.length === 0) { @@ -171,12 +171,10 @@ export default function RollingStockEditor({ rollingStocks }: RollingStockEditor )} {displayList()} @@ -186,4 +184,6 @@ export default function RollingStockEditor({ rollingStocks }: RollingStockEditor )} ); -} +}; + +export default RollingStockEditor; diff --git a/front/src/applications/stdcm/Home.tsx b/front/src/applications/stdcm/Home.tsx index 3f487b6d1f3..0281fbed8fe 100644 --- a/front/src/applications/stdcm/Home.tsx +++ b/front/src/applications/stdcm/Home.tsx @@ -1,19 +1,30 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; import { Route, Routes } from 'react-router-dom'; +import StdcmViewV1 from 'applications/stdcm/views/StdcmViewV1'; +import StdcmViewV2 from 'applications/stdcmV2/views/StdcmViewV2'; import NavBarSNCF from 'common/BootstrapSNCF/NavBarSNCF'; - -import StdcmView from './views/StdcmView'; +import { getStdcmV2Activated } from 'reducers/user/userSelectors'; export default function HomeStdcm() { + const stdcmV2Activated = useSelector(getStdcmV2Activated); const { t } = useTranslation('home/home'); + if (stdcmV2Activated) { + return ( + + } /> + + ); + } + return ( <> - } /> + } /> ); diff --git a/front/src/applications/stdcm/hooks/useStdcm.tsx b/front/src/applications/stdcm/hooks/useStdcm.tsx new file mode 100644 index 00000000000..003e559d129 --- /dev/null +++ b/front/src/applications/stdcm/hooks/useStdcm.tsx @@ -0,0 +1,186 @@ +import { useState } from 'react'; + +import { cloneDeep } from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import STDCM_REQUEST_STATUS from 'applications/stdcm/consts'; +import formatStdcmConf from 'applications/stdcm/formatStdcmConf'; +import type { StdcmRequestStatus, StdcmV2SuccessResponse } from 'applications/stdcm/types'; +import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; +import type { SimulationReport, PostStdcmApiResponse } from 'common/api/osrdEditoastApi'; +import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; +import createTrain from 'modules/simulationResult/components/SpaceTimeChart/createTrain'; +import { CHART_AXES } from 'modules/simulationResult/consts'; +import { setFailure } from 'reducers/main'; +import type { OsrdStdcmConfState } from 'reducers/osrdconf/types'; +import { + updateConsolidatedSimulation, + updateSelectedTrainId, + updateSimulation, + updateSelectedProjection, +} from 'reducers/osrdsimulation/actions'; +import type { Train } from 'reducers/osrdsimulation/types'; +import { getStdcmV2Activated, getTrainScheduleV2Activated } from 'reducers/user/userSelectors'; +import { useAppDispatch } from 'store'; +import { castErrorToFailure } from 'utils/error'; + +import { checkStdcmConf, formatStdcmPayload } from '../utils/formatStdcmConfV2'; + +export default function useStdcm() { + const [stdcmResults, setStdcmResults] = useState(); + const [stdcmV2Results, setStdcmV2Results] = useState(); + const [currentStdcmRequestStatus, setCurrentStdcmRequestStatus] = useState( + STDCM_REQUEST_STATUS.idle + ); + + const dispatch = useAppDispatch(); + const { t } = useTranslation(['translation', 'stdcm']); + const { getConf } = useOsrdConfSelectors(); + const osrdconf = useSelector(getConf); + const trainScheduleV2Activated = useSelector(getTrainScheduleV2Activated); + const stdcmV2Activated = useSelector(getStdcmV2Activated); + + const [postStdcm] = osrdEditoastApi.endpoints.postStdcm.useMutation(); + const [postV2TimetableByIdStdcm] = + osrdEditoastApi.endpoints.postV2TimetableByIdStdcm.useMutation(); + const [postTrainScheduleResults] = + osrdEditoastApi.endpoints.postTrainScheduleResults.useMutation(); + + const [getTimetable] = osrdEditoastApi.endpoints.getTimetableById.useLazyQuery(); + + const { updateItinerary } = useOsrdConfActions(); + + // https://developer.mozilla.org/en-US/docs/Web/API/AbortController + const controller = new AbortController(); + + const { timetableID } = osrdconf; + + const resetResults = () => { + dispatch(updateSelectedTrainId(undefined)); + dispatch(updateConsolidatedSimulation([])); + dispatch(updateSimulation({ trains: [] })); + setStdcmResults(undefined); + }; + + const launchStdcmRequestV1 = async () => { + const payload = formatStdcmConf(dispatch, t, osrdconf as OsrdStdcmConfState); + if (payload && timetableID) { + resetResults(); + postStdcm(payload) + .unwrap() + .then((result) => { + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.success); + setStdcmResults(result); + dispatch(updateItinerary(result.path)); + + const fakedNewTrain = { + ...cloneDeep(result.simulation), + id: 1500, + isStdcm: true, + }; + getTimetable({ id: timetableID }).then(({ data: timetable }) => { + const trainIdsToFetch = + timetable?.train_schedule_summaries.map((train) => train.id) ?? []; + postTrainScheduleResults({ + body: { + path_id: result.path.id, + train_ids: trainIdsToFetch, + }, + }) + .unwrap() + .then((timetableTrains) => { + const trains: SimulationReport[] = [...timetableTrains.simulations, fakedNewTrain]; + const consolidatedSimulation = createTrain( + CHART_AXES.SPACE_TIME, + trains as Train[] // TODO: remove Train interface + ); + dispatch(updateConsolidatedSimulation(consolidatedSimulation)); + dispatch(updateSimulation({ trains })); + dispatch(updateSelectedTrainId(fakedNewTrain.id)); + + dispatch( + updateSelectedProjection({ + id: fakedNewTrain.id, + path: result.path.id, + }) + ); + }) + .catch((e) => { + dispatch( + setFailure( + castErrorToFailure(e, { + name: t('stdcm:stdcmError'), + message: t('translation:common.error'), + }) + ) + ); + }); + }); + }) + .catch((e) => { + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.rejected); + dispatch(setFailure(castErrorToFailure(e, { name: t('stdcm:stdcmError') }))); + }); + } + }; + + const launchStdcmRequestV2 = async () => { + const validConfig = checkStdcmConf(dispatch, t, osrdconf as OsrdStdcmConfState); + if (validConfig) { + const payload = formatStdcmPayload(validConfig); + try { + const response = await postV2TimetableByIdStdcm(payload).unwrap(); + if (response.status === 'success') { + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.success); + setStdcmV2Results(response); + } else { + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.rejected); + dispatch( + setFailure({ + name: t('stdcm:stdcmError'), + message: t('translation:common.error'), + }) + ); + } + } catch (e) { + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.rejected); + dispatch(setFailure(castErrorToFailure(e, { name: t('stdcm:stdcmError') }))); + } + } else { + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.rejected); + } + }; + + const launchStdcmRequest = async () => { + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.pending); + if (trainScheduleV2Activated || stdcmV2Activated) { + launchStdcmRequestV2(); + } else { + launchStdcmRequestV1(); + } + }; + + const cancelStdcmRequest = () => { + // when http ready https://axios-http.com/docs/cancellation + + controller.abort(); + setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.canceled); + + const emptySimulation = { trains: [] }; + const consolidatedSimulation = createTrain(CHART_AXES.SPACE_TIME, emptySimulation.trains); + dispatch(updateConsolidatedSimulation(consolidatedSimulation)); + dispatch(updateSimulation(emptySimulation)); + }; + + return { + stdcmResults, + stdcmV2Results, + currentStdcmRequestStatus, + launchStdcmRequest, + setStdcmResults, + setStdcmV2Results, + setCurrentStdcmRequestStatus, + cancelStdcmRequest, + }; +} diff --git a/front/src/applications/stdcm/views/StdcmConfig.tsx b/front/src/applications/stdcm/views/StdcmConfig.tsx index 6de0e8454c9..cf708ad43f1 100644 --- a/front/src/applications/stdcm/views/StdcmConfig.tsx +++ b/front/src/applications/stdcm/views/StdcmConfig.tsx @@ -7,7 +7,7 @@ import { useSelector } from 'react-redux'; import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; import RunningTime from 'applications/stdcm/components/RunningTime'; import STDCM_REQUEST_STATUS from 'applications/stdcm/consts'; -import type { StdcmRequestStatus, StdcmV2SuccessResponse } from 'applications/stdcm/types'; +import type { StdcmV2SuccessResponse } from 'applications/stdcm/types'; import StdcmResults from 'applications/stdcm/views/StdcmResults'; import { osrdEditoastApi, type PostStdcmApiResponse } from 'common/api/osrdEditoastApi'; import { useInfraID, useOsrdConfSelectors } from 'common/osrdContext'; @@ -27,7 +27,7 @@ import StdcmResultsV2 from './StdcmResultsV2'; type OSRDStdcmConfigProps = { currentStdcmRequestStatus: string; - setCurrentStdcmRequestStatus: (currentStdcmRequestStatus: StdcmRequestStatus) => void; + launchStdcmRequest: () => Promise; stdcmResults?: PostStdcmApiResponse; stdcmV2Results?: StdcmV2SuccessResponse; pathProperties?: ManageTrainSchedulePathProperties; @@ -36,7 +36,7 @@ type OSRDStdcmConfigProps = { const StdcmConfig = ({ currentStdcmRequestStatus, - setCurrentStdcmRequestStatus, + launchStdcmRequest, stdcmResults, stdcmV2Results, pathProperties, @@ -104,7 +104,7 @@ const StdcmConfig = ({ const handleClick = () => { const currentDateTime = new Date(); setCreationDate(currentDateTime); - setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.pending); + launchStdcmRequest(); }; useEffect(() => { diff --git a/front/src/applications/stdcm/views/StdcmRequestModal.tsx b/front/src/applications/stdcm/views/StdcmRequestModal.tsx index 1159aa22efa..cf880c24b71 100644 --- a/front/src/applications/stdcm/views/StdcmRequestModal.tsx +++ b/front/src/applications/stdcm/views/StdcmRequestModal.tsx @@ -1,192 +1,23 @@ -import React, { useEffect } from 'react'; +import React from 'react'; -import { cloneDeep } from 'lodash'; import { useTranslation } from 'react-i18next'; import ReactModal from 'react-modal'; -import { useSelector } from 'react-redux'; -import STDCM_REQUEST_STATUS from 'applications/stdcm/consts'; -import formatStdcmConf from 'applications/stdcm/formatStdcmConf'; -import type { StdcmRequestStatus, StdcmV2SuccessResponse } from 'applications/stdcm/types'; -import type { PostStdcmApiResponse, SimulationReport } from 'common/api/osrdEditoastApi'; -import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; import ModalBodySNCF from 'common/BootstrapSNCF/ModalSNCF/ModalBodySNCF'; import ModalHeaderSNCF from 'common/BootstrapSNCF/ModalSNCF/ModalHeaderSNCF'; import { Spinner } from 'common/Loaders'; -import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; -import createTrain from 'modules/simulationResult/components/SpaceTimeChart/createTrain'; -import { CHART_AXES } from 'modules/simulationResult/consts'; -import { setFailure } from 'reducers/main'; -import type { OsrdStdcmConfState } from 'reducers/osrdconf/types'; -import { - updateConsolidatedSimulation, - updateSelectedTrainId, - updateSimulation, - updateSelectedProjection, -} from 'reducers/osrdsimulation/actions'; -import type { Train } from 'reducers/osrdsimulation/types'; -import { getTrainScheduleV2Activated } from 'reducers/user/userSelectors'; -import { useAppDispatch } from 'store'; -import { castErrorToFailure } from 'utils/error'; -import { checkStdcmConf, formatStdcmPayload } from '../utils/formatStdcmConfV2'; - -type StdcmRequestModalProps = { - setCurrentStdcmRequestStatus: (currentStdcmRequestStatus: StdcmRequestStatus) => void; - currentStdcmRequestStatus: StdcmRequestStatus; - setStdcmResults: (stdcmResults?: PostStdcmApiResponse) => void; - setStdcmV2Results: (stdcmV2Results: StdcmV2SuccessResponse | undefined) => void; +export type StdcmRequestModalProps = { + isOpen: boolean; + cancelStdcmRequest: () => void; }; -const StdcmRequestModal = ({ - setCurrentStdcmRequestStatus, - currentStdcmRequestStatus, - setStdcmResults, - setStdcmV2Results, -}: StdcmRequestModalProps) => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(['translation', 'stdcm']); - const { getConf } = useOsrdConfSelectors(); - const trainScheduleV2Activated = useSelector(getTrainScheduleV2Activated); - const osrdconf = useSelector(getConf); - - const [postStdcm] = osrdEditoastApi.endpoints.postStdcm.useMutation(); - const [postV2TimetableByIdStdcm] = - osrdEditoastApi.endpoints.postV2TimetableByIdStdcm.useMutation(); - const [postTrainScheduleResults] = - osrdEditoastApi.endpoints.postTrainScheduleResults.useMutation(); - - const [getTimetable] = osrdEditoastApi.endpoints.getTimetableById.useLazyQuery(); - - const { updateItinerary } = useOsrdConfActions(); - - // https://developer.mozilla.org/en-US/docs/Web/API/AbortController - const controller = new AbortController(); - - const { timetableID } = osrdconf; - - const resetResults = () => { - dispatch(updateSelectedTrainId(undefined)); - dispatch(updateConsolidatedSimulation([])); - dispatch(updateSimulation({ trains: [] })); - setStdcmResults(undefined); - }; - - useEffect(() => { - const launchStdcmRequest = async () => { - const payload = formatStdcmConf(dispatch, t, osrdconf as OsrdStdcmConfState); - if (payload && timetableID) { - resetResults(); - postStdcm(payload) - .unwrap() - .then((result) => { - setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.success); - setStdcmResults(result); - dispatch(updateItinerary(result.path)); - - const fakedNewTrain = { - ...cloneDeep(result.simulation), - id: 1500, - isStdcm: true, - }; - getTimetable({ id: timetableID }).then(({ data: timetable }) => { - const trainIdsToFetch = - timetable?.train_schedule_summaries.map((train) => train.id) ?? []; - postTrainScheduleResults({ - body: { - path_id: result.path.id, - train_ids: trainIdsToFetch, - }, - }) - .unwrap() - .then((timetableTrains) => { - const trains: SimulationReport[] = [ - ...timetableTrains.simulations, - fakedNewTrain, - ]; - const consolidatedSimulation = createTrain( - CHART_AXES.SPACE_TIME, - trains as Train[] // TODO: remove Train interface - ); - dispatch(updateConsolidatedSimulation(consolidatedSimulation)); - dispatch(updateSimulation({ trains })); - dispatch(updateSelectedTrainId(fakedNewTrain.id)); - - dispatch( - updateSelectedProjection({ - id: fakedNewTrain.id, - path: result.path.id, - }) - ); - }) - .catch((e) => { - dispatch( - setFailure( - castErrorToFailure(e, { - name: t('stdcm:stdcmError'), - message: t('translation:common.error'), - }) - ) - ); - }); - }); - }) - .catch((e) => { - setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.rejected); - dispatch(setFailure(castErrorToFailure(e, { name: t('stdcm:stdcmError') }))); - }); - } - }; - - const launchStdcmRequestV2 = async () => { - const validConfig = checkStdcmConf(dispatch, t, osrdconf as OsrdStdcmConfState); - if (validConfig) { - const payload = formatStdcmPayload(validConfig); - try { - const response = await postV2TimetableByIdStdcm(payload).unwrap(); - if (response.status === 'success') { - setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.success); - setStdcmV2Results(response); - } else { - setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.rejected); - dispatch( - setFailure({ - name: t('stdcm:stdcmError'), - message: t('translation:common.error'), - }) - ); - } - } catch (e) { - setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.rejected); - dispatch(setFailure(castErrorToFailure(e, { name: t('stdcm:stdcmError') }))); - } - } - }; - - if (currentStdcmRequestStatus === STDCM_REQUEST_STATUS.pending) { - if (trainScheduleV2Activated) { - launchStdcmRequestV2(); - } else { - launchStdcmRequest(); - } - } - }, [currentStdcmRequestStatus]); - - const cancelStdcmRequest = () => { - // when http ready https://axios-http.com/docs/cancellation - - controller.abort(); - setCurrentStdcmRequestStatus(STDCM_REQUEST_STATUS.canceled); - - const emptySimulation = { trains: [] }; - const consolidatedSimulation = createTrain(CHART_AXES.SPACE_TIME, emptySimulation.trains); - dispatch(updateConsolidatedSimulation(consolidatedSimulation)); - dispatch(updateSimulation(emptySimulation)); - }; +const StdcmRequestModal = ({ isOpen, cancelStdcmRequest }: StdcmRequestModalProps) => { + const { t } = useTranslation('stdcm'); return (
-

{t('stdcm:stdcmComputation')}

+

{t('stdcmComputation')}

- {currentStdcmRequestStatus === STDCM_REQUEST_STATUS.pending && ( -
- {t('stdcm:pleaseWait')} - -
- )} +
+ {t('pleaseWait')} + +
+ {t('simulation.pendingSimulation')} +

{t('loaderImageLegend')}

+
+ ); + } +); + +export default StdcmLoader; diff --git a/front/src/applications/stdcmV2/components/StdcmOperationalPoint.tsx b/front/src/applications/stdcmV2/components/StdcmOperationalPoint.tsx new file mode 100644 index 00000000000..e03d2dfa4e3 --- /dev/null +++ b/front/src/applications/stdcmV2/components/StdcmOperationalPoint.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useMemo } from 'react'; + +import type { ActionCreatorWithPayload } from '@reduxjs/toolkit'; +import { useTranslation } from 'react-i18next'; +import nextId from 'react-id-generator'; + +import type { SearchResultItemOperationalPoint } from 'common/api/osrdEditoastApi'; +import SelectSNCF from 'common/BootstrapSNCF/SelectSNCF'; +import useSearchOperationalPoint, { + MAIN_OP_CH_CODES, +} from 'common/Map/Search/useSearchOperationalPoint'; +import type { PathStep } from 'reducers/osrdconf/types'; +import { useAppDispatch } from 'store'; + +import StdcmSuggestions from './StdcmSuggestions'; + +type UpdatePointActions = + | ActionCreatorWithPayload + | ActionCreatorWithPayload; + +type StdcmOperationalPointProps = { + updatePoint: UpdatePointActions; + point: PathStep | null; + disabled?: boolean; +}; + +function formatChCode(chCode: string) { + return MAIN_OP_CH_CODES.includes(chCode) ? 'BV' : chCode; +} + +const StdcmOperationalPoint = ({ updatePoint, point, disabled }: StdcmOperationalPointProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation('stdcm'); + + const { + searchTerm, + chCodeFilter, + sortedSearchResults, + filteredAndSortedSearchResults, + setSearchTerm, + setChCodeFilter, + } = useSearchOperationalPoint({ initialSearchTerm: point?.name, initialChCodeFilter: point?.ch }); + + const operationalPointsSuggestions = useMemo( + () => + sortedSearchResults.reduce( + (acc, p) => { + const newObject = { + label: [p.trigram, p.name].join(' '), + value: p.name, + uic: p.uic, + }; + const isDuplicate = acc.some((pr) => pr.label === newObject.label); + if (!isDuplicate) acc.push(newObject); + return acc; + }, + [] as { label: string; value: string; uic: number }[] + ), + [sortedSearchResults] + ); + + const sortedChOptions = useMemo( + () => + sortedSearchResults.reduce( + (acc, pr) => { + const newObject = { + label: formatChCode(pr.ch), + id: pr.ch, + }; + const isDuplicate = acc.some((option) => option.label === newObject.label); + if (!isDuplicate) acc.push(newObject); + return acc; + }, + [] as { label: string; id: string }[] + ), + [point, sortedSearchResults] + ); + + const dispatchNewPoint = (p?: SearchResultItemOperationalPoint) => { + const newPoint = p + ? { + name: p.name, + ch: p.ch, + id: nextId(), + uic: p.uic, + coordinates: p.geographic.coordinates, + } + : null; + dispatch(updatePoint(newPoint)); + }; + + const onInputChange = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); + if (e.target.value.trim().length === 0) { + dispatchNewPoint(undefined); + } + }; + + const onInputOnblur = () => { + const newPoint = + operationalPointsSuggestions.length === 1 + ? filteredAndSortedSearchResults.find( + (pr) => pr.name === operationalPointsSuggestions[0].value + ) + : undefined; + dispatchNewPoint(newPoint); + if (newPoint === undefined) { + setSearchTerm(''); + setChCodeFilter(undefined); + } + }; + + useEffect(() => { + if (point) { + setSearchTerm(point.name || ''); + setChCodeFilter(point.ch || ''); + } else { + setSearchTerm(''); + setChCodeFilter(undefined); + } + }, [point]); + + const updateSelectedPoint = ( + refList: SearchResultItemOperationalPoint[], + selectedUic: number, + selectedChCode?: string + ) => { + const newPoint = refList.find( + (pr) => pr.uic === selectedUic && (selectedChCode ? pr.ch === selectedChCode : true) + ); + dispatchNewPoint(newPoint); + }; + + const onSelectSuggestion = ({ value: suggestionName, uic }: { value: string; uic: number }) => { + setSearchTerm(suggestionName); + updateSelectedPoint(sortedSearchResults, uic); + }; + + const onSelectChCodeFilter = (selectedChCode?: { id: string }) => { + setChCodeFilter(selectedChCode?.id); + if (point && 'uic' in point) + updateSelectedPoint(sortedSearchResults, point.uic, selectedChCode?.id); + }; + + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default StdcmOperationalPoint; diff --git a/front/src/applications/stdcmV2/components/StdcmOrigin.tsx b/front/src/applications/stdcmV2/components/StdcmOrigin.tsx new file mode 100644 index 00000000000..3a3b5bee2cb --- /dev/null +++ b/front/src/applications/stdcmV2/components/StdcmOrigin.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import InputSNCF from 'common/BootstrapSNCF/InputSNCF'; +import { useOsrdConfSelectors, useOsrdConfActions } from 'common/osrdContext'; +import type { StdcmConfSliceActions } from 'reducers/osrdconf/stdcmConf'; +import { useAppDispatch } from 'store'; + +import StdcmCard from './StdcmCard'; +import StdcmOperationalPoint from './StdcmOperationalPoint'; + +const StdcmOrigin = ({ disabled = false }: { disabled?: boolean }) => { + const { t } = useTranslation('stdcm'); + const { getOriginV2, getOriginDate, getOriginTime } = useOsrdConfSelectors(); + const { updateOriginV2, updateOriginDate, updateOriginTime } = + useOsrdConfActions() as StdcmConfSliceActions; + const origin = useSelector(getOriginV2); + const originDate = useSelector(getOriginDate); + const originTime = useSelector(getOriginTime); + const dispatch = useAppDispatch(); + return ( + +
+ +
+
+ dispatch(updateOriginDate(e.target.value))} + value={originDate} + disabled={disabled} + /> +
+
+ ) => + dispatch(updateOriginTime(e.target.value)) + } + value={originTime} + disabled={disabled} + /> +
+
+
+
+ ); +}; + +export default StdcmOrigin; diff --git a/front/src/applications/stdcmV2/components/StdcmSuggestions.tsx b/front/src/applications/stdcmV2/components/StdcmSuggestions.tsx new file mode 100644 index 00000000000..cf1c23a728b --- /dev/null +++ b/front/src/applications/stdcmV2/components/StdcmSuggestions.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import { Input, type InputProps } from '@osrd-project/ui-core'; +import { isEmpty } from 'lodash'; + +import SelectImprovedSNCF, { + type SelectOptionObject, +} from 'common/BootstrapSNCF/SelectImprovedSNCF'; + +export interface StdcmSuggestionsProps extends InputProps { + options: T[]; + onSelectSuggestion: (option: T) => void; +} + +const StdcmSuggestions = ({ + options, + onSelectSuggestion, + onFocus, + onBlur, + disabled, + ...rest +}: StdcmSuggestionsProps) => { + const [isSelectVisible, setIsSelectVisible] = useState(false); + + const [isFocused, setIsFocused] = useState(false); + + const containerRef = useRef(null); + + useEffect(() => { + if (isFocused) { + setIsSelectVisible(!isEmpty(options)); + } + }, [options, isFocused]); + + return ( + <> + { + setIsFocused(true); + onFocus?.(e); + }} + onBlur={(e) => { + setIsFocused(false); + + // this will allow us to avoid the onBlur event when clicking on a suggestion + // TODO: find a better way to do this when ui-core suggestion component is implemented + const avoidOnBlur = containerRef.current?.contains(e.relatedTarget); + if (!avoidOnBlur && onBlur) { + setIsSelectVisible(false); + onBlur(e); + } + }} + disabled={disabled} + /> + {isSelectVisible && ( +
+ { + onSelectSuggestion(option); + setIsSelectVisible(false); + }} + setSelectVisibility={setIsSelectVisible} + withSearch={false} + disabled={disabled} + noTogglingHeader + isOpened + bgWhite + disableShadow + /> +
+ )} + + ); +}; + +export default StdcmSuggestions; diff --git a/front/src/applications/stdcmV2/views/StdcmViewV2.tsx b/front/src/applications/stdcmV2/views/StdcmViewV2.tsx new file mode 100644 index 00000000000..ac6107b8002 --- /dev/null +++ b/front/src/applications/stdcmV2/views/StdcmViewV2.tsx @@ -0,0 +1,96 @@ +import React, { useRef, useEffect } from 'react'; + +import { Button } from '@osrd-project/ui-core'; +// import { Location, ArrowUp, ArrowDown } from '@osrd-project/ui-icons'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import STDCM_REQUEST_STATUS from 'applications/stdcm/consts'; +import useStdcm from 'applications/stdcm/hooks/useStdcm'; +import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; +import ScenarioExplorer from 'modules/scenario/components/ScenarioExplorer'; +import { Map } from 'modules/trainschedule/components/ManageTrainSchedule'; +import type { StdcmConfSliceActions } from 'reducers/osrdconf/stdcmConf'; +import { useAppDispatch } from 'store'; + +import StdcmConsist from '../components/StdcmConsist'; +// import StdcmDefaultCard from '../components/StdcmDefaultCard'; +import StdcmDestination from '../components/StdcmDestination'; +import StdcmHeader from '../components/StdcmHeader'; +import StdcmLoader from '../components/StdcmLoader'; +import StdcmOrigin from '../components/StdcmOrigin'; + +const StdcmViewV2 = () => { + const { getProjectID, getScenarioID, getStudyID } = useOsrdConfSelectors(); + const studyID = useSelector(getStudyID); + const projectID = useSelector(getProjectID); + const scenarioID = useSelector(getScenarioID); + const { launchStdcmRequest, cancelStdcmRequest, currentStdcmRequestStatus } = useStdcm(); + const isPending = currentStdcmRequestStatus === STDCM_REQUEST_STATUS.pending; + const loaderRef = useRef(null); + const { t } = useTranslation('stdcm'); + + const dispatch = useAppDispatch(); + const { updateGridMarginAfter, updateGridMarginBefore, updateStdcmStandardAllowance } = + useOsrdConfActions() as StdcmConfSliceActions; + + useEffect(() => { + if (isPending) { + loaderRef?.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [isPending]); + + useEffect(() => { + dispatch(updateGridMarginAfter(35)); + dispatch(updateGridMarginBefore(35)); + dispatch(updateStdcmStandardAllowance({ type: 'time_per_distance', value: 4.5 })); + }, []); + + return ( +
+ +
+
+
+
+ +
+ {scenarioID && } +
+ {scenarioID && ( + <> +
+
+ {/* //TODO: rename StdcmDefaultCard */} + {/* } /> */} + + {/* } /> */} + + {/* } /> */} +
+ + )} +
+ {scenarioID && ( +
+ +
+ )} +
+
+
+ ); +}; + +export default StdcmViewV2; diff --git a/front/src/assets/pictures/views/stdcm_v2_loader.jpg b/front/src/assets/pictures/views/stdcm_v2_loader.jpg new file mode 100644 index 00000000000..13aed63a1f6 Binary files /dev/null and b/front/src/assets/pictures/views/stdcm_v2_loader.jpg differ diff --git a/front/src/common/BootstrapSNCF/NavBarSNCF.scss b/front/src/common/BootstrapSNCF/NavBarSNCF.scss new file mode 100644 index 00000000000..06e9474bed3 --- /dev/null +++ b/front/src/common/BootstrapSNCF/NavBarSNCF.scss @@ -0,0 +1,5 @@ +.user-settings-btn:disabled { + cursor: not-allowed; + color: #c0c0c0 !important; + pointer-events: auto; +} diff --git a/front/src/common/BootstrapSNCF/NavBarSNCF.tsx b/front/src/common/BootstrapSNCF/NavBarSNCF.tsx index ae88c538fb6..70325bf7b63 100644 --- a/front/src/common/BootstrapSNCF/NavBarSNCF.tsx +++ b/front/src/common/BootstrapSNCF/NavBarSNCF.tsx @@ -5,7 +5,7 @@ import getUnicodeFlagIcon from 'country-flag-icons/unicode'; import i18n from 'i18next'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import logoOSRD from 'assets/fav-osrd-color.svg'; import ChangeLanguageModal from 'common/ChangeLanguageModal'; @@ -28,6 +28,7 @@ export default function LegacyNavBarSNCF({ appName, logo = logoOSRD }: Props) { const safeWord = useSelector(getUserSafeWord); const { t } = useTranslation('home/navbar'); const { logout, username } = useAuth(); + const { pathname } = useLocation(); return (
@@ -91,9 +92,11 @@ export default function LegacyNavBarSNCF({ appName, logo = logoOSRD }: Props) { , @@ -134,7 +139,10 @@ function SelectImproved({ className={cx('input-group', { 'input-group-sm': sm })} tabIndex={0} role="button" - onClick={() => setIsOpen(!isOpen)} + onClick={() => { + if (disabled) return; + setIsOpen(!isOpen); + }} >

({ type="button" aria-expanded="false" aria-controls="selecttoggle" + disabled={disabled} > ({ className="form-control form-control-sm clear-option" onChange={(e) => setFilterText(e.target.value)} value={filterText} + disabled={disabled} /> @@ -186,6 +196,7 @@ function SelectImproved({ className="btn-clear btn-primary" style={{ width: '2em', height: '2em' }} onClick={() => setFilterText('')} + disabled={disabled} > Clear text