Skip to content

Commit

Permalink
feat(api,admin): 57 - Basculer des jeunes validés sur le prochain séj…
Browse files Browse the repository at this point in the history
…our éligible
  • Loading branch information
achorein committed Jan 29, 2025
1 parent 443e3cb commit f5e6314
Show file tree
Hide file tree
Showing 39 changed files with 1,278 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface ActionsSubTabProps {
export default function ActionsSubTab({ session }: ActionsSubTabProps) {
return (
<div className="flex flex-col gap-8">
<InscriptionsSection sessionId={session._id!} />
<InscriptionsSection session={session} />
<AffectationsSection session={session} />
<ApreSejourSection sessionId={session._id!} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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<InscriptionRoutes["GetBasculerJeunesValides"]["response"]>({
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 (
<div className="flex items-center justify-between p-4">
<div className="flex gap-2">
<div className="text-sm leading-5 font-bold">Bascule des jeunes validés</div>
<Tooltip id="basule-jeunes-valides" title="Bascule des jeunes validés">
<HiOutlineInformationCircle className="text-gray-400" size={20} />
</Tooltip>
{isInProgress && <div className="text-xs leading-4 font-normal text-orange-500 italic">Simulation en cours...</div>}
</div>
<div className="flex gap-2">
<Button
title="Voir les simulations"
type="wired"
onClick={() => history.push(`?tab=simulations&cohort=${session.name}&action=${TaskName.BACULE_JEUNES_VALIDES_SIMULATION}`)}
/>
<Button title="Lancer une simulation" onClick={toggleModal} loading={isInProgress || isLoading} disabled={!isValidSession || isLoading || isInProgress || isError} />
</div>
{showModal && <BasculeJeuneValidesModal session={session} onClose={toggleModal} />}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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<string, string[]>;
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<Phase1Routes["GetSimulationsRoute"]["response"]>(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 (
<Modal
isOpen
onClose={onClose}
className="md:max-w-[800px]"
content={
<div className="scroll-y-auto overflow-y-auto max-h-[80vh]">
<div className="flex flex-col items-center text-center gap-2 mb-8">
<div className="flex items-center">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-gray-50">
<HiOutlineLightningBolt className="w-6 h-6" />
</div>
</div>
<h1 className="font-bold text-xl m-0">Bascule des jeunes validés</h1>
</div>
<div className="flex items-start flex-col w-full gap-8">
<div className="flex flex-col w-full gap-2.5">
<h2 className="text-lg leading-7 font-bold m-0">Statuts de phase</h2>
<CollapsableSelectSwitcher
title="Phase 0"
options={Object.values([YOUNG_STATUS.WAITING_VALIDATION, YOUNG_STATUS.VALIDATED]).map((statut) => ({ 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) && (
<CollapsableSelectSwitcher
title="Phase 1"
options={Object.values([YOUNG_STATUS_PHASE1.WAITING_AFFECTATION, YOUNG_STATUS_PHASE1.AFFECTED, YOUNG_STATUS_PHASE1.NOT_DONE]).map((statut) => ({
label: translate(statut),
value: statut,
}))}
values={state.statusPhase1 as string[]}
onChange={(values) => setState({ statusPhase1: values as string[] })}
isOpen={false}
/>
)}
</div>
{state.status.includes(YOUNG_STATUS.VALIDATED) && (
<>
<div className="flex flex-col w-full gap-2.5">
<h2 className="text-lg leading-7 font-bold m-0">Arrivées et départs</h2>
<SectionSwitcher
title="Présence à l'arrivée"
value={state.cohesionStayPresence}
onChange={(cohesionStayPresence) => setState({ cohesionStayPresence })}
className="py-2.5"
/>
<CollapsableSelectSwitcher
title="Motif de départ"
options={Object.values(DEPART_SEJOUR_MOTIFS).map((motif) => ({ label: translate(motif), value: motif }))}
values={state.statusPhase1Motif as string[]}
onChange={(values) => setState({ statusPhase1Motif: values as string[] })}
isOpen={false}
/>
</div>
<div className="flex flex-col w-full gap-2.5">
<h2 className="text-lg leading-7 font-bold m-0">Situations scolaires</h2>
<CollapsableSelectSwitcher
title="Niveaux"
options={Object.values(GRADES).map((grade) => ({ label: translate(grade), value: grade }))}
values={state.niveauScolaires}
onChange={(values) => setState({ niveauScolaires: values as string[] })}
isOpen={false}
/>
</div>
</>
)}
<div className="flex flex-col w-full gap-2.5">
<h2 className="text-lg leading-7 font-bold m-0">Départements de résidence</h2>
<div className="flex flex-col w-full">
{RegionsMetropole.map((region) => (
<CollapsableSelectSwitcher
key={region}
title={region}
options={region2department[region].map((department) => ({ label: formatDepartement(department), value: department }))}
values={state.departements[region]}
onChange={(values) => setState({ departements: { ...state.departements, [region]: values } })}
isOpen={false}
/>
))}
<SectionSwitcher title="Etranger" value={state.etranger} onChange={(etranger) => setState({ etranger })} className="py-2.5" />
</div>
</div>
<div className="flex flex-col w-full gap-2.5">
<h2 className="text-lg leading-7 font-bold m-0">Cohorte à venir</h2>
<SectionSwitcher title="Basculer tous les jeunes vers la cohorte à venir" value={state.avenir} onChange={(avenir) => setState({ avenir })} className="py-2.5" />
</div>
</div>
</div>
}
footer={
<div className="flex items-center justify-between gap-6">
<Button title="Annuler" type="secondary" className="flex-1 justify-center" onClick={onClose} />
<Button disabled={!isReady || isPending} onClick={() => mutate()} title="Lancer une simulation" className="flex-1" />
</div>
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Collapsable open={false} title="Inscriptions">
à compléter
<Collapsable title="Inscriptions">
<BasculeJeuneValides session={session} />
</Collapsable>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +92,8 @@ export default function SimulationsSubTab({ session }: SimulationsSubTabProps) {
return <SimulationHtsResultCell simulation={simulation} />;
} else if (simulation.name === TaskName.AFFECTATION_CLE_SIMULATION) {
return <SimulationCleResultCell simulation={simulation} />;
} else if (simulation.name === TaskName.BACULE_JEUNES_VALIDES_SIMULATION) {
return <BasculeJeuneValidesCell simulation={simulation} />;
}
return null;
},
Expand All @@ -113,6 +117,8 @@ export default function SimulationsSubTab({ session }: SimulationsSubTabProps) {
return <SimulationHtsResultStartButton simulation={simulation} />;
} else if (simulation.name === TaskName.AFFECTATION_CLE_SIMULATION) {
return <SimulationCleResultStartButton simulation={simulation} />;
} else if (simulation.name === TaskName.BACULE_JEUNES_VALIDES_SIMULATION) {
return <BasculeJeuneValidesStartButton simulation={simulation} />;
}
return <HiPlay className="text-gray-400" size={50} />;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export default function SimulationHtsResultCell({ simulation }: SimulationHtsRes

return (
<div className="text-xs leading-4 font-normal">
<div>Affectés : {simulationHts.metadata?.results?.jeunesNouvellementAffected ?? "--"}</div>
<div>En attente : {simulationHts.metadata?.results?.jeuneAttenteAffectation ?? "--"}</div>
<div className="whitespace-nowrap">Affectés : {simulationHts.metadata?.results?.jeunesNouvellementAffected ?? "--"}</div>
<div className="whitespace-nowrap">En attente : {simulationHts.metadata?.results?.jeuneAttenteAffectation ?? "--"}</div>
<div className="flex gap-1 items-center">
<div>Performance : {simulationHts.metadata?.results?.selectedCost ? formatPourcentage(1 - simulationHts.metadata.results.selectedCost) : "--"}</div>
<Tooltip id={`selectedCost-${simulationHts.id}`} title="Indicateur de performance de la répartition des jeunes dans les centres." className="flex items-center">
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="text-xs leading-4 font-normal">
<div className="whitespace-nowrap">Prochain séjour : {simulationHts.metadata?.results?.jeunesProchainSejour ?? "--"}</div>
<div className="whitespace-nowrap">À venir : {simulationHts.metadata?.results?.jeunesAvenir ?? "--"}</div>
<div className="whitespace-nowrap">Refusés : {simulationHts.metadata?.results?.jeunesRefuses ?? "--"}</div>
</div>
);
}
Loading

0 comments on commit f5e6314

Please sign in to comment.