From 4921b888eb397d2008d769c878b63cc17b51495f Mon Sep 17 00:00:00 2001 From: GaelFerrand <45355989+GaelFerrand@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:58:00 +0200 Subject: [PATCH] =?UTF-8?q?[TRA-15092]=20ETQ=20utilisateur=20je=20peux=20c?= =?UTF-8?q?r=C3=A9er,=20r=C3=A9voquer=20et=20consulter=20mes=20demandes=20?= =?UTF-8?q?de=20d=C3=A9l=C3=A9gation=20RNDTS=20(#3588)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implemented create endpoint * feat: added endpoint rndtsDeclarationDelegation * refacto: improved condition check on overlap + tests * refacto * feat: checking that delegator & delegate are different * feat: small opti on queries * refacto: making endpoints session only * feat: removed isDeleted, using isRevoked instead + added status sub-resolver * feat: cleaned up dates to midnight * feat: huge refactoring, nesting companies within delegation + started revoke endpoint * test: added test for revocation * fix: fixed tests * feat: added endpoints delegationS * fix: fixes & refacto * fix: fixed test * fix: moved delegate & delegator to subResolvers + dates fixing to zod only * fix: changing where.id to where.orgId * feat: started implemeting front * fix: submitting create form * fix: using .nullish() instead of .optional() * feat: added prisma migration script * feat: started implementing lists * feat: starting table pagination. need backend fix first * feat: changed pagination args * feat: polishes the table * feat: added revoke * feat: trying to fix test * fix: trying to fix tests * fix: fixing typing issues due to sub-resolvers * fix: let's chill on the capslock * fix: trying to fix subresolvers (isDormant error) * feat: adding created delegation to table * feat: added givenName to CompanyPublic * feat: displaying givenName * feat: sending email on creation. Needing content though * fix: displaying error message on required for delegatorId (companySelector) * feat: added email content. Still some details missing * fix: lint * feat: added feature flag on companies * feat: not showing action buttons to non-admins * fix: fixes after reviews with the bowss * fix: fixed email with links * fix: fixed permissions using can() * fix: renamed rndtsDeclarationDelegation -> registryDelegation * feat: re-generated migration script with new name registryDelegation * feat: added events in repository methods * lint: format * feat: renamed rndtsDeclarationDelegation -> registryDelegation * fix: PR review fixes * feat: added endDate in mail --- front/src/Apps/Companies/CompanyDetails.tsx | 19 +- .../CompanyRegistryDelegation.tsx | 18 ++ .../CompanyRegistryDelegationAsDelegate.tsx | 23 ++ .../CompanyRegistryDelegationAsDelegator.tsx | 54 ++++ .../CreateRegistryDelegationModal.tsx | 231 +++++++++++++++++ .../RegistryDelegationsTable.tsx | 234 ++++++++++++++++++ .../RevokeRegistryDelegationModal.tsx | 95 +++++++ .../companyRegistryDelegation.scss | 17 ++ .../Creation/bspaoh/steps/Destination.tsx | 2 +- .../queries/registryDelegation/queries.ts | 68 +++++ 10 files changed, 757 insertions(+), 4 deletions(-) create mode 100644 front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegation.tsx create mode 100644 front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegate.tsx create mode 100644 front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegator.tsx create mode 100644 front/src/Apps/Companies/CompanyRegistryDelegation/CreateRegistryDelegationModal.tsx create mode 100644 front/src/Apps/Companies/CompanyRegistryDelegation/RegistryDelegationsTable.tsx create mode 100644 front/src/Apps/Companies/CompanyRegistryDelegation/RevokeRegistryDelegationModal.tsx create mode 100644 front/src/Apps/Companies/CompanyRegistryDelegation/companyRegistryDelegation.scss create mode 100644 front/src/Apps/common/queries/registryDelegation/queries.ts diff --git a/front/src/Apps/Companies/CompanyDetails.tsx b/front/src/Apps/Companies/CompanyDetails.tsx index bd8f678dbc..da2ff5bdaa 100644 --- a/front/src/Apps/Companies/CompanyDetails.tsx +++ b/front/src/Apps/Companies/CompanyDetails.tsx @@ -22,11 +22,14 @@ import CompanyMembers from "./CompanyMembers/CompanyMembers"; import CompanyDigestSheetForm from "./CompanyDigestSheet/CompanyDigestSheet"; import { Tabs, TabsProps } from "@codegouvfr/react-dsfr/Tabs"; import { FrIconClassName } from "@codegouvfr/react-dsfr"; +import { CompanyRegistryDelegation } from "./CompanyRegistryDelegation/CompanyRegistryDelegation"; export type TabContentProps = { company: CompanyPrivate; }; +const REGISTRY_V2_FLAG = "REGISTRY_V2"; + const buildTabs = ( company: CompanyPrivate ): { @@ -35,6 +38,9 @@ const buildTabs = ( } => { const isAdmin = company.userRole === UserRole.Admin; + // RNDTS features protected by feature flag + const canViewRndtsFeatures = company.featureFlags.includes(REGISTRY_V2_FLAG); + const iconId = "fr-icon-checkbox-line" as FrIconClassName; const tabs = [ { @@ -70,14 +76,21 @@ const buildTabs = ( tab4: CompanyContactForm, tab5: CompanyDigestSheetForm }; - - if (isAdmin) { + if (canViewRndtsFeatures) { tabs.push({ tabId: "tab6", + label: "Délégations RNDTS", + iconId + }); + tabsContent["tab6"] = CompanyRegistryDelegation; + } + if (isAdmin) { + tabs.push({ + tabId: "tab7", label: "Avancé", iconId }); - tabsContent["tab6"] = CompanyAdvanced; + tabsContent["tab7"] = CompanyAdvanced; } return { tabs, tabsContent }; diff --git a/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegation.tsx b/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegation.tsx new file mode 100644 index 0000000000..1443d70d09 --- /dev/null +++ b/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegation.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import "./companyRegistryDelegation.scss"; +import { CompanyPrivate } from "@td/codegen-ui"; +import { CompanyRegistryDelegationAsDelegator } from "./CompanyRegistryDelegationAsDelegator"; +import { CompanyRegistryDelegationAsDelegate } from "./CompanyRegistryDelegationAsDelegate"; + +interface Props { + company: CompanyPrivate; +} + +export const CompanyRegistryDelegation = ({ company }: Props) => { + return ( + <> + + + + ); +}; diff --git a/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegate.tsx b/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegate.tsx new file mode 100644 index 0000000000..5e7087ebd3 --- /dev/null +++ b/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegate.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import "./companyRegistryDelegation.scss"; +import { CompanyPrivate } from "@td/codegen-ui"; +import { RegistryDelegationsTable } from "./RegistryDelegationsTable"; + +interface Props { + company: CompanyPrivate; +} + +export const CompanyRegistryDelegationAsDelegate = ({ company }: Props) => { + return ( + <> +

Délégataires

+
+ Les entreprises ci-dessous m'autorisent à faire leurs déclarations au + Registre National des Déchets, Terres Excavées et Sédiments (RNDTS) +
+
+ +
+ + ); +}; diff --git a/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegator.tsx b/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegator.tsx new file mode 100644 index 0000000000..4758d8901b --- /dev/null +++ b/front/src/Apps/Companies/CompanyRegistryDelegation/CompanyRegistryDelegationAsDelegator.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import "./companyRegistryDelegation.scss"; +import { CompanyPrivate, UserRole } from "@td/codegen-ui"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { CreateRegistryDelegationModal } from "./CreateRegistryDelegationModal"; +import { RegistryDelegationsTable } from "./RegistryDelegationsTable"; + +interface Props { + company: CompanyPrivate; +} + +export const CompanyRegistryDelegationAsDelegator = ({ company }: Props) => { + const isAdmin = company.userRole === UserRole.Admin; + + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( + <> +

Délégations

+
+ J'autorise les entreprises ci-dessous à faire mes déclarations au + Registre National des Déchets, Terres Excavées et Sédiments (RNDTS) +
+ + {isAdmin && ( +
+ +
+ )} + +
+ +
+ + setIsModalOpen(false)} + /> + + ); +}; diff --git a/front/src/Apps/Companies/CompanyRegistryDelegation/CreateRegistryDelegationModal.tsx b/front/src/Apps/Companies/CompanyRegistryDelegation/CreateRegistryDelegationModal.tsx new file mode 100644 index 0000000000..7aea16b09e --- /dev/null +++ b/front/src/Apps/Companies/CompanyRegistryDelegation/CreateRegistryDelegationModal.tsx @@ -0,0 +1,231 @@ +import React from "react"; +import { + CompanyPrivate, + Mutation, + MutationCreateRegistryDelegationArgs +} from "@td/codegen-ui"; +import { FieldError, useForm } from "react-hook-form"; +import { Modal } from "../../../common/components"; +import CompanySelectorWrapper from "../../common/Components/CompanySelectorWrapper/CompanySelectorWrapper"; +import Input from "@codegouvfr/react-dsfr/Input"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { isSiret } from "@td/constants"; +import { datetimeToYYYYMMDD } from "../../Dashboard/Validation/BSPaoh/paohUtils"; +import { startOfDay } from "date-fns"; +import { useMutation } from "@apollo/client"; +import { + CREATE_REGISTRY_DELEGATION, + REGISTRY_DELEGATIONS +} from "../../common/queries/registryDelegation/queries"; +import toast from "react-hot-toast"; + +const displayError = (error: FieldError | undefined) => { + return error ? <>{error.message} : null; +}; + +const getSchema = () => + z + .object({ + delegateOrgId: z + .string({ required_error: "Ce champ est requis" }) + .refine(isSiret, "Siret non valide"), + startDate: z.coerce + .date({ + required_error: "La date de début est requise", + invalid_type_error: "La date de début est invalide" + }) + .min(startOfDay(new Date()), { + message: "La date de début ne peut pas être dans le passé" + }) + .transform(val => val.toISOString()) + .nullish(), + // Date & "" hack: https://github.com/colinhacks/zod/issues/1721 + endDate: z.preprocess( + arg => (arg === "" ? null : arg), + z.coerce + .date({ + invalid_type_error: "La date de fin est invalide" + }) + .min(new Date(), { + message: "La date de fin ne peut pas être dans le passé" + }) + .transform(val => { + if (val) return val.toISOString(); + return val; + }) + .nullish() + ), + comment: z.string().max(500).optional() + }) + .refine( + data => { + const { startDate, endDate } = data; + + if (startDate && endDate) { + return new Date(startDate) < new Date(endDate); + } + + return true; + }, + { + path: ["startDate"], + message: "La date de début doit être avant la date de fin." + } + ); + +interface Props { + company: CompanyPrivate; + isOpen: boolean; + onClose: () => void; +} + +export const CreateRegistryDelegationModal = ({ + company, + onClose, + isOpen +}: Props) => { + const [createRegistryDelegation, { loading }] = useMutation< + Pick, + MutationCreateRegistryDelegationArgs + >(CREATE_REGISTRY_DELEGATION, { + refetchQueries: [REGISTRY_DELEGATIONS] + }); + + const validationSchema = getSchema(); + const { + register, + handleSubmit, + setValue, + watch, + reset, + formState: { errors, isSubmitting } + } = useForm>({ + defaultValues: { + startDate: datetimeToYYYYMMDD(new Date()) + }, + resolver: zodResolver(validationSchema) + }); + + const closeModal = () => { + reset(); + onClose(); + }; + + const onSubmit = async input => { + await createRegistryDelegation({ + variables: { + input: { + ...input, + delegatorOrgId: company.orgId + } + }, + onCompleted: () => toast.success("Délégation créée!"), + onError: err => toast.error(err.message) + }); + + closeModal(); + }; + + const delegateOrgId = watch("delegateOrgId") ?? {}; + + const isLoading = loading || isSubmitting; + + return ( + +
+
+

Créer une délégation

+ + { + if (selectedCompany?.orgId === company.orgId) { + return "Le délégant et le délégataire doivent être différents"; + } + if (!selectedCompany?.siret) { + return "L'entreprise doit avoir un n° de SIRET"; + } + return null; + }} + onCompanySelected={company => { + if (company) { + setValue("delegateOrgId", company.orgId); + } + }} + /> + {errors.delegateOrgId && ( + + {errors.delegateOrgId.message} + + )} + +
+
+
+ +
+
+ +
+
+
+ + + +
+ + +
+ +
+
+ ); +}; diff --git a/front/src/Apps/Companies/CompanyRegistryDelegation/RegistryDelegationsTable.tsx b/front/src/Apps/Companies/CompanyRegistryDelegation/RegistryDelegationsTable.tsx new file mode 100644 index 0000000000..7146e31b65 --- /dev/null +++ b/front/src/Apps/Companies/CompanyRegistryDelegation/RegistryDelegationsTable.tsx @@ -0,0 +1,234 @@ +import React, { useState } from "react"; +import { + CompanyPrivate, + Query, + QueryRegistryDelegationsArgs, + RegistryDelegation, + RegistryDelegationStatus, + UserRole +} from "@td/codegen-ui"; +import { isDefinedStrict } from "../../../common/helper"; +import { formatDateViewDisplay } from "../common/utils"; +import Pagination from "@codegouvfr/react-dsfr/Pagination"; +import "./companyRegistryDelegation.scss"; +import { useQuery } from "@apollo/client"; +import { REGISTRY_DELEGATIONS } from "../../common/queries/registryDelegation/queries"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { RevokeRegistryDelegationModal } from "./RevokeRegistryDelegationModal"; +import Badge from "@codegouvfr/react-dsfr/Badge"; +import { AlertProps } from "@codegouvfr/react-dsfr/Alert"; + +const getStatusLabel = (status: RegistryDelegationStatus) => { + switch (status) { + case RegistryDelegationStatus.Ongoing: + return "EN COURS"; + case RegistryDelegationStatus.Incoming: + return "À VENIR"; + case RegistryDelegationStatus.Closed: + return "CLÔTURÉE"; + } +}; + +const getStatusBadge = (status: RegistryDelegationStatus) => { + let severity: AlertProps.Severity = "success"; + if (status === RegistryDelegationStatus.Incoming) severity = "info"; + if (status === RegistryDelegationStatus.Closed) severity = "error"; + + return ( + + {getStatusLabel(status)} + + ); +}; + +const getTextTooltip = (id: string, value: string | undefined | null) => { + return ( + + ); +}; + +interface Props { + as: "delegator" | "delegate"; + company: CompanyPrivate; +} + +export const RegistryDelegationsTable = ({ as, company }: Props) => { + const [pageIndex, setPageIndex] = useState(0); + const [delegationToRevoke, setDelegationToRevoke] = + useState(null); + + const isAdmin = company.userRole === UserRole.Admin; + + const { data, loading, refetch } = useQuery< + Pick, + QueryRegistryDelegationsArgs + >(REGISTRY_DELEGATIONS, { + skip: !company.orgId, + fetchPolicy: "network-only", + variables: { + where: + as === "delegate" + ? { delegateOrgId: company.orgId } + : { delegatorOrgId: company.orgId } + } + }); + + const totalCount = data?.registryDelegations.totalCount; + const delegations = + data?.registryDelegations.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 ( +
+
+
+
+ + + + + + + + + + {isAdmin && } + + + + {delegations.map(delegation => { + const { + id, + delegate, + delegator, + startDate, + endDate, + comment, + status + } = delegation; + + const company = as === "delegate" ? delegator : delegate; + + const name = isDefinedStrict(company.givenName) + ? company.givenName + : company.name; + + return ( + + + + + + + + {isAdmin && ( + + )} + + ); + })} + + {loading &&

Chargement...

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

Aucune délégation

+ )} + +
+ Établissement + SiretObjetDébutFinStatutRévoquer
+ {name} + + {getTextTooltip( + `company-name-${company.orgId}-${as}`, + `${name} ${ + company.givenName ? `(${company.name})` : "" + }` + )} + {company?.orgId} + {isDefinedStrict(comment) ? ( + <> + {comment} + {getTextTooltip( + `company-comment-${company.orgId}-${as}`, + comment + )} + + ) : ( + "-" + )} + {formatDateViewDisplay(startDate)} + {endDate ? formatDateViewDisplay(endDate) : "Illimité"} + {getStatusBadge(status)} + {status !== RegistryDelegationStatus.Closed && ( + + )} +
+
+
+
+ +
+ ({ + onClick: event => { + event.preventDefault(); + gotoPage(pageNumber - 1); + }, + href: "#", + key: `pagination-link-${pageNumber}` + })} + className={"fr-mt-1w"} + /> +
+ + {delegationToRevoke && ( + setDelegationToRevoke(null)} + /> + )} +
+ ); +}; diff --git a/front/src/Apps/Companies/CompanyRegistryDelegation/RevokeRegistryDelegationModal.tsx b/front/src/Apps/Companies/CompanyRegistryDelegation/RevokeRegistryDelegationModal.tsx new file mode 100644 index 0000000000..f46a070aaa --- /dev/null +++ b/front/src/Apps/Companies/CompanyRegistryDelegation/RevokeRegistryDelegationModal.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { Mutation, MutationRevokeRegistryDelegationArgs } from "@td/codegen-ui"; +import { Modal } from "../../../common/components"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { useMutation } from "@apollo/client"; +import { REVOKE_REGISTRY_DELEGATION } from "../../common/queries/registryDelegation/queries"; +import toast from "react-hot-toast"; +import { isDefined } from "../../../common/helper"; + +const WarningIcon = () => ( + +); + +interface Props { + delegationId: string; + to: string | null | undefined; + from: string | null | undefined; + onClose: () => void; +} + +export const RevokeRegistryDelegationModal = ({ + delegationId, + to, + from, + onClose +}: Props) => { + const [revokeRegistryDelegation, { loading }] = useMutation< + Pick, + MutationRevokeRegistryDelegationArgs + >(REVOKE_REGISTRY_DELEGATION); + + const onRevoke = async () => { + await revokeRegistryDelegation({ + variables: { + delegationId + }, + onCompleted: () => toast.success("Délégation révoquée!"), + onError: err => toast.error(err.message) + }); + + // Delegation is automatically updated in Apollo's cache + onClose(); + }; + + // Wording changes if delegator or delegate + let title = "Révoquer la délégation"; + let content = `Vous vous apprêtez à révoquer la délégation pour ${to}.`; + let acceptLabel = "Révoquer"; + let refuseLabel = "Ne pas révoquer"; + let closeModalLabel = "Ne pas révoquer"; + + if (isDefined(from)) { + title = "Annuler la délégation"; + content = `Vous vous apprêtez à annuler la délégation de ${from}.`; + acceptLabel = "Annuler"; + refuseLabel = "Ne pas annuler"; + closeModalLabel = "Ne pas annuler"; + } + + return ( + +
+

+ {title} +

+ +

{content}

+ +
+ + +
+
+
+ ); +}; diff --git a/front/src/Apps/Companies/CompanyRegistryDelegation/companyRegistryDelegation.scss b/front/src/Apps/Companies/CompanyRegistryDelegation/companyRegistryDelegation.scss new file mode 100644 index 0000000000..5a7c9f39f1 --- /dev/null +++ b/front/src/Apps/Companies/CompanyRegistryDelegation/companyRegistryDelegation.scss @@ -0,0 +1,17 @@ +h4 { + font-size: 1.5rem; //24px; + font-weight: 700; + margin: 0; + padding: 0; + margin-bottom: 2vh; +} + +.delegations-table { + th, + td { + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/front/src/Apps/Dashboard/Creation/bspaoh/steps/Destination.tsx b/front/src/Apps/Dashboard/Creation/bspaoh/steps/Destination.tsx index cd30c6aa30..347b56004c 100644 --- a/front/src/Apps/Dashboard/Creation/bspaoh/steps/Destination.tsx +++ b/front/src/Apps/Dashboard/Creation/bspaoh/steps/Destination.tsx @@ -98,7 +98,7 @@ export function Destination({ errors }) { ); const selectedCompanyError = (company?: CompanySearchResult) => { - // Le destinatiare doi être inscrit et avec un profil crématorium ou sous-type crémation + // Le destinatiare doit être inscrit et avec un profil crématorium ou sous-type crémation // Le profil crématorium sera bientôt supprimé if (company) { if (!company.isRegistered) { diff --git a/front/src/Apps/common/queries/registryDelegation/queries.ts b/front/src/Apps/common/queries/registryDelegation/queries.ts new file mode 100644 index 0000000000..2e341cfbbb --- /dev/null +++ b/front/src/Apps/common/queries/registryDelegation/queries.ts @@ -0,0 +1,68 @@ +import { gql } from "@apollo/client"; + +export const CREATE_REGISTRY_DELEGATION = gql` + mutation createRegistryDelegation($input: CreateRegistryDelegationInput!) { + createRegistryDelegation(input: $input) { + id + updatedAt + delegate { + orgId + } + delegator { + orgId + } + startDate + endDate + comment + status + } + } +`; + +export const REGISTRY_DELEGATIONS = gql` + query registryDelegations( + $skip: Int + $first: Int + $where: RegistryDelegationWhere + ) { + registryDelegations(skip: $skip, first: $first, where: $where) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + node { + id + updatedAt + delegate { + name + givenName + orgId + } + delegator { + name + givenName + orgId + } + startDate + endDate + comment + status + } + } + } + } +`; + +export const REVOKE_REGISTRY_DELEGATION = gql` + mutation revokeRegistryDelegation($delegationId: ID!) { + revokeRegistryDelegation(delegationId: $delegationId) { + id + isRevoked + status + } + } +`;