Skip to content

Commit

Permalink
gestion des membres, invitation, emailplanners ...
Browse files Browse the repository at this point in the history
  • Loading branch information
pprev94 committed Nov 26, 2024
1 parent 51f80a7 commit bb5bc49
Show file tree
Hide file tree
Showing 26 changed files with 610 additions and 362 deletions.
29 changes: 28 additions & 1 deletion assets/@types/app_espaceco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,41 @@ export type UserType = {
surname: string | null;
};

export type Role = "pending" | "member" | "admin";
export type Role = "pending" | "member" | "admin" | "invited";
export type CommunityMember = UserType & {
grids: GridDTO[];
role: Role;
active: boolean;
date: string;
};

export type CommunityMemberDetailed = {
user_id: number;
community_name: string;
community_id: number;
grids: GridDTO[];
role: Role;
active: boolean;
date: string;
};

export type Profile = {
community_id: number;
themes: ThemeDTO[];
};

export type UserMe = {
id: number;
email: string;
username: string;
surname: string | null;
description: string | null;
administrator: boolean;
profile: Profile;
shared_themes: SharedThemesDTO[];
communities_member: CommunityMemberDetailed[];
};

/* FORMULAIRES */
export type ReportFormType = {
attributes: ThemeDTO[];
Expand Down
16 changes: 16 additions & 0 deletions assets/espaceco/api/community.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ const getCommunityMembershipRequests = (communityId: number, signal: AbortSignal
});
};

const addMembers = (communityId: number, members: (number | string)[]) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_add_members", { communityId });
return jsonFetch<CommunityMember[]>(
url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
},
{ members: members }
);
};

const updateMemberRole = (communityId: number, userId: number, role: Role) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_update_member_role", { communityId, userId });
return jsonFetch<CommunityMember>(
Expand Down Expand Up @@ -118,6 +133,7 @@ const community = {
getCommunity,
getCommunityMembers,
getCommunityMembershipRequests,
addMembers,
searchByName,
getAsMember,
updateMemberRole,
Expand Down
11 changes: 10 additions & 1 deletion assets/espaceco/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { UserMe } from "../../@types/app_espaceco";
import { UserDTO, UserSharedThemesDTO } from "../../@types/espaceco";
import { jsonFetch } from "../../modules/jsonFetch";
import SymfonyRouting from "../../modules/Routing";

const getMe = (signal: AbortSignal) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_user_me");
return jsonFetch<UserMe>(url, {
method: "GET",
signal: signal,
});
};

const getSharedThemes = () => {
const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_user_shared_themes");
return jsonFetch<UserSharedThemesDTO[]>(url);
Expand All @@ -15,6 +24,6 @@ const search = (search: string, signal: AbortSignal) => {
});
};

const user = { search, getSharedThemes };
const user = { getMe, search, getSharedThemes };

export default user;
223 changes: 223 additions & 0 deletions assets/espaceco/pages/communities/MemberInvitation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { fr } from "@codegouvfr/react-dsfr";
import Alert from "@codegouvfr/react-dsfr/Alert";
import ButtonsGroup from "@codegouvfr/react-dsfr/ButtonsGroup";
import CallOut from "@codegouvfr/react-dsfr/CallOut";
import { cx } from "@codegouvfr/react-dsfr/tools/cx";
import { useMutation, useQuery } from "@tanstack/react-query";
import { FC, useMemo } from "react";
import { CommunityMember, Role, UserMe } from "../../../@types/app_espaceco";
import { CommunityResponseDTO } from "../../../@types/espaceco";
import AppLayout from "../../../components/Layout/AppLayout";
import LoadingText from "../../../components/Utils/LoadingText";
import Wait from "../../../components/Utils/Wait";
import { datastoreNavItems } from "../../../config/datastoreNavItems";
import { declareComponentKeys, Translations, useTranslation } from "../../../i18n/i18n";
import RQKeys from "../../../modules/espaceco/RQKeys";
import { CartesApiException } from "../../../modules/jsonFetch";
import { routes } from "../../../router/router";
import api from "../../api";

import "../../../../assets/sass/pages/espaceco/member_invitation.scss";

type MemberInvitationProps = {
communityId: number;
};

const MemberInvitation: FC<MemberInvitationProps> = ({ communityId }) => {
const { t } = useTranslation("MemberInvitation");
const { t: tBreadcrumb } = useTranslation("Breadcrumb");

const navItems = useMemo(() => datastoreNavItems(), []);

const query = useQuery<CommunityResponseDTO, CartesApiException>({
queryKey: RQKeys.community(communityId),
queryFn: () => api.community.getCommunity(communityId),
staleTime: 3600000,
});

const meQuery = useQuery<UserMe>({
queryKey: RQKeys.getMe(),
queryFn: ({ signal }) => api.user.getMe(signal),
staleTime: 3600000,
});

const myRole = useMemo<Role | undefined>(() => {
let role: Role | undefined;
if (meQuery.data) {
const user_id = meQuery.data.id;
const invitations = meQuery.data.communities_member.filter((m) => m.community_id === communityId && m.user_id === user_id);
if (invitations.length === 1) {
role = invitations[0].role;
}
}
return role;
}, [meQuery.data, communityId]);

/* Invitation : role : "invited" => "member" */
const updateRoleMutation = useMutation<CommunityMember | undefined, CartesApiException>({
mutationFn: () => {
if (meQuery.data?.id) {
return api.community.updateMemberRole(communityId, meQuery.data.id, "member");
}
return Promise.resolve(undefined);
},
});

/* Suppression du membre, Mais comme il a créé son compte, il reste inscrit sur cartes.gouv. */
const removeMemberMutation = useMutation<{ user_id: number } | undefined, CartesApiException>({
mutationFn: () => {
if (meQuery.data?.id) {
return api.community.removeMember(communityId, meQuery.data.id);
}
return Promise.resolve(undefined);
},
onSuccess: () => routes.espaceco_community_list().push(),
});

const community = useMemo(() => query.data, [query.data]);

return (
<AppLayout
navItems={navItems}
customBreadcrumbProps={{
homeLinkProps: routes.home().link,
segments: [],
currentPageLabel: tBreadcrumb("espaceco_member_invitation"),
}}
documentTitle={t("document_title")}
>
<h1>{t("document_title")}</h1>
{query.isLoading && <LoadingText as="h6" message={t("community_loading")} />}
{meQuery.isLoading && <LoadingText as="h6" message={t("userme_loading")} />}

{query.isError && <Alert severity="error" closable title={t("community_loading_failed")} />}
{meQuery.isError && <Alert severity="error" closable title={t("userme_loading_failed")} />}

{updateRoleMutation.isError && <Alert severity="error" closable title={updateRoleMutation.error.message} />}
{updateRoleMutation.isPending && (
<Wait>
<div className={fr.cx("fr-grid-row")}>
<LoadingText as="h6" message={t("inviting")} withSpinnerIcon={true} />
</div>
</Wait>
)}

{removeMemberMutation.isError && <Alert severity="error" closable title={removeMemberMutation.error.message} />}
{removeMemberMutation.isPending && (
<Wait>
<div className={fr.cx("fr-grid-row")}>
<LoadingText as="h6" message={t("rejecting")} withSpinnerIcon={true} />
</div>
</Wait>
)}

{community && myRole === "invited" ? (
<div>
<CallOut
title={
<div className={fr.cx("fr-grid-row", "fr-grid-row--middle")}>
<img
className={cx(fr.cx("fr-mr-2v"), "frx-invitation-img")}
alt={t("logo")}
src={community.logo_url ? community.logo_url : "https://www.systeme-de-design.gouv.fr/img/placeholder.1x1.png"}
/>
{t("community_name", { name: community.name })}
</div>
}
>
<div>
{t("community_description", { description: community.description })}
<ButtonsGroup
buttons={[
{
children: t("reject"),
priority: "secondary",
onClick: () => removeMemberMutation.mutate(),
},
{
children: t("accept"),
priority: "primary",
onClick: () => updateRoleMutation.mutate(),
},
]}
inlineLayoutWhen="always"
alignment="left"
className={fr.cx("fr-mt-2w")}
/>
</div>
</CallOut>
</div>
) : myRole !== undefined ? (
<p>{t("already_member")}</p>
) : (
<p>{t("no_invitation")}</p>
)}
</AppLayout>
);
};

export default MemberInvitation;

export const { i18n } = declareComponentKeys<
| "document_title"
| "community_loading"
| "community_loading_failed"
| "userme_loading"
| "userme_loading_failed"
| "logo"
| { K: "community_name"; P: { name: string }; R: JSX.Element }
| { K: "community_description"; P: { description: string | null }; R: JSX.Element }
| { K: "invitation"; R: JSX.Element }
| "already_member"
| "no_invitation"
| "accept"
| "reject"
| "inviting"
| "rejecting"
>()("MemberInvitation");

export const MemberInvitationFrTranslations: Translations<"fr">["MemberInvitation"] = {
document_title: "Invitation",
community_loading: "Chargement du guichet en cours ...",
community_loading_failed: "La récupération des informations du guichet a échoué.",
userme_loading: "Chargement de vos données utilisateur en cours ...",
userme_loading_failed: "La récupération de vous données d'utilisateur a échoué.",
logo: "Logo du guichet",
community_name: ({ name }) => (
<div>
Guichet <strong>{name}</strong>
</div>
),
community_description: ({ description }) => {
return description ? <p dangerouslySetInnerHTML={{ __html: description }} /> : <p>Aucune description</p>;
},
invitation: <p>Vous avez reçu une invitation à rejoindre le guichet :</p>,
already_member: "Vous êtes déjà membre de ce guichet",
no_invitation: "Vous n'avez pas reçu d'invitation de ce guichet",
accept: "Accepter et rejoindre le guichet",
reject: "Refuser l'invitation",
inviting: "Invitation en cours ...",
rejecting: "Refus en cours ...",
};

export const MemberInvitationEnTranslations: Translations<"en">["MemberInvitation"] = {
document_title: "Invitation",
community_loading: "Community loading ...",
community_loading_failed: undefined,
userme_loading: undefined,
userme_loading_failed: undefined,
logo: undefined,
community_name: ({ name }) => (
<p>
Community <strong>`${name}`</strong>
</p>
),
community_description: undefined,
invitation: undefined,
already_member: undefined,
no_invitation: undefined,
accept: undefined,
reject: undefined,
inviting: undefined,
rejecting: undefined,
};
4 changes: 1 addition & 3 deletions assets/espaceco/pages/communities/management/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ const Description: FC<DescriptionProps> = ({ community }) => {
staleTime: 3600000,
});

// TODO DECOMMENTER
const communityDocumentsQuery = useQuery<DocumentDTO[], CartesApiException>({
queryKey: RQKeys.communityDocuments(community.id),
queryFn: ({ signal }) => api.communityDocuments.getAll(community.id, [], signal),
Expand Down Expand Up @@ -81,7 +80,6 @@ const Description: FC<DescriptionProps> = ({ community }) => {
control,
register,
formState: { errors },
// setValue: setFormValue,
} = useForm({
resolver: yupResolver(schema(tValid)),
mode: "onChange",
Expand Down Expand Up @@ -154,7 +152,7 @@ export default Description;
export const { i18n } = declareComponentKeys<"loading_documents">()("Description");

export const DescriptionFrTranslations: Translations<"fr">["Description"] = {
loading_documents: "Recherche des tables pour la configuration des thèmes ...",
loading_documents: "Chargement des documents",
};

export const DescriptionEnTranslations: Translations<"en">["Description"] = {
Expand Down
Loading

0 comments on commit bb5bc49

Please sign in to comment.