From c5ba1514616724ebd49875ad3c3003947be7eec1 Mon Sep 17 00:00:00 2001 From: C2Chandelier Date: Tue, 29 Oct 2024 17:39:55 +0100 Subject: [PATCH 01/16] up --- admin/src/components/UserCard.jsx | 11 +- .../scenes/classe/components/NavbarClasse.tsx | 44 ++++ admin/src/scenes/classe/components/types.ts | 15 ++ .../src/scenes/classe/header/ClasseHeader.tsx | 46 ++++ .../classe/{view.tsx => view/Details.tsx} | 66 +---- admin/src/scenes/classe/view/Historique.tsx | 242 ++++++++++++++++++ admin/src/scenes/classe/view/Inscriptions.tsx | 17 ++ admin/src/scenes/classe/view/index.tsx | 44 ++++ admin/src/utils/index.jsx | 4 +- admin/src/utils/translateFieldsModel.js | 58 +++++ api/src/cle/classe/classeController.ts | 21 +- api/src/controllers/patches.js | 3 +- api/src/controllers/young/index.ts | 11 +- api/src/referent/referentController.ts | 15 +- packages/ds/src/admin/form/Select/theme.ts | 2 +- 15 files changed, 534 insertions(+), 65 deletions(-) create mode 100644 admin/src/scenes/classe/components/NavbarClasse.tsx create mode 100644 admin/src/scenes/classe/header/ClasseHeader.tsx rename admin/src/scenes/classe/{view.tsx => view/Details.tsx} (78%) create mode 100644 admin/src/scenes/classe/view/Historique.tsx create mode 100644 admin/src/scenes/classe/view/Inscriptions.tsx create mode 100644 admin/src/scenes/classe/view/index.tsx diff --git a/admin/src/components/UserCard.jsx b/admin/src/components/UserCard.jsx index 49cbaeac1c..8de73d8ff9 100644 --- a/admin/src/components/UserCard.jsx +++ b/admin/src/components/UserCard.jsx @@ -13,6 +13,13 @@ export default function UserCard({ user }) { if (user?.role === "Volontaire") return `/volontaire/${user._id}`; return null; } + function getAuthor(user) { + if (user.role) { + return translate(user.role); + } else { + return "Script"; + } + } if (!user) return null; return ( @@ -22,10 +29,10 @@ export default function UserCard({ user }) { {getAvatar(user)}
-

+

{user.firstName} {user.lastName && user.lastName}

-

{translate(user?.role)}

+

{getAuthor(user)}

diff --git a/admin/src/scenes/classe/components/NavbarClasse.tsx b/admin/src/scenes/classe/components/NavbarClasse.tsx new file mode 100644 index 0000000000..fa53b4e7cb --- /dev/null +++ b/admin/src/scenes/classe/components/NavbarClasse.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { useHistory } from "react-router-dom"; +import { HiOutlineClipboardList } from "react-icons/hi"; +import { LuHistory } from "react-icons/lu"; + +import { Navbar } from "@snu/ds/admin"; + +interface Props { + classeId: string; +} + +export default function NavbarClasse({ classeId }: Props) { + const history = useHistory(); + return ( + , + isActive: location.pathname === `/classes/${classeId}`, + onClick: () => { + history.push(`/classes/${classeId}`); + }, + }, + { + title: "Historique de la classe", + leftIcon: , + isActive: location.pathname === `/classes/${classeId}/historique`, + onClick: () => { + history.push(`/classes/${classeId}/historique`); + }, + }, + { + title: "Historique des inscriptions", + leftIcon: , + isActive: location.pathname === `/classes/${classeId}/inscriptions`, + onClick: () => { + history.push(`/classes/${classeId}/inscriptions`); + }, + }, + ]} + /> + ); +} diff --git a/admin/src/scenes/classe/components/types.ts b/admin/src/scenes/classe/components/types.ts index 7658a91b42..d0fe35cf05 100644 --- a/admin/src/scenes/classe/components/types.ts +++ b/admin/src/scenes/classe/components/types.ts @@ -20,3 +20,18 @@ export type InfoBus = { returnDate: string; returnHour: string; }; + +type ClasseUpdateOperation = { + op: "add" | "remove" | "replace"; + path: string; + value?: any; +}; + +export type ClassePatchesType = { + _id: string; + ops: ClasseUpdateOperation[]; + modelName: "classe"; + ref: string; + date: string; + user?: { firstName: string; lastName?: string }; +}; diff --git a/admin/src/scenes/classe/header/ClasseHeader.tsx b/admin/src/scenes/classe/header/ClasseHeader.tsx new file mode 100644 index 0000000000..ce14fcfee3 --- /dev/null +++ b/admin/src/scenes/classe/header/ClasseHeader.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { useSelector } from "react-redux"; + +import { Header, Badge } from "@snu/ds/admin"; +import { translateStatusClasse, ClassesRoutes } from "snu-lib"; +import { TStatus } from "@/types"; +import { AuthState } from "@/redux/auth/reducer"; +import { appURL } from "@/config"; + +import { getHeaderActionList } from "./index"; +import NavbarClasse from "../components/NavbarClasse"; +import { statusClassForBadge } from "../utils"; + +interface Props { + classe: NonNullable; + setClasse: (classe: ClassesRoutes["GetOne"]["response"]["data"]) => void; + isLoading: boolean; + setIsLoading: (b: boolean) => void; + studentStatus: { [key: string]: number }; + page: string; +} + +export default function ClasseHeader({ classe, setClasse, isLoading, setIsLoading, studentStatus, page }: Props) { + const user = useSelector((state: AuthState) => state.Auth.user); + + const url = `${appURL}/je-rejoins-ma-classe-engagee?id=${classe._id.toString()}`; + const id = classe._id; + return ( + <> +
} + breadcrumb={[ + { title: "Séjours" }, + { + title: "Classes", + to: "/classes", + }, + { title: page }, + ]} + actions={getHeaderActionList({ user, classe, setClasse, isLoading, setIsLoading, url, id, studentStatus })} + /> + + + ); +} diff --git a/admin/src/scenes/classe/view.tsx b/admin/src/scenes/classe/view/Details.tsx similarity index 78% rename from admin/src/scenes/classe/view.tsx rename to admin/src/scenes/classe/view/Details.tsx index 80c8f1e730..bbce0e213b 100644 --- a/admin/src/scenes/classe/view.tsx +++ b/admin/src/scenes/classe/view/Details.tsx @@ -4,30 +4,26 @@ import { useSelector } from "react-redux"; import dayjs from "dayjs"; import { toastr } from "react-redux-toastr"; -import { Page, Header, Badge } from "@snu/ds/admin"; +import { Page } from "@snu/ds/admin"; import { capture } from "@/sentry"; import api from "@/services/api"; -import { translate, YOUNG_STATUS, STATUS_CLASSE, translateStatusClasse, COHORT_TYPE, FUNCTIONAL_ERRORS, LIMIT_DATE_ESTIMATED_SEATS, ClassesRoutes } from "snu-lib"; -import { appURL } from "@/config"; +import { translate, YOUNG_STATUS, STATUS_CLASSE, COHORT_TYPE, FUNCTIONAL_ERRORS, LIMIT_DATE_ESTIMATED_SEATS } from "snu-lib"; import Loader from "@/components/Loader"; import { AuthState } from "@/redux/auth/reducer"; import { CohortState } from "@/redux/cohorts/reducer"; -import { ClasseService } from "@/services/classeService"; -import { TStatus } from "@/types"; -import { getRights, statusClassForBadge } from "./utils"; -import GeneralInfos from "./components/GeneralInfos"; -import ReferentInfos from "./components/ReferentInfos"; -import SejourInfos from "./components/SejourInfos"; -import StatsInfos from "./components/StatsInfos"; -import ModaleCohort from "./components/modaleCohort"; -import { InfoBus, Rights } from "./components/types"; -import { getHeaderActionList } from "./header"; +import { getRights } from "../utils"; +import GeneralInfos from "../components/GeneralInfos"; +import ReferentInfos from "../components/ReferentInfos"; +import SejourInfos from "../components/SejourInfos"; +import StatsInfos from "../components/StatsInfos"; +import ModaleCohort from "../components/modaleCohort"; +import ClasseHeader from "../header/ClasseHeader"; +import { InfoBus, Rights } from "../components/types"; -export default function View() { - const [classe, setClasse] = useState(); - const [url, setUrl] = useState(""); - const [studentStatus, setStudentStatus] = useState<{ [key: string]: number }>({}); +export default function Details(props) { + const [classe, setClasse] = useState(props.classe); + const studentStatus = props.studentStatus; const [showModaleCohort, setShowModaleCohort] = useState(false); const { id } = useParams<{ id: string }>(); const [errors, setErrors] = useState({}); @@ -48,8 +44,6 @@ export default function View() { const getClasse = async () => { try { - const classe = await ClasseService.getOne(id); - setClasse(classe); setOldClasseCohort(classe.cohort); if (classe?.ligneId) { //Bus @@ -68,32 +62,12 @@ export default function View() { returnHour: meetingPoint?.returnHour, }); } - - //Logical stuff - setUrl(`${appURL}/je-rejoins-ma-classe-engagee?id=${classe._id.toString()}`); - if (!([STATUS_CLASSE.CREATED, STATUS_CLASSE.VERIFIED] as string[]).includes(classe.status)) { - getStudents(classe._id); - } } catch (e) { capture(e); toastr.error("Oups, une erreur est survenue lors de la récupération de la classe", translate(e.message)); } }; - const getStudents = async (id) => { - try { - const { ok, code, data: response } = await api.get(`/cle/young/by-classe-stats/${id}`); - - if (!ok) { - return toastr.error("Oups, une erreur est survenue lors de la récupération des élèves", translate(code)); - } - setStudentStatus(response); - } catch (e) { - capture(e); - toastr.error("Oups, une erreur est survenue lors de la récupération des élèves", e); - } - }; - useEffect(() => { getClasse(); }, [id, edit, editStay, editRef]); @@ -228,19 +202,7 @@ export default function View() { return ( -
} - breadcrumb={[ - { title: "Séjours" }, - { - title: "Classes", - to: "/classes", - }, - { title: "Fiche de la classe" }, - ]} - actions={getHeaderActionList({ user, classe, setClasse, isLoading, setIsLoading, url, id, studentStatus })} - /> + ([]); + const [filteredPatches, setFilteredPatches] = useState([]); + const [pathFilter, setPathFilter] = useState(""); + const [actionFilter, setActionFilter] = useState(""); + const [userFilter, setUserFilter] = useState(""); + const [pathOptions, setPathOptions] = useState([]); + const [actionOptions, setActionOptions] = useState([]); + const [userOptions, setUserOptions] = useState([]); + + const getPatches = async () => { + try { + const { ok, code, data } = await api.get(`/cle/classe/${classe._id}/patches`); + if (!ok) { + return toastr.error("Oups, une erreur est survenue lors de la récupération de l'historique", translate(code)); + } + setPatches(data); + setFilteredPatches(data); + + const pathOptions: SelectOption[] = data.flatMap((patch) => + patch.ops.map((op) => ({ + value: op.path, + label: translateModelFields("classe", op.path.slice(1)), + })), + ); + const uniquePathOptions: SelectOption[] = Array.from(new Map(pathOptions.map((item) => [item.value, item])).values()); + setPathOptions(uniquePathOptions); + + const actionOptions: SelectOption[] = data.flatMap((patch) => + patch.ops.map((op) => ({ + value: op.op, + label: translateAction(op.op), + })), + ); + const uniqueActionOptions: SelectOption[] = Array.from(new Map(actionOptions.map((item) => [item.value, item])).values()); + setActionOptions(uniqueActionOptions); + + const userOptions: SelectOption[] = data.flatMap((patch) => { + if (!patch.user || !patch.user.lastName) return []; + return { + value: patch.user.firstName, + label: patch.user.lastName ? `${patch.user.firstName} ${patch.user.lastName}` : patch.user.firstName, + }; + }); + const uniqueUserOptions: SelectOption[] = Array.from(new Map(userOptions.map((item) => [item.value, item])).values()); + setUserOptions(uniqueUserOptions); + } catch (e) { + capture(e); + toastr.error("Oups, une erreur est survenue lors de la récupération de l'historique", translate(e.message)); + } + }; + + useEffect(() => { + if (classe?._id) getPatches(); + }, [classe]); + + useEffect(() => { + let filteredData = patches; + if (pathFilter !== "") { + filteredData = filteredData.map((patch) => { + const filteredOps = patch.ops.filter((op) => op.path === pathFilter); + + return { + ...patch, + ops: filteredOps, + }; + }); + } + if (actionFilter !== "") { + filteredData = filteredData.map((patch) => { + const filteredOps = patch.ops.filter((op) => op.op === actionFilter); + + return { + ...patch, + ops: filteredOps, + }; + }); + } + if (userFilter !== "") { + filteredData = filteredData.filter((patch) => patch.user?.firstName === userFilter); + } + setFilteredPatches(filteredData); + }, [pathFilter, actionFilter, userFilter, patches]); + + if (!classe || !patches.length) return ; + + return ( + + + + + + + + Action + Avant + + Après + + Auteur + + + + {filteredPatches.map((hit) => ( + //@ts-ignore + + ))} + +
+
+
+ ); +} + +function FilterComponent({ pathFilter, setPathFilter, actionFilter, setActionFilter, userFilter, setUserFilter, uniquePathOptions, actionOptions, uniqueUserOptions }) { + const isFilterActive = pathFilter !== "" || actionFilter !== "" || userFilter !== ""; + + return ( +
+
+ +

Filter par :

+
+ option.value === actionFilter) || null} + onChange={(option) => { + if (!option) return setActionFilter(""); + setActionFilter(option.value); + }} + /> + : undefined} + badgePosition="left" + key={menuClosed.toString()} maxMenuHeight={400} - key={Math.random()} isActive={pathFilter !== ""} - placeholder={"Donnée modifiée"} - options={uniquePathOptions} + placeholder={placeholder} + onMenuOpen={() => setPlaceholder("Rechercher...")} + onMenuClose={() => { + setPlaceholder("Donnée modifiée"); + setMenuClosed((prev) => !prev); + }} + options={pathOptions} closeMenuOnSelect={true} isClearable={true} - isSearchable={false} - value={uniquePathOptions.find((option) => option.value === pathFilter) || null} + isSearchable={true} + value={pathOptions.find((option) => option.value === pathFilter) || null} onChange={(option) => { if (!option) return setPathFilter(""); setPathFilter(option.value); @@ -190,11 +182,11 @@ function FilterComponent({ pathFilter, setPathFilter, actionFilter, setActionFil key={Math.random()} isActive={userFilter !== ""} placeholder={"Auteur"} - options={uniqueUserOptions} + options={userOptions} closeMenuOnSelect={true} isClearable={true} isSearchable={false} - value={uniqueUserOptions.find((option) => option.value === userFilter) || null} + value={userOptions.find((option) => option.value === userFilter) || null} onChange={(option) => { if (!option) return setUserFilter(""); setUserFilter(option.value); @@ -213,30 +205,3 @@ function FilterComponent({ pathFilter, setPathFilter, actionFilter, setActionFil
); } - -function HistoryRow({ patch }) { - return ( - <> - {patch.ops.map((op, index) => ( - - -

{translateModelFields("classe", op.path.slice(1))}

-

- {translateAction(op.op)} • {formatLongDateFR(patch.date)} -

- - -

{translateHistory(op.path, op.originalValue || "Vide")}

- - - -

{translateHistory(op.path, op.value)}

- - - - - - ))} - - ); -} diff --git a/admin/src/scenes/classe/view/Inscriptions.tsx b/admin/src/scenes/classe/view/Inscriptions.tsx index a9d108257d..12027b63c8 100644 --- a/admin/src/scenes/classe/view/Inscriptions.tsx +++ b/admin/src/scenes/classe/view/Inscriptions.tsx @@ -1,17 +1,241 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; +import { toastr } from "react-redux-toastr"; +import { HiArrowRight, HiFilter, HiTrash, HiSearch } from "react-icons/hi"; +import { LuHistory } from "react-icons/lu"; -import { Page, Container } from "@snu/ds/admin"; +import { Page, Container, Select } from "@snu/ds/admin"; +import api from "@/services/api"; +import Loader from "@/components/Loader"; +import { capture } from "@/sentry"; +import { translate } from "snu-lib"; import ClasseHeader from "../header/ClasseHeader"; +import { ClasseYoungPatchesType } from "../components/types"; +import HistoryRow from "../components/HistoryRow"; +import { getValueOptions, getUserOptions, getYoungOptions } from "../utils"; export default function Inscriptions(props) { const [classe, setClasse] = useState(props.classe); const studentStatus = props.studentStatus; const [isLoading, setIsLoading] = useState(false); + const [patches, setPatches] = useState([]); + const [oldYoungPatches, setOldYoungPatches] = useState([]); + const [allPatches, setAllPatches] = useState([]); + const [noYoung, setNoYoung] = useState(false); + const [youngFilter, setYoungFilter] = useState(""); + const [valueFilter, setValueFilter] = useState(""); + const [userFilter, setUserFilter] = useState(""); + const filteredPatches = getFilteredPatches(allPatches); + const youngOptions = getYoungOptions(allPatches); + const valueOptions = getValueOptions(patches); + const userOptions = getUserOptions(allPatches); + + const getPatches = async () => { + try { + const { ok, code, data } = await api.get(`/cle/young/by-classe-historic/${classe._id}/patches`); + if (!ok) { + return toastr.error("Oups, une erreur est survenue lors de la récupération de l'historique", translate(code)); + } + setPatches(data); + } catch (e) { + capture(e); + toastr.error("Oups, une erreur est survenue lors de la récupération de l'historique", translate(e.message)); + } + }; + + const getOldPatches = async () => { + setIsLoading(true); + try { + const { ok, code, data } = await api.get(`/cle/young/by-classe-historic/${classe._id}/patches/old-student`); + if (!ok) { + return toastr.error("Oups, une erreur est survenue lors de la récupération de l'historique", translate(code)); + } + setOldYoungPatches(data); + } catch (e) { + capture(e); + toastr.error("Oups, une erreur est survenue lors de la récupération de l'historique", translate(e.message)); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (classe?._id) { + getPatches(); + getOldPatches(); + } + }, [classe]); + + console.log(oldYoungPatches); + + useEffect(() => { + setAllPatches([...patches, ...oldYoungPatches]); + if (patches.length === 0 && oldYoungPatches.length === 0 && !isLoading) setNoYoung(true); + else setNoYoung(false); + }, [patches, oldYoungPatches, isLoading]); + + function getFilteredPatches(patches: ClasseYoungPatchesType[]) { + let filteredPatches = patches; + if (youngFilter !== "") { + filteredPatches = filteredPatches.filter((patch) => { + const fullName = `${patch.young.firstName} ${patch.young.lastName}` + .trim() + .replace(/[.,\s]/g, "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); + const filter = youngFilter + .trim() + .replace(/[.,\s]/g, "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); + + return fullName.includes(filter); + }); + } + if (valueFilter !== "") { + filteredPatches = filteredPatches.map((patch) => { + const filteredOps = patch.ops.filter((op) => op.value === valueFilter); + + return { + ...patch, + ops: filteredOps, + }; + }); + } + if (userFilter !== "") { + filteredPatches = filteredPatches.filter((patch) => patch.user?.firstName === userFilter); + } + filteredPatches = filteredPatches.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + return filteredPatches; + } + + if (!classe) return ; + return ( - + + {noYoung ? ( +
+ +

Il n'y a aucun élève inscrit dans cette classe

+
+ ) : allPatches.length === 0 ? ( + + ) : ( + <> + + + + + Action + Avant + + Après + + Auteur + + + + {filteredPatches.map((hit) => ( + + ))} + +
+ + )} +
); } + +function FilterComponent({ youngFilter, setYoungFilter, valueFilter, setValueFilter, userFilter, setUserFilter, youngOptions, valueOptions, uniqueUserOptions }) { + const isFilterActive = youngFilter !== "" || valueFilter !== "" || userFilter !== ""; + const [placeholder, setPlaceholder] = useState("Élève modifié"); + const [menuClosed, setMenuClosed] = useState(false); + + return ( +
+
+ +

Filter par :

+
+ option.value === valueFilter) || null} + onChange={(option) => { + if (!option) return setValueFilter(""); + setValueFilter(option.value); + }} + /> + onChange?.(e)} - maxLength={max} - /> +
+ {icon} + onChange?.(e)} + maxLength={max} + /> +
{isErrorActive && } @@ -90,7 +97,7 @@ export default function InputText({ const getInputClass = ({ label }: { label?: string }) => { const baseClass = - "font-normal leading-5 text-gray-900 text-sm placeholder:text-gray-500 disabled:text-gray-500 disabled:bg-gray-50 disabled:cursor-default read-only:cursor-default"; + "font-normal leading-5 text-gray-900 text-sm placeholder:text-gray-500 disabled:text-gray-500 disabled:bg-gray-50 disabled:cursor-default read-only:cursor-default w-full"; if (label) { return classNames(baseClass, ""); } else { diff --git a/packages/ds/src/admin/form/Select/Select.tsx b/packages/ds/src/admin/form/Select/Select.tsx index c689d4fd73..32e47003f8 100644 --- a/packages/ds/src/admin/form/Select/Select.tsx +++ b/packages/ds/src/admin/form/Select/Select.tsx @@ -23,7 +23,7 @@ export type SelectProps = { options?: SelectOption[]; defaultValue?: string | null; className?: string; - placeholder?: string; + placeholder?: string | ReactElement; label?: string; isActive?: boolean; readOnly?: boolean; @@ -37,6 +37,7 @@ export type SelectProps = { isSearchable?: boolean; isOpen?: boolean; badge?: ReactElement; + badgePosition?: "left" | "right"; controlCustomStyle?: CSSObject; menuCustomStyle?: CSSObject; optionCustomStyle?: CSSObject; @@ -54,7 +55,7 @@ export type SelectProps = { | boolean | GroupBase[] | (() => Promise[]>); - size?: "sm" | "lg"; + size?: "sm" | "md" | "lg"; }; export default function SelectButton(props: SelectProps) { @@ -84,6 +85,7 @@ export default function SelectButton(props: SelectProps) { defaultOptions = true, isOpen, badge, + badgePosition = "right", } = props; const customStyles = useReactSelectTheme(props); @@ -130,6 +132,14 @@ export default function SelectButton(props: SelectProps) { }; const ValueContainerComponent = (props: ValueContainerProps) => { if (!badge) return ; + if (badgePosition === "left") { + return ( +
+ {badge} + +
+ ); + } return (
diff --git a/packages/ds/src/admin/form/Select/theme.ts b/packages/ds/src/admin/form/Select/theme.ts index 9842a3d15f..7c89eb1bf7 100644 --- a/packages/ds/src/admin/form/Select/theme.ts +++ b/packages/ds/src/admin/form/Select/theme.ts @@ -23,7 +23,7 @@ export default function useReactSelectTheme({ control: (styles, state) => ({ ...styles, cursor: "pointer", - boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", + boxShadow: "0 0 0 0 rgb(0 0 0 / 0.05)", backgroundColor: disabled ? "#F9FAFB" : "white", border: cx({ "1px solid #EF4444": error, @@ -33,7 +33,7 @@ export default function useReactSelectTheme({ "&:hover": { border: cx({ "1px solid #EF4444": error, - "1px solid #3B82F6": !error && isActive, + "2px solid #3B82F6": !error && isActive, "1px solid #E5E7EB": !error && !isActive, }), }, @@ -71,7 +71,7 @@ export default function useReactSelectTheme({ }, input: (styles) => ({ ...styles, - height: size === "sm" ? 24 : 46, + height: size === "sm" ? 24 : size === "md" ? 29 : 46, cursor: "pointer", padding: paddingStyle, }), From b198a0e02507f100f0b3709e338751c253d2907f Mon Sep 17 00:00:00 2001 From: C2Chandelier Date: Mon, 4 Nov 2024 12:38:31 +0100 Subject: [PATCH 10/16] add test --- api/src/__tests__/cle/young.test.ts | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/api/src/__tests__/cle/young.test.ts b/api/src/__tests__/cle/young.test.ts index 94f70192d3..cb8d0b23bc 100644 --- a/api/src/__tests__/cle/young.test.ts +++ b/api/src/__tests__/cle/young.test.ts @@ -63,3 +63,45 @@ describe("GET /cle/young/by-classe-historic/:id/patches", () => { expect(passport.lastTypeCalledOnAuthenticate).toEqual("referent"); }); }); + +describe("GET /cle/young/by-classe-historic/:id/patches/old-student", () => { + it("should return 404 if classe not found", async () => { + const classeId = new ObjectId(); + const res = await request(getAppHelper()).get(`/cle/young/by-classe-historic/${classeId}/patches/old-student`).send(); + expect(res.statusCode).toEqual(404); + }); + it("should return 403 if not admin", async () => { + const classe = await createClasse(createFixtureClasse()); + classe.name = "MY NEW NAME"; + await classe.save(); + + const res = await request(getAppHelper({ role: ROLES.RESPONSIBLE })) + .get(`/cle/young/by-classe-historic/${classe._id}/patches/old-student`) + .send(); + expect(res.status).toBe(403); + }); + it("should return 200 if classe found with young and patches", async () => { + const classe = await createClasse(createFixtureClasse()); + const young = await createYoungHelper(getNewYoungFixture({ classeId: classe._id })); + const fakeClasseId = new ObjectId().toString(); + young.classeId = fakeClasseId; + await young.save(); + const res = await request(getAppHelper({ role: ROLES.ADMIN })) + .get(`/cle/young/by-classe-historic/${classe._id}/patches/old-student`) + .send(); + expect(res.statusCode).toEqual(200); + expect(res.body.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ops: expect.arrayContaining([expect.objectContaining({ op: "replace", path: "/classeId", value: fakeClasseId })]), + }), + ]), + ); + }); + it("should be only accessible by referents", async () => { + const passport = require("passport"); + const classeId = new ObjectId(); + await request(getAppHelper()).get(`/cle/young/by-classe-historic/${classeId}/patches/old-student`).send(); + expect(passport.lastTypeCalledOnAuthenticate).toEqual("referent"); + }); +}); From 560431890a643e165ac98bb779c582e853dab105 Mon Sep 17 00:00:00 2001 From: C2Chandelier Date: Mon, 4 Nov 2024 12:44:33 +0100 Subject: [PATCH 11/16] up --- api/src/referent/referentController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/referent/referentController.ts b/api/src/referent/referentController.ts index fe4a162501..dd36bb25c5 100644 --- a/api/src/referent/referentController.ts +++ b/api/src/referent/referentController.ts @@ -116,7 +116,6 @@ import { mightAddInProgressStatus, shouldSwitchYoungByIdToLC, switchYoungByIdToL import { getCohortIdsFromCohortName } from "../cohort/cohortService"; import { getCompletionObjectifs } from "../services/inscription-goal"; import SNUpport from "../SNUpport"; -import { path } from "pdfkit"; const router = express.Router(); const ReferentAuth = new AuthObject(ReferentModel); From d37bf5621773b4c915e7ea1ea98f1d467fdf8184 Mon Sep 17 00:00:00 2001 From: C2Chandelier Date: Mon, 4 Nov 2024 16:31:15 +0100 Subject: [PATCH 12/16] up --- .../scenes/classe/components/HistoryRow.tsx | 13 +++++++++---- admin/src/scenes/classe/components/types.ts | 2 +- admin/src/scenes/classe/view/Historique.tsx | 8 ++++---- admin/src/scenes/classe/view/Inscriptions.tsx | 19 ++++++++----------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/admin/src/scenes/classe/components/HistoryRow.tsx b/admin/src/scenes/classe/components/HistoryRow.tsx index 969e0bff23..df668d168d 100644 --- a/admin/src/scenes/classe/components/HistoryRow.tsx +++ b/admin/src/scenes/classe/components/HistoryRow.tsx @@ -23,6 +23,13 @@ export default function HistoryRow({ patch, type }: Props) { if (patch.young) return `/volontaire/${patch.ref}`; return ""; } + + if (type === "young" && patch.user) { + if (!patch.user.role) { + if (patch.user.email) patch.user.role = "Volontaire"; + } + } + return ( <> {patch.ops.map((op, index) => ( @@ -47,12 +54,10 @@ export default function HistoryRow({ patch, type }: Props) { )} - {/* @ts-ignore */} - {!patch.oldStudent &&

{translateHistory(op.path, op.originalValue || "Vide")}

} + {type === "young" && !patch.oldStudent &&

{translateHistory(op.path, op.originalValue || "Vide")}

} - {/* @ts-ignore */} - {patch.oldStudent ? ( + {type === "young" && patch.oldStudent ? (

A quitté la classe

) : ( <> diff --git a/admin/src/scenes/classe/components/types.ts b/admin/src/scenes/classe/components/types.ts index 3a0fab6874..8b5b28022c 100644 --- a/admin/src/scenes/classe/components/types.ts +++ b/admin/src/scenes/classe/components/types.ts @@ -43,7 +43,7 @@ export type ClasseYoungPatchesType = { modelName: "young"; ref: string; date: string; - user?: { firstName: string; lastName?: string }; + user?: { firstName: string; lastName?: string; role?: string; email?: string }; young: { firstName: string; lastName: string }; oldStudent?: boolean; }; diff --git a/admin/src/scenes/classe/view/Historique.tsx b/admin/src/scenes/classe/view/Historique.tsx index ef7c5e1ea1..791250726f 100644 --- a/admin/src/scenes/classe/view/Historique.tsx +++ b/admin/src/scenes/classe/view/Historique.tsx @@ -83,13 +83,13 @@ export default function Historique(props) { - {isNoPatches ? ( + {patches.length === 0 ? ( + + ) : isNoPatches ? (
-

Il n'y a aucun historique pour cette classe

+

Il n'y a aucun élève inscrit dans cette classe

- ) : patches.length === 0 ? ( - ) : ( <> ([]); const [oldYoungPatches, setOldYoungPatches] = useState([]); const [allPatches, setAllPatches] = useState([]); - const [noYoung, setNoYoung] = useState(false); + const [isNoYoung, setIsNoYoung] = useState(false); const [youngFilter, setYoungFilter] = useState(""); const [valueFilter, setValueFilter] = useState(""); const [userFilter, setUserFilter] = useState(""); @@ -44,7 +44,6 @@ export default function Inscriptions(props) { }; const getOldPatches = async () => { - setIsLoading(true); try { const { ok, code, data } = await api.get(`/cle/young/by-classe-historic/${classe._id}/patches/old-student`); if (!ok) { @@ -66,13 +65,11 @@ export default function Inscriptions(props) { } }, [classe]); - console.log(oldYoungPatches); - useEffect(() => { setAllPatches([...patches, ...oldYoungPatches]); - if (patches.length === 0 && oldYoungPatches.length === 0 && !isLoading) setNoYoung(true); - else setNoYoung(false); - }, [patches, oldYoungPatches, isLoading]); + if (patches.length === 0 && oldYoungPatches.length === 0 && !isLoading) setIsNoYoung(true); + else setIsNoYoung(false); + }, [patches, oldYoungPatches]); function getFilteredPatches(patches: ClasseYoungPatchesType[]) { let filteredPatches = patches; @@ -117,13 +114,13 @@ export default function Inscriptions(props) { - {noYoung ? ( + {allPatches.length === 0 ? ( + + ) : isNoYoung ? (

Il n'y a aucun élève inscrit dans cette classe

- ) : allPatches.length === 0 ? ( - ) : ( <> Date: Mon, 4 Nov 2024 16:40:36 +0100 Subject: [PATCH 13/16] up --- admin/src/scenes/classe/components/HistoryRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/src/scenes/classe/components/HistoryRow.tsx b/admin/src/scenes/classe/components/HistoryRow.tsx index df668d168d..a5bab1a7ea 100644 --- a/admin/src/scenes/classe/components/HistoryRow.tsx +++ b/admin/src/scenes/classe/components/HistoryRow.tsx @@ -54,7 +54,7 @@ export default function HistoryRow({ patch, type }: Props) { )} - {type === "young" && !patch.oldStudent &&

{translateHistory(op.path, op.originalValue || "Vide")}

} + {(type !== "young" || !patch.oldStudent) &&

{translateHistory(op.path, op.originalValue || "Vide")}

} {type === "young" && patch.oldStudent ? ( From 6e09911a656ee0589de5d5fb417f9e00712120ee Mon Sep 17 00:00:00 2001 From: C2Chandelier Date: Tue, 5 Nov 2024 11:45:28 +0100 Subject: [PATCH 14/16] up --- admin/src/components/UserCard.jsx | 2 ++ packages/lib/src/translation.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/admin/src/components/UserCard.jsx b/admin/src/components/UserCard.jsx index 75b1aedc8a..51eb88dfbc 100644 --- a/admin/src/components/UserCard.jsx +++ b/admin/src/components/UserCard.jsx @@ -31,6 +31,8 @@ export default function UserCard({ user }) { return ( { return "Inscription Ouverte"; case "CLOSED": return "Inscription Fermée"; + case "DRAFT": + return "Brouillon"; + case "INSCRIPTION_IN_PROGRESS": + return "Inscription en cours"; default: return status; } From fbec152699b0d176cfc0bb0c6b9d9a4a0ebc0f00 Mon Sep 17 00:00:00 2001 From: C2Chandelier Date: Thu, 7 Nov 2024 16:39:55 +0100 Subject: [PATCH 15/16] up --- admin/src/components/Loader.jsx | 2 +- .../scenes/classe/components/HistoryRow.tsx | 35 +++- admin/src/scenes/classe/utils/index.ts | 9 + admin/src/scenes/classe/view/Inscriptions.tsx | 16 +- api/src/cle/classe/classeController.ts | 61 +++---- api/src/cle/young/youngController.ts | 156 ++++++++++-------- api/src/controllers/application.js | 48 +++--- api/src/controllers/contract.js | 49 +++--- api/src/controllers/mission.js | 51 +++--- api/src/controllers/structure.js | 51 +++--- api/src/controllers/young/index.ts | 48 +++--- api/src/referent/referentController.ts | 46 +++--- 12 files changed, 319 insertions(+), 253 deletions(-) diff --git a/admin/src/components/Loader.jsx b/admin/src/components/Loader.jsx index 407ba56674..92f59de06b 100644 --- a/admin/src/components/Loader.jsx +++ b/admin/src/components/Loader.jsx @@ -2,7 +2,7 @@ import React from "react"; import ReactLoading from "react-loading"; import styled from "styled-components"; -export default function Loader({ type = "spin", size = "3rem", color = "#5245cc", className = "", containerClassName = "" }) { +export default function Loader({ type = "spin", size = "3rem", color = "#2563eb", className = "", containerClassName = "" }) { return ( diff --git a/admin/src/scenes/classe/components/HistoryRow.tsx b/admin/src/scenes/classe/components/HistoryRow.tsx index a5bab1a7ea..a3a32a5635 100644 --- a/admin/src/scenes/classe/components/HistoryRow.tsx +++ b/admin/src/scenes/classe/components/HistoryRow.tsx @@ -1,9 +1,13 @@ import React from "react"; import { HiArrowRight } from "react-icons/hi"; +import { useSelector } from "react-redux"; +import cx from "classnames"; -import { formatLongDateFR, translateAction } from "snu-lib"; +import { formatLongDateFR, translateAction, ROLES } from "snu-lib"; import { translateHistory, translateModelFields } from "@/utils"; import UserCard from "@/components/UserCard"; +import { AuthState } from "@/redux/auth/reducer"; + import { ClasseYoungPatchesType, ClassePatchesType } from "./types"; interface ClasseProps { @@ -16,13 +20,10 @@ interface YoungProps { patch: ClasseYoungPatchesType; } -type Props = ClasseProps | YoungProps; +type HistoryRowProps = ClasseProps | YoungProps; -export default function HistoryRow({ patch, type }: Props) { - function getLink(patch) { - if (patch.young) return `/volontaire/${patch.ref}`; - return ""; - } +export default function HistoryRow({ patch, type }: HistoryRowProps) { + const user = useSelector((state: AuthState) => state.Auth.user); if (type === "young" && patch.user) { if (!patch.user.role) { @@ -30,6 +31,12 @@ export default function HistoryRow({ patch, type }: Props) { } } + function getLink(patch) { + if (patch.oldStudent && [ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE].includes(user.role)) return ""; + if (patch.young) return `/volontaire/${patch.ref}`; + return ""; + } + return ( <> {patch.ops.map((op, index) => ( @@ -43,8 +50,18 @@ export default function HistoryRow({ patch, type }: Props) {

) : ( -
-

+ +

{patch.young.firstName} {patch.young.lastName}

diff --git a/admin/src/scenes/classe/utils/index.ts b/admin/src/scenes/classe/utils/index.ts index fc80c8fd9a..1cd64de0bf 100644 --- a/admin/src/scenes/classe/utils/index.ts +++ b/admin/src/scenes/classe/utils/index.ts @@ -460,3 +460,12 @@ export function getYoungOptions(patches) { const uniqueYoungOptions: SelectOption[] = Array.from(new Map(youngOptions.map((item) => [item.value, item])).values()); return uniqueYoungOptions; } + +export function NormalizeYoungName(name: string) { + return name + .trim() + .replace(/[.,\s]/g, "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); +} diff --git a/admin/src/scenes/classe/view/Inscriptions.tsx b/admin/src/scenes/classe/view/Inscriptions.tsx index 18680f8ea6..a60dc4181a 100644 --- a/admin/src/scenes/classe/view/Inscriptions.tsx +++ b/admin/src/scenes/classe/view/Inscriptions.tsx @@ -12,7 +12,7 @@ import { translate } from "snu-lib"; import ClasseHeader from "../header/ClasseHeader"; import { ClasseYoungPatchesType } from "../components/types"; import HistoryRow from "../components/HistoryRow"; -import { getValueOptions, getUserOptions, getYoungOptions } from "../utils"; +import { getValueOptions, getUserOptions, getYoungOptions, NormalizeYoungName } from "../utils"; export default function Inscriptions(props) { const [classe, setClasse] = useState(props.classe); @@ -75,18 +75,8 @@ export default function Inscriptions(props) { let filteredPatches = patches; if (youngFilter !== "") { filteredPatches = filteredPatches.filter((patch) => { - const fullName = `${patch.young.firstName} ${patch.young.lastName}` - .trim() - .replace(/[.,\s]/g, "") - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase(); - const filter = youngFilter - .trim() - .replace(/[.,\s]/g, "") - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase(); + const fullName = NormalizeYoungName(`${patch.young.firstName} ${patch.young.lastName}`); + const filter = NormalizeYoungName(youngFilter); return fullName.includes(filter); }); diff --git a/api/src/cle/classe/classeController.ts b/api/src/cle/classe/classeController.ts index f65aa9a921..98f0f03ce4 100644 --- a/api/src/cle/classe/classeController.ts +++ b/api/src/cle/classe/classeController.ts @@ -29,7 +29,6 @@ import { YOUNG_STATUS, YOUNG_STATUS_PHASE1, ReferentType, - canViewPatchesHistory, } from "snu-lib"; import { capture, captureMessage } from "../../sentry"; @@ -80,7 +79,6 @@ import { accessControlMiddleware } from "../../middlewares/accessControlMiddlewa import { authMiddleware } from "../../middlewares/authMiddleware"; import { requestValidatorMiddleware } from "../../middlewares/requestValidatorMiddleware"; import { isCohortInscriptionOpen } from "../../cohort/cohortService"; -import { isEmpty } from "bullmq"; const router = express.Router(); router.use(authMiddleware("referent")); @@ -638,38 +636,41 @@ router.put("/:id/verify", async (req: UserRequest, res) => { } }); -router.get("/:id/patches", async (req: UserRequest, res) => { - try { - const { error, value: id } = validateId(req.params.id); - if (error) { - capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); - } +router.get( + "/:id/patches", + [ + requestValidatorMiddleware({ + params: Joi.object({ id: idSchema().required() }), + }), + accessControlMiddleware([ROLES.ADMINISTRATEUR_CLE, ROLES.REFERENT_CLASSE, ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN]), + ], + async (req: UserRequest, res) => { + try { + const id = req.params.id; - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); + const classe = await ClasseModel.findById(id); + if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const classe = await ClasseModel.findById(id); - if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - - let classePatches = await patches.get(req, ClasseModel); - if (!classePatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + let classePatches = await patches.get(req, ClasseModel); + if (!classePatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const pathsToIgnore = ["/seatsTaken", "/cohortId", "/uniqueKey", "/uniqueId", "/comments", "/trimester", "/metadata", "/id", "/updatedAt", "/referents"]; - classePatches.forEach((patch) => { - patch.ops = patch.ops.filter((op) => !pathsToIgnore.includes(op.path)); - patch.ops.forEach((op) => { - if (op.path === "/status") { - op.path = "/classeStatus"; - } + const pathsToIgnore = ["/seatsTaken", "/cohortId", "/uniqueKey", "/uniqueId", "/comments", "/trimester", "/metadata", "/id", "/updatedAt", "/referents"]; + classePatches.forEach((patch) => { + patch.ops = patch.ops.filter((op) => !pathsToIgnore.includes(op.path)); + patch.ops.forEach((op) => { + if (op.path === "/status") { + op.path = "/classeStatus"; + } + }); }); - }); - classePatches = classePatches.filter((patch) => patch.ops.length > 0); + classePatches = classePatches.filter((patch) => patch.ops.length > 0); - return res.status(200).send({ ok: true, data: classePatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: error.message }); - } -}); + return res.status(200).send({ ok: true, data: classePatches }); + } catch (error) { + capture(error); + res.status(500).send({ ok: false, code: error.message }); + } + }, +); export default router; diff --git a/api/src/cle/young/youngController.ts b/api/src/cle/young/youngController.ts index 6b6046634d..68a7c153f1 100644 --- a/api/src/cle/young/youngController.ts +++ b/api/src/cle/young/youngController.ts @@ -1,18 +1,22 @@ import express, { Response } from "express"; +import Joi from "joi"; import passport from "passport"; -import { canSearchStudent, ROLES, YOUNG_STATUS, YoungDto, canViewPatchesHistory } from "snu-lib"; +import { canSearchStudent, ROLES, YOUNG_STATUS, YoungDto } from "snu-lib"; -import { validateId } from "../../utils/validator"; +import { validateId, idSchema } from "../../utils/validator"; import { ERRORS } from "../../utils"; import { capture } from "../../sentry"; import { ClasseModel, YoungModel, EtablissementModel } from "../../models"; import { UserRequest } from "../../controllers/request"; import { getValidatedYoungsWithSession, getYoungsImageRight, getYoungsParentAllowSNU } from "../../young/youngService"; import patches from "../../controllers/patches"; -import { th } from "date-fns/locale"; +import { requestValidatorMiddleware } from "../../middlewares/requestValidatorMiddleware"; +import { authMiddleware } from "../../middlewares/authMiddleware"; +import { accessControlMiddleware } from "../../middlewares/accessControlMiddleware"; const router = express.Router(); +router.use(authMiddleware("referent")); router.get("/by-classe-stats/:idClasse", passport.authenticate("referent", { session: false, failWithError: true }), async (req: UserRequest, res: Response) => { try { @@ -61,101 +65,107 @@ router.get("/by-classe-stats/:idClasse", passport.authenticate("referent", { ses } }); -router.get("/by-classe-historic/:idClasse/patches", passport.authenticate("referent", { session: false, failWithError: true }), async (req: UserRequest, res) => { - try { - const { error, value: id } = validateId(req.params.idClasse); - if (error) { - capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); - } +router.get( + "/by-classe-historic/:idClasse/patches", + [ + requestValidatorMiddleware({ + params: Joi.object({ idClasse: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN, ROLES.REFERENT_CLASSE, ROLES.ADMINISTRATEUR_CLE]), + ], + async (req: UserRequest, res) => { + try { + const id = req.params.idClasse; - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); + const classe = await ClasseModel.findById(id); + if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const classe = await ClasseModel.findById(id); - if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + const youngs = await YoungModel.find({ classeId: classe._id }); - const youngs = await YoungModel.find({ classeId: classe._id }); + const youngPatches: any = []; - const youngPatches: any = []; + const pathsToFilter = ["/status"]; - const pathsToFilter = ["/status"]; + const promises = youngs.map(async (young) => { + let youngPatche = await patches.get({ params: { id: young._id.toString() }, user: req.user }, YoungModel); - const promises = youngs.map(async (young) => { - let youngPatche = await patches.get({ params: { id: young._id.toString() }, user: req.user }, YoungModel); + if (!youngPatche) { + throw new Error(`patch not found for young ${young._id}`); + } - if (!youngPatche) { - throw new Error(`patch not found for young ${young._id}`); - } + youngPatche.forEach((patch) => { + patch.ops = patch.ops.filter((op) => pathsToFilter.includes(op.path)); + }); - youngPatche.forEach((patch) => { - patch.ops = patch.ops.filter((op) => pathsToFilter.includes(op.path)); - }); + youngPatche = youngPatche.filter((patch) => patch.ops.length > 0); - youngPatche = youngPatche.filter((patch) => patch.ops.length > 0); + youngPatche.forEach((patch) => { + patch.young = { firstName: young.firstName, lastName: young.lastName }; + }); - youngPatche.forEach((patch) => { - patch.young = { firstName: young.firstName, lastName: young.lastName }; + return youngPatche; }); - return youngPatche; - }); + try { + const results = await Promise.all(promises); + results.forEach((result) => { + youngPatches.push(...result); + }); + } catch (error) { + throw new Error(error); + } - try { - const results = await Promise.all(promises); - results.forEach((result) => { - youngPatches.push(...result); - }); + return res.status(200).send({ ok: true, data: youngPatches }); } catch (error) { - throw new Error(error); + capture(error); + res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); } - - return res.status(200).send({ ok: true, data: youngPatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); - } -}); + }, +); //Cette route recupere les patches des anciens élèves qui sont passés par la classe mais n'y sont plus -router.get("/by-classe-historic/:idClasse/patches/old-student", passport.authenticate("referent", { session: false, failWithError: true }), async (req: UserRequest, res) => { - try { - const { error, value: id } = validateId(req.params.idClasse); - if (error) { - capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); - } +router.get( + "/by-classe-historic/:idClasse/patches/old-student", + [ + requestValidatorMiddleware({ + params: Joi.object({ idClasse: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN, ROLES.REFERENT_CLASSE, ROLES.ADMINISTRATEUR_CLE]), + ], + async (req: UserRequest, res) => { + try { + const id = req.params.idClasse; - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); + const classe = await ClasseModel.findById(id); + if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const classe = await ClasseModel.findById(id); - if (!classe) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + let youngPatches = await patches.getOldStudentPatches({ classeId: id, user: req.user }); - let youngPatches = await patches.getOldStudentPatches({ classeId: id, user: req.user }); + const pathsToFilter = ["/classeId"]; - const pathsToFilter = ["/classeId"]; + const promises = youngPatches.map(async (youngPatch) => { + const young = await YoungModel.findById(youngPatch.ref); + if (!young) { + throw new Error(`Young not found ${youngPatch.ref}`); + } + youngPatch.ops = youngPatch.ops.filter((op) => pathsToFilter.includes(op.path)); + youngPatch.young = { firstName: young.firstName, lastName: young.lastName }; + youngPatch.oldStudent = true; + return youngPatch; + }); - const promises = youngPatches.map(async (youngPatch) => { - const young = await YoungModel.findById(youngPatch.ref); - if (!young) { - throw new Error(`Young not found ${youngPatch.ref}`); + try { + youngPatches = await Promise.all(promises); + } catch (error) { + throw new Error(error); } - youngPatch.ops = youngPatch.ops.filter((op) => pathsToFilter.includes(op.path)); - youngPatch.young = { firstName: young.firstName, lastName: young.lastName }; - youngPatch.oldStudent = true; - return youngPatch; - }); - try { - youngPatches = await Promise.all(promises); + return res.status(200).send({ ok: true, data: youngPatches }); } catch (error) { - throw new Error(error); + capture(error); + res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); } - - return res.status(200).send({ ok: true, data: youngPatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); - } -}); + }, +); export default router; diff --git a/api/src/controllers/application.js b/api/src/controllers/application.js index 3681fa934e..fe2fe5072c 100644 --- a/api/src/controllers/application.js +++ b/api/src/controllers/application.js @@ -12,7 +12,7 @@ const { YoungModel, CohortModel, ReferentModel, ApplicationModel, ContractModel, const { decrypt, encrypt } = require("../cryptoUtils"); const { sendTemplate } = require("../brevo"); -const { validateUpdateApplication, validateNewApplication, validateId } = require("../utils/validator"); +const { validateUpdateApplication, validateNewApplication, validateId, idSchema } = require("../utils/validator"); const config = require("config"); const { ROLES, @@ -24,7 +24,6 @@ const { translateAddFilePhase2, translateAddFilesPhase2, APPLICATION_STATUS, - canViewPatchesHistory, } = require("snu-lib"); const { serializeApplication, serializeYoung, serializeContract } = require("../utils/serializer"); const { @@ -44,6 +43,9 @@ const scanFile = require("../utils/virusScanner"); const { getAuthorizationToApply } = require("../services/application"); const { apiEngagement } = require("../services/gouv.fr/api-engagement"); const { getMimeFromBuffer, getMimeFromFile } = require("../utils/file"); +const { requestValidatorMiddleware } = require("../middlewares/requestValidatorMiddleware"); +const { authMiddleware } = require("../middlewares/authMiddleware"); +const { accessControlMiddleware } = require("../middlewares/accessControlMiddleware"); const canUpdateApplication = async (user, application, young, structures) => { // - admin can update all applications @@ -840,27 +842,31 @@ router.get("/:id/file/:key/:name", passport.authenticate(["referent", "young"], } }); -router.get("/:id/patches", passport.authenticate("referent", { session: false, failWithError: true }), async (req, res) => { - try { - const { error, value: id } = validateId(req.params.id); - if (error) { - capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); - } - - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); +router.get( + "/:id/patches", + authMiddleware("referent"), + [ + requestValidatorMiddleware({ + params: Joi.object({ id: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN]), + ], + async (req, res) => { + try { + const { id } = req.params; - const application = await ApplicationModel.findById(id); - if (!application) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + const application = await ApplicationModel.findById(id); + if (!application) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const applicationPatches = await patches.get(req, ApplicationModel); - if (!applicationPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + const applicationPatches = await patches.get(req, ApplicationModel); + if (!applicationPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - return res.status(200).send({ ok: true, data: applicationPatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: error.message }); - } -}); + return res.status(200).send({ ok: true, data: applicationPatches }); + } catch (error) { + capture(error); + res.status(500).send({ ok: false, code: error.message }); + } + }, +); module.exports = router; diff --git a/api/src/controllers/contract.js b/api/src/controllers/contract.js index 01c7b48df1..6bdf607ab2 100644 --- a/api/src/controllers/contract.js +++ b/api/src/controllers/contract.js @@ -2,19 +2,22 @@ const express = require("express"); const router = express.Router(); const passport = require("passport"); const crypto = require("crypto"); -const { SENDINBLUE_TEMPLATES, getAge, canCreateOrUpdateContract, canViewContract, ROLES, canViewPatchesHistory } = require("snu-lib"); +const { SENDINBLUE_TEMPLATES, getAge, canCreateOrUpdateContract, canViewContract, ROLES } = require("snu-lib"); const { capture } = require("../sentry"); const { ContractModel, YoungModel, ApplicationModel, StructureModel, ReferentModel } = require("../models"); const { ERRORS, isYoung, isReferent } = require("../utils"); const { sendTemplate } = require("../brevo"); const config = require("config"); const { logger } = require("../logger"); -const { validateId, validateContract, validateOptionalId } = require("../utils/validator"); +const { validateId, validateContract, validateOptionalId, idSchema } = require("../utils/validator"); const { serializeContract } = require("../utils/serializer"); const { updateYoungPhase2StatusAndHours, updateYoungStatusPhase2Contract, checkStatusContract } = require("../utils"); const Joi = require("joi"); const patches = require("./patches"); const { generatePdfIntoStream } = require("../utils/pdf-renderer"); +const { requestValidatorMiddleware } = require("../middlewares/requestValidatorMiddleware"); +const { accessControlMiddleware } = require("../middlewares/accessControlMiddleware"); +const { authMiddleware } = require("../middlewares/authMiddleware"); async function createContract(data, fromUser) { const { sendMessage } = data; @@ -327,28 +330,32 @@ router.get("/:id", passport.authenticate(["referent", "young"], { session: false } }); -router.get("/:id/patches", passport.authenticate("referent", { session: false, failWithError: true }), async (req, res) => { - try { - const { error, value: id } = validateId(req.params.id); - if (error) { - capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); - } - - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); +router.get( + "/:id/patches", + authMiddleware("referent"), + [ + requestValidatorMiddleware({ + params: Joi.object({ id: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN]), + ], + async (req, res) => { + try { + const { id } = req.params; - const contract = await ContractModel.findById(id); - if (!contract) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + const contract = await ContractModel.findById(id); + if (!contract) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const contractPatches = await patches.get(req, ContractModel); - if (!contractPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + const contractPatches = await patches.get(req, ContractModel); + if (!contractPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - return res.status(200).send({ ok: true, data: contractPatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: error.message }); - } -}); + return res.status(200).send({ ok: true, data: contractPatches }); + } catch (error) { + capture(error); + res.status(500).send({ ok: false, code: error.message }); + } + }, +); // Get a contract by its token. router.get("/token/:token", async (req, res) => { diff --git a/api/src/controllers/mission.js b/api/src/controllers/mission.js index 98049afaaf..25747a4f0e 100644 --- a/api/src/controllers/mission.js +++ b/api/src/controllers/mission.js @@ -10,13 +10,16 @@ const { MissionModel, ApplicationModel, StructureModel, ReferentModel, CohortMod const { ERRORS, isYoung } = require("../utils/index"); const { updateApplicationStatus, updateApplicationTutor, getAuthorizationToApply } = require("../services/application"); const { getTutorName } = require("../services/mission"); -const { validateId, validateMission } = require("../utils/validator"); -const { SENDINBLUE_TEMPLATES, MISSION_STATUS, ROLES, canCreateOrModifyMission, canViewMission, canModifyMissionStructureId, canViewPatchesHistory } = require("snu-lib"); +const { validateId, validateMission, idSchema } = require("../utils/validator"); +const { SENDINBLUE_TEMPLATES, MISSION_STATUS, ROLES, canCreateOrModifyMission, canViewMission, canModifyMissionStructureId } = require("snu-lib"); const { serializeMission, serializeApplication } = require("../utils/serializer"); const patches = require("./patches"); const { sendTemplate } = require("../brevo"); const config = require("config"); const { getNearestLocation } = require("../services/gouv.fr/api-adresse"); +const { requestValidatorMiddleware } = require("../middlewares/requestValidatorMiddleware"); +const { accessControlMiddleware } = require("../middlewares/accessControlMiddleware"); +const { authMiddleware } = require("../middlewares/authMiddleware"); //@todo: temporary fix for avoiding date inconsistencies (only works for French metropolitan timezone) const fixDate = (dateString) => { @@ -315,28 +318,32 @@ router.get("/:id", passport.authenticate(["referent", "young"], { session: false } }); -router.get("/:id/patches", passport.authenticate("referent", { session: false, failWithError: true }), async (req, res) => { - try { - const { error, value: id } = validateId(req.params.id); - if (error) { +router.get( + "/:id/patches", + authMiddleware("referent"), + [ + requestValidatorMiddleware({ + params: Joi.object({ id: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN]), + ], + async (req, res) => { + try { + const { id } = req.params; + + const mission = await MissionModel.findById(id); + if (!mission) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + + const missionPatches = await patches.get(req, MissionModel); + if (!missionPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + + return res.status(200).send({ ok: true, data: missionPatches }); + } catch (error) { capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); + res.status(500).send({ ok: false, code: error.message }); } - - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); - - const mission = await MissionModel.findById(id); - if (!mission) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - - const missionPatches = await patches.get(req, MissionModel); - if (!missionPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - - return res.status(200).send({ ok: true, data: missionPatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: error.message }); - } -}); + }, +); router.get("/:id/application", passport.authenticate("referent", { session: false, failWithError: true }), async (req, res) => { try { diff --git a/api/src/controllers/structure.js b/api/src/controllers/structure.js index 94b8b10652..277d7780c6 100644 --- a/api/src/controllers/structure.js +++ b/api/src/controllers/structure.js @@ -1,4 +1,5 @@ const express = require("express"); +const Joi = require("joi"); const router = express.Router(); const passport = require("passport"); const { capture } = require("../sentry"); @@ -17,14 +18,16 @@ const { isSupervisor, isAdmin, SENDINBLUE_TEMPLATES, - canViewPatchesHistory, } = require("snu-lib"); const patches = require("./patches"); const { sendTemplate } = require("../brevo"); -const { validateId, validateStructure, validateStructureManager } = require("../utils/validator"); +const { validateId, validateStructure, validateStructureManager, idSchema } = require("../utils/validator"); const { serializeStructure, serializeArray, serializeMission } = require("../utils/serializer"); const { serializeMissions, serializeReferents } = require("../utils/es-serializer"); const { allRecords } = require("../es/utils"); +const { requestValidatorMiddleware } = require("../middlewares/requestValidatorMiddleware"); +const { accessControlMiddleware } = require("../middlewares/accessControlMiddleware"); +const { authMiddleware } = require("../middlewares/authMiddleware"); const setAndSave = async (data, keys, fromUser) => { data.set({ ...keys }); @@ -203,28 +206,32 @@ router.get("/:id/mission", passport.authenticate("referent", { session: false, f } }); -router.get("/:id/patches", passport.authenticate("referent", { session: false, failWithError: true }), async (req, res) => { - try { - const { error, value: id } = validateId(req.params.id); - if (error) { +router.get( + "/:id/patches", + authMiddleware("referent"), + [ + requestValidatorMiddleware({ + params: Joi.object({ id: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN]), + ], + async (req, res) => { + try { + const { id } = req.params; + + const structure = await StructureModel.findById(id); + if (!structure) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + + const structurePatches = await patches.get(req, StructureModel); + if (!structurePatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + + return res.status(200).send({ ok: true, data: structurePatches }); + } catch (error) { capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); + res.status(500).send({ ok: false, code: error.message }); } - - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); - - const structure = await StructureModel.findById(id); - if (!structure) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - - const structurePatches = await patches.get(req, StructureModel); - if (!structurePatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - - return res.status(200).send({ ok: true, data: structurePatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: error.message }); - } -}); + }, +); router.get("/:id", passport.authenticate(["referent", "young"], { session: false, failWithError: true }), async (req, res) => { try { diff --git a/api/src/controllers/young/index.ts b/api/src/controllers/young/index.ts index d231538568..038fdf3a47 100644 --- a/api/src/controllers/young/index.ts +++ b/api/src/controllers/young/index.ts @@ -33,7 +33,7 @@ import { import { getMimeFromFile, getMimeFromBuffer } from "../../utils/file"; import { sendTemplate, unsync } from "../../brevo"; import { cookieOptions, COOKIE_SIGNIN_MAX_AGE_MS } from "../../cookie-options"; -import { validateYoung, validateId, validatePhase1Document } from "../../utils/validator"; +import { validateYoung, validateId, validatePhase1Document, idSchema } from "../../utils/validator"; import patches from "../patches"; import { serializeYoung, serializeApplication } from "../../utils/serializer"; import { @@ -57,7 +57,6 @@ import { ReferentType, getDepartmentForEligibility, FUNCTIONAL_ERRORS, - canViewPatchesHistory, CohortDto, } from "snu-lib"; import { getFilteredSessions } from "../../utils/cohort"; @@ -69,6 +68,9 @@ import scanFile from "../../utils/virusScanner"; import emailsEmitter from "../../emails"; import { UserRequest } from "../request"; import { FileTypeResult } from "file-type"; +import { requestValidatorMiddleware } from "../../middlewares/requestValidatorMiddleware"; +import { authMiddleware } from "../../middlewares/authMiddleware"; +import { accessControlMiddleware } from "../../middlewares/accessControlMiddleware"; const router = express.Router(); const YoungAuth = new AuthObject(YoungModel); @@ -406,27 +408,31 @@ router.put("/update_phase3/:young", passport.authenticate("referent", { session: } }); -router.get("/:id/patches", passport.authenticate("referent", { session: false, failWithError: true }), async (req: UserRequest, res) => { - try { - const { error, value: id } = validateId(req.params.id); - if (error) { - capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); - } - - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); +router.get( + "/:id/patches", + authMiddleware("referent"), + [ + requestValidatorMiddleware({ + params: Joi.object({ id: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN, ROLES.REFERENT_CLASSE, ROLES.ADMINISTRATEUR_CLE]), + ], + async (req: UserRequest, res) => { + try { + const { id } = req.params; - const young = await YoungModel.findById(id); - if (!young) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + const young = await YoungModel.findById(id); + if (!young) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const youngPatches = await patches.get(req, YoungModel); - if (!youngPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - return res.status(200).send({ ok: true, data: youngPatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); - } -}); + const youngPatches = await patches.get(req, YoungModel); + if (!youngPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + return res.status(200).send({ ok: true, data: youngPatches }); + } catch (error) { + capture(error); + res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); + } + }, +); router.put("/:id/validate-mission-phase3", passport.authenticate("young", { session: false, failWithError: true }), async (req: UserRequest, res) => { try { diff --git a/api/src/referent/referentController.ts b/api/src/referent/referentController.ts index dd36bb25c5..4c1aa6d8e4 100644 --- a/api/src/referent/referentController.ts +++ b/api/src/referent/referentController.ts @@ -106,7 +106,6 @@ import { YoungType, getDepartmentForEligibility, isAdmin, - canViewPatchesHistory, } from "snu-lib"; import { getFilteredSessions, getAllSessions } from "../utils/cohort"; import scanFile from "../utils/virusScanner"; @@ -116,6 +115,9 @@ import { mightAddInProgressStatus, shouldSwitchYoungByIdToLC, switchYoungByIdToL import { getCohortIdsFromCohortName } from "../cohort/cohortService"; import { getCompletionObjectifs } from "../services/inscription-goal"; import SNUpport from "../SNUpport"; +import { requestValidatorMiddleware } from "../middlewares/requestValidatorMiddleware"; +import { accessControlMiddleware } from "../middlewares/accessControlMiddleware"; +import { authMiddleware } from "../middlewares/authMiddleware"; const router = express.Router(); const ReferentAuth = new AuthObject(ReferentModel); @@ -1383,27 +1385,31 @@ router.get("/young/:id", passport.authenticate("referent", { session: false, fai } }); -router.get("/:id/patches", passport.authenticate("referent", { session: false, failWithError: true }), async (req: UserRequest, res: Response) => { - try { - const { error, value: id } = validateId(req.params.id); - if (error) { - capture(error); - return res.status(400).send({ ok: false, code: ERRORS.INVALID_PARAMS }); - } - - if (!canViewPatchesHistory(req.user)) return res.status(403).send({ ok: false, code: ERRORS.OPERATION_UNAUTHORIZED }); +router.get( + "/:id/patches", + authMiddleware("referent"), + [ + requestValidatorMiddleware({ + params: Joi.object({ id: idSchema().required() }), + }), + accessControlMiddleware([ROLES.REFERENT_DEPARTMENT, ROLES.REFERENT_REGION, ROLES.ADMIN]), + ], + async (req: UserRequest, res: Response) => { + try { + const { id } = req.params; - const referent = await ReferentModel.findById(id); - if (!referent) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + const referent = await ReferentModel.findById(id); + if (!referent) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - const referentPatches = await patches.get(req, ReferentModel); - if (!referentPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); - return res.status(200).send({ ok: true, data: referentPatches }); - } catch (error) { - capture(error); - res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); - } -}); + const referentPatches = await patches.get(req, ReferentModel); + if (!referentPatches) return res.status(404).send({ ok: false, code: ERRORS.NOT_FOUND }); + return res.status(200).send({ ok: true, data: referentPatches }); + } catch (error) { + capture(error); + res.status(500).send({ ok: false, code: ERRORS.SERVER_ERROR }); + } + }, +); async function populateReferent(ref) { if (ref.subRole === SUB_ROLES.referent_etablissement) { From f4f31d37094e91e798cb3e73bcda3812b74f5faa Mon Sep 17 00:00:00 2001 From: C2Chandelier Date: Fri, 8 Nov 2024 14:20:11 +0100 Subject: [PATCH 16/16] up --- admin/src/scenes/classe/components/HistoryRow.tsx | 1 - ...lateFieldsModel.js => translateFieldsModel.ts} | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) rename admin/src/utils/{translateFieldsModel.js => translateFieldsModel.ts} (98%) diff --git a/admin/src/scenes/classe/components/HistoryRow.tsx b/admin/src/scenes/classe/components/HistoryRow.tsx index a3a32a5635..c836015f96 100644 --- a/admin/src/scenes/classe/components/HistoryRow.tsx +++ b/admin/src/scenes/classe/components/HistoryRow.tsx @@ -24,7 +24,6 @@ type HistoryRowProps = ClasseProps | YoungProps; export default function HistoryRow({ patch, type }: HistoryRowProps) { const user = useSelector((state: AuthState) => state.Auth.user); - if (type === "young" && patch.user) { if (!patch.user.role) { if (patch.user.email) patch.user.role = "Volontaire"; diff --git a/admin/src/utils/translateFieldsModel.js b/admin/src/utils/translateFieldsModel.ts similarity index 98% rename from admin/src/utils/translateFieldsModel.js rename to admin/src/utils/translateFieldsModel.ts index 2c1bc618f8..4e28f9cf45 100644 --- a/admin/src/utils/translateFieldsModel.js +++ b/admin/src/utils/translateFieldsModel.ts @@ -1,3 +1,5 @@ +import { ClasseType } from "snu-lib"; + const translateFieldStructure = (f) => { switch (f) { case "name": @@ -712,8 +714,15 @@ const translateFieldContract = (f) => { } }; -export const translateFieldClasse = (f) => { - switch (f) { +type ClasseField = ClasseType & { + "grades/0": string; + "grades/1": string; + "grades/2": string; + classeStatus: string; +}; + +export const translateFieldClasse = (field: keyof ClasseField) => { + switch (field) { case "etablissementId": return "ID de l'établissement"; case "referentClasseIds": @@ -765,7 +774,7 @@ export const translateFieldClasse = (f) => { case "createdAt": return "Créé(e) le"; default: - return f; + return field; } };