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

feat(api,admin): 3565 - Ajout d'une page Historique de la classe #4496

Merged
merged 23 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
40 changes: 30 additions & 10 deletions admin/src/components/UserCard.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
import React from "react";
import { ROLES, translate } from "snu-lib";
import cx from "classnames";

export default function UserCard({ user }) {
function getAvatar(user) {
if (!user) return "??";
if (user?.firstName === "Acteur inconnu") return "?";
if (user?.firstName && user?.lastName) return `${user?.firstName?.substring(0, 1)}${user?.lastName ? user.lastName.substring(0, 1) : null}`;
return "🤖";
// return "?";
if (user?.firstName && !user?.lastname) return "🤖";
}
function getlink(user) {
function getLink(user) {
if (Object.values(ROLES).includes(user?.role)) return `/user/${user._id}`;
if (user?.role === "Volontaire") return `/volontaire/${user._id}`;
return null;
}
function getRole(user) {
if (!user) return "Donnée indisponible";
if (user.role) {
return translate(user.role);
} else {
return "Script";
}
}
function getAuthor(user) {
if (!user) return "Auteur inconnu";
if (user.firstName && user.lastName) return `${user.firstName} ${user.lastName}`;
if (user.firstName && !user.lastName) return user.firstName;
}

if (!user) return null;
return (
<a href={getlink(user)} className=" group flex w-full flex-col hover:cursor-pointer">
<a
href={getLink(user)}
className={cx("group flex w-full flex-col", {
"hover:cursor-pointer": getLink(user) !== null,
"hover:text-inherit": getLink(user) === null,
})}>
<div className="flex items-center gap-2">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 border-white bg-slate-100 font-medium uppercase text-blue-600 transition group-hover:bg-blue-600 group-hover:text-slate-100">
<div
className={cx("flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 border-white bg-slate-100 font-medium uppercase ", {
"text-blue-600 transition group-hover:bg-blue-600 group-hover:text-slate-100": getLink(user) !== null,
"text-cyan-900": getLink(user) === null,
})}>
{getAvatar(user)}
</div>
<div className="flex w-10/12 flex-col leading-5">
<p className="w-full truncate font-medium decoration-2 underline-offset-2">
{user.firstName} {user.lastName && user.lastName}
</p>
<p className="w-full truncate capitalize text-gray-400 decoration-2 underline-offset-2">{translate(user?.role)}</p>
<p className="w-full truncate font-medium decoration-2 underline-offset-2 text-sm">{getAuthor(user)}</p>
<p className="w-full truncate capitalize text-gray-500 text-xs decoration-2 underline-offset-2">{getRole(user)}</p>
</div>
</div>
</a>
Expand Down
44 changes: 44 additions & 0 deletions admin/src/scenes/classe/components/NavbarClasse.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Navbar
tab={[
{
title: "Informations",
leftIcon: <HiOutlineClipboardList size={20} className="mt-0.5 ml-2.5" />,
isActive: location.pathname === `/classes/${classeId}`,
onClick: () => {
history.push(`/classes/${classeId}`);
},
},
{
title: "Historique de la classe",
leftIcon: <LuHistory size={20} className="mt-0.5 ml-2.5" />,
isActive: location.pathname === `/classes/${classeId}/historique`,
onClick: () => {
history.push(`/classes/${classeId}/historique`);
},
},
{
title: "Historique des inscriptions",
leftIcon: <LuHistory size={20} className="mt-0.5 ml-2.5" />,
isActive: location.pathname === `/classes/${classeId}/inscriptions`,
onClick: () => {
history.push(`/classes/${classeId}/inscriptions`);
},
},
]}
/>
);
}
15 changes: 15 additions & 0 deletions admin/src/scenes/classe/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
};
56 changes: 56 additions & 0 deletions admin/src/scenes/classe/header/ClasseHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useMemo } from "react";
import { useSelector } from "react-redux";

import { Header, Badge } from "@snu/ds/admin";
import { translateStatusClasse, ClassesRoutes, canInviteYoung, COHORT_TYPE } from "snu-lib";
import { TStatus } from "@/types";
import { AuthState } from "@/redux/auth/reducer";
import { CohortState } from "@/redux/cohorts/reducer";
import { appURL } from "@/config";

import { getHeaderActionList } from "./index";
import NavbarClasse from "../components/NavbarClasse";
import { statusClassForBadge, getRights } from "../utils";

interface Props {
classe: NonNullable<ClassesRoutes["GetOne"]["response"]["data"]>;
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 cohorts = useSelector((state: CohortState) => state.Cohorts).filter(
(c) => classe?.cohort === c.name || (c.type === COHORT_TYPE.CLE && getRights(user, classe, c).canEditCohort),
);
const cohort = cohorts.find((c) => c.name === classe?.cohort);

const canPerformManualInscriptionActions = useMemo(() => {
return canInviteYoung(user, cohort ?? null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user.role, cohort]);

const url = `${appURL}/je-rejoins-ma-classe-engagee?id=${classe._id.toString()}`;
const id = classe._id;
return (
<>
<Header
title={classe.name || "Informations nécessaires"}
titleComponent={<Badge className="mx-4 mt-2" title={translateStatusClasse(classe.status)} status={statusClassForBadge(classe.status) as TStatus} />}
breadcrumb={[
{ title: "Séjours" },
{
title: "Classes",
to: "/classes",
},
{ title: page },
]}
actions={getHeaderActionList({ user, classe, setClasse, isLoading, setIsLoading, url, id, studentStatus, canPerformManualInscriptionActions })}
/>
<NavbarClasse classeId={id} />
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
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, canInviteYoung } 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<ClassesRoutes["GetOne"]["response"]["data"]>();
const [url, setUrl] = useState("");
const [studentStatus, setStudentStatus] = useState<{ [key: string]: number }>({});
export default function Details(props) {
const [classe, setClasse] = useState(props.classe);
C2Chandelier marked this conversation as resolved.
Show resolved Hide resolved
const studentStatus = props.studentStatus;
const [showModaleCohort, setShowModaleCohort] = useState(false);
const { id } = useParams<{ id: string }>();
const [errors, setErrors] = useState({});
Expand All @@ -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
Expand All @@ -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]);
Expand Down Expand Up @@ -224,28 +198,11 @@ export default function View() {
setErrors({});
};

const canPerformManualInscriptionActions = useMemo(() => {
return canInviteYoung(user, cohort ?? null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user.role, cohort]);

if (!classe) return <Loader />;

return (
<Page>
<Header
title={classe.name || "Informations nécessaires"}
titleComponent={<Badge className="mx-4 mt-2" title={translateStatusClasse(classe.status)} status={statusClassForBadge(classe.status) as TStatus} />}
breadcrumb={[
{ title: "Séjours" },
{
title: "Classes",
to: "/classes",
},
{ title: "Fiche de la classe" },
]}
actions={getHeaderActionList({ user, classe, setClasse, isLoading, setIsLoading, url, id, studentStatus, canPerformManualInscriptionActions })}
/>
<ClasseHeader classe={classe} setClasse={setClasse} isLoading={isLoading} setIsLoading={setIsLoading} studentStatus={studentStatus} page={"Fiche de la classe"} />
<GeneralInfos
classe={classe}
setClasse={setClasse}
Expand Down
Loading
Loading