Skip to content

Commit

Permalink
Merge branch 'main' into 3404-monstache
Browse files Browse the repository at this point in the history
  • Loading branch information
pitiscarf authored Oct 8, 2024
2 parents eaf8e21 + 3ff4c0b commit 96806e3
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 46 deletions.
66 changes: 62 additions & 4 deletions admin/src/scenes/etablissement/components/ButtonAddCoordinator.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { HiPlus } from "react-icons/hi";
import { toastr } from "react-redux-toastr";

import { Button, ModalConfirmation, InputText } from "@snu/ds/admin";
import { Button, ModalConfirmation, InputText, Label, Select } from "@snu/ds/admin";
import { ProfilePic } from "@snu/ds";
import validator from "validator";
import { capture } from "@/sentry";
import api from "@/services/api";
import { ERRORS, translate, EtablissementDto } from "snu-lib";
import { ERRORS, translate, ReferentType, EtablissementType } from "snu-lib";

interface Props {
etablissement: EtablissementDto;
etablissement: EtablissementType;
onChange: () => void;
}
interface Errors {
Expand All @@ -20,6 +20,7 @@ interface Errors {
coordinator?: string;
}
interface NewCoordinator {
_id?: string;
firstName: string;
lastName: string;
email: string;
Expand All @@ -29,6 +30,44 @@ export default function ButtonAddCoordinator({ etablissement, onChange }: Props)
const [modalAddCoordinator, setModalAddCoordinator] = useState(false);
const [newCoordinator, setNewCoordinator] = useState<NewCoordinator>({ firstName: "", lastName: "", email: "" });
const [errors, setErrors] = useState<Errors>({});
const [referentList, setReferentList] = useState<ReferentType[]>([]);

useEffect(() => {
loadReferents({ etablissementId: etablissement._id, coordinateurs: etablissement.coordinateurs });
}, []);

const loadReferents = async ({ etablissementId, coordinateurs }) => {
try {
const { ok, code, data: classes } = await api.get(`/cle/classe/from-etablissement/${etablissementId}`);
if (!ok) {
return toastr.error("Oups, une erreur est survenue lors de la récupération des referents", translate(code));
}

const refList = classes.flatMap((classe) =>
classe.referent
.filter((r) => Boolean(r))
.map((referent) => ({
...referent,
value: referent._id,
label: `${referent.firstName} ${referent.lastName}`,
})),
);
const uniqueIds = new Set();

const uniqueArray = [...refList].filter((item) => {
if (!uniqueIds.has(item._id)) {
uniqueIds.add(item._id);
return true;
}
return false;
});
const uniqueArrayFiltered = uniqueArray.filter((referent) => !coordinateurs.find((c) => c._id === referent._id));
setReferentList(uniqueArrayFiltered);
} catch (e) {
capture(e);
toastr.error("Oups, une erreur est survenue lors de la récupération des référents", "");
}
};

const sendInvitation = async () => {
try {
Expand Down Expand Up @@ -91,6 +130,7 @@ export default function ButtonAddCoordinator({ etablissement, onChange }: Props)
text={
<div className="mt-6 w-[636px] text-left text-ds-gray-900">
{errors.coordinator && <div className="text-red-500 mb-2">{errors.coordinator}</div>}
<Label name="" title="Renseignez les informations du nouveau coordinateur ..." />
<InputText
className="mb-3"
label="Nom"
Expand Down Expand Up @@ -119,6 +159,24 @@ export default function ButtonAddCoordinator({ etablissement, onChange }: Props)
value={newCoordinator.email}
onChange={(e) => setNewCoordinator({ ...newCoordinator, email: e.target.value })}
/>
<div className="flex-1">
<Label name="" title="... ou ajoutez référent de classe existant" />
<Select
className="mb-3"
isActive={true}
placeholder={"Choisir un référent existant"}
noOptionsMessage={"Aucun référent trouvé"}
// @ts-ignore
options={referentList}
closeMenuOnSelect={true}
isClearable={true}
value={newCoordinator?._id ? { label: `${newCoordinator?.firstName} ${newCoordinator?.lastName}`, value: newCoordinator._id } : null}
onChange={(options) => {
// @ts-ignore
options ? setNewCoordinator(referentList.find((referent) => referent._id === options.value)) : setNewCoordinator({});
}}
/>
</div>
</div>
}
actions={[
Expand Down
6 changes: 3 additions & 3 deletions admin/src/scenes/etablissement/components/GeneralInfos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import api from "@/services/api";
import { capture } from "@/sentry";
import { Button, Container, InputText, Label, Select } from "@snu/ds/admin";
import { AddressForm, Input } from "@snu/ds/common";
import { ROLES, SUB_ROLES, CLE_TYPE_LIST, CLE_SECTOR_LIST, useAddress, translate, EtablissementDto } from "snu-lib";
import { ROLES, SUB_ROLES, CLE_TYPE_LIST, CLE_SECTOR_LIST, useAddress, translate, EtablissementType } from "snu-lib";
import { User } from "@/types";

interface Props {
etablissement: EtablissementDto;
onUpdateEtab: (etablissement: EtablissementDto) => void;
etablissement: EtablissementType;
onUpdateEtab: (etablissement: EtablissementType) => void;
user: User;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { useHistory } from "react-router-dom";
import { ProfilePic } from "@snu/ds";
import { ModalConfirmation } from "@snu/ds/admin";
import { User } from "@/types";
import { isChefEtablissement, EtablissementDto } from "snu-lib";
import { isChefEtablissement, EtablissementType } from "snu-lib";

import { REFERENT_SIGNUP_FIRSTTIME_LOCAL_STORAGE_KEY } from "@/services/cle";

interface Props {
user: User;
etablissement: EtablissementDto;
etablissement: EtablissementType;
}

/* First login ADMINISTRATEUR_CLE referent_etablissement */
Expand Down
18 changes: 6 additions & 12 deletions admin/src/scenes/etablissement/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ export interface EtablissementExport extends EtablissementType {
phone: string;
email: string;
}[];
coordinateurs: {
firstName: string;
lastName: string;
phone: string;
email: string;
}[];
}

export function exportExcelSheet(etablissements: EtablissementExport[]) {
Expand All @@ -40,12 +34,12 @@ export function exportExcelSheet(etablissements: EtablissementExport[]) {
phone: e.referentEtablissement.length ? e.referentEtablissement[0].phone : "",
email: e.referentEtablissement.length ? e.referentEtablissement[0].email : "",
// coordinateurs
coordinateur1FullName: e.coordinateurs.length ? `${e.coordinateurs[0]?.firstName} ${e.coordinateurs[0]?.lastName}` : "",
coordinateur1Phone: e.coordinateurs.length ? e.coordinateurs[0]?.phone : "",
coordinateur1Email: e.coordinateurs.length ? e.coordinateurs[0]?.email : "",
coordinateur2FullName: e.coordinateurs.length > 1 ? `${e.coordinateurs[1]?.firstName} ${e.coordinateurs[1]?.lastName}` : "",
coordinateur2Phone: e.coordinateurs.length ? e.coordinateurs[1]?.phone : "",
coordinateur2Email: e.coordinateurs.length ? e.coordinateurs[1]?.email : "",
coordinateur1FullName: e.coordinateurs ? `${e.coordinateurs[0]?.firstName} ${e.coordinateurs[0]?.lastName}` : "",
coordinateur1Phone: e.coordinateurs ? e.coordinateurs[0]?.phone : "",
coordinateur1Email: e.coordinateurs ? e.coordinateurs[0]?.email : "",
coordinateur2FullName: e.coordinateurs && e.coordinateurs.length > 1 ? `${e.coordinateurs[1]?.firstName} ${e.coordinateurs[1]?.lastName}` : "",
coordinateur2Phone: e.coordinateurs && e.coordinateurs.length > 1 ? e.coordinateurs[1]?.phone : "",
coordinateur2Email: e.coordinateurs && e.coordinateurs.length > 1 ? e.coordinateurs[1]?.email : "",
updatedAt: dayjs(e.updatedAt).format("DD/MM/YYYY HH:mm"),
createdAt: dayjs(e.createdAt).format("DD/MM/YYYY HH:mm"),
}));
Expand Down
4 changes: 2 additions & 2 deletions admin/src/scenes/etablissement/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HiHome, HiPlus } from "react-icons/hi";
import { toastr } from "react-redux-toastr";

import { Page, Header, Button } from "@snu/ds/admin";
import { ROLES, translate, isCoordinateurEtablissement, isChefEtablissement, isReferentOrAdmin, ReferentDto, EtablissementDto } from "snu-lib";
import { ROLES, translate, isCoordinateurEtablissement, isChefEtablissement, isReferentOrAdmin, ReferentDto, EtablissementType } from "snu-lib";
import api from "@/services/api";
import { capture } from "@/sentry";
import Loader from "@/components/Loader";
Expand All @@ -22,7 +22,7 @@ export default function View() {
const user = useSelector((state: AuthState) => state.Auth.user);
const { id } = useParams<{ id: string }>();
const [classeId, setClasseId] = useState("");
const [etablissement, setEtablissement] = useState<EtablissementDto | null>(null);
const [etablissement, setEtablissement] = useState<EtablissementType | null>(null);
const [contacts, setContacts] = useState<ReferentDto[]>([]);

const history = useHistory();
Expand Down
11 changes: 8 additions & 3 deletions api/src/cle/referent/referentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { UserRequest } from "../../controllers/request";
import { capture } from "../../sentry";
import { ERRORS } from "../../utils";
import { EtablissementModel } from "../../models";
import { findOrCreateReferent, inviteReferent } from "../../services/cle/referent";
import { findOrCreateReferent, inviteReferent, inviteReferentClasseAsCoordinator } from "../../services/cle/referent";
import { generateCSVStream } from "../../services/fileService";
import { isFeatureAvailable } from "../../featureFlag/featureFlagService";

Expand Down Expand Up @@ -66,8 +66,13 @@ router.post("/invite-coordonnateur", passport.authenticate("referent", { session

await etablissement.save({ fromUser: req.user });

// We send the email invitation once we are sure both the referent and the classe are created
await inviteReferent(referent, { role: SUB_ROLES.coordinateur_cle, from: req.user }, etablissement);
if (referent.role === ROLES.REFERENT_CLASSE) {
referent.set({ role: ROLES.ADMINISTRATEUR_CLE, subRole: SUB_ROLES.coordinateur_cle });
await referent.save({ fromUser: req.user });
await inviteReferentClasseAsCoordinator(referent, { from: req.user }, etablissement);
} else {
await inviteReferent(referent, { role: SUB_ROLES.coordinateur_cle, from: req.user }, etablissement);
}

return res.status(200).send({ ok: true });
} catch (error) {
Expand Down
39 changes: 21 additions & 18 deletions api/src/referent/referentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1524,25 +1524,28 @@ router.delete("/:id", passport.authenticate("referent", { session: false, failWi

if (!canDeleteReferent({ actor: req.user, originalTarget: referent, structure })) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED });

const referents = await ReferentModel.find({ structureId: referent.structureId });
const missionsLinkedToReferent = await MissionModel.find({ tutorId: referent._id }).countDocuments();
if (referents.length === 1) return res.status(409).send({ ok: false, code: ERRORS.LINKED_STRUCTURE });
if (missionsLinkedToReferent) return res.status(409).send({ ok: false, code: ERRORS.LINKED_MISSIONS });

const classes = await ClasseModel.find({ referentClasseIds: { $in: [referent._id] } });
if (classes.length > 0) return res.status(409).send({ ok: false, code: ERRORS.LINKED_CLASSES });

if (referent.subRole === SUB_ROLES.referent_etablissement && referent.role === ROLES.ADMINISTRATEUR_CLE) {
const etablissement = await EtablissementModel.findOne({ referentEtablissementIds: { $in: [referent._id] } });
if (etablissement) return res.status(409).send({ ok: false, code: ERRORS.LINKED_ETABLISSEMENT });
}
if (referent.role === ROLES.RESPONSIBLE || referent.role === ROLES.SUPERVISOR) {
const referents = await ReferentModel.find({ structureId: referent.structureId });
const missionsLinkedToReferent = await MissionModel.find({ tutorId: referent._id }).countDocuments();
if (referents.length === 1) return res.status(409).send({ ok: false, code: ERRORS.LINKED_STRUCTURE });
if (missionsLinkedToReferent) return res.status(409).send({ ok: false, code: ERRORS.LINKED_MISSIONS });
}
if (referent.role === ROLES.ADMINISTRATEUR_CLE || referent.role === ROLES.REFERENT_CLASSE) {
const classes = await ClasseModel.find({ referentClasseIds: { $in: [referent._id] }, schoolYear: ClasseSchoolYear.YEAR_2024_2025 });
if (classes.length > 0) return res.status(409).send({ ok: false, code: ERRORS.LINKED_CLASSES });

if (referent.subRole === SUB_ROLES.referent_etablissement) {
const etablissement = await EtablissementModel.findOne({ referentEtablissementIds: { $in: [referent._id] } });
if (etablissement) return res.status(409).send({ ok: false, code: ERRORS.LINKED_ETABLISSEMENT });
}

if (referent.subRole === SUB_ROLES.coordinateur_cle && referent.role === ROLES.ADMINISTRATEUR_CLE) {
const etablissement = await EtablissementModel.findOne({ coordinateurIds: { $in: [referent._id] } });
if (etablissement) {
const coordinateur = etablissement.coordinateurIds.filter((c) => c.toString() !== referent._id.toString());
etablissement.set({ coordinateurIds: coordinateur });
await etablissement.save({ fromUser: req.user });
if (referent.subRole === SUB_ROLES.coordinateur_cle) {
const etablissement = await EtablissementModel.findOne({ coordinateurIds: { $in: [referent._id] } });
if (etablissement) {
const coordinateur = etablissement.coordinateurIds.filter((c) => c.toString() !== referent._id.toString());
etablissement.set({ coordinateurIds: coordinateur });
await etablissement.save({ fromUser: req.user });
}
}
}

Expand Down
21 changes: 21 additions & 0 deletions api/src/services/cle/referent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export const findOrCreateReferent = async (referent, { etablissement, role, subR
// Return if already exists
if (referent._id) return referent;

const classes = await ClasseModel.find({ etablissementId: etablissement._id, schoolYear: ClasseSchoolYear.YEAR_2024_2025 });
const refClasseIds = classes.map((c) => c.referentClasseIds).flat();
const referentClasse = await ReferentModel.findOne({ email: referent.email, role: ROLES.REFERENT_CLASSE, _id: { $in: refClasseIds } });
if (referentClasse) return referentClasse;

// Create referent
if (!referent.email || !referent.firstName || !referent.lastName) throw new Error("Missing referent email or firstName or lastName");
const invitationToken = crypto.randomBytes(20).toString("hex");
Expand All @@ -55,6 +60,22 @@ export const findOrCreateReferent = async (referent, { etablissement, role, subR
}
};

export const inviteReferentClasseAsCoordinator = async (
referent: Pick<ReferentType, "firstName" | "lastName" | "email">,
{ from }: { from: UserDto | null },
etablissement: EtablissementType,
) => {
// Send invite
const fromName = `${from?.firstName || null} ${from?.lastName || null}`;
const toName = `${referent.firstName} ${referent.lastName}`;
const name_school = `${etablissement.name}`;

return await sendTemplate(SENDINBLUE_TEMPLATES.CLE.REFERENT_CLASSE_ADDED_AS_COORDINATOR, {
emailTo: [{ name: `${referent.firstName} ${referent.lastName}`, email: referent.email }],
params: { fromName, toName, name_school },
});
};

export const inviteReferent = async (
referent: Pick<ReferentType, "firstName" | "lastName" | "email" | "invitationToken">,
{ role, from }: { role: UserDto["role"]; from: UserDto | null },
Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/constants/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ const SENDINBLUE_TEMPLATES = {
},
CLE: {
CLASSE_CREATED: "2083",
REFERENT_CLASSE_ADDED_AS_COORDINATOR: "2206",
CONFIRM_SIGNUP_COORDINATEUR: "1413",
CONFIRM_SIGNUP_REFERENT_ETABLISSEMENT: "1395",
CONFIRM_REINSCRIPTION_REFERENT_ETABLISSEMENT: "2088",
Expand Down
6 changes: 4 additions & 2 deletions packages/lib/src/mongoSchema/cle/etablissement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Schema, InferSchemaType } from "mongoose";

import { CLE_SECTOR_LIST, CLE_TYPE_LIST } from "../../constants/constants";

import { InterfaceExtended } from "..";
import { InterfaceExtended, ReferentType } from "..";

export const EtablissementSchema = {
schoolId: {
Expand Down Expand Up @@ -142,4 +142,6 @@ export const EtablissementSchema = {
};

const schema = new Schema(EtablissementSchema);
export type EtablissementType = InterfaceExtended<InferSchemaType<typeof schema>>;
export type EtablissementType = InterfaceExtended<InferSchemaType<typeof schema>> & {
coordinateurs?: Pick<ReferentType, "_id" | "email" | "firstName" | "lastName" | "phone" | "role" | "subRole">[];
};

0 comments on commit 96806e3

Please sign in to comment.