diff --git a/admin/src/scenes/volontaires/view/phase2MilitaryPreparationV2.jsx b/admin/src/scenes/volontaires/view/phase2MilitaryPreparationV2.jsx index 2af286abe0..2c2cb4f003 100644 --- a/admin/src/scenes/volontaires/view/phase2MilitaryPreparationV2.jsx +++ b/admin/src/scenes/volontaires/view/phase2MilitaryPreparationV2.jsx @@ -55,7 +55,7 @@ export default function Phase2militaryPrepartionV2({ young }) { const getApplications = async () => { if (!young) return; - const { ok, data, code } = await api.get(`/young/${young._id}/application`); + const { ok, data, code } = await api.get(`/young/${young._id}/application?isMilitaryPreparation=true`); if (!ok) { capture(new Error(code)); return toastr.error("Oups, une erreur est survenue", code); diff --git a/admin/src/scenes/volontaires/view/phase2bis/EquivalenceList.tsx b/admin/src/scenes/volontaires/view/phase2bis/EquivalenceList.tsx new file mode 100644 index 0000000000..aaedec9c38 --- /dev/null +++ b/admin/src/scenes/volontaires/view/phase2bis/EquivalenceList.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useState } from "react"; +import CardEquivalence from "../../components/Equivalence"; +import API from "@/services/api"; +import { MissionEquivalenceType, YoungType } from "snu-lib"; +import Loader from "@/components/Loader"; +import { capture } from "@/sentry"; +import { toastr } from "react-redux-toastr"; + +export default function EquivalenceList({ young }: { young: YoungType }) { + const [loading, setLoading] = useState(false); + const [equivalences, setEquivalences] = useState([]); + + async function getEquivalences() { + setLoading(true); + try { + const { ok, data } = await API.get(`/young/${young._id.toString()}/phase2/equivalences`); + if (ok) setEquivalences(data); + } catch (e) { + capture(e); + toastr.error("Oups, une erreur est survenue", e.message); + } + setLoading(false); + } + + useEffect(() => { + if (!young?._id) return; + getEquivalences(); + }, [young]); + + if (loading) return ; + + return ( +
+ {equivalences.map((equivalence) => ( + + ))} +
+ ); +} diff --git a/admin/src/scenes/volontaires/view/phase2bis/applicationList2.jsx b/admin/src/scenes/volontaires/view/phase2bis/applicationList2.tsx similarity index 60% rename from admin/src/scenes/volontaires/view/phase2bis/applicationList2.jsx rename to admin/src/scenes/volontaires/view/phase2bis/applicationList2.tsx index 5ed8fbe7b0..1057bea84d 100644 --- a/admin/src/scenes/volontaires/view/phase2bis/applicationList2.jsx +++ b/admin/src/scenes/volontaires/view/phase2bis/applicationList2.tsx @@ -7,77 +7,66 @@ import Loader from "../../../../components/Loader"; import { capture } from "../../../../sentry"; import api from "../../../../services/api"; import ModalConfirm from "../../../../components/modals/ModalConfirm"; -import { APPLICATION_STATUS, formatStringDateTimezoneUTC, translate, SENDINBLUE_TEMPLATES } from "../../../../utils"; +import { APPLICATION_STATUS, formatStringDateTimezoneUTC, translate, SENDINBLUE_TEMPLATES, ApplicationType, MissionType, ContractType } from "../../../../utils"; import { SelectStatusApplicationPhase2 } from "./components/SelectStatusApplicationPhase2"; import Tag from "../../../../components/Tag"; import ReactTooltip from "react-tooltip"; +type PopulatedApplicationType = ApplicationType & { mission: MissionType; contract: ContractType }; + export default function ApplicationList({ young, onChangeApplication }) { - const [applications, setApplications] = useState(null); - const optionsType = ["contractAvenantFiles", "justificatifsFiles", "feedBackExperienceFiles", "othersFiles"]; + const [loading, setLoading] = useState(false); + const [applications, setApplications] = useState(); useEffect(() => { + if (!young?._id) return; getApplications(); - }, []); + }, [young]); - const getApplications = async () => { - if (!young) return; - const { ok, data, code } = await api.get(`/young/${young._id}/application`); - if (!ok) { - capture(new Error(code)); - return toastr.error("Oups, une erreur est survenue", code); + async function getApplications() { + setLoading(true); + try { + const { ok, data, code } = await api.get(`/young/${young._id}/application`); + if (!ok) { + capture(new Error(code)); + return toastr.error("Oups, une erreur est survenue", code); + } + setApplications(data); + } catch (e) { + capture(e); + toastr.error("Oups, une erreur est survenue", e.message); } - return setApplications(data); - }; + setLoading(false); + } - if (!applications) return ; - if (!applications.length) return
Aucune candidature n'est liée à ce volontaire.
; + if (loading) return ; + if (!applications?.length) return
Aucune candidature n'est liée à ce volontaire.
; return (
- {applications.map((hit, i) => ( - + {applications.map((hit) => ( + ))}
); } -const Hit = ({ hit, index, young, onChangeApplication }) => { - const [mission, setMission] = useState(); - const [contract, setContract] = useState(); +type modalType = { isOpen: boolean; onConfirm?: () => void; title: string; message: string }; + +const Hit = ({ hit, young, onChangeApplication }) => { const numberOfFiles = hit?.contractAvenantFiles.length + hit?.justificatifsFiles.length + hit?.feedBackExperienceFiles.length + hit?.othersFiles.length; const history = useHistory(); - const [modal, setModal] = useState({ isOpen: false, onConfirm: null }); - - useEffect(() => { - (async () => { - if (!hit.missionId) return; - const { ok, data, code } = await api.get(`/mission/${hit.missionId}`); - if (!ok) { - capture(new Error(code)); - return toastr.error("Oups, une erreur est survenue", code); - } - setMission(data); - if (!hit.contractId) return; - const { ok: okContract, data: dataContract, code: codeContract } = await api.get(`/contract/${hit.contractId}`); - if (!okContract) { - capture(codeContract); - return toastr.error("Oups, une erreur est survenue", codeContract); - } - setContract(dataContract); - })(); - }, []); + const [modal, setModal] = useState({ isOpen: false, onConfirm: undefined, title: "", message: "" }); - if (!mission) return null; return (
{/* icon */}
- +
-

{mission.structureName}

+

{hit.mission.structureName}

{/* Choix*/}

{hit.status === APPLICATION_STATUS.WAITING_ACCEPTATION && "Mission proposée au volontaire"}

@@ -85,7 +74,7 @@ const Hit = ({ hit, index, young, onChangeApplication }) => {
- {mission.name} + {hit.mission.name}
@@ -93,11 +82,11 @@ const Hit = ({ hit, index, young, onChangeApplication }) => {
Du - {formatStringDateTimezoneUTC(mission.startAt)} + {formatStringDateTimezoneUTC(hit.mission.startAt)}
Au - {formatStringDateTimezoneUTC(mission.endAt)} + {formatStringDateTimezoneUTC(hit.mission.endAt)}
@@ -105,7 +94,7 @@ const Hit = ({ hit, index, young, onChangeApplication }) => {
{["VALIDATED", "IN_PROGRESS", "DONE"].includes(hit.status) ? (
- {contract?.invitationSent === "true" ? ( + {hit.contract?.invitationSent === "true" ? (
Contrat {hit.contractStatus === "VALIDATED" ? "signé" : "envoyé"}
) : (
@@ -122,10 +111,10 @@ const Hit = ({ hit, index, young, onChangeApplication }) => {
)}
- ) : mission.placesLeft <= 1 ? ( -
{mission.placesLeft} place disponible
+ ) : hit.mission.placesLeft <= 1 ? ( +
{hit.mission.placesLeft} place disponible
) : ( -
{mission.placesLeft} places disponibles
+
{hit.mission.placesLeft} places disponibles
)}
{ data-for="tooltip-application"> - +
Voir la candidature
@@ -154,45 +143,48 @@ const Hit = ({ hit, index, young, onChangeApplication }) => { dropdownClassName="right-3" /> {hit.status === "WAITING_VALIDATION" && ( -
{ - setModal({ - isOpen: true, - title: "Renvoyer un mail", - message: "Souhaitez-vous renvoyer un mail à la structure ?", - onConfirm: async () => { - try { - const responseNotification = await api.post(`/application/${hit._id}/notify/${SENDINBLUE_TEMPLATES.referent.RELANCE_APPLICATION}`); - if (!responseNotification?.ok) return toastr.error(translate(responseNotification?.code), "Une erreur s'est produite avec le service de notification."); - toastr.success("L'email a bien été envoyé"); - } catch (e) { - toastr.error("Une erreur est survenue lors de l'envoi du mail", e.message); - } - }, - }); - }}> - Relancer la structure + <> + + {/* @ts-expect-error jsx component */} setModal({ isOpen: false, onConfirm: null })} + onCancel={() => setModal({ isOpen: false, onConfirm: undefined, title: "", message: "" })} onConfirm={() => { - modal?.onConfirm(); - setModal({ isOpen: false, onConfirm: null }); + modal.onConfirm && modal?.onConfirm(); + setModal({ isOpen: false, onConfirm: undefined, title: "", message: "" }); }} /> -
+ )}
{/* tags */}
- - {mission.domains && mission.domains.map((tag, index) => )} - {mission.isMilitaryPreparation === "true" && } + + {hit.mission.domains && hit.mission.domains.map((tag, index) => )} + {hit.mission.isMilitaryPreparation === "true" && }
diff --git a/admin/src/scenes/volontaires/view/phase2bis/components/SelectStatusApplicationPhase2.jsx b/admin/src/scenes/volontaires/view/phase2bis/components/SelectStatusApplicationPhase2.jsx index 540c24a153..3a6f187666 100644 --- a/admin/src/scenes/volontaires/view/phase2bis/components/SelectStatusApplicationPhase2.jsx +++ b/admin/src/scenes/volontaires/view/phase2bis/components/SelectStatusApplicationPhase2.jsx @@ -1,14 +1,13 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { useSelector } from "react-redux"; import { toastr } from "react-redux-toastr"; import api from "../../../../../services/api"; import ModalConfirm from "../../../../../components/modals/ModalConfirm"; import ModalConfirmWithMessage from "../../../../../components/modals/ModalConfirmWithMessage"; -import { APPLICATION_STATUS, ROLES, colors, SENDINBLUE_TEMPLATES, translate, translateApplication } from "../../../../../utils"; +import { APPLICATION_STATUS, ROLES, SENDINBLUE_TEMPLATES, translate, translateApplication } from "../../../../../utils"; import { BiChevronDown } from "react-icons/bi"; export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dropdownClassName = "" }) => { - const [application, setApplication] = useState(null); const [modalConfirm, setModalConfirm] = useState({ isOpen: false, onConfirm: null }); const [modalRefuse, setModalRefuse] = useState({ isOpen: false, onConfirm: null }); const [modalDone, setModalDone] = useState({ isOpen: false, onConfirm: null }); @@ -54,25 +53,7 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro }, }; - const fetchApplication = async () => { - try { - const id = hit && hit._id; - if (!id) return setApplication(null); - const { data, ok } = await api.get(`/application/${id}`); - if (!ok) return setApplication(null); - setApplication(data); - } catch (error) { - console.log(error); - toastr.error("Oups, une erreur est survenue lors de la récupération du statut de la candidature", translate(error.code)); - } - }; - useEffect(() => { - fetchApplication(); - }, [hit._id]); - - if (!application) return Chargement...; - - options = lookUpAuthorizedStatus({ status: application.status, role: user.role }); + options = lookUpAuthorizedStatus({ status: hit.status, role: user.role }); const onClickStatus = (status) => { setDropDownOpen(false); @@ -91,9 +72,8 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro const setStatus = async (status, message, duration) => { try { - const { ok, code, data } = await api.put("/application", { _id: application._id, status, missionDuration: duration }); + const { ok, code, data } = await api.put("/application", { _id: hit._id, status, missionDuration: duration }); if (!ok) return toastr.error("Une erreur s'est produite :", translate(code)); - setApplication(data); toastr.success("Mis à jour!"); if (status === APPLICATION_STATUS.VALIDATED) { await api.post(`/application/${data._id}/notify/${SENDINBLUE_TEMPLATES.referent.YOUNG_VALIDATED}`); @@ -114,15 +94,15 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro <>
1 && "cursor-pointer"}`}>
setDropDownOpen((dropDownOpen) => !dropDownOpen)}> -
-
{translateApplication(application.status)}
+
+
{translateApplication(hit.status)}
{options.length >= 1 && }
{dropDownOpen && (
{options - .filter((e) => e !== application.status) + .filter((e) => e !== hit.status) .map((status) => { return (
onClickStatus(status)}> @@ -137,7 +117,7 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro setModalRefuse({ isOpen: false, onConfirm: null })} onConfirm={(msg) => { setStatus(APPLICATION_STATUS.REFUSED, msg); @@ -147,10 +127,10 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro setModalDone({ isOpen: false, onConfirm: null })} type="missionduration" - defaultInput={application.missionDuration} + defaultInput={hit.missionDuration} placeholder="Nombre d'heures" onConfirm={(duration) => { setStatus(APPLICATION_STATUS.DONE, null, duration); diff --git a/admin/src/scenes/volontaires/view/phase2bis/view.jsx b/admin/src/scenes/volontaires/view/phase2bis/view.jsx index ffdfc34959..8f0518997a 100644 --- a/admin/src/scenes/volontaires/view/phase2bis/view.jsx +++ b/admin/src/scenes/volontaires/view/phase2bis/view.jsx @@ -7,22 +7,19 @@ import { ROLES, applicationExportFields, formatDateFRTimezoneUTC, formatLongDate import Menu from "../../../../assets/icons/Menu"; import Pencil from "../../../../assets/icons/Pencil"; import { Box, BoxTitle } from "../../../../components/box"; -import DownloadAttestationButton from "../../../../components/buttons/DownloadAttestationButton"; -import MailAttestationButton from "../../../../components/buttons/MailAttestationButton"; import { ModalExport } from "../../../../components/filters-system-v2"; import SelectStatus from "../../../../components/selectStatus"; import { capture } from "../../../../sentry"; import api from "../../../../services/api"; -import { ENABLE_PM, ES_NO_LIMIT, YOUNG_PHASE, YOUNG_STATUS_PHASE2, translate } from "../../../../utils"; +import { YOUNG_PHASE, YOUNG_STATUS_PHASE2, translate } from "../../../../utils"; import YoungHeader from "../../../phase0/components/YoungHeader"; -import CardEquivalence from "../../components/Equivalence"; import Toolbox from "../../components/Toolbox"; -import Phase2militaryPrepartionV2 from "../phase2MilitaryPreparationV2"; +import Phase2MilitaryPreparation from "../phase2MilitaryPreparationV2"; import ApplicationList2 from "./applicationList2"; import Preferences from "./preferences"; +import EquivalenceList from "./EquivalenceList"; export default function Phase2({ young, onChange }) { - const [equivalences, setEquivalences] = React.useState([]); const [blocOpened, setBlocOpened] = useState("missions"); const [editPreference, setEditPreference] = useState(false); const [savePreference, setSavePreference] = useState(false); @@ -33,49 +30,26 @@ export default function Phase2({ young, onChange }) { const user = useSelector((state) => state.Auth.user); const [dataPreference, setDataPreference] = React.useState({ - professionnalProject: "", - professionnalProjectPrecision: "", - engaged: "", - desiredLocation: "", - engagedDescription: "", - domains: [], - missionFormat: "", - mobilityTransport: [], - period: "", - mobilityTransportOther: "", - mobilityNearHome: "false", - mobilityNearSchool: "false", - mobilityNearRelative: "false", - mobilityNearRelativeName: "", - mobilityNearRelativeAddress: "", - mobilityNearRelativeZip: "", - mobilityNearRelativeCity: "", - periodRanking: [], + professionnalProject: young?.professionnalProject || "", + professionnalProjectPrecision: young?.professionnalProjectPrecision || "", + engaged: young?.engaged || "false", + desiredLocation: young?.desiredLocation || "", + engagedDescription: young?.engagedDescription || "", + domains: young?.domains ? [...young.domains] : [], + missionFormat: young?.missionFormat || "", + mobilityTransport: young?.mobilityTransport ? [...young.mobilityTransport] : [], + period: young?.period || "", + mobilityTransportOther: young?.mobilityTransportOther || "", + mobilityNearHome: young?.mobilityNearHome || "false", + mobilityNearSchool: young?.mobilityNearSchool || "false", + mobilityNearRelative: young?.mobilityNearRelative || "false", + mobilityNearRelativeName: young?.mobilityNearRelativeName || "", + mobilityNearRelativeAddress: young?.mobilityNearRelativeAddress || "", + mobilityNearRelativeZip: young?.mobilityNearRelativeZip || "", + mobilityNearRelativeCity: young?.mobilityNearRelativeCity || "", + periodRanking: young?.periodRanking ? [...young.periodRanking] : [], }); - React.useEffect(() => { - setDataPreference({ - professionnalProject: young?.professionnalProject || "", - professionnalProjectPrecision: young?.professionnalProjectPrecision || "", - engaged: young?.engaged || "false", - desiredLocation: young?.desiredLocation || "", - engagedDescription: young?.engagedDescription || "", - domains: young?.domains ? [...young.domains] : [], - missionFormat: young?.missionFormat || "", - mobilityTransport: young?.mobilityTransport ? [...young.mobilityTransport] : [], - period: young?.period || "", - mobilityTransportOther: young?.mobilityTransportOther || "", - mobilityNearHome: young?.mobilityNearHome || "false", - mobilityNearSchool: young?.mobilityNearSchool || "false", - mobilityNearRelative: young?.mobilityNearRelative || "false", - mobilityNearRelativeName: young?.mobilityNearRelativeName || "", - mobilityNearRelativeAddress: young?.mobilityNearRelativeAddress || "", - mobilityNearRelativeZip: young?.mobilityNearRelativeZip || "", - mobilityNearRelativeCity: young?.mobilityNearRelativeCity || "", - periodRanking: young?.periodRanking ? [...young.periodRanking] : [], - }); - }, [young, editPreference]); - const onSubmit = async () => { try { let error = {}; @@ -281,13 +255,6 @@ export default function Phase2({ young, onChange }) { }); } - React.useEffect(() => { - (async () => { - const { ok, data } = await api.get(`/young/${young._id.toString()}/phase2/equivalences`); - if (ok) return setEquivalences(data); - })(); - }, [young]); - function getExportFields() { if ([ROLES.RESPONSIBLE, ROLES.SUPERVISOR].includes(user.role)) { return applicationExportFields.filter((e) => !["choices", "identity", "contact", "address", "location"].includes(e.id)); @@ -345,10 +312,8 @@ export default function Phase2({ young, onChange }) { - {ENABLE_PM ? : null} - {equivalences.map((equivalence, index) => ( - - ))} + + diff --git a/api/src/__tests__/referent.test.ts b/api/src/__tests__/referent.test.ts index 966cc4b073..17d23eca1a 100644 --- a/api/src/__tests__/referent.test.ts +++ b/api/src/__tests__/referent.test.ts @@ -541,7 +541,7 @@ describe("Referent", () => { expect(res.statusCode).toEqual(200); expect(res.body.data.applications).toEqual([ { - ...application.toObject(), + ...JSON.parse(JSON.stringify(application.toObject())), structure: { ...structure.toObject(), _id: structure._id.toString(), diff --git a/api/src/controllers/young/index.ts b/api/src/controllers/young/index.ts index 3ea1c7d5a5..9fa8b4bd20 100644 --- a/api/src/controllers/young/index.ts +++ b/api/src/controllers/young/index.ts @@ -14,7 +14,7 @@ import { getRedisClient } from "../../redis"; import config from "config"; import { logger } from "../../logger"; import { capture, captureMessage } from "../../sentry"; -import { ReferentModel, YoungModel, ApplicationModel, MissionModel, SessionPhase1Model, LigneBusModel, ClasseModel, CohortModel } from "../../models"; +import { ReferentModel, YoungModel, ApplicationModel, SessionPhase1Model, LigneBusModel, ClasseModel, CohortModel, ApplicationDocument } from "../../models"; import AuthObject from "../../auth"; import { uploadFile, @@ -58,6 +58,8 @@ import { getDepartmentForEligibility, FUNCTIONAL_ERRORS, CohortDto, + MissionType, + ContractType, } from "snu-lib"; import { getFilteredSessions } from "../../utils/cohort"; import { anonymizeApplicationsFromYoungId } from "../../services/application"; @@ -654,18 +656,26 @@ router.get("/:id/application", passport.authenticate(["referent", "young"], { se return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); } - let data = await ApplicationModel.find({ youngId: id }); - if (!data) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - - for (let i = 0; i < data.length; i++) { - const application = data[i]; - const mission = await MissionModel.findById(application.missionId); - let tutor: ReferentType | null = null; - if (mission?.tutorId) tutor = await ReferentModel.findById(mission.tutorId); - if (mission?.tutorId && !application.tutorId) application.tutorId = mission.tutorId; - if (mission?.structureId && !application.structureId) application.structureId = mission.structureId; - data[i] = { ...serializeApplication(application), mission, tutor }; + const { error: queryError, value: isMilitaryPreparation } = Joi.boolean().validate(req.query.isMilitaryPreparation); + if (queryError) { + capture(queryError); + return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); + } + + const query: any = { youngId: id }; + if (isMilitaryPreparation !== undefined) { + query.isMilitaryPreparation = isMilitaryPreparation; } + + type PopulatedApplication = ApplicationDocument & { mission: MissionType; tutor: ReferentType; contract: ContractType }; + let data: PopulatedApplication[] = await ApplicationModel.find(query).populate("mission").populate("contract").populate("tutor"); + + for (let application of data) { + if (application.mission?.tutorId && !application.tutorId) application.tutorId = application.mission.tutorId; + if (application.mission?.structureId && !application.structureId) application.structureId = application.mission.structureId; + application = { ...serializeApplication(application), mission: application.mission, tutor: application.tutor, contract: application.contract }; + } + return res.status(200).send({ ok: true, data }); } catch (error) { capture(error); diff --git a/api/src/models/application.ts b/api/src/models/application.ts index 902b4675c3..4b37205212 100644 --- a/api/src/models/application.ts +++ b/api/src/models/application.ts @@ -4,6 +4,7 @@ import patchHistory from "mongoose-patch-history"; import mongooseElastic from "@selego/mongoose-elastic"; import esClient from "../es"; import anonymize from "../anonymization/application"; +import { MissionModel, ReferentModel, ContractModel } from "."; import { ApplicationSchema, InterfaceExtended } from "snu-lib"; import { DocumentExtended, CustomSaveParams, UserExtension, UserSaved } from "./types"; @@ -19,6 +20,20 @@ schema.virtual("mission", { justOne: true, }); +schema.virtual("contract", { + ref: "contract", + localField: "contractId", + foreignField: "_id", + justOne: true, +}); + +schema.virtual("tutor", { + ref: "referent", + localField: "tutorId", + foreignField: "_id", + justOne: true, +}); + schema.methods.anonymise = function () { return anonymize(this); }; @@ -30,6 +45,9 @@ schema.virtual("user").set(function (user: UserSaved) { } }); +schema.set("toObject", { virtuals: true }); +schema.set("toJSON", { virtuals: true }); + schema.pre("save", function (next, params: CustomSaveParams) { this.user = params?.fromUser; this.updatedAt = new Date(); diff --git a/api/src/referent/referentController.ts b/api/src/referent/referentController.ts index 9c53644a04..c9b6edc04a 100644 --- a/api/src/referent/referentController.ts +++ b/api/src/referent/referentController.ts @@ -1362,7 +1362,7 @@ router.get("/young/:id", passport.authenticate("referent", { session: false, fai let applications: any[] = []; for (let application of applicationsFromDb) { const structure = await StructureModel.findById(application.structureId); - applications.push({ ...application._doc, structure: structure ? serializeStructure(structure, req.user) : null }); + applications.push({ ...application.toObject(), structure: structure ? serializeStructure(structure, req.user) : null }); } let etablissement: EtablissementDocument | null = null;