Skip to content

Commit

Permalink
feat: ajout des filtres sur la liste des utilisateurs (#3387)
Browse files Browse the repository at this point in the history
  • Loading branch information
sbenfares authored Nov 23, 2023
1 parent bab4c5a commit a1cec5f
Show file tree
Hide file tree
Showing 22 changed files with 688 additions and 190 deletions.
12 changes: 11 additions & 1 deletion server/src/common/actions/users.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,17 @@ export const getAllUsers = async (
},
},
},
{ $project: { type: 1, nom: 1, raison_sociale: 1, reseaux: 1, nature: 1 } },
{
$project: {
type: 1,
nom: 1,
raison_sociale: 1,
reseaux: 1,
nature: 1,
"adresse.departement": 1,
"adresse.region": 1,
},
},
],
},
},
Expand Down
6 changes: 6 additions & 0 deletions ui/common/constants/usersConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ export const USER_STATUS_LABELS = {
PENDING_ADMIN_VALIDATION: "en attente validation admin",
CONFIRMED: "accès confirmé",
};

export const USER_STATUS_STYLE = {
PENDING_EMAIL_VALIDATION: "red",
PENDING_ADMIN_VALIDATION: "orange",
CONFIRMED: "green",
};
14 changes: 14 additions & 0 deletions ui/common/internal/Organisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ export const organisationTypes = [
"ADMINISTRATEUR",
] as const;

export const TYPES_ORGANISATION = [
{ key: "ACADEMIE", nom: "Académie" },
{ key: "ADMINISTRATEUR", nom: "Administrateur" },
{ key: "CARIF_OREF_NATIONAL", nom: "Carif-Oref national" },
{ key: "CARIF_OREF_REGIONAL", nom: "Carif-Oref régional" },
{ key: "CONSEIL_REGIONAL", nom: "Conseil régional" },
{ key: "DDETS", nom: "DDETS" },
{ key: "DRAAF", nom: "DRAAF" },
{ key: "DREETS", nom: "DREETS" },
{ key: "OPERATEUR_PUBLIC_NATIONAL", nom: "Opérateur public national" },
{ key: "ORGANISME_FORMATION", nom: "Organisme de formation" },
{ key: "TETE_DE_RESEAU", nom: "Tête de réseau" },
] as const;

export type Organisation = { _id: string } & (
| OrganisationOrganismeFormation
| OrganisationTeteReseau
Expand Down
67 changes: 42 additions & 25 deletions ui/common/internal/User.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
// récupéré de l'API et adapté pour ne pas avoir certains champs optionnels
export interface UsersPaginated {
pagination: Pagination;
data: User[];
}

export interface User {
_id: string;
/**
* Email utilisateur
*/
account_status: string;
password_updated_at: Date;
created_at: Date;
email: string;
/**
* civilité
*/
civility: "Madame" | "Monsieur";
/**
* Le nom de l'utilisateur
*/
civility: string;
nom: string;
/**
* Le prénom de l'utilisateur
*/
prenom: string;
/**
* Le téléphone de l'utilisateur
*/
telephone: string;
/**
* La fonction de l'utilisateur
*/
fonction: string;
/**
* Date de création du compte
*/
created_at: string;
telephone: string;
has_accept_cgu_version: string;
organisation_id: string;
organisation: UserOrganisation;
last_connection: string;
}

export interface UserOrganisation {
_id: string;
created_at: Date;
type: string;
uai: string;
siret: string;
organisme: UserOrganisme;
label: string;
}

export interface UserOrganisme {
_id: string;
nom: string;
reseaux: any[];
nature: string;
raison_sociale: string;
adresse?: {
departement?: string;
region?: string;
};
}

export interface Pagination {
total: number;
page: number;
limit: number;
lastPage: number;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Badge, Button, HStack, Stack, Text } from "@chakra-ui/react";
import { Dispatch, SetStateAction } from "react";

interface OrganismesFilterButtonProps {
interface FilterButtonProps {
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
buttonLabel: string;
badge?: number;
}

export function OrganismesFilterButton(props: OrganismesFilterButtonProps) {
export function FilterButton(props: FilterButtonProps) {
const hasFilters = props.badge !== undefined && props.badge > 0;
return (
<Button
Expand Down
71 changes: 71 additions & 0 deletions ui/hooks/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/router";
import { useMemo } from "react";

import { _get, _post, _put } from "@/common/httpClient";
import { UsersPaginated } from "@/common/internal/User";
import { UserNormalized, toUserNormalized } from "@/modules/admin/users/models/users";
import {
UsersFiltersQuery,
filterUsersArrayFromUsersFilters,
parseUsersFiltersFromQuery,
} from "@/modules/admin/users/models/users-filters";

export function useUsers() {
const NO_LIMIT = 10_000;

const {
data: usersPaginated,
refetch: refetchUsers,
isLoading,
} = useQuery<UsersPaginated, any>(["admin/users"], () =>
_get("/api/v1/admin/users/", {
params: {
page: 1,
limit: NO_LIMIT,
},
})
);

const allUsers = useMemo(() => {
if (!usersPaginated) return [];
return usersPaginated.data.map((user) => toUserNormalized(user));
}, [usersPaginated]);

return {
isLoading,
refetchUsers,
allUsers,
};
}

export function useUsersFiltered(users: UserNormalized[]) {
const router = useRouter();

const filteredUsers = useMemo(() => {
return users
? filterUsersArrayFromUsersFilters(
users,
parseUsersFiltersFromQuery(router.query as unknown as UsersFiltersQuery)
)
: [];
}, [users, router.query]);

return { filteredUsers };
}

export function useUsersSearched(usersFiltered: UserNormalized[], search: string) {
const searchedUsers = useMemo(() => {
if (!search) return usersFiltered;
return usersFiltered?.filter((user) => {
const searchLower = search.toLowerCase();
return (
user.normalizedNomPrenom.includes(searchLower) ||
user.normalizedEmail.includes(searchLower) ||
user.normalizedOrganismeNom.toLowerCase().includes(searchLower)
);
});
}, [search, usersFiltered]);

return { searchedUsers };
}
102 changes: 102 additions & 0 deletions ui/modules/admin/users/UsersFiltersPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Button, HStack, Stack, Text } from "@chakra-ui/react";
import { useRouter } from "next/router";
import { useMemo } from "react";

import {
PaginationInfosQuery,
convertPaginationInfosToQuery,
parsePaginationInfosFromQuery,
} from "@/modules/models/pagination";

import FiltreUsersAccountStatut from "./filters/FiltreUsersAccountStatut";
import FiltreUsersDepartment from "./filters/FiltreUsersDepartements";
import FiltreUsersRegion from "./filters/FiltreUsersRegion";
import FiltreUsersReseaux from "./filters/FiltreUsersReseaux";
import FiltreUserTypes from "./filters/FiltreUsersType";
import {
UsersFilters,
UsersFiltersQuery,
convertUsersFiltersToQuery,
parseUsersFiltersFromQuery,
} from "./models/users-filters";

const UsersFiltersPanel = () => {
const router = useRouter();

const { usersFilters, sort } = useMemo(() => {
const { pagination, sort } = parsePaginationInfosFromQuery(router.query as unknown as PaginationInfosQuery);
return {
usersFilters: parseUsersFiltersFromQuery(router.query as unknown as UsersFiltersQuery),
pagination: pagination,
sort: sort ?? [{ desc: false, id: "nom" }],
};
}, [JSON.stringify(router.query)]);

const updateState = (newParams: Partial<{ [key in keyof UsersFilters]: any }>) => {
void router.push(
{
pathname: router.pathname,
query: {
...(router.query.organismeId ? { organismeId: router.query.organismeId } : {}),
...convertUsersFiltersToQuery({ ...usersFilters, ...newParams }),
...convertPaginationInfosToQuery({ sort, ...newParams }),
},
},
undefined,
{ shallow: true }
);
};

const resetFilters = () => {
void router.push(
{
pathname: router.pathname,
query: { ...(router.query.organismeId ? { organismeId: router.query.organismeId } : {}) },
},
undefined,
{ shallow: true }
);
};

return (
<Stack spacing="0.5">
<Text fontSize="zeta" fontWeight="extrabold">
FILTRER PAR
</Text>
<HStack>
{/* FILTRE Département */}
<FiltreUsersDepartment
value={usersFilters.departements}
onChange={(departements) => updateState({ departements })}
/>

{/* FILTRE Région */}
<FiltreUsersRegion value={usersFilters.regions} onChange={(regions) => updateState({ regions })} />

{/* FILTRE Type Utilisateur */}
<FiltreUserTypes
value={usersFilters.type_utilisateur}
onChange={(type_utilisateur) => updateState({ type_utilisateur })}
/>

{/* FILTRE Réseau */}
<FiltreUsersReseaux value={usersFilters.reseaux} onChange={(reseaux) => updateState({ reseaux })} />

{/* FILTRE Statut du compte */}
<FiltreUsersAccountStatut
value={usersFilters.account_status}
onChange={(account_status) => updateState({ account_status })}
/>

{/* FILTRE Période */}

{/* REINITIALISER */}
<Button variant="link" onClick={resetFilters} fontSize="omega">
réinitialiser
</Button>
</HStack>
</Stack>
);
};

export default UsersFiltersPanel;
50 changes: 50 additions & 0 deletions ui/modules/admin/users/filters/FiltreUsersAccountStatut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Checkbox, CheckboxGroup, Stack } from "@chakra-ui/react";
import { useState } from "react";

import { USER_STATUS_LABELS } from "@/common/constants/usersConstants";
import { FilterButton } from "@/components/FilterButton/FilterButton";
import SimpleOverlayMenu from "@/modules/dashboard/SimpleOverlayMenu";

interface FiltreUsersAccountStatutTypesProps {
value: string[];
onChange: (statutsCompte: string[]) => void;
}

function FiltreUsersAccountStatut(props: FiltreUsersAccountStatutTypesProps) {
const [isOpen, setIsOpen] = useState(false);
const statutsCompte = props.value;

return (
<div>
<FilterButton
isOpen={isOpen}
setIsOpen={setIsOpen}
buttonLabel="Statut du compte"
badge={statutsCompte?.length}
/>
{isOpen && (
<SimpleOverlayMenu onClose={() => setIsOpen(false)} width="auto" p="3w">
<CheckboxGroup
defaultValue={statutsCompte}
size="sm"
onChange={(selectedAccountStatuts: string[]) => props.onChange(selectedAccountStatuts)}
>
<Stack>
<Checkbox iconSize="0.5rem" value="CONFIRMED" key="CONFIRMED">
{USER_STATUS_LABELS.CONFIRMED}
</Checkbox>
<Checkbox iconSize="0.5rem" value="PENDING_EMAIL_VALIDATION" key="PENDING_EMAIL_VALIDATION">
{USER_STATUS_LABELS.PENDING_EMAIL_VALIDATION}
</Checkbox>
<Checkbox iconSize="0.5rem" value="PENDING_ADMIN_VALIDATION" key="PENDING_ADMIN_VALIDATION">
{USER_STATUS_LABELS.PENDING_ADMIN_VALIDATION}
</Checkbox>
</Stack>
</CheckboxGroup>
</SimpleOverlayMenu>
)}
</div>
);
}

export default FiltreUsersAccountStatut;
Loading

0 comments on commit a1cec5f

Please sign in to comment.