diff --git a/back/src/users/resolvers/queries/__tests__/membershipRequest.integration.ts b/back/src/users/resolvers/queries/__tests__/membershipRequest.integration.ts index a58e249281..85c0c52fe4 100644 --- a/back/src/users/resolvers/queries/__tests__/membershipRequest.integration.ts +++ b/back/src/users/resolvers/queries/__tests__/membershipRequest.integration.ts @@ -101,7 +101,7 @@ describe("query membershipRequest", () => { status: "PENDING", email: requester.email, siret: company.siret, - name: company.name, + name: requester.name, sentTo: [admin.email, "so****@test.fr"] // emails not belonging to user email domain are partially redacted }); }); @@ -127,7 +127,7 @@ describe("query membershipRequest", () => { status: "PENDING", email: requester.email, siret: company.siret, - name: company.name + name: requester.name }); }); @@ -157,7 +157,7 @@ describe("query membershipRequest", () => { status: "PENDING", email: requester.email, siret: company.orgId, - name: company.name + name: requester.name }); }); }); diff --git a/back/src/users/resolvers/queries/membershipRequest.ts b/back/src/users/resolvers/queries/membershipRequest.ts index d4c402d795..639b7e0a63 100644 --- a/back/src/users/resolvers/queries/membershipRequest.ts +++ b/back/src/users/resolvers/queries/membershipRequest.ts @@ -62,7 +62,7 @@ const invitationRequestResolver: QueryResolvers["membershipRequest"] = async ( status: invitationRequest.status, email: membershipRequestUser.email, siret: company.orgId, - name: company.name ?? "" + name: membershipRequestUser.name ?? "" }; }; diff --git a/front/src/Apps/Account/AccountMembershipRequest.tsx b/front/src/Apps/Account/AccountMembershipRequest.tsx deleted file mode 100644 index fbb0f535e7..0000000000 --- a/front/src/Apps/Account/AccountMembershipRequest.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import React, { useState } from "react"; -import { useQuery, useMutation, gql } from "@apollo/client"; -import { - InlineError, - NotificationError -} from "../common/Components/Error/Error"; -import Loader from "../common/Components/Loader/Loaders"; -import { - MembershipRequestStatus, - Mutation, - Query, - UserRole -} from "@td/codegen-ui"; -import { useNavigate, useParams } from "react-router-dom"; -import toast from "react-hot-toast"; -import { TOAST_DURATION } from "../../common/config"; - -const MEMBERSHIP_REQUEST = gql` - query MembershipRequest($id: ID!) { - membershipRequest(id: $id) { - email - siret - name - status - } - } -`; - -const ACCEPT_MEMBERSHIP_REQUEST = gql` - mutation AcceptMembershipRequest($id: ID!, $role: UserRole!) { - acceptMembershipRequest(id: $id, role: $role) { - id - users { - id - role - } - } - } -`; - -const REFUSE_MEMBERSHIP_REQUEST = gql` - mutation RefuseMembershipRequest($id: ID!) { - refuseMembershipRequest(id: $id) { - id - users { - id - role - } - } - } -`; - -export default function AccountMembershipRequest() { - const { id } = useParams<{ id: string }>(); - - const navigate = useNavigate(); - - const [userRole, setUserRole] = useState(UserRole.Member); - - const { loading, error, data } = useQuery>( - MEMBERSHIP_REQUEST, - { - variables: { id } - } - ); - - const [ - acceptMembershipRequest, - { loading: acceptLoading, error: acceptError } - ] = useMutation>( - ACCEPT_MEMBERSHIP_REQUEST, - { - onCompleted: () => { - toast.success("La demande de rattachement a bien été acceptée", { - duration: TOAST_DURATION - }); - navigate("/"); - } - } - ); - - const [ - refuseMembershipRequest, - { loading: refuseLoading, error: refuseError } - ] = useMutation>( - REFUSE_MEMBERSHIP_REQUEST, - { - onCompleted: () => { - toast.success("La demande de rattachement a bien été refusée", { - duration: TOAST_DURATION - }); - navigate("/"); - } - } - ); - - if (loading) { - return ; - } - - if (error) { - return ; - } - - if ( - data && - data.membershipRequest?.status !== MembershipRequestStatus.Pending - ) { - return ( -
Cette demande de rattachement a déjà été acceptée ou refusée
- ); - } - - if (data && data.membershipRequest) { - const { email, siret, name } = data.membershipRequest; - - return ( -
-
-

- Un utilisateur aimerait rejoindre l'établissement {name} ({siret}) -

- -
- - -
- -
- - Annuler - - - -
- {acceptError && } - {refuseError && } -
-
- ); - } - - return null; -} diff --git a/front/src/Apps/Companies/CompanyMembers/CompanyMembers.tsx b/front/src/Apps/Companies/CompanyMembers/CompanyMembers.tsx index 54ac2d2b10..0ece990776 100644 --- a/front/src/Apps/Companies/CompanyMembers/CompanyMembers.tsx +++ b/front/src/Apps/Companies/CompanyMembers/CompanyMembers.tsx @@ -9,6 +9,7 @@ import * as COMPANY_CONSTANTS from "@td/constants"; import "./companyMembers.scss"; import CompanyMembersList from "./CompanyMembersList"; +import CompanyMembersRequestsList from "./CompanyMembersRequestsList"; const { VITE_VERIFY_COMPANY } = import.meta.env; @@ -35,6 +36,7 @@ const CompanyMembers = ({ return (
+ {isAdmin && } {isAdmin && (VITE_VERIFY_COMPANY !== "true" || !isProfessional || diff --git a/front/src/Apps/Companies/CompanyMembers/CompanyMembersRequestModal.tsx b/front/src/Apps/Companies/CompanyMembers/CompanyMembersRequestModal.tsx new file mode 100644 index 0000000000..a56c6211dd --- /dev/null +++ b/front/src/Apps/Companies/CompanyMembers/CompanyMembersRequestModal.tsx @@ -0,0 +1,181 @@ +import React, { useState } from "react"; +import { Modal } from "../../../common/components"; +import { useMutation, useQuery } from "@apollo/client"; +import { + ACCEPT_MEMBERSHIP_REQUEST, + REFUSE_MEMBERSHIP_REQUEST, + MEMBERSHIP_REQUEST, + MEMBERSHIP_REQUESTS, + MY_COMPANIES +} from "../common/queries"; +import { NotificationError } from "../../common/Components/Error/Error"; +import { Loader } from "../../common/Components"; +import { + MembershipRequestStatus, + Query, + UserRole, + Mutation +} from "@td/codegen-ui"; +import { userRoleSwitchOptions } from "./CompanyMembersList"; +import { Select } from "@codegouvfr/react-dsfr/Select"; +import { userRoleLabel } from "../common/utils"; + +type CompanyMembersRequestModalProps = { + id: string; + onClose: () => void; +}; + +export default function CompanyMembersRequestModal({ + id, + onClose +}: CompanyMembersRequestModalProps) { + const [userRole, setUserRole] = useState(UserRole.Member); + + const { + data: requestData, + loading: requestLoading, + error: requestError + } = useQuery>(MEMBERSHIP_REQUEST, { + variables: { id } + }); + + const [ + acceptMembershipRequest, + { data: acceptData, loading: acceptLoading, error: acceptError } + ] = useMutation>( + ACCEPT_MEMBERSHIP_REQUEST, + { + refetchQueries: [MEMBERSHIP_REQUESTS, MY_COMPANIES] + } + ); + + const [ + refuseMembershipRequest, + { data: refuseData, loading: refuseLoading, error: refuseError } + ] = useMutation>( + REFUSE_MEMBERSHIP_REQUEST, + { + refetchQueries: [MEMBERSHIP_REQUESTS] + } + ); + + const loading = acceptLoading || refuseLoading; + + const renderContent = () => { + if (requestLoading) { + return ( +
+ +
+ ); + } + + if (requestError) return ; + + if ( + requestData && + requestData.membershipRequest?.status !== MembershipRequestStatus.Pending + ) + return ( +
Cette demande de rattachement a déjà été acceptée ou refusée
+ ); + + if (acceptData) { + return ( +
+ L'utilisateur {requestData?.membershipRequest.name} a été rattaché à + votre établissement en tant que {userRoleLabel(userRole)}. +
+ ); + } + + if (refuseData) { + return ( +
+ La demande de rattachement de l'utilisateur{" "} + {requestData?.membershipRequest.name} a été refusée. +
+ ); + } + + if (requestData && requestData.membershipRequest) + return ( + <> +
+
+ + + + + + + + + + + + + + + +
NomEmailRôle
{requestData.membershipRequest.name}{requestData.membershipRequest.email} + +
+
+
+
+ + +
+ {acceptError && } + {refuseError && } + + ); + + return null; + }; + + return ( + + {renderContent()} + + ); +} diff --git a/front/src/Apps/Companies/CompanyMembers/CompanyMembersRequestsList.tsx b/front/src/Apps/Companies/CompanyMembers/CompanyMembersRequestsList.tsx new file mode 100644 index 0000000000..54940e41f1 --- /dev/null +++ b/front/src/Apps/Companies/CompanyMembers/CompanyMembersRequestsList.tsx @@ -0,0 +1,157 @@ +import React, { useState } from "react"; +import { CompanyPrivateMembers } from "./CompanyMembers"; +import { Query, QueryMembershipRequestsArgs } from "@td/codegen-ui"; +import { MEMBERSHIP_REQUESTS } from "../common/queries"; +import { useQuery } from "@apollo/client"; +import Pagination from "@codegouvfr/react-dsfr/Pagination"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { generatePath, useLocation, useNavigate } from "react-router-dom"; +import * as queryString from "query-string"; +import CompanyMembersRequestModal from "./CompanyMembersRequestModal"; +import routes from "../../routes"; + +interface CompanyMembersRequestsListProps { + company: CompanyPrivateMembers; +} + +const CompanyMembersRequestsList = ({ + company +}: CompanyMembersRequestsListProps) => { + const location = useLocation(); + const navigate = useNavigate(); + const queries = queryString.parse(location.search); + + const [requestToManage, setRequestToManage] = useState( + (queries.demande as string) ?? null + ); + + const [pageIndex, setPageIndex] = useState(0); + + const { data, loading, refetch } = useQuery< + Pick, + QueryMembershipRequestsArgs + >(MEMBERSHIP_REQUESTS, { + fetchPolicy: "network-only", + variables: { + where: { + orgId: company.orgId + } + } + }); + + const totalCount = data?.membershipRequests.totalCount; + const membershipRequests = + data?.membershipRequests.edges.map(edge => edge.node) ?? []; + + const PAGE_SIZE = 10; + const pageCount = totalCount ? Math.ceil(totalCount / PAGE_SIZE) : 0; + + const gotoPage = (page: number) => { + setPageIndex(page); + + refetch({ + skip: page * PAGE_SIZE, + first: PAGE_SIZE + }); + }; + + return ( + <> + {!!requestToManage && ( + { + navigate( + `${generatePath(routes.companies.details, { + siret: company.orgId + })}#membres` + ); + setRequestToManage(null); + }} + /> + )} +
+

Demandes de rattachement

+
+
+
+
+
+
+
+ + + + + + + + + + {membershipRequests.map(request => { + const { id, name, email } = request; + + return ( + + + + + + ); + })} + + {loading &&

Chargement...

} + {!loading && !membershipRequests.length && ( +

+ Aucune demande de rattachement en attente. +

+ )} + +
NomEmailAction
{name}{email} + +
+
+
+
+ +
+ ({ + onClick: event => { + event.preventDefault(); + gotoPage(pageNumber - 1); + }, + href: "#", + key: `pagination-link-${pageNumber}` + })} + className={"fr-mt-1w"} + /> +
+
+
+
+
+
+
+ + ); +}; + +export default CompanyMembersRequestsList; diff --git a/front/src/Apps/Companies/common/queries.ts b/front/src/Apps/Companies/common/queries.ts index d63e55bf15..f49e9cd9d7 100644 --- a/front/src/Apps/Companies/common/queries.ts +++ b/front/src/Apps/Companies/common/queries.ts @@ -523,3 +523,57 @@ export const CHANGE_USER_ROLE = gql` } ${AccountCompanyMemberFragment.user} `; + +export const MEMBERSHIP_REQUESTS = gql` + query membershipRequests( + $skip: Int + $first: Int + $where: MembershipRequestsWhere + ) { + membershipRequests(skip: $skip, first: $first, where: $where) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + node { + id + email + name + status + createdAt + } + } + } + } +`; + +export const MEMBERSHIP_REQUEST = gql` + query MembershipRequest($id: ID!) { + membershipRequest(id: $id) { + id + email + name + status + } + } +`; + +export const ACCEPT_MEMBERSHIP_REQUEST = gql` + mutation AcceptMembershipRequest($id: ID!, $role: UserRole!) { + acceptMembershipRequest(id: $id, role: $role) { + id + } + } +`; + +export const REFUSE_MEMBERSHIP_REQUEST = gql` + mutation RefuseMembershipRequest($id: ID!) { + refuseMembershipRequest(id: $id) { + id + } + } +`; diff --git a/front/src/Apps/common/Components/layout/LayoutContainer.tsx b/front/src/Apps/common/Components/layout/LayoutContainer.tsx index 04b5241b16..147c4c5507 100644 --- a/front/src/Apps/common/Components/layout/LayoutContainer.tsx +++ b/front/src/Apps/common/Components/layout/LayoutContainer.tsx @@ -26,9 +26,6 @@ const CompaniesRoutes = lazy( () => import("../../../Companies/CompaniesRoutes") ); const Account = lazy(() => import("../../../Account/Account")); -const AccountMembershipRequest = lazy( - () => import("../../../Account/AccountMembershipRequest") -); const FormContainer = lazy(() => import("../../../../form/bsdd/FormContainer")); const BsffFormContainer = lazy( @@ -280,15 +277,6 @@ export default function LayoutContainer() { } /> - - - - } - /> -

Pour valider ou refuser sa demande, cliquez sur - ce lience lien.

diff --git a/libs/back/mail/src/templates/mustache/pending-membership-request.html b/libs/back/mail/src/templates/mustache/pending-membership-request.html index 9a6142e76a..fd26958779 100644 --- a/libs/back/mail/src/templates/mustache/pending-membership-request.html +++ b/libs/back/mail/src/templates/mustache/pending-membership-request.html @@ -6,6 +6,8 @@

Veuillez accepter ou refuser sa demande de rattachement en cliquant sur - ce lien.ce lien.