Skip to content

Commit

Permalink
[TRA-15092] ETQ utilisateur je peux créer, révoquer et consulter mes …
Browse files Browse the repository at this point in the history
…demandes de délégation RNDTS (#3588)

* 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
  • Loading branch information
GaelFerrand authored Oct 11, 2024
1 parent 6a809b1 commit 4921b88
Show file tree
Hide file tree
Showing 10 changed files with 757 additions and 4 deletions.
19 changes: 16 additions & 3 deletions front/src/Apps/Companies/CompanyDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
): {
Expand All @@ -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 = [
{
Expand Down Expand Up @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<>
<CompanyRegistryDelegationAsDelegator company={company} />
<CompanyRegistryDelegationAsDelegate company={company} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h4>Délégataires</h4>
<div>
Les entreprises ci-dessous m'autorisent à faire leurs déclarations au
Registre National des Déchets, Terres Excavées et Sédiments (RNDTS)
</div>
<div>
<RegistryDelegationsTable as="delegate" company={company} />
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h4>Délégations</h4>
<div>
J'autorise les entreprises ci-dessous à faire mes déclarations au
Registre National des Déchets, Terres Excavées et Sédiments (RNDTS)
</div>

{isAdmin && (
<div>
<Button
priority="primary"
size="small"
className="fr-my-4v"
nativeButtonProps={{
type: "button",
"data-testid": "company-add-registryDelegation"
}}
disabled={isModalOpen}
onClick={() => setIsModalOpen(true)}
>
Créer une délégation
</Button>
</div>
)}

<div>
<RegistryDelegationsTable as="delegator" company={company} />
</div>

<CreateRegistryDelegationModal
company={company}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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<Mutation, "createRegistryDelegation">,
MutationCreateRegistryDelegationArgs
>(CREATE_REGISTRY_DELEGATION, {
refetchQueries: [REGISTRY_DELEGATIONS]
});

const validationSchema = getSchema();
const {
register,
handleSubmit,
setValue,
watch,
reset,
formState: { errors, isSubmitting }
} = useForm<z.infer<typeof validationSchema>>({
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 (
<Modal
onClose={closeModal}
ariaLabel="Créer une délégation"
isOpen={isOpen}
size="L"
>
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<h4>Créer une délégation</h4>

<CompanySelectorWrapper
orgId={company.orgId}
disabled={isLoading}
selectedCompanyOrgId={delegateOrgId}
selectedCompanyError={selectedCompany => {
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 && (
<span className="fr-error-text">
{errors.delegateOrgId.message}
</span>
)}

<div className="fr-container--fluid fr-mb-8v">
<div className="fr-grid-row fr-grid-row--gutters fr-grid-row--bottom">
<div className="fr-col-6">
<Input
label="Date de début"
state={errors?.startDate && "error"}
stateRelatedMessage={displayError(errors?.startDate)}
disabled={isLoading}
nativeInputProps={{
type: "date",
min: datetimeToYYYYMMDD(new Date()),
max: datetimeToYYYYMMDD(new Date("2050-12-31")),
...register("startDate")
}}
/>
</div>
<div className="fr-col-6">
<Input
label="Date de fin (optionnelle)"
hintText="Illimité s'il n'y a pas de date renseignée"
state={errors?.endDate && "error"}
stateRelatedMessage={displayError(errors?.endDate)}
disabled={isLoading}
nativeInputProps={{
type: "date",
max: datetimeToYYYYMMDD(new Date("2050-12-31")),
...register("endDate")
}}
/>
</div>
</div>
</div>

<Input
label="Objet"
state={errors?.comment && "error"}
stateRelatedMessage={displayError(errors?.comment)}
disabled={isLoading}
nativeInputProps={{
...register("comment")
}}
/>

<div className="dsfr-modal-actions fr-mt-3w">
<Button
disabled={isLoading}
priority="secondary"
onClick={onClose}
type="button"
>
Annuler
</Button>
<Button type="submit" disabled={isLoading}>
Créer
</Button>
</div>
</form>
</div>
</Modal>
);
};
Loading

0 comments on commit 4921b88

Please sign in to comment.