From 3226fbbcb9530a439982f5309f7a5263dc7e1be4 Mon Sep 17 00:00:00 2001 From: Anselme Date: Wed, 29 Jan 2025 07:39:08 +0100 Subject: [PATCH] =?UTF-8?q?feat(api,admin):=2057=20-=20Basculer=20des=20je?= =?UTF-8?q?unes=20valid=C3=A9s=20sur=20le=20prochain=20s=C3=A9jour=20?= =?UTF-8?q?=C3=A9ligible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operations/actions/ActionsSubTab.tsx | 2 +- .../AffectationHTSSimulationMetropole.tsx | 2 +- .../BasculeJeuneValides.tsx | 54 +++++ .../BasculeJeuneValidesModal.tsx | 189 +++++++++++++++++ .../actions/InscriptionsSection.tsx | 12 +- .../simulations/SimulationsSubTab.tsx | 6 + .../SimulationHtsResultCell.tsx | 4 +- .../BasculeJeuneValidesCell.tsx | 19 ++ .../BasculeJeuneValidesStartButton.tsx | 198 ++++++++++++++++++ admin/src/services/inscriptionService.ts | 38 ++++ apiv2/src/admin/Admin.module.ts | 4 + apiv2/src/admin/AdminJob.module.ts | 8 + .../admin/core/sejours/jeune/Jeune.gateway.ts | 7 + .../phase1/inscription/Inscription.service.ts | 75 +++++++ .../SimulationBasculeJeunesValides.ts | 180 ++++++++++++++++ ...imulationBasculeJeunesValidesTask.model.ts | 59 ++++++ .../sejours/phase1/session/Session.gateway.ts | 1 + .../repository/mongo/JeuneMongo.repository.ts | 17 ++ .../sejours/phase1/api/Phase1.controller.ts | 9 +- .../inscription/api/Inscription.controller.ts | 122 +++++++++++ .../inscription/api/Inscription.validation.ts | 47 +++++ .../mongo/SessionMongo.repository.ts | 7 + .../admin/infra/task/AdminTask.consumer.ts | 6 + .../AdminTaskInscriptionSelectorService.ts | 34 +++ packages/ds/src/common/index.ts | 1 + packages/ds/src/common/inputs/Checkbox.tsx | 44 ++++ packages/ds/src/common/inputs/Input.tsx | 2 +- packages/lib/src/constants/task.ts | 2 + packages/lib/src/dto/phase1/index.ts | 1 + .../SimulationBasculerJeunesValidesTaskDto.ts | 24 +++ .../ValiderBasculerJeunesValidesTaskDto.ts | 14 ++ .../lib/src/dto/phase1/inscription/index.ts | 2 + packages/lib/src/routes/phase1/index.ts | 1 + .../getBasculerJeunesValidesRoute.ts | 16 ++ .../src/routes/phase1/inscription/index.ts | 9 + .../postBasculerJeunesValidesRoute.ts | 22 ++ .../postValiderBasculerJeunesValidesRoute.ts | 10 + packages/lib/src/translation.ts | 3 + 38 files changed, 1241 insertions(+), 10 deletions(-) create mode 100644 admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValides.tsx create mode 100644 admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValidesModal.tsx create mode 100644 admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesCell.tsx create mode 100644 admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesStartButton.tsx create mode 100644 admin/src/services/inscriptionService.ts create mode 100644 apiv2/src/admin/core/sejours/phase1/inscription/Inscription.service.ts create mode 100644 apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValides.ts create mode 100644 apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValidesTask.model.ts create mode 100644 apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.controller.ts create mode 100644 apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.validation.ts create mode 100644 apiv2/src/admin/infra/task/AdminTaskInscriptionSelectorService.ts create mode 100644 packages/ds/src/common/inputs/Checkbox.tsx create mode 100644 packages/lib/src/dto/phase1/inscription/SimulationBasculerJeunesValidesTaskDto.ts create mode 100644 packages/lib/src/dto/phase1/inscription/ValiderBasculerJeunesValidesTaskDto.ts create mode 100644 packages/lib/src/dto/phase1/inscription/index.ts create mode 100644 packages/lib/src/routes/phase1/inscription/getBasculerJeunesValidesRoute.ts create mode 100644 packages/lib/src/routes/phase1/inscription/index.ts create mode 100644 packages/lib/src/routes/phase1/inscription/postBasculerJeunesValidesRoute.ts create mode 100644 packages/lib/src/routes/phase1/inscription/postValiderBasculerJeunesValidesRoute.ts diff --git a/admin/src/scenes/settings/operations/actions/ActionsSubTab.tsx b/admin/src/scenes/settings/operations/actions/ActionsSubTab.tsx index c95ab480ae..df32842b09 100644 --- a/admin/src/scenes/settings/operations/actions/ActionsSubTab.tsx +++ b/admin/src/scenes/settings/operations/actions/ActionsSubTab.tsx @@ -13,7 +13,7 @@ interface ActionsSubTabProps { export default function ActionsSubTab({ session }: ActionsSubTabProps) { return (
- +
diff --git a/admin/src/scenes/settings/operations/actions/AffectationSimulation/AffectationHTSSimulationMetropole.tsx b/admin/src/scenes/settings/operations/actions/AffectationSimulation/AffectationHTSSimulationMetropole.tsx index 50f1d296b1..5f7eae9327 100644 --- a/admin/src/scenes/settings/operations/actions/AffectationSimulation/AffectationHTSSimulationMetropole.tsx +++ b/admin/src/scenes/settings/operations/actions/AffectationSimulation/AffectationHTSSimulationMetropole.tsx @@ -28,7 +28,7 @@ export default function AffectationHTSSimulationMetropole({ session }: Affectati queryFn: async () => AffectationService.getAffectation(session._id!, "HTS"), }); - const isValidSession = session.type === "VOLONTAIRE"; + const isValidSession = session.type === "VOLONTAIRE"; // HTS const isInProgress = affectationStatus && [TaskStatus.IN_PROGRESS, TaskStatus.PENDING].includes(affectationStatus?.simulation?.status as TaskStatus); return ( diff --git a/admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValides.tsx b/admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValides.tsx new file mode 100644 index 0000000000..85ba11dca0 --- /dev/null +++ b/admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValides.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { useHistory } from "react-router-dom"; +import { useToggle } from "react-use"; +import { HiOutlineInformationCircle } from "react-icons/hi"; + +import { CohortDto, InscriptionRoutes, TaskName, TaskStatus } from "snu-lib"; +import { Button, Tooltip } from "@snu/ds/admin"; + +import BasculeJeuneValidesModal from "./BasculeJeuneValidesModal"; +import { InscriptionService } from "@/services/inscriptionService"; +import { useQuery } from "@tanstack/react-query"; + +interface BasculeJeuneValidesProps { + session: CohortDto; +} + +export default function BasculeJeuneValides({ session }: BasculeJeuneValidesProps) { + const history = useHistory(); + + const [showModal, toggleModal] = useToggle(false); + + const { + isPending: isLoading, + isError, + data: inscriptionStatus, + } = useQuery({ + queryKey: ["inscription", "bacule-jeunes-valides", session._id], // check SimulationHtsResultStartButton.tsx and AffectationSimulationMetropoleModal.tsx queryKey + queryFn: async () => InscriptionService.getBasculerJeunesValides(session._id!), + }); + + const isValidSession = session.type === "VOLONTAIRE"; // HTS + const isInProgress = inscriptionStatus && [TaskStatus.IN_PROGRESS, TaskStatus.PENDING].includes(inscriptionStatus?.simulation?.status as TaskStatus); + + return ( +
+
+
Bascule des jeunes validés
+ + + + {isInProgress &&
Simulation en cours...
} +
+
+
+ {showModal && } +
+ ); +} diff --git a/admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValidesModal.tsx b/admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValidesModal.tsx new file mode 100644 index 0000000000..807e7a5b92 --- /dev/null +++ b/admin/src/scenes/settings/operations/actions/BasculeJeuneValides/BasculeJeuneValidesModal.tsx @@ -0,0 +1,189 @@ +import React from "react"; + +import { HiOutlineLightningBolt } from "react-icons/hi"; + +import { + CohortDto, + DEPART_SEJOUR_MOTIFS, + formatDepartement, + GRADES, + Phase1Routes, + region2department, + RegionsMetropole, + translate, + YOUNG_STATUS, + YOUNG_STATUS_PHASE1, + YoungDto, +} from "snu-lib"; +import { Button, CollapsableSelectSwitcher, Modal, SectionSwitcher } from "@snu/ds/admin"; +import { useSetState, useUpdateEffect } from "react-use"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toastr } from "react-redux-toastr"; +import { capture } from "@/sentry"; +import { InscriptionService } from "@/services/inscriptionService"; + +interface BasculeJeuneValidesMetropoleProps { + session: CohortDto; + onClose: () => void; +} + +export default function BasculeJeuneValidesMetropoleModal({ session, onClose }: BasculeJeuneValidesMetropoleProps) { + const queryClient = useQueryClient(); + + const [state, setState] = useSetState<{ + status: YoungDto["status"][]; + statusPhase1: YoungDto["statusPhase1"][]; + statusPhase1Motif: YoungDto["statusPhase1Motif"][]; + cohesionStayPresence: boolean; + niveauScolaires: string[]; + departements: Record; + etranger: boolean; + avenir: boolean; + }>({ + status: [YOUNG_STATUS.WAITING_VALIDATION, YOUNG_STATUS.VALIDATED], + statusPhase1: [YOUNG_STATUS_PHASE1.NOT_DONE], + statusPhase1Motif: [], + cohesionStayPresence: false, + niveauScolaires: session.eligibility?.schoolLevels?.filter((level: any) => Object.values(GRADES).includes(level)) || Object.values(GRADES), + departements: RegionsMetropole.reduce((acc, region) => { + acc[region] = region2department[region].filter((departement) => !session.eligibility?.zones || session.eligibility.zones.includes(departement)); + return acc; + }, {}), + etranger: true, + avenir: false, + }); + + const isReady = state.niveauScolaires.length > 0 && Object.values(state.departements).some((departements) => departements.length > 0); + + useUpdateEffect(() => { + if (!state.status.includes(YOUNG_STATUS.VALIDATED)) { + setState({ statusPhase1: [], statusPhase1Motif: [], cohesionStayPresence: false }); + } + }, [state.status]); + + const { isPending, mutate } = useMutation({ + mutationFn: async () => { + return await InscriptionService.postBasculerJeunesValides(session._id!, { + status: state.status, + statusPhase1: state.statusPhase1, + statusPhase1Motif: state.statusPhase1Motif, + cohesionStayPresence: state.cohesionStayPresence, + departements: Object.keys(state.departements).reduce((acc, region) => [...acc, ...state.departements[region]], []), + niveauScolaires: state.niveauScolaires as any, + etranger: state.etranger, + avenir: state.avenir, + }); + }, + onSuccess: (task) => { + toastr.success("Le traitement a bien été ajouté", "", { timeOut: 5000 }); + const queryKey = ["inscription", "bacule-jeunes-valides", session._id]; + const oldStatus = queryClient.getQueryData(queryKey) || []; + queryClient.setQueryData(queryKey, { ...oldStatus, simulation: { status: task.status } }); + onClose(); + }, + onError: (error: any) => { + capture(error); + toastr.error("Une erreur est survenue lors de l'ajout du traitement", translate(JSON.parse(error.message).message), { timeOut: 5000 }); + }, + }); + + return ( + +
+
+
+ +
+
+

Bascule des jeunes validés

+
+
+
+

Statuts de phase

+ ({ label: translate(statut), value: statut }))} + values={state.status as string[]} + onChange={(values) => setState({ status: values as string[] })} + isOpen={false} + /> + {state.status.includes(YOUNG_STATUS.VALIDATED) && ( + ({ + label: translate(statut), + value: statut, + }))} + values={state.statusPhase1 as string[]} + onChange={(values) => setState({ statusPhase1: values as string[] })} + isOpen={false} + /> + )} +
+ {state.status.includes(YOUNG_STATUS.VALIDATED) && ( + <> +
+

Arrivées et départs

+ setState({ cohesionStayPresence })} + className="py-2.5" + /> + ({ label: translate(motif), value: motif }))} + values={state.statusPhase1Motif as string[]} + onChange={(values) => setState({ statusPhase1Motif: values as string[] })} + isOpen={false} + /> +
+
+

Situations scolaires

+ ({ label: translate(grade), value: grade }))} + values={state.niveauScolaires} + onChange={(values) => setState({ niveauScolaires: values as string[] })} + isOpen={false} + /> +
+ + )} +
+

Départements de résidence

+
+ {RegionsMetropole.map((region) => ( + ({ label: formatDepartement(department), value: department }))} + values={state.departements[region]} + onChange={(values) => setState({ departements: { ...state.departements, [region]: values } })} + isOpen={false} + /> + ))} + setState({ etranger })} className="py-2.5" /> +
+
+
+

Cohorte à venir

+ setState({ avenir })} className="py-2.5" /> +
+
+ + } + footer={ +
+
+ } + /> + ); +} diff --git a/admin/src/scenes/settings/operations/actions/InscriptionsSection.tsx b/admin/src/scenes/settings/operations/actions/InscriptionsSection.tsx index 7af6485f9d..c674185639 100644 --- a/admin/src/scenes/settings/operations/actions/InscriptionsSection.tsx +++ b/admin/src/scenes/settings/operations/actions/InscriptionsSection.tsx @@ -1,10 +1,16 @@ import React from "react"; import { Collapsable } from "@snu/ds/admin"; +import { CohortDto } from "snu-lib"; +import BasculeJeuneValides from "./BasculeJeuneValides/BasculeJeuneValides"; -export default function InscriptionsSection({ sessionId }) { +interface InscriptionsSectionProps { + session: CohortDto; +} + +export default function InscriptionsSection({ session }: InscriptionsSectionProps) { return ( - - à compléter + + ); } diff --git a/admin/src/scenes/settings/operations/simulations/SimulationsSubTab.tsx b/admin/src/scenes/settings/operations/simulations/SimulationsSubTab.tsx index 51402c24e5..85b113bf79 100644 --- a/admin/src/scenes/settings/operations/simulations/SimulationsSubTab.tsx +++ b/admin/src/scenes/settings/operations/simulations/SimulationsSubTab.tsx @@ -15,6 +15,8 @@ import SimulationHtsResultCell from "./affectationHts/SimulationHtsResultCell"; import SimulationHtsResultStartButton from "./affectationHts/SimulationHtsResultStartButton"; import SimulationCleResultCell from "./affectationCle/SimulationCleResultCell"; import SimulationCleResultStartButton from "./affectationCle/SimulationCleResultStartButton"; +import BasculeJeuneValidesCell from "./basculeJeuneValides/BasculeJeuneValidesCell"; +import BasculeJeuneValidesStartButton from "./basculeJeuneValides/BasculeJeuneValidesStartButton"; interface SimulationsSubTabProps { session: CohortDto; @@ -90,6 +92,8 @@ export default function SimulationsSubTab({ session }: SimulationsSubTabProps) { return ; } else if (simulation.name === TaskName.AFFECTATION_CLE_SIMULATION) { return ; + } else if (simulation.name === TaskName.BACULE_JEUNES_VALIDES_SIMULATION) { + return ; } return null; }, @@ -113,6 +117,8 @@ export default function SimulationsSubTab({ session }: SimulationsSubTabProps) { return ; } else if (simulation.name === TaskName.AFFECTATION_CLE_SIMULATION) { return ; + } else if (simulation.name === TaskName.BACULE_JEUNES_VALIDES_SIMULATION) { + return ; } return ; }, diff --git a/admin/src/scenes/settings/operations/simulations/affectationHts/SimulationHtsResultCell.tsx b/admin/src/scenes/settings/operations/simulations/affectationHts/SimulationHtsResultCell.tsx index c348f18002..172d4c536d 100644 --- a/admin/src/scenes/settings/operations/simulations/affectationHts/SimulationHtsResultCell.tsx +++ b/admin/src/scenes/settings/operations/simulations/affectationHts/SimulationHtsResultCell.tsx @@ -17,8 +17,8 @@ export default function SimulationHtsResultCell({ simulation }: SimulationHtsRes return (
-
Affectés : {simulationHts.metadata?.results?.jeunesNouvellementAffected ?? "--"}
-
En attente : {simulationHts.metadata?.results?.jeuneAttenteAffectation ?? "--"}
+
Affectés : {simulationHts.metadata?.results?.jeunesNouvellementAffected ?? "--"}
+
En attente : {simulationHts.metadata?.results?.jeuneAttenteAffectation ?? "--"}
Performance : {simulationHts.metadata?.results?.selectedCost ? formatPourcentage(1 - simulationHts.metadata.results.selectedCost) : "--"}
diff --git a/admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesCell.tsx b/admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesCell.tsx new file mode 100644 index 0000000000..2a82ed7b5b --- /dev/null +++ b/admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesCell.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import { SimulationBasculerJeunesValidesTaskDto } from "snu-lib"; + +interface BasculeJeuneValidesCellProps { + simulation: unknown; +} + +export default function BasculeJeuneValidesCell({ simulation }: BasculeJeuneValidesCellProps) { + const simulationHts = simulation as SimulationBasculerJeunesValidesTaskDto; + + return ( +
+
Prochain séjour : {simulationHts.metadata?.results?.jeunesProchainSejour ?? "--"}
+
À venir : {simulationHts.metadata?.results?.jeunesAvenir ?? "--"}
+
Refusés : {simulationHts.metadata?.results?.jeunesRefuses ?? "--"}
+
+ ); +} diff --git a/admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesStartButton.tsx b/admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesStartButton.tsx new file mode 100644 index 0000000000..9d87752097 --- /dev/null +++ b/admin/src/scenes/settings/operations/simulations/basculeJeuneValides/BasculeJeuneValidesStartButton.tsx @@ -0,0 +1,198 @@ +import React, { useMemo } from "react"; +import { useToggle } from "react-use"; +import { toastr } from "react-redux-toastr"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import cx from "classnames"; +import { HiOutlineLightningBolt, HiPlay } from "react-icons/hi"; + +import { formatDepartement, InscriptionRoutes, region2department, RegionsMetropole, SimulationBasculerJeunesValidesTaskDto, TaskStatus, translate, YOUNG_STATUS } from "snu-lib"; +import { Button, Modal } from "@snu/ds/admin"; + +import { capture } from "@/sentry"; +import { isBefore } from "date-fns"; +import { Link } from "react-router-dom"; +import { InscriptionService } from "@/services/inscriptionService"; +import { Checkbox } from "@snu/ds"; + +interface BasculeJeuneValidesStartButtonProps { + simulation: unknown; +} + +export default function BasculeJeuneValidesStartButton({ simulation }: BasculeJeuneValidesStartButtonProps) { + const simulationBascule = simulation as SimulationBasculerJeunesValidesTaskDto; + const queryClient = useQueryClient(); + + const [sendEmail, toggleSentEmail] = useToggle(true); + const [showModal, toggleModal] = useToggle(false); + + const regions = useMemo( + () => + RegionsMetropole.reduce((acc, region) => { + acc[region] = simulationBascule.metadata?.parameters?.departements?.filter((dep) => region2department[region].includes(dep)) || []; + return acc; + }, {}), + [simulationBascule], + ); + + const affectationKey = ["inscription", "bacule-jeunes-valides", simulationBascule.metadata!.parameters!.sessionId]; // check BasculeJeuneValidesModal.tsx queryKey + const { + isPending: isLoading, + isError, + data: affectationStatus, + } = useQuery({ + queryKey: affectationKey, + queryFn: async () => InscriptionService.getBasculerJeunesValides(simulationBascule.metadata!.parameters!.sessionId), + }); + + const isOutdated = + [TaskStatus.IN_PROGRESS, TaskStatus.PENDING].includes(affectationStatus?.traitement.status as TaskStatus) || + (!!affectationStatus?.traitement?.lastCompletedAt && isBefore(new Date(simulationBascule.createdAt), new Date(affectationStatus.traitement.lastCompletedAt))); + + const isDisabled = simulationBascule.status !== TaskStatus.COMPLETED || isLoading || isError || isOutdated; + + const totalJeunes = + (simulationBascule.metadata?.results?.jeunesProchainSejour || 0) + + (simulationBascule.metadata?.results?.jeunesAvenir || 0) + + (simulationBascule.metadata?.results?.jeunesRefuses || 0); + + const { isPending, mutate } = useMutation({ + mutationFn: async () => { + return await InscriptionService.postValiderBasculerJeunesValides(simulationBascule.metadata!.parameters!.sessionId!, simulationBascule.id); + }, + onSuccess: () => { + toastr.success("Le traitement a bien été ajouté", "", { timeOut: 5000 }); + queryClient.invalidateQueries({ queryKey: affectationKey }); + toggleModal(false); + }, + onError: (error: any) => { + capture(error); + toastr.error("Une erreur est survenue lors de l'ajout du traitement", translate(JSON.parse(error.message).message), { timeOut: 5000 }); + }, + }); + + return ( + <> + + +
+
+
+ +
+
+

Bascule des jeunes validés

+

Vérifier les paramètres avant de lancer ce traitement.

+
+
+
+

Suivi

+
Sur un séjour : {simulationBascule.metadata?.results?.jeunesProchainSejour ?? "--"}
+
Sur le séjour à venir : {simulationBascule.metadata?.results?.jeunesAvenir ?? "--"}
+
Refusés : {simulationBascule.metadata?.results?.jeunesRefuses ?? "--"}
+
+
+

Statuts de phase

+
+
Phase 0 :
+
{simulationBascule.metadata?.parameters?.status?.map((status) => translate(status)).join(", ") || "Aucun"}
+
+
+
Phase 1 :
+
+ {simulationBascule.metadata?.parameters?.statusPhase1?.map((status) => translate(status)).join(", ") || "Aucun"} +
+
+
+
+

Arrivées et départs

+
+
Présence à l'arrivée :
+
{simulationBascule.metadata?.parameters?.cohesionStayPresence ? "oui" : "non"}
+
+
+
Motif de départ :
+
+ {simulationBascule.metadata?.parameters?.statusPhase1Motif?.map((motif) => translate(motif)).join(", ") || "Aucun"} +
+
+
+
+

Situations scolaires

+
+
Niveaux :
+
+ {simulationBascule.metadata?.parameters?.niveauScolaires?.map((niveau) => translate(niveau)).join(", ") || "Aucun"} +
+
+
+
+

Départements de résidence

+
+ {RegionsMetropole.map((region) => ( +
+
{region} :
+
{regions[region].map(formatDepartement).join(", ") || "Aucun"}
+
+ ))} +
+
+
Etranger :
+
{simulationBascule.metadata?.parameters?.etranger ? "Oui" : "Non"}
+
+
+
+

Cohorte à venir

+
+
Basculer tous les jeunes vers la cohorte à venir:
+
{simulationBascule.metadata?.parameters?.avenir ? "Oui" : "Non"}
+
+
+
+ +
+ Envoyer une campagne d'emailing aux volontaires ({totalJeunes}) et à leurs représentants légaux. +
+ Jeune {translate(YOUNG_STATUS.VALIDATED)} : + + Visualiser l'aperçu du template 1633 + {" "} + {simulationBascule.metadata?.results?.jeunesRefuses && ( + + Visualiser l'aperçu du template 1632 + + )} +
+
+ Jeune {translate(YOUNG_STATUS.WAITING_LIST)} : + + Visualiser l'aperçu du template 1265 + {" "} + {simulationBascule.metadata?.results?.jeunesRefuses && ( + + Visualiser l'aperçu du template 1264 + + )} +
+
+
+
+
+ } + footer={ +
+
+ } + /> + + ); +} diff --git a/admin/src/services/inscriptionService.ts b/admin/src/services/inscriptionService.ts new file mode 100644 index 0000000000..1861e853cf --- /dev/null +++ b/admin/src/services/inscriptionService.ts @@ -0,0 +1,38 @@ +import { InscriptionRoutes } from "snu-lib"; + +import { buildRequest } from "@/utils/buildRequest"; + +const InscriptionService = { + getBasculerJeunesValides: async (sessionId: string) => { + return await buildRequest({ + path: "/inscription/{sessionId}/bacule-jeunes-valides/status", + method: "GET", + params: { sessionId }, + target: "API_V2", + })(); + }, + + postBasculerJeunesValides: async ( + sessionId: string, + { status, statusPhase1, cohesionStayPresence, statusPhase1Motif, niveauScolaires, departements, etranger, avenir }: InscriptionRoutes["PostBasculerJeunesValides"]["payload"], + ) => { + return await buildRequest({ + path: "/inscription/{sessionId}/bacule-jeunes-valides/simulation", + method: "POST", + params: { sessionId }, + payload: { status, statusPhase1, cohesionStayPresence, statusPhase1Motif, niveauScolaires, departements, etranger, avenir }, + target: "API_V2", + })(); + }, + + postValiderBasculerJeunesValides: async (sessionId: string, simulationId: string) => { + return await buildRequest({ + path: "/inscription/{sessionId}/simulation/{simulationId}/bacule-jeunes-valides/valider", + method: "POST", + params: { sessionId, simulationId }, + target: "API_V2", + })(); + }, +}; + +export { InscriptionService }; diff --git a/apiv2/src/admin/Admin.module.ts b/apiv2/src/admin/Admin.module.ts index 602ae39c86..e24c16762e 100644 --- a/apiv2/src/admin/Admin.module.ts +++ b/apiv2/src/admin/Admin.module.ts @@ -60,6 +60,8 @@ import { TransactionalAdapterMongoose } from "@infra/TransactionalAdatpterMongoo import { referentielServiceProvider } from "./infra/referentiel/initProvider/service"; import { segmentDeLigneMongoProviders } from "./infra/sejours/phase1/segmentDeLigne/provider/SegmentDeLigneMongo.provider"; import { demandeModificationLigneDeBusMongoProviders } from "./infra/sejours/phase1/demandeModificationLigneDeBus/provider/DemandeModificationLigneDeBusMongo.provider"; +import { InscriptionController } from "./infra/sejours/phase1/inscription/api/Inscription.controller"; +import { InscriptionService } from "./core/sejours/phase1/inscription/Inscription.service"; @Module({ imports: [ @@ -83,6 +85,7 @@ import { demandeModificationLigneDeBusMongoProviders } from "./infra/sejours/pha controllers: [ ClasseController, AffectationController, + InscriptionController, Phase1Controller, ImportReferentielController, AuthController, @@ -93,6 +96,7 @@ import { demandeModificationLigneDeBusMongoProviders } from "./infra/sejours/pha providers: [ ClasseService, AffectationService, + InscriptionService, SimulationAffectationHTSService, SimulationAffectationCLEService, ReferentielRoutesService, diff --git a/apiv2/src/admin/AdminJob.module.ts b/apiv2/src/admin/AdminJob.module.ts index 8143c46818..47cc68adda 100644 --- a/apiv2/src/admin/AdminJob.module.ts +++ b/apiv2/src/admin/AdminJob.module.ts @@ -52,6 +52,9 @@ import { ContactGateway } from "./infra/iam/Contact.gateway"; import { ContactProducer } from "@notification/infra/email/Contact.producer"; import { ValiderAffectationCLE } from "./core/sejours/phase1/affectation/ValiderAffectationCLE"; import { AdminTaskAffectationSelectorService } from "./infra/task/AdminTaskAffectationSelector.service"; +import { AdminTaskInscriptionSelectorService } from "./infra/task/AdminTaskInscriptionSelectorService"; +import { SimulationBasculeJeunesValides } from "./core/sejours/phase1/inscription/SimulationBasculeJeunesValides"; +import { InscriptionService } from "./core/sejours/phase1/inscription/Inscription.service"; @Module({ imports: [ @@ -100,6 +103,7 @@ import { AdminTaskAffectationSelectorService } from "./infra/task/AdminTaskAffec { provide: ClockGateway, useClass: ClockProvider }, // add use case here AffectationService, + InscriptionService, SimulationAffectationHTSService, SimulationAffectationHTS, SimulationAffectationCLEService, @@ -109,6 +113,10 @@ import { AdminTaskAffectationSelectorService } from "./infra/task/AdminTaskAffec ...referentielUseCaseProvider, ...referentielServiceProvider, AdminTaskAffectationSelectorService, + SimulationBasculeJeunesValides, + ...referentielUseCaseProvider, + ...referentielServiceProvider, + AdminTaskInscriptionSelectorService, AdminTaskImportReferentielSelectorService, ], }) diff --git a/apiv2/src/admin/core/sejours/jeune/Jeune.gateway.ts b/apiv2/src/admin/core/sejours/jeune/Jeune.gateway.ts index 5246a6a149..e9d3887c49 100644 --- a/apiv2/src/admin/core/sejours/jeune/Jeune.gateway.ts +++ b/apiv2/src/admin/core/sejours/jeune/Jeune.gateway.ts @@ -11,6 +11,13 @@ export interface JeuneGateway { departements: string[], ): Promise; findBySessionIdClasseIdAndStatus(sessionId: string, classeId: string, status: string): Promise; + findBySessionIdStatutsStatutsPhase1NiveauScolairesAndDepartements( + sessionId: string, + status: string[], + statusPhase1: string[], + niveauScolaires: string[], + departements: string[], + ): Promise; findBySessionId(sessionId: string): Promise; update(jeune: JeuneModel): Promise; updateSession( diff --git a/apiv2/src/admin/core/sejours/phase1/inscription/Inscription.service.ts b/apiv2/src/admin/core/sejours/phase1/inscription/Inscription.service.ts new file mode 100644 index 0000000000..86ef6f8d5d --- /dev/null +++ b/apiv2/src/admin/core/sejours/phase1/inscription/Inscription.service.ts @@ -0,0 +1,75 @@ +import { Inject, Injectable, Logger } from "@nestjs/common"; + +import { getDepartmentForEligibility, getRegionForEligibility, TaskName, TaskStatus } from "snu-lib"; +import { TaskGateway } from "@task/core/Task.gateway"; +import { JeuneModel } from "../../jeune/Jeune.model"; +import { SessionModel } from "../session/Session.model"; +import { SessionGateway } from "../session/Session.gateway"; +import { FunctionalException, FunctionalExceptionCode } from "@shared/core/FunctionalException"; + +@Injectable() +export class InscriptionService { + constructor( + @Inject(TaskGateway) private readonly taskGateway: TaskGateway, + @Inject(SessionGateway) private readonly sessionGateway: SessionGateway, + private readonly logger: Logger, + ) {} + + async getStatusSimulation(sessionId: string, taskName: TaskName) { + const simulations = await this.taskGateway.findByNames( + [taskName], + { + "metadata.parameters.sessionId": sessionId, + }, + "DESC", + 1, + ); + return { + status: simulations?.[0]?.status || "NONE", + }; + } + + async getStatusValidation(sessionId: string, taskName: TaskName) { + const lastTraitement = ( + await this.taskGateway.findByNames( + [taskName], + { + "metadata.parameters.sessionId": sessionId, + }, + "DESC", + 1, + ) + )?.[0]; + const lastTraitementCompleted = ( + await this.taskGateway.findByNames( + [taskName], + { + status: TaskStatus.COMPLETED, + "metadata.parameters.sessionId": sessionId, + }, + "DESC", + 1, + ) + )?.[0]; + return { + status: lastTraitement?.status || "NONE", + lastCompletedAt: lastTraitementCompleted?.updatedAt, + }; + } + + async getSessionsEligible(jeune: JeuneModel): Promise { + if (!jeune.sessionId) { + throw new FunctionalException(FunctionalExceptionCode.NOT_IMPLEMENTED_YET); + } + const session = await this.sessionGateway.findById(jeune.sessionId); + + // const cohorts = await this.sessionGateway.find({ + // status: COHORT_STATUS.PUBLISHED + // cohortGroupId: session.cohortGroupId, + // _id: { $ne: session.id }, + // }); + // const region = getRegionForEligibility(young); + // const department = getDepartmentForEligibility(young); + return []; + } +} diff --git a/apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValides.ts b/apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValides.ts new file mode 100644 index 0000000000..0d01601754 --- /dev/null +++ b/apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValides.ts @@ -0,0 +1,180 @@ +import { Inject, Logger } from "@nestjs/common"; + +import { COHORTS, RegionsHorsMetropole, YOUNG_STATUS, department2region } from "snu-lib"; + +import { UseCase } from "@shared/core/UseCase"; +import { FunctionalException, FunctionalExceptionCode } from "@shared/core/FunctionalException"; + +import { JeuneGateway } from "../../jeune/Jeune.gateway"; + +import { FileGateway } from "@shared/core/File.gateway"; +import { + SimulationBasculeJeunesValidesTaskParameters, + JeuneRapport, + RAPPORT_SHEETS, + RapportData, +} from "./SimulationBasculeJeunesValidesTask.model"; +import { JeuneModel } from "../../jeune/Jeune.model"; +import { SessionGateway } from "../session/Session.gateway"; +import { InscriptionService } from "./Inscription.service"; + +export type SimulationBasculeJeunesValidesResult = { + analytics: { + jeunesAvenir: number; + jeunesProchainSejour: number; + jeunesRefuses: number; + }; + rapportData: RapportData; + rapportFile: { + Location: string; + ETag: string; + Bucket: string; + Key: string; + }; +}; + +export class SimulationBasculeJeunesValides implements UseCase { + constructor( + @Inject(InscriptionService) + private readonly inscriptionService: InscriptionService, + // @Inject(SimulationBasculeJeunesValidesService) + // private readonly simulationAffectationHTSService: SimulationBasculeJeunesValidesService, + @Inject(SessionGateway) private readonly sessionGateway: SessionGateway, + @Inject(JeuneGateway) private readonly jeuneGateway: JeuneGateway, + @Inject(FileGateway) private readonly fileGateway: FileGateway, + private readonly logger: Logger, + ) {} + async execute({ + sessionId, + status, + statusPhase1, + cohesionStayPresence, + statusPhase1Motif, + niveauScolaires, + departements, + etranger, + avenir, + }: SimulationBasculeJeunesValidesTaskParameters): Promise { + if (departements.some((departement) => RegionsHorsMetropole.includes(department2region[departement]))) { + throw new FunctionalException(FunctionalExceptionCode.AFFECTATION_DEPARTEMENT_HORS_METROPOLE); + } + + // const { ligneDeBusList, sejoursList, pdrList } = await this.affectationService.loadAffectationData(sessionId); + + let jeuneList = await this.jeuneGateway.findBySessionIdStatutsStatutsPhase1NiveauScolairesAndDepartements( + sessionId, + status, + statusPhase1, + niveauScolaires, + departements, + ); + if (!etranger) { + jeuneList = jeuneList.filter((jeune) => jeune.paysScolarite === "FRANCE"); + } + if (cohesionStayPresence) { + jeuneList = jeuneList.filter((jeune) => jeune.cohesionStayPresence === "true"); + } + if (statusPhase1Motif.length) { + jeuneList = jeuneList.filter( + (jeune) => jeune.departSejourMotif && statusPhase1Motif.includes(jeune.departSejourMotif), + ); + } + if (jeuneList.length === 0) { + throw new FunctionalException(FunctionalExceptionCode.AFFECTATION_NOT_ENOUGH_DATA, "Aucun jeune !"); + } + + this.logger.log(`Jeunes a basculer : ${jeuneList.length}`); + + const rapportData: RapportData = { + jeunesAvenir: [], + jeunesProchainSejour: [], + jeunesRefuses: [], + }; + + if (avenir) { + const sessionAVenir = await this.sessionGateway.findByName(COHORTS.AVENIR); + if (!sessionAVenir) { + throw new FunctionalException( + FunctionalExceptionCode.AFFECTATION_NOT_ENOUGH_DATA, + `Session ${COHORTS.AVENIR} introuvable`, + ); + } + for (const jeune of jeuneList) { + rapportData.jeunesAvenir.push( + this.mapJeuneRapport({ + ...jeune, + sessionId: sessionAVenir.id, + sessionNom: sessionAVenir.nom, + }), + ); + } + } else { + // prochaine sejour eligible + for (const jeune of jeuneList) { + const sessions = await this.inscriptionService.getSessionsEligible(jeune); + if (sessions.length > 0) { + rapportData.jeunesProchainSejour.push( + this.mapJeuneRapport({ + ...jeune, + sessionId: sessions[0].id, + sessionNom: sessions[0].nom, + }), + ); + } else { + rapportData.jeunesRefuses.push( + this.mapJeuneRapport({ + ...jeune, + statut: YOUNG_STATUS.REFUSED, + }), + ); + } + } + } + + // Calcul des données pour le rapport excel + const fileBuffer = await this.fileGateway.generateExcel({ + [RAPPORT_SHEETS.PROCHAINSEJOUR]: rapportData.jeunesProchainSejour, + [RAPPORT_SHEETS.AVENIR]: rapportData.jeunesAvenir, + [RAPPORT_SHEETS.REFUSES]: rapportData.jeunesRefuses, + }); + + const timestamp = `${new Date().toISOString()?.replaceAll(":", "-")?.replace(".", "-")}`; + const fileName = `simulation-bascule-jeunes-valides/bascule-jeunes-valides_simulation_${sessionId}_${timestamp}.xlsx`; + const rapportFile = await this.fileGateway.uploadFile( + `file/admin/sejours/phase1/inscription/simulation/${sessionId}/${fileName}`, + { + data: fileBuffer, + mimetype: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }, + ); + + return { + analytics: { + jeunesAvenir: rapportData.jeunesAvenir.length, + jeunesProchainSejour: rapportData.jeunesProchainSejour.length, + jeunesRefuses: rapportData.jeunesRefuses.length, + }, + rapportData, + rapportFile, + }; + } + + mapJeuneRapport(jeune: JeuneModel): JeuneRapport { + return { + id: jeune.id, + statut: jeune.statut, + statutPhase1: jeune.statutPhase1, + prenom: jeune.prenom, + nom: jeune.nom, + genre: jeune.genre === "female" ? "fille" : "garçon", + qpv: ["true", "oui"].includes(jeune.qpv!) ? "oui" : "non", + psh: ["true", "oui"].includes(jeune.psh!) ? "oui" : "non", + handicapMemeDepartement: ["true", "oui"].includes(jeune.handicapMemeDepartement!) ? "oui" : "non", + sessionNom: jeune.sessionNom, + region: jeune.region, + departement: jeune.departement, + regionScolarite: jeune.regionScolarite, + departementScolarite: jeune.departementScolarite, + }; + } +} diff --git a/apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValidesTask.model.ts b/apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValidesTask.model.ts new file mode 100644 index 0000000000..5efad99f3d --- /dev/null +++ b/apiv2/src/admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValidesTask.model.ts @@ -0,0 +1,59 @@ +import { TaskModel } from "@task/core/Task.model"; +import { JeuneModel } from "../../jeune/Jeune.model"; + +export interface SimulationBasculeJeunesValidesTaskParameters { + sessionId: string; + status: string[]; + statusPhase1: string[]; + cohesionStayPresence: boolean; + statusPhase1Motif: string[]; + niveauScolaires: string[]; + departements: string[]; + etranger: boolean; + avenir: boolean; +} + +export type SimulationBasculeJeunesValidesTaskResult = { + jeunesAvenir: number; + jeunesProchainSejour: number; + jeunesRefuses: number; + rapportKey: string; +}; + +export type SimulationBasculeJeunesValidesTaskModel = TaskModel< + SimulationBasculeJeunesValidesTaskParameters, + SimulationBasculeJeunesValidesTaskResult +>; + +export type RapportData = { + jeunesProchainSejour: JeuneRapport[]; + jeunesAvenir: JeuneRapport[]; + jeunesRefuses: JeuneRapport[]; +}; + +export type JeuneRapport = Pick< + JeuneModel, + | "id" + | "statutPhase1" + | "genre" + | "qpv" + | "psh" + | "sessionNom" + | "region" + | "departement" + | "regionScolarite" + | "departementScolarite" + | "pointDeRassemblementId" + | "ligneDeBusId" + | "centreId" + | "statut" + | "prenom" + | "nom" + | "handicapMemeDepartement" +>; + +export const RAPPORT_SHEETS = { + PROCHAINSEJOUR: "Prochain séjour", + AVENIR: "À venir", + REFUSES: "Refusés", +}; diff --git a/apiv2/src/admin/core/sejours/phase1/session/Session.gateway.ts b/apiv2/src/admin/core/sejours/phase1/session/Session.gateway.ts index ce5f4d17bc..7500169a97 100644 --- a/apiv2/src/admin/core/sejours/phase1/session/Session.gateway.ts +++ b/apiv2/src/admin/core/sejours/phase1/session/Session.gateway.ts @@ -2,6 +2,7 @@ import { CreateSessionModel, SessionModel } from "./Session.model"; export interface SessionGateway { findById(id: string): Promise; + findByName(name: string): Promise; findBySnuId(snuId: string): Promise; create(session: CreateSessionModel): Promise; } diff --git a/apiv2/src/admin/infra/sejours/jeune/repository/mongo/JeuneMongo.repository.ts b/apiv2/src/admin/infra/sejours/jeune/repository/mongo/JeuneMongo.repository.ts index d3963a554e..b1e3ef53dd 100644 --- a/apiv2/src/admin/infra/sejours/jeune/repository/mongo/JeuneMongo.repository.ts +++ b/apiv2/src/admin/infra/sejours/jeune/repository/mongo/JeuneMongo.repository.ts @@ -76,6 +76,23 @@ export class JeuneRepository implements JeuneGateway { return JeuneMapper.toModels(jeunes); } + async findBySessionIdStatutsStatutsPhase1NiveauScolairesAndDepartements( + sessionId: string, + status: string[], + statusPhase1: string[], + niveauScolaires: string[], + departements: string[], + ): Promise { + const jeunes = await this.jeuneMongooseEntity.find({ + cohortId: sessionId, + status: { $in: status }, + statusPhase1: { $in: statusPhase1 }, + grade: { $in: niveauScolaires }, + department: { $in: departements }, + }); + return JeuneMapper.toModels(jeunes); + } + async findBySessionId(sessionId: string): Promise { const jeunes = await this.jeuneMongooseEntity.find({ cohortId: sessionId }); return JeuneMapper.toModels(jeunes); diff --git a/apiv2/src/admin/infra/sejours/phase1/api/Phase1.controller.ts b/apiv2/src/admin/infra/sejours/phase1/api/Phase1.controller.ts index fff363afec..476be691ce 100644 --- a/apiv2/src/admin/infra/sejours/phase1/api/Phase1.controller.ts +++ b/apiv2/src/admin/infra/sejours/phase1/api/Phase1.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Delete, Get, Inject, Param, ParseArrayPipe, Query, UseGuards } from "@nestjs/common"; +import { Controller, Delete, Get, Inject, Param, Query, UseGuards } from "@nestjs/common"; import { Phase1Routes, TaskName, TaskStatus } from "snu-lib"; @@ -8,10 +8,15 @@ import { TaskMapper } from "@task/infra/Task.mapper"; import { SupprimerPlanDeTransport } from "@admin/core/sejours/phase1/affectation/SupprimerPlanDeTransport"; import { SuperAdminGuard } from "@admin/infra/iam/guard/SuperAdmin.guard"; -const PHASE1_SIMULATIONS_TASK_NAMES = [TaskName.AFFECTATION_HTS_SIMULATION, TaskName.AFFECTATION_CLE_SIMULATION]; +const PHASE1_SIMULATIONS_TASK_NAMES = [ + TaskName.AFFECTATION_HTS_SIMULATION, + TaskName.AFFECTATION_CLE_SIMULATION, + TaskName.BACULE_JEUNES_VALIDES_SIMULATION, +]; const PHASE1_TRAITEMENTS_TASK_NAMES = [ TaskName.AFFECTATION_HTS_SIMULATION_VALIDER, TaskName.AFFECTATION_CLE_SIMULATION_VALIDER, + TaskName.BACULE_JEUNES_VALIDES_SIMULATION_VALIDER, ]; @Controller("phase1") diff --git a/apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.controller.ts b/apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.controller.ts new file mode 100644 index 0000000000..727cbc7491 --- /dev/null +++ b/apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.controller.ts @@ -0,0 +1,122 @@ +import { Body, Controller, Get, Inject, Param, Post, Request, UseGuards } from "@nestjs/common"; + +import { + InscriptionRoutes, + TaskName, + TaskStatus, + ValiderAffectationHTSTaskParameters, + SimulationBasculerJeunesValidesTaskParameters, +} from "snu-lib"; + +import { TaskGateway } from "@task/core/Task.gateway"; +import { AdminGuard } from "@admin/infra/iam/guard/Admin.guard"; +import { TaskMapper } from "@task/infra/Task.mapper"; +import { CustomRequest } from "@shared/infra/CustomRequest"; + +import { PostSimulationsPayloadDto, PostSimulationValiderPayloadDto } from "./Inscription.validation"; +import { FunctionalException, FunctionalExceptionCode } from "@shared/core/FunctionalException"; +import { InscriptionService } from "@admin/core/sejours/phase1/inscription/Inscription.service"; + +@Controller("inscription") +export class InscriptionController { + constructor( + private readonly inscriptionService: InscriptionService, + @Inject(TaskGateway) private readonly taskGateway: TaskGateway, + ) {} + + @UseGuards(AdminGuard) + @Get("/:sessionId/bacule-jeunes-valides/status") + async getBaculeJeunesValidesStatus( + @Param("sessionId") sessionId: string, + ): Promise { + const traitement = await this.inscriptionService.getStatusValidation( + sessionId, + TaskName.BACULE_JEUNES_VALIDES_SIMULATION_VALIDER, + ); + return { + simulation: await this.inscriptionService.getStatusSimulation( + sessionId, + TaskName.BACULE_JEUNES_VALIDES_SIMULATION, + ), + traitement: { + ...traitement, + lastCompletedAt: traitement.lastCompletedAt?.toISOString(), + }, + }; + } + + @UseGuards(AdminGuard) + @Post("/:sessionId/bacule-jeunes-valides/simulation") + async basuleJeunesValidesSimulation( + @Request() request: CustomRequest, + @Param("sessionId") sessionId: string, + @Body() payload: PostSimulationsPayloadDto, + ): Promise { + const task = await this.taskGateway.create({ + name: TaskName.BACULE_JEUNES_VALIDES_SIMULATION, + status: TaskStatus.PENDING, + metadata: { + parameters: { + sessionId, + status: payload.status, + statusPhase1: payload.statusPhase1, + statusPhase1Motif: payload.statusPhase1Motif, + departements: payload.departements, + niveauScolaires: payload.niveauScolaires, + avenir: payload.avenir, + auteur: { + id: request.user.id, + prenom: request.user.prenom, + nom: request.user.nom, + role: request.user.role, + sousRole: request.user.sousRole, + }, + } as SimulationBasculerJeunesValidesTaskParameters, + }, + }); + return TaskMapper.toDto(task); + } + + @UseGuards(AdminGuard) + @Post("/:sessionId/simulation/:taskId/bacule-jeunes-valides/valider") + async basuleJeunesValidesValider( + @Request() request: CustomRequest, + @Param("sessionId") sessionId: string, + @Param("taskId") taskId: string, + @Body() payload: PostSimulationValiderPayloadDto, + ): Promise { + const simulationTask = await this.taskGateway.findById(taskId); + + // On verifie qu'une simulation n'a pas déjà été affecté en amont + const { status, lastCompletedAt } = await this.inscriptionService.getStatusValidation( + sessionId, + TaskName.BACULE_JEUNES_VALIDES_SIMULATION_VALIDER, + ); + + if ( + [TaskStatus.IN_PROGRESS, TaskStatus.PENDING].includes(status) || + (lastCompletedAt && simulationTask.createdAt <= lastCompletedAt) + ) { + throw new FunctionalException(FunctionalExceptionCode.AFFECTATION_SIMULATION_OUTDATED); + } + + const task = await this.taskGateway.create({ + name: TaskName.BACULE_JEUNES_VALIDES_SIMULATION_VALIDER, + status: TaskStatus.PENDING, + metadata: { + parameters: { + sessionId, + simulationTaskId: taskId, + auteur: { + id: request.user.id, + prenom: request.user.prenom, + nom: request.user.nom, + role: request.user.role, + sousRole: request.user.sousRole, + }, + } as ValiderAffectationHTSTaskParameters, + }, + }); + return TaskMapper.toDto(task); + } +} diff --git a/apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.validation.ts b/apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.validation.ts new file mode 100644 index 0000000000..3afc7d88c0 --- /dev/null +++ b/apiv2/src/admin/infra/sejours/phase1/inscription/api/Inscription.validation.ts @@ -0,0 +1,47 @@ +import { ArrayMinSize, IsArray, IsBoolean, IsIn, IsMongoId } from "class-validator"; + +import { + DEPART_SEJOUR_MOTIFS, + GRADES, + region2department, + RegionsMetropole, + YOUNG_STATUS, + YOUNG_STATUS_PHASE1, +} from "snu-lib"; + +export class PostSimulationsPayloadDto { + @IsArray() + @IsIn(Object.values(YOUNG_STATUS), { each: true }) + @ArrayMinSize(1) + status: Array; + + @IsArray() + @IsIn(Object.values(YOUNG_STATUS_PHASE1), { each: true }) + @ArrayMinSize(1) + statusPhase1: Array; + + @IsBoolean() + cohesionStayPresence; + + @IsArray() + @IsIn(Object.values(DEPART_SEJOUR_MOTIFS), { each: true }) + statusPhase1Motif: Array; + + @IsArray() + @IsIn(RegionsMetropole.flatMap((region) => region2department[region]), { each: true }) + @ArrayMinSize(1) + departements: string[]; + + @IsArray() + @IsIn(Object.values(GRADES), { each: true }) + @ArrayMinSize(1) + niveauScolaires: Array; + + @IsBoolean() + etranger: boolean; + + @IsBoolean() + avenir: boolean; +} + +export class PostSimulationValiderPayloadDto {} diff --git a/apiv2/src/admin/infra/sejours/phase1/session/repository/mongo/SessionMongo.repository.ts b/apiv2/src/admin/infra/sejours/phase1/session/repository/mongo/SessionMongo.repository.ts index 97f9b4389b..601f37b999 100644 --- a/apiv2/src/admin/infra/sejours/phase1/session/repository/mongo/SessionMongo.repository.ts +++ b/apiv2/src/admin/infra/sejours/phase1/session/repository/mongo/SessionMongo.repository.ts @@ -35,6 +35,13 @@ export class SessionRepository implements SessionGateway { } return SessionMapper.toModel(session); } + async findByName(name: string): Promise { + const session = await this.sesssionMongooseEntity.findOne({ name }); + if (!session) { + throw new FunctionalException(FunctionalExceptionCode.NOT_FOUND); + } + return SessionMapper.toModel(session); + } async update(session: SessionModel): Promise { const sessionEntity = SessionMapper.toEntity(session); const retrievedSession = await this.sesssionMongooseEntity.findById(session.id); diff --git a/apiv2/src/admin/infra/task/AdminTask.consumer.ts b/apiv2/src/admin/infra/task/AdminTask.consumer.ts index 54aef2de69..06c8cd7534 100644 --- a/apiv2/src/admin/infra/task/AdminTask.consumer.ts +++ b/apiv2/src/admin/infra/task/AdminTask.consumer.ts @@ -11,6 +11,7 @@ import { AdminTaskRepository } from "./AdminTaskMongo.repository"; import { ReferentielImportTaskModel } from "@admin/core/referentiel/routes/ReferentielImportTask.model"; import { AdminTaskImportReferentielSelectorService } from "./AdminTaskImportReferentielSelector.service"; import { AdminTaskAffectationSelectorService } from "./AdminTaskAffectationSelector.service"; +import { AdminTaskInscriptionSelectorService } from "./AdminTaskInscriptionSelectorService"; @Processor(QueueName.ADMIN_TASK) export class AdminTaskConsumer extends WorkerHost { @@ -18,6 +19,7 @@ export class AdminTaskConsumer extends WorkerHost { private readonly logger: Logger, private readonly adminTaskRepository: AdminTaskRepository, private readonly adminTaskAffectationSelectorService: AdminTaskAffectationSelectorService, + private readonly adminTaskInscriptionSelectorService: AdminTaskInscriptionSelectorService, private readonly referentielTaskService: AdminTaskImportReferentielSelectorService, private readonly cls: ClsService, ) { @@ -42,6 +44,10 @@ export class AdminTaskConsumer extends WorkerHost { case TaskName.AFFECTATION_CLE_SIMULATION_VALIDER: results = await this.adminTaskAffectationSelectorService.handleAffectation(job, task); break; + case TaskName.BACULE_JEUNES_VALIDES_SIMULATION: + case TaskName.BACULE_JEUNES_VALIDES_SIMULATION_VALIDER: + results = await this.adminTaskInscriptionSelectorService.handleInscription(job, task); + break; case TaskName.REFERENTIEL_IMPORT: const importTask = task as ReferentielImportTaskModel; this.logger.log( diff --git a/apiv2/src/admin/infra/task/AdminTaskInscriptionSelectorService.ts b/apiv2/src/admin/infra/task/AdminTaskInscriptionSelectorService.ts new file mode 100644 index 0000000000..cc22d84171 --- /dev/null +++ b/apiv2/src/admin/infra/task/AdminTaskInscriptionSelectorService.ts @@ -0,0 +1,34 @@ +import { Job } from "bullmq"; +import { TaskName } from "snu-lib"; + +import { Injectable } from "@nestjs/common"; +import { TaskQueue } from "@shared/infra/Queue"; +import { TaskModel } from "@task/core/Task.model"; +import { + SimulationBasculeJeunesValidesTaskModel, + SimulationBasculeJeunesValidesTaskResult, +} from "@admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValidesTask.model"; +import { SimulationBasculeJeunesValides } from "@admin/core/sejours/phase1/inscription/SimulationBasculeJeunesValides"; + +@Injectable() +export class AdminTaskInscriptionSelectorService { + constructor(private readonly simulationBasculeJeunesValides: SimulationBasculeJeunesValides) {} + async handleInscription(job: Job, task: TaskModel): Promise> { + let results = {} as Record; + switch (job.name) { + case TaskName.BACULE_JEUNES_VALIDES_SIMULATION: + const simulationBasculeTask = task as SimulationBasculeJeunesValidesTaskModel; + const simulation = await this.simulationBasculeJeunesValides.execute( + simulationBasculeTask.metadata!.parameters!, + ); + results = { + rapportKey: simulation.rapportFile.Key, + ...simulation.analytics, + } as SimulationBasculeJeunesValidesTaskResult; + break; + default: + throw new Error(`Task of type ${job.name} not handle yet for inscription`); + } + return results; + } +} diff --git a/packages/ds/src/common/index.ts b/packages/ds/src/common/index.ts index d880c3a2c6..d5dd39f48c 100644 --- a/packages/ds/src/common/index.ts +++ b/packages/ds/src/common/index.ts @@ -10,6 +10,7 @@ import AddressForm from "./forms/AddressForm"; import CityForm from "./forms/CityForm"; import Input from "./inputs/Input"; import { Address } from "./forms/AddressForm"; +export { default as Checkbox } from "./inputs/Checkbox"; export { Hint, PHONE_ZONES, ProfilePic, AddressForm, Input, CityForm }; diff --git a/packages/ds/src/common/inputs/Checkbox.tsx b/packages/ds/src/common/inputs/Checkbox.tsx new file mode 100644 index 0000000000..ce3b1efb35 --- /dev/null +++ b/packages/ds/src/common/inputs/Checkbox.tsx @@ -0,0 +1,44 @@ +import React from "react"; + +interface proptype { + label?: string; + className?: string; + name?: string; + checked?: boolean; + onChange?: (value: boolean) => void; + error?: string | null; + disabled?: boolean; + readOnly?: boolean; +} + +const Checkbox = ({ + className, + name, + checked, + onChange, + error, + disabled, + readOnly, +}: proptype) => { + const handleChange = (event: React.ChangeEvent) => { + // @ts-ignore + onChange?.(event.target.value); + }; + + return ( +
+ + {error &&

{error}

} +
+ ); +}; + +export default Checkbox; diff --git a/packages/ds/src/common/inputs/Input.tsx b/packages/ds/src/common/inputs/Input.tsx index 42cf14fcff..784e612aba 100644 --- a/packages/ds/src/common/inputs/Input.tsx +++ b/packages/ds/src/common/inputs/Input.tsx @@ -8,7 +8,7 @@ interface proptype { value?: string; onChange?: (value: string) => void; error?: string | null; - type?: "text" | "email"; + type?: "text" | "email" | "checkbox"; disabled?: boolean; readOnly?: boolean; } diff --git a/packages/lib/src/constants/task.ts b/packages/lib/src/constants/task.ts index 9ffa51614e..b0899ac124 100644 --- a/packages/lib/src/constants/task.ts +++ b/packages/lib/src/constants/task.ts @@ -11,4 +11,6 @@ export enum TaskName { AFFECTATION_CLE_SIMULATION = "AFFECTATION_CLE_SIMULATION", AFFECTATION_HTS_SIMULATION_VALIDER = "AFFECTATION_HTS_SIMULATION_VALIDER", AFFECTATION_CLE_SIMULATION_VALIDER = "AFFECTATION_CLE_SIMULATION_VALIDER", + BACULE_JEUNES_VALIDES_SIMULATION = "BACULE_JEUNES_VALIDES_SIMULATION", + BACULE_JEUNES_VALIDES_SIMULATION_VALIDER = "BACULE_JEUNES_VALIDES_SIMULATION_VALIDER", } diff --git a/packages/lib/src/dto/phase1/index.ts b/packages/lib/src/dto/phase1/index.ts index 54a192d94f..70cd24bcfe 100644 --- a/packages/lib/src/dto/phase1/index.ts +++ b/packages/lib/src/dto/phase1/index.ts @@ -1,4 +1,5 @@ export * from "./affectation"; +export * from "./inscription"; export type { Phase1HTSTaskDto } from "./Phase1HTSTaskDto"; export type { Phase1CLETaskDto } from "./Phase1CLETaskDto"; diff --git a/packages/lib/src/dto/phase1/inscription/SimulationBasculerJeunesValidesTaskDto.ts b/packages/lib/src/dto/phase1/inscription/SimulationBasculerJeunesValidesTaskDto.ts new file mode 100644 index 0000000000..872c827cb9 --- /dev/null +++ b/packages/lib/src/dto/phase1/inscription/SimulationBasculerJeunesValidesTaskDto.ts @@ -0,0 +1,24 @@ +import { YoungDto } from "../../youngDto"; +import { GRADES } from "../../../constants/constants"; +import { TaskDto } from "../../taskDto"; +import { Phase1TaskParameters } from "../Phase1HTSTaskDto"; + +export interface SimulationBasculerJeunesValidesTaskParameters extends Phase1TaskParameters { + status: YoungDto["status"][]; + statusPhase1: YoungDto["statusPhase1"][]; + statusPhase1Motif: YoungDto["statusPhase1Motif"][]; + cohesionStayPresence: boolean; + niveauScolaires: Array; + departements: string[]; + etranger: boolean; + avenir: boolean; +} + +export type SimulationBasculerJeunesValidesTaskResult = { + rapportKey: string; + jeunesAvenir: number; + jeunesProchainSejour: number; + jeunesRefuses: number; +}; + +export interface SimulationBasculerJeunesValidesTaskDto extends TaskDto {} diff --git a/packages/lib/src/dto/phase1/inscription/ValiderBasculerJeunesValidesTaskDto.ts b/packages/lib/src/dto/phase1/inscription/ValiderBasculerJeunesValidesTaskDto.ts new file mode 100644 index 0000000000..bbd45bb2eb --- /dev/null +++ b/packages/lib/src/dto/phase1/inscription/ValiderBasculerJeunesValidesTaskDto.ts @@ -0,0 +1,14 @@ +import { TaskDto } from "../../taskDto"; +import { Phase1TaskParameters } from "../Phase1HTSTaskDto"; + +export interface ValiderBasculerJeunesValidesTaskParameters extends Phase1TaskParameters { + affecterPDR: boolean; +} + +export type ValiderBasculerJeunesValidesTaskResult = { + rapportKey: string; + jeunesUpdated: number; + errors: number; +}; + +export interface ValiderBasculerJeunesValidesTaskDto extends TaskDto {} diff --git a/packages/lib/src/dto/phase1/inscription/index.ts b/packages/lib/src/dto/phase1/inscription/index.ts new file mode 100644 index 0000000000..a971b49e7b --- /dev/null +++ b/packages/lib/src/dto/phase1/inscription/index.ts @@ -0,0 +1,2 @@ +export type * from "./SimulationBasculerJeunesValidesTaskDto"; +export type * from "./ValiderBasculerJeunesValidesTaskDto"; diff --git a/packages/lib/src/routes/phase1/index.ts b/packages/lib/src/routes/phase1/index.ts index c9e687506b..d83b0f188b 100644 --- a/packages/lib/src/routes/phase1/index.ts +++ b/packages/lib/src/routes/phase1/index.ts @@ -9,3 +9,4 @@ export type Phase1Routes = { }; export type { AffectationRoutes } from "./affectation"; +export type { InscriptionRoutes } from "./inscription"; diff --git a/packages/lib/src/routes/phase1/inscription/getBasculerJeunesValidesRoute.ts b/packages/lib/src/routes/phase1/inscription/getBasculerJeunesValidesRoute.ts new file mode 100644 index 0000000000..34af922f59 --- /dev/null +++ b/packages/lib/src/routes/phase1/inscription/getBasculerJeunesValidesRoute.ts @@ -0,0 +1,16 @@ +import { BasicRoute, RouteResponseBodyV2, TaskStatus } from "../../.."; + +export interface GetBasculerJeunesValidesRoute extends BasicRoute { + method: "GET"; + path: "/inscription/{sessionId}/bacule-jeunes-valides/status"; + params: { sessionId: string }; + response: RouteResponseBodyV2<{ + simulation: { + status: TaskStatus | "NONE"; + }; + traitement: { + status: TaskStatus | "NONE"; + lastCompletedAt: string; + }; + }>; +} diff --git a/packages/lib/src/routes/phase1/inscription/index.ts b/packages/lib/src/routes/phase1/inscription/index.ts new file mode 100644 index 0000000000..515e605551 --- /dev/null +++ b/packages/lib/src/routes/phase1/inscription/index.ts @@ -0,0 +1,9 @@ +import { GetBasculerJeunesValidesRoute } from "./getBasculerJeunesValidesRoute"; +import { PostBasculerJeunesValidesRoute } from "./postBasculerJeunesValidesRoute"; +import { PostValiderBasculerJeunesValidesRoute } from "./postValiderBasculerJeunesValidesRoute"; + +export type InscriptionRoutes = { + GetBasculerJeunesValides: GetBasculerJeunesValidesRoute; + PostBasculerJeunesValides: PostBasculerJeunesValidesRoute; + PostValiderBasculerJeunesValides: PostValiderBasculerJeunesValidesRoute; +}; diff --git a/packages/lib/src/routes/phase1/inscription/postBasculerJeunesValidesRoute.ts b/packages/lib/src/routes/phase1/inscription/postBasculerJeunesValidesRoute.ts new file mode 100644 index 0000000000..664764f0bd --- /dev/null +++ b/packages/lib/src/routes/phase1/inscription/postBasculerJeunesValidesRoute.ts @@ -0,0 +1,22 @@ +import { GRADES } from "../../../constants/constants"; +import { YoungDto } from "../../../dto"; +import { BasicRoute, RouteResponseBodyV2 } from "../.."; + +import { SimulationBasculerJeunesValidesTaskDto } from "../../../dto/phase1"; + +export interface PostBasculerJeunesValidesRoute extends BasicRoute { + method: "POST"; + path: "/inscription/{sessionId}/bacule-jeunes-valides/simulation"; + params: { sessionId: string }; + payload: { + status: YoungDto["status"][]; + statusPhase1: YoungDto["statusPhase1"][]; + cohesionStayPresence: boolean; + statusPhase1Motif: YoungDto["statusPhase1Motif"][]; + niveauScolaires: Array; + departements: string[]; + etranger: boolean; + avenir: boolean; + }; + response: RouteResponseBodyV2; +} diff --git a/packages/lib/src/routes/phase1/inscription/postValiderBasculerJeunesValidesRoute.ts b/packages/lib/src/routes/phase1/inscription/postValiderBasculerJeunesValidesRoute.ts new file mode 100644 index 0000000000..4e5d853f67 --- /dev/null +++ b/packages/lib/src/routes/phase1/inscription/postValiderBasculerJeunesValidesRoute.ts @@ -0,0 +1,10 @@ +import { BasicRoute, RouteResponseBodyV2 } from "../../.."; + +import { ValiderBasculerJeunesValidesTaskDto } from "../../../dto/phase1"; + +export interface PostValiderBasculerJeunesValidesRoute extends BasicRoute { + method: "POST"; + path: "/inscription/{sessionId}/simulation/{simulationId}/bacule-jeunes-valides/valider"; + params: { sessionId: string; simulationId: string }; + response: RouteResponseBodyV2; +} diff --git a/packages/lib/src/translation.ts b/packages/lib/src/translation.ts index 216d278b65..3b8bb8bc06 100644 --- a/packages/lib/src/translation.ts +++ b/packages/lib/src/translation.ts @@ -1192,6 +1192,9 @@ export const translateSimulationName = (name: string) => { case "AFFECTATION_CLE_SIMULATION": case "AFFECTATION_CLE_SIMULATION_VALIDER": return "Affectation CLE (Metropole)"; + case "BACULE_JEUNES_VALIDES_SIMULATION": + case "BACULE_JEUNES_VALIDES_SIMULATION_VALIDER": + return "Bascule des jeunes validés"; default: return name; }