Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(admin): 3559 - Optimiser le chargement de la liste de candidatures #4500

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions admin/src/scenes/volontaires/view/phase2bis/EquivalenceList.tsx
Original file line number Diff line number Diff line change
@@ -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<MissionEquivalenceType[]>([]);

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 <Loader />;

return (
<section id="equivalences">
{equivalences.map((equivalence) => (
<CardEquivalence key={equivalence._id} equivalence={equivalence} young={young} />
))}
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,105 +7,94 @@ 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<PopulatedApplicationType[]>();

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 <Loader />;
if (!applications.length) return <div className="m-8 text-center italic">Aucune candidature n&apos;est liée à ce volontaire.</div>;
if (loading) return <Loader />;
if (!applications?.length) return <div className="m-8 text-center italic">Aucune candidature n&apos;est liée à ce volontaire.</div>;
return (
<div className="space-y-8 px-12 pt-6 pb-12">
{applications.map((hit, i) => (
<Hit key={hit._id} young={young} hit={hit} index={i} onChangeApplication={onChangeApplication} optionsType={optionsType} />
{applications.map((hit) => (
<Hit key={hit._id} young={young} hit={hit} onChangeApplication={onChangeApplication} />
))}
</div>
);
}

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<modalType>({ isOpen: false, onConfirm: undefined, title: "", message: "" });

if (!mission) return null;
return (
<div className="flex gap-6 rounded-xl bg-white p-3 shadow-ninaButton">
{/* icon */}
<div className="my-auto pl-2">
<IconDomain domain={mission?.isMilitaryPreparation === "true" ? "PREPARATION_MILITARY" : mission?.mainDomain} />
<IconDomain domain={hit.mission?.isMilitaryPreparation === "true" ? "PREPARATION_MILITARY" : hit.mission?.mainDomain} />
</div>

<div className="grid flex-1 grid-rows-4">
<div className="flex items-center justify-between text-xs font-medium uppercase tracking-wider text-gray-500">
<p className="">{mission.structureName}</p>
<p className="">{hit.mission.structureName}</p>
{/* Choix*/}
<p className="">{hit.status === APPLICATION_STATUS.WAITING_ACCEPTATION && "Mission proposée au volontaire"}</p>
</div>

<div className="row-span-2 flex items-center justify-between">
<div className="w-1/2 overflow-hidden">
<Link to={`/mission/${hit.missionId}`} className="my-auto text-lg font-semibold leading-6 text-gray-900">
{mission.name}
{hit.mission.name}
</Link>
</div>

{/* date */}
<div className="w-1/6 space-y-1 text-xs font-medium">
<div>
<span className="text-gray-500">Du </span>
<span className="text-gray-700">{formatStringDateTimezoneUTC(mission.startAt)}</span>
<span className="text-gray-700">{formatStringDateTimezoneUTC(hit.mission.startAt)}</span>
</div>
<div>
<span className="text-gray-500">Au </span>
<span className="text-gray-700">{formatStringDateTimezoneUTC(mission.endAt)}</span>
<span className="text-gray-700">{formatStringDateTimezoneUTC(hit.mission.endAt)}</span>
</div>
</div>

{/* places disponibles */}
<div className="flex w-1/6 justify-between">
{["VALIDATED", "IN_PROGRESS", "DONE"].includes(hit.status) ? (
<div className="flex flex-col">
{contract?.invitationSent === "true" ? (
{hit.contract?.invitationSent === "true" ? (
<div className="text-xs font-medium text-gray-700 ">Contrat {hit.contractStatus === "VALIDATED" ? "signé" : "envoyé"}</div>
) : (
<div className="flex flex-row items-center">
Expand All @@ -122,10 +111,10 @@ const Hit = ({ hit, index, young, onChangeApplication }) => {
</div>
)}
</div>
) : mission.placesLeft <= 1 ? (
<div className="text-xs font-medium text-gray-700"> {mission.placesLeft} place disponible</div>
) : hit.mission.placesLeft <= 1 ? (
<div className="text-xs font-medium text-gray-700"> {hit.mission.placesLeft} place disponible</div>
) : (
<div className="text-xs font-medium text-gray-700"> {mission.placesLeft} places disponibles</div>
<div className="text-xs font-medium text-gray-700"> {hit.mission.placesLeft} places disponibles</div>
)}
<div>
<NavLink
Expand All @@ -135,7 +124,7 @@ const Hit = ({ hit, index, young, onChangeApplication }) => {
data-for="tooltip-application">
<Eye width={16} height={16} />
</NavLink>
<ReactTooltip id="tooltip-application" type="light" place="top" effect="solid" className="custom-tooltip-radius shadow-xl" tooltipRadius="6">
<ReactTooltip id="tooltip-application" type="light" place="top" effect="solid" className="custom-tooltip-radius shadow-xl">
<div className="text-xs">Voir la candidature</div>
</ReactTooltip>
</div>
Expand All @@ -154,45 +143,48 @@ const Hit = ({ hit, index, young, onChangeApplication }) => {
dropdownClassName="right-3"
/>
{hit.status === "WAITING_VALIDATION" && (
<div
className="mt-1 cursor-pointer text-xs text-blue-600 underline"
onClick={async () => {
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
<>
<button
className="mt-1 cursor-pointer text-xs text-blue-600 underline"
onClick={async () => {
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
</button>
{/* @ts-expect-error jsx component */}
<ModalConfirm
isOpen={modal?.isOpen}
title={modal?.title}
message={modal?.message}
onCancel={() => 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: "" });
}}
/>
</div>
</>
)}
</div>
</div>

{/* tags */}
<div className="flex h-8 items-center gap-2">
<Tag tag={`${mission.city} ${mission.zip}`} />
{mission.domains && mission.domains.map((tag, index) => <Tag key={index} tag={translate(tag)} />)}
{mission.isMilitaryPreparation === "true" && <Tag tag="Préparation militaire" />}
<Tag tag={`${hit.mission.city} ${hit.mission.zip}`} />
{hit.mission.domains && hit.mission.domains.map((tag, index) => <Tag key={index} tag={translate(tag)} />)}
{hit.mission.isMilitaryPreparation === "true" && <Tag tag="Préparation militaire" />}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 });
Expand Down Expand Up @@ -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 <i style={{ color: colors.darkPurple }}>Chargement...</i>;

options = lookUpAuthorizedStatus({ status: application.status, role: user.role });
options = lookUpAuthorizedStatus({ status: hit.status, role: user.role });

const onClickStatus = (status) => {
setDropDownOpen(false);
Expand All @@ -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}`);
Expand All @@ -114,15 +94,15 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro
<>
<div ref={ref} className={`relative ${options.length > 1 && "cursor-pointer"}`}>
<div className="inline-block" onClick={() => setDropDownOpen((dropDownOpen) => !dropDownOpen)}>
<div className={`bg-${theme.background[application.status]} text-${theme.text[application.status]} flex flex-row items-center rounded`}>
<div className="p-1 text-xs font-normal">{translateApplication(application.status)}</div>
<div className={`bg-${theme.background[hit.status]} text-${theme.text[hit.status]} flex flex-row items-center rounded`}>
<div className="p-1 text-xs font-normal">{translateApplication(hit.status)}</div>
{options.length >= 1 && <BiChevronDown size={20} />}
</div>
</div>
{dropDownOpen && (
<div className={"absolute z-10 bg-white " + dropdownClassName}>
{options
.filter((e) => e !== application.status)
.filter((e) => e !== hit.status)
.map((status) => {
return (
<div key={status} className="dropdown-item" onClick={() => onClickStatus(status)}>
Expand All @@ -137,7 +117,7 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro
<ModalConfirmWithMessage
isOpen={modalRefuse.isOpen}
title="Veuillez éditer le message ci-dessous pour préciser les raisons du refus avant de l'envoyer"
message={`Une fois le message ci-dessous validé, il sera transmis par mail à ${application.youngFirstName} (${application.youngEmail}).`}
message={`Une fois le message ci-dessous validé, il sera transmis par mail à ${hit.youngFirstName} (${hit.youngEmail}).`}
onChange={() => setModalRefuse({ isOpen: false, onConfirm: null })}
onConfirm={(msg) => {
setStatus(APPLICATION_STATUS.REFUSED, msg);
Expand All @@ -147,10 +127,10 @@ export const SelectStatusApplicationPhase2 = ({ hit, options = [], callback, dro
<ModalConfirmWithMessage
isOpen={modalDone.isOpen}
title="Validation de réalisation de mission"
message={`Merci de valider le nombre d'heures effectuées par ${application.youngFirstName} pour la mission ${application.missionName}.`}
message={`Merci de valider le nombre d'heures effectuées par ${hit.youngFirstName} pour la mission ${hit.missionName}.`}
onChange={() => 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);
Expand Down
Loading
Loading