Skip to content

Commit

Permalink
Merge pull request #342 from mission-apprentissage/ui/fixes-for-multi…
Browse files Browse the repository at this point in the history
…campagnes

[UI - Server] Ajout sticky bouton , vue table sur la création de campagne, corrige retours
  • Loading branch information
yohanngab authored Oct 29, 2024
2 parents 1172136 + ac7ca58 commit 87e0c38
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 50 deletions.
3 changes: 2 additions & 1 deletion server/src/controllers/formations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ export const updateFormation = tryCatch(async (req: any, res: any) => {

export const getFormationsEtablissementsDiplomesWithCampagnesCount = tryCatch(async (req: any, res: any) => {
const isObserver = req.user?.role === USER_ROLES.OBSERVER;
const isAdmin = req.user?.role === USER_ROLES.ADMIN;
const scope = isObserver ? req.user?.scope : null;
const userSiret = req.user?.etablissements?.map((etablissement) => etablissement.siret);
const userSiret = isAdmin ? [] : req.user?.etablissements?.map((etablissement) => etablissement.siret);

const { success, body } = await formationsService.getFormationsEtablissementsDiplomesWithCampagnesCount({
userSiret,
Expand Down
15 changes: 8 additions & 7 deletions server/src/dao/verbatims.dao.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { UpdateResult } from "kysely";
import { sql } from "kysely";
import { executeWithOffsetPagination } from "kysely-paginate";

Expand Down Expand Up @@ -201,18 +200,20 @@ export const getOne = async (query: { temoignageId: string; questionKey: string
.executeTakeFirst();
};

export const deleteManyByCampagneIds = async (campagneIds: string[]): Promise<UpdateResult[]> => {
const temoignages = await getKbdClient()
export const deleteManyByCampagneIds = async (campagneIds: string[]): Promise<boolean> => {
const temoignages = (await getKbdClient()
.selectFrom("temoignages_campagnes")
.select("temoignage_id")
.where("campagne_id", "in", campagneIds)
.execute();
.execute()) as unknown as { temoignageId: string }[];

const temoignagesIds = temoignages.map((t) => t.temoignage_id);
const temoignagesIds = temoignages.map((t) => t.temoignageId as string);

return getKbdClient()
return (await getKbdClient()
.updateTable("verbatims")
.set({ deleted_at: new Date() })
.where("temoignage_id", "in", temoignagesIds)
.execute();
.execute())
? true
: false;
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { Duration, IntituleFormation, StyledBadge } from "../styles/shared.style";
import { formatDate, isPlural } from "../utils";

const Cards = ({ displayedFormations, selectedFormations, setSelectedFormations, campagnes }) => {
const FormationsGrid = ({ displayedFormations, selectedFormations, setSelectedFormations, campagnes }) => {
return (
<FormationCardContainer>
{displayedFormations?.map((formation) => {
Expand Down Expand Up @@ -146,4 +146,4 @@ const Cards = ({ displayedFormations, selectedFormations, setSelectedFormations,
);
};

export default Cards;
export default FormationsGrid;
129 changes: 93 additions & 36 deletions ui/src/campagnes/CreateCampagnes/FormationsSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@ import { Alert } from "@codegouvfr/react-dsfr/Alert";
import { Button } from "@codegouvfr/react-dsfr/Button";
import { Checkbox } from "@codegouvfr/react-dsfr/Checkbox";
import { Pagination } from "@codegouvfr/react-dsfr/Pagination";
import { SegmentedControl } from "@codegouvfr/react-dsfr/SegmentedControl";
import { Table } from "@codegouvfr/react-dsfr/Table";
import { useContext, useState } from "react";
import BeatLoader from "react-spinners/BeatLoader";

import { DIPLOME_TYPE_MATCHER, USER_ROLES } from "../../constants";
import { DIPLOME_TYPE_MATCHER, USER_ROLES, VIEW_TYPES } from "../../constants";
import { UserContext } from "../../context/UserContext";
import useFetchCampagnes from "../../hooks/useFetchCampagnes";
import useFetchDiplomesWithCampagnesCount from "../../hooks/useFetchDiplomesWithCampagnesCount";
import useFetchRemoteFormations from "../../hooks/useFetchRemoteFormations";
import { remoteEtablissementLabelGetterFromFormation } from "../../utils/etablissement";
import FilterButtons from "../Shared/FilterButtons/FilterButtons";
import { LoaderContainer, SelectAllFormationContainer, TableContainer } from "../styles/shared.style";
import {
ActionContainer,
HeaderItem,
LoaderContainer,
SelectAllFormationContainer,
TableContainer,
} from "../styles/shared.style";
import { isPlural } from "../utils";
import Cards from "./Cards";
import FormationsGrid from "./FormationsGrid";
import pickFormationTableRows from "./pickFormationTableRows";

const REMOTE_FORMATION_BASE_QUERY = {
published: "true",
Expand Down Expand Up @@ -67,6 +77,7 @@ const FormationsSelector = ({ selectedFormations, setSelectedFormations }) => {
const [etablissementsOptions, setEtablissementsOptions] = useState([]);
const [userContext] = useContext(UserContext);
const [page, setPage] = useState(1);
const [viewType, setViewType] = useState(VIEW_TYPES.GRID);

const isAdmin = userContext.user?.role === USER_ROLES.ADMIN;
const userSiret = userContext.user?.etablissements?.map((etablissement) => etablissement.siret) || [];
Expand Down Expand Up @@ -99,14 +110,22 @@ const FormationsSelector = ({ selectedFormations, setSelectedFormations }) => {
pageSize: 100,
});

const {
diplomesFilter,
etablissementsFilter,
isSuccess: isSuccessDiplomesAndEtablissementsFilter,
} = useFetchDiplomesWithCampagnesCount();

const {
campagnes: campagnes,
isSuccess: isSuccessCampagnes,
isError: isErrorCampagnes,
isLoading: isLoadingCampagnes,
} = useFetchCampagnes({
key: search,
enabled: !!remoteFormations?.length,
diplome: diplomesFilter?.map((diplome) => diplome.intitule),
siret: etablissementsFilter?.map((etablissement) => etablissement.etablissementFormateurSiret),
enabled: !!remoteFormations?.length && isSuccessDiplomesAndEtablissementsFilter,
pageSize: 1000,
});

Expand Down Expand Up @@ -180,32 +199,57 @@ const FormationsSelector = ({ selectedFormations, setSelectedFormations }) => {
showSelect={!!remoteFormationsPagination?.nombre_de_page < 2}
/>
<SelectAllFormationContainer>
<Checkbox
disabled={!remoteFormations?.length}
options={[
{
label: checkboxLabel,
hintText: remoteFormationsPagination?.total
? `${selectedFormations.length}/${
remoteFormationsPagination?.total
} formation${isPlural(selectedFormations.length)} sélectionnée${isPlural(
selectedFormations.length
)}`
: "",
nativeInputProps: {
name: `selectAll`,
checked: selectedFormations?.length === remoteFormations?.length,
onChange: () => {
if (selectedFormations.length === remoteFormations.length) {
setSelectedFormations([]);
} else {
setSelectedFormations(remoteFormations);
}
<ActionContainer>
<Checkbox
disabled={!remoteFormations?.length}
options={[
{
label: checkboxLabel,
hintText: remoteFormationsPagination?.total
? `${selectedFormations.length}/${
remoteFormationsPagination?.total
} formation${isPlural(selectedFormations.length)} sélectionnée${isPlural(
selectedFormations.length
)}`
: "",
nativeInputProps: {
name: `selectAll`,
checked: selectedFormations?.length === remoteFormations?.length,
onChange: () => {
if (selectedFormations.length === remoteFormations.length) {
setSelectedFormations([]);
} else {
setSelectedFormations(remoteFormations);
}
},
},
},
},
]}
/>
]}
/>
<SegmentedControl
small
segments={[
{
iconId: "fr-icon-layout-grid-line",
label: "Grille",
nativeInputProps: {
checked: viewType === VIEW_TYPES.GRID,
onClick: () => setViewType(VIEW_TYPES.GRID),
readOnly: true,
},
},
{
iconId: "fr-icon-menu-fill",
label: "Tableau",
nativeInputProps: {
checked: viewType === VIEW_TYPES.TABLE,
onClick: () => setViewType(VIEW_TYPES.TABLE),
readOnly: true,
},
},
]}
/>
</ActionContainer>
</SelectAllFormationContainer>
</>
)}
Expand All @@ -226,13 +270,26 @@ const FormationsSelector = ({ selectedFormations, setSelectedFormations }) => {
</LoaderContainer>
) : (
<TableContainer>
<Cards
displayedFormations={remoteFormations}
selectedFormations={selectedFormations}
setSelectedFormations={setSelectedFormations}
campagnes={campagnes || []}
/>
{remoteFormationsPagination?.nombre_de_page > 1 && (
{viewType === VIEW_TYPES.GRID ? (
<FormationsGrid
displayedFormations={remoteFormations}
selectedFormations={selectedFormations}
setSelectedFormations={setSelectedFormations}
campagnes={campagnes || []}
/>
) : null}
{viewType === VIEW_TYPES.TABLE ? (
<Table
headers={["", "Formation", <HeaderItem key="createdCampagne">Campagnes créées</HeaderItem>]}
data={pickFormationTableRows({
remoteFormations,
selectedFormations,
setSelectedFormations,
campagnes: campagnes || [],
})}
/>
) : null}
{remoteFormationsPagination?.nombre_de_page > 1 ? (
<Pagination
count={remoteFormationsPagination.nombre_de_page}
defaultPage={page}
Expand All @@ -244,7 +301,7 @@ const FormationsSelector = ({ selectedFormations, setSelectedFormations }) => {
key: `pagination-link-${pageNumber}`,
})}
/>
)}
) : null}
</TableContainer>
)}
</>
Expand Down
109 changes: 109 additions & 0 deletions ui/src/campagnes/CreateCampagnes/pickFormationTableRows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { fr } from "@codegouvfr/react-dsfr";
import { Checkbox } from "@codegouvfr/react-dsfr/Checkbox";
import React from "react";
import { Tooltip } from "react-tooltip";

import { DIPLOME_TYPE_MATCHER } from "../../constants";
import { DiplomeLabel } from "../Shared/CampagnesTable/campagnesTable.style";
import { CampagnesCreatedCount, EtablissementLabelContainer } from "../styles/createCampagnes.style";
import { Duration, FormationContainer, IntituleFormation, StyledBadge } from "../styles/shared.style";
import { formatDate, isPlural } from "../utils";

const pickFormationTableRows = ({ remoteFormations, selectedFormations, setSelectedFormations, campagnes }) => {
return remoteFormations?.map((formation) => {
const alreadyCreatedCampagnes = campagnes?.filter((campagne) => campagne.formation.catalogueId === formation._id);
const alreadyCreatedCount = alreadyCreatedCampagnes.length;
const isSelected = selectedFormations.some((selectedFormation) => selectedFormation._id === formation._id);
const selectedFormationIds = selectedFormations.map((selectedFormation) => selectedFormation._id);

return [
<Checkbox
key={formation.id}
options={[
{
nativeInputProps: {
name: `campagne-${formation.id}`,
checked: isSelected,
onChange: () => {
setSelectedFormations((prevValue) =>
isSelected
? prevValue.filter((selectedFormation) => selectedFormation._id !== formation._id)
: [...prevValue, formation]
);
},
},
},
]}
/>,
<>
<FormationContainer key={formation._id}>
<div>
<StyledBadge small>{formation.tags.join("-")}</StyledBadge>
{formation?.duree && parseInt(formation?.duree) && (
<Duration>
· En {formation.duree} an{isPlural(parseInt(formation.duree))}
</Duration>
)}
</div>
<IntituleFormation>{formation.intitule_long}</IntituleFormation>
</FormationContainer>
<EtablissementLabelContainer>
<p data-tooltip-id={`tooltip-gestionnaire-formateur-${formation._id}`}>
{formation.etablissement_formateur_entreprise_raison_sociale || formation.etablissement_formateur_enseigne}
</p>
<Tooltip
id={`tooltip-gestionnaire-formateur-${formation._id}`}
variant="light"
opacity={1}
style={{ zIndex: 99999, boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)", maxWidth: "500px" }}
>
<p>
{formation.lieu_formation_adresse_computed ||
`${formation.lieu_formation_adresse}, ${formation.code_postal} ${formation.localite}`}
</p>
<p>N° Siret: {formation.etablissement_formateur_siret}</p>
{formation.etablissement_formateur_siret === formation.etablissement_gestionnaire_siret ? (
<p>
<span className={fr.cx("fr-icon-award-fill")} aria-hidden={true} /> Cet établissement est gestionnaire
et rattaché à votre compte Sirius
</p>
) : (
<p>
<span className={fr.cx("fr-icon-award-line")} aria-hidden={true} /> Cet établissement est formateur et
dispense des formations pour un établissement gestionnaire
</p>
)}
</Tooltip>
</EtablissementLabelContainer>
<DiplomeLabel>{DIPLOME_TYPE_MATCHER[formation.diplome] || formation.diplome}</DiplomeLabel>
</>,
<>
<CampagnesCreatedCount data-tooltip-id={`tooltip-already-created-${formation._id}`}>
{alreadyCreatedCount}
</CampagnesCreatedCount>
{alreadyCreatedCount > 0 && (
<Tooltip
id={`tooltip-already-created-${formation._id}`}
variant="light"
opacity={1}
style={{ zIndex: 99999, boxShadow: "0 0 5px rgba(0, 0, 0, 0.1)" }}
>
<p>
Campagne{isPlural(alreadyCreatedCount)} créée{isPlural(alreadyCreatedCount)} pour cette formation :
</p>
<ul>
{alreadyCreatedCampagnes.map((campagne) => (
<li key={campagne.id}>
{formatDate(campagne.startDate)} - {formatDate(campagne.endDate)} {campagne.temoignagesCount}{" "}
répondant{isPlural(campagne.temoignagesCount)}
</li>
))}
</ul>
</Tooltip>
)}
</>,
];
});
};

export default pickFormationTableRows;
4 changes: 3 additions & 1 deletion ui/src/campagnes/CreateCampagnesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ const CreateCampagnesPage = () => {
severity="error"
/>
)}
</CreateCampagneContainer>
{selectedFormations.length && (
<ButtonContainer>
{step === 1 && (
<Button iconId="fr-icon-add-line" disabled={!selectedFormations.length} onClick={() => setStep(2)}>
Expand Down Expand Up @@ -185,7 +187,7 @@ const CreateCampagnesPage = () => {
</>
)}
</ButtonContainer>
</CreateCampagneContainer>
)}
</Container>
<SupportModal modal={supportModal} token={userContext.token} />
</>
Expand Down
Loading

0 comments on commit 87e0c38

Please sign in to comment.