-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(candidate): add candidacy contestation caducite functionality
- Introduced new contestation hooks and page for candidates to submit contestations regarding their candidacy status. - Added CandidacyBanner component to manage and display different messages based on candidacy status (actualisation warning, caduque warning, and welcome message). - Created ActualisationWarning and CaduqueWarning components to provide specific alerts to users. - Updated candidacy context to include isCaduque field.
- Loading branch information
Showing
10 changed files
with
369 additions
and
63 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions
49
packages/reva-candidate/src/app/(candidate)/contestation/contestation.hooks.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { useGraphQlClient } from "@/components/graphql/graphql-client/GraphqlClient"; | ||
import { graphql } from "@/graphql/generated"; | ||
import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||
|
||
const CREATE_CONTESTATION = graphql(` | ||
mutation createContestation( | ||
$candidacyId: ID! | ||
$contestationReason: String! | ||
$readyForJuryEstimatedAt: Timestamp! | ||
) { | ||
candidacy_contestation_caducite_create_contestation( | ||
candidacyId: $candidacyId | ||
contestationReason: $contestationReason | ||
readyForJuryEstimatedAt: $readyForJuryEstimatedAt | ||
) { | ||
id | ||
} | ||
} | ||
`); | ||
|
||
export const useContestation = () => { | ||
const { graphqlClient } = useGraphQlClient(); | ||
const queryClient = useQueryClient(); | ||
|
||
const { mutateAsync: createContestation } = useMutation({ | ||
mutationKey: ["createContestation"], | ||
mutationFn: ({ | ||
candidacyId, | ||
contestationReason, | ||
readyForJuryEstimatedAt, | ||
}: { | ||
candidacyId: string; | ||
contestationReason: string; | ||
readyForJuryEstimatedAt: number; | ||
}) => | ||
graphqlClient.request(CREATE_CONTESTATION, { | ||
candidacyId, | ||
contestationReason, | ||
readyForJuryEstimatedAt, | ||
}), | ||
onSuccess: () => { | ||
queryClient.invalidateQueries({ | ||
queryKey: ["candidate"], | ||
}); | ||
}, | ||
}); | ||
|
||
return { createContestation }; | ||
}; |
202 changes: 202 additions & 0 deletions
202
packages/reva-candidate/src/app/(candidate)/contestation/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
"use client"; | ||
|
||
import { useCandidacy } from "@/components/candidacy/candidacy.context"; | ||
import { useFeatureFlipping } from "@/components/feature-flipping/featureFlipping"; | ||
import { FormButtons } from "@/components/form/form-footer/FormButtons"; | ||
import { FormOptionalFieldsDisclaimer } from "@/components/legacy/atoms/FormOptionalFieldsDisclaimer/FormOptionalFieldsDisclaimer"; | ||
import { graphqlErrorToast, successToast } from "@/components/toast/toast"; | ||
import Button from "@codegouvfr/react-dsfr/Button"; | ||
import Input from "@codegouvfr/react-dsfr/Input"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { format, isBefore } from "date-fns"; | ||
import Image from "next/image"; | ||
import Link from "next/link"; | ||
import { useRouter } from "next/navigation"; | ||
import { useCallback, useEffect, useMemo, useState } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import { z } from "zod"; | ||
import { useContestation } from "./contestation.hooks"; | ||
|
||
const schema = z | ||
.object({ | ||
contestationReason: z | ||
.string() | ||
.trim() | ||
.min(1, "Veuillez indiquer une raison"), | ||
readyForJuryEstimatedAt: z.string().nullable(), | ||
}) | ||
.superRefine(({ readyForJuryEstimatedAt }, ctx) => { | ||
if (!readyForJuryEstimatedAt) { | ||
ctx.addIssue({ | ||
code: z.ZodIssueCode.custom, | ||
message: | ||
"Veuillez sélectionner une date prévisionnelle de dépôt du dossier de validation", | ||
path: ["readyForJuryEstimatedAt"], | ||
}); | ||
} else if (isBefore(new Date(readyForJuryEstimatedAt), new Date())) { | ||
ctx.addIssue({ | ||
code: z.ZodIssueCode.custom, | ||
message: "Merci d'indiquer une date postérieure à la date du jour", | ||
path: ["readyForJuryEstimatedAt"], | ||
}); | ||
} | ||
}); | ||
|
||
type ContestationForm = z.infer<typeof schema>; | ||
|
||
const HasContestedComponent = () => { | ||
return ( | ||
<div className="flex justify-between w-full"> | ||
<div className="flex flex-col justify-center"> | ||
<h1>Votre contestation est enregistrée</h1> | ||
<p className="text-xl"> | ||
Elle a été envoyée à votre certificateur qui y répondra dans les | ||
meilleurs délais. | ||
</p> | ||
<div> | ||
<Link href="/"> | ||
<Button data-test="contestation-continue-button"> | ||
Retour à l'accueil | ||
</Button> | ||
</Link> | ||
</div> | ||
</div> | ||
<Image | ||
src="/candidat/images/letter-with-sent-icon.png" | ||
alt="Contestation réussie" | ||
width={282} | ||
height={319} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default function ContestationPage() { | ||
const [hasContested, setHasContested] = useState(false); | ||
const router = useRouter(); | ||
const { isFeatureActive } = useFeatureFlipping(); | ||
const candidacyActualisationFeatureIsActive = isFeatureActive( | ||
"candidacy_actualisation", | ||
); | ||
const { candidacy } = useCandidacy(); | ||
const { createContestation } = useContestation(); | ||
|
||
const defaultValues = useMemo( | ||
() => ({ | ||
contestationReason: "", | ||
readyForJuryEstimatedAt: candidacy?.readyForJuryEstimatedAt | ||
? format(new Date(candidacy.readyForJuryEstimatedAt), "yyyy-MM-dd") | ||
: null, | ||
}), | ||
[candidacy?.readyForJuryEstimatedAt], | ||
); | ||
|
||
const { | ||
register, | ||
handleSubmit, | ||
reset, | ||
formState: { isDirty, isSubmitting, errors }, | ||
} = useForm<ContestationForm>({ | ||
resolver: zodResolver(schema), | ||
defaultValues, | ||
}); | ||
|
||
const handleFormSubmit = async ({ | ||
contestationReason, | ||
readyForJuryEstimatedAt, | ||
}: ContestationForm) => { | ||
if (!candidacy?.id || !contestationReason || !readyForJuryEstimatedAt) { | ||
return; | ||
} | ||
try { | ||
await createContestation({ | ||
candidacyId: candidacy?.id, | ||
contestationReason, | ||
readyForJuryEstimatedAt: new Date(readyForJuryEstimatedAt).getTime(), | ||
}); | ||
successToast("Votre contestation est enregistrée"); | ||
setHasContested(true); | ||
} catch (error) { | ||
graphqlErrorToast(error); | ||
} | ||
}; | ||
|
||
const resetForm = useCallback( | ||
() => reset(defaultValues), | ||
[reset, defaultValues], | ||
); | ||
|
||
useEffect(resetForm, [resetForm]); | ||
|
||
if (!candidacyActualisationFeatureIsActive) { | ||
router.push("/"); | ||
return null; | ||
} | ||
|
||
return hasContested ? ( | ||
<HasContestedComponent /> | ||
) : ( | ||
<div className="flex flex-col"> | ||
<h1 className="mb-0">Faire une contestation</h1> | ||
<FormOptionalFieldsDisclaimer /> | ||
<p className="text-xl mb-12"> | ||
Vous souhaitez continuer votre parcours VAE malgré la décision sur votre | ||
recevabilité ? Pour cela, expliquez la raison de votre non-actualisation | ||
puis complétez la date prévisionnelle de dépot de dossier de validation. | ||
</p> | ||
|
||
<form | ||
onSubmit={handleSubmit(handleFormSubmit)} | ||
onReset={(e) => { | ||
e.preventDefault(); | ||
resetForm(); | ||
}} | ||
> | ||
<h2 className="mb-2">Raison de la non-actualisation</h2> | ||
<p className="text-lg"> | ||
Une recevabilité n'est plus valable lorsque le candidat ne s'est pas | ||
actualisé. Vous devez expliquer au certificateur la raison qui vous a | ||
empêché de le faire (exemple : congé maternité ou arrêt maladie). Il | ||
pourra vous demander des pièces justificatives à envoyer par mail. | ||
</p> | ||
<Input | ||
label="Raison de la non-actualisation :" | ||
nativeTextAreaProps={register("contestationReason")} | ||
textArea | ||
state={errors.contestationReason ? "error" : "default"} | ||
stateRelatedMessage={errors.contestationReason?.message} | ||
className="mb-10" | ||
data-test="contestation-candidate-confirmation-checkbox" | ||
/> | ||
<h2 className="mb-2"> | ||
Date prévisionnelle de dépôt du dossier de validation | ||
</h2> | ||
<p className="text-lg"> | ||
La date prévisionnelle de dépôt du dossier de validation est une | ||
simple estimation, elle ne vous engage pas. Elle permet au | ||
certificateur d'avoir une idée du moment où vous aurez fini votre | ||
dossier et d'anticiper l'organisation de votre jury. | ||
</p> | ||
<Input | ||
className="max-w-xs mt-4" | ||
label="Date prévisionnelle" | ||
hintText="Si un accompagnateur a renseigné une date, vous la retrouverez ci-dessous." | ||
nativeInputProps={{ | ||
type: "date", | ||
...register("readyForJuryEstimatedAt"), | ||
}} | ||
state={errors.readyForJuryEstimatedAt ? "error" : "default"} | ||
stateRelatedMessage={errors.readyForJuryEstimatedAt?.message} | ||
data-test="contestation-date-input" | ||
/> | ||
<FormButtons | ||
backUrl="/" | ||
formState={{ | ||
isDirty, | ||
isSubmitting, | ||
}} | ||
/> | ||
</form> | ||
</div> | ||
); | ||
} |
43 changes: 43 additions & 0 deletions
43
packages/reva-candidate/src/app/_components/ActualisationWarning.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import Button from "@codegouvfr/react-dsfr/Button"; | ||
import { addMonths, format } from "date-fns"; | ||
import Image from "next/image"; | ||
import Link from "next/link"; | ||
|
||
export const ActualisationWarning = ({ | ||
lastActivityDate, | ||
}: { | ||
lastActivityDate: number; | ||
}) => { | ||
// La candidature sera considérée comme caduque après cette date, 6 mois après la dernière actualisation | ||
const thresholdDate = format(addMonths(lastActivityDate, 6), "dd/MM/yyyy"); | ||
|
||
return ( | ||
<div | ||
className="mt-12 flex flex-col gap-4" | ||
data-test="actualisation-warning" | ||
> | ||
<div className="static w-full border-b-[4px] border-b-[#FFA180] px-8 py-8 shadow-[0px_6px_18px_0px_rgba(0,0,18,0.16)] flex flex-col items-center text-start lg:relative lg:h-[85px] lg:flex-row"> | ||
<Image | ||
src="/candidat/images/image-home-character-young-man-glasses.png" | ||
width={132} | ||
height={153} | ||
alt="Homme portant des lunettes" | ||
className="relative hidden -top-28 lg:block lg:top-0 lg:-left-9" | ||
/> | ||
<div className="flex flex-col justify-center px-4 text-justify lg:mt-0 lg:p-0"> | ||
<p className="my-0"> | ||
<strong> | ||
Actualisez-vous dès maintenant pour que votre recevabilité reste | ||
valable ! | ||
</strong>{" "} | ||
Sans actualisation de votre part d'ici le {thresholdDate}, vous ne | ||
pourrez plus continuer votre parcours. | ||
</p> | ||
</div> | ||
</div> | ||
<Link href="/actualisation" className="self-end"> | ||
<Button data-test="actualisation-warning-button">S'actualiser</Button> | ||
</Link> | ||
</div> | ||
); | ||
}; |
28 changes: 28 additions & 0 deletions
28
packages/reva-candidate/src/app/_components/CaduqueWarning.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import Button from "@codegouvfr/react-dsfr/Button"; | ||
import Image from "next/image"; | ||
import Link from "next/link"; | ||
|
||
export const CaduqueWarning = () => ( | ||
<div className="mt-12 flex flex-col gap-4" data-test="caduque-warning"> | ||
<div className="static w-full border-b-[4px] border-b-[#FFA180] px-8 py-8 shadow-[0px_6px_18px_0px_rgba(0,0,18,0.16)] flex flex-col items-center text-start lg:relative lg:h-[85px] lg:flex-row"> | ||
<Image | ||
src="/candidat/images/image-warning-hand.png" | ||
width={132} | ||
height={153} | ||
alt="Main levée en signe d'avertissement" | ||
className="relative hidden -top-28 lg:block lg:top-0 lg:-left-9" | ||
/> | ||
<div className="flex flex-col justify-center px-4 text-justify lg:mt-0 lg:p-0"> | ||
<p className="my-0"> | ||
Parce que vous ne vous êtes pas actualisé à temps, votre recevabilité | ||
n'est plus valable. Cela signifie que votre parcours VAE s'arrête ici. | ||
Si vous souhaitez contester cette décision, cliquez sur le bouton | ||
“Contester”. | ||
</p> | ||
</div> | ||
</div> | ||
<Link href="/contestation" className="self-end"> | ||
<Button data-test="caduque-warning-button">Contester</Button> | ||
</Link> | ||
</div> | ||
); |
23 changes: 23 additions & 0 deletions
23
packages/reva-candidate/src/app/_components/CandidacyBanner.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { ActualisationWarning } from "./ActualisationWarning"; | ||
import { CaduqueWarning } from "./CaduqueWarning"; | ||
import { WelcomeMessage } from "./WelcomeMessage"; | ||
|
||
export const CandidacyBanner = ({ | ||
displayCaduqueWarning, | ||
displayActualisationWarning, | ||
lastActivityDate, | ||
}: { | ||
displayCaduqueWarning: boolean; | ||
displayActualisationWarning: boolean; | ||
lastActivityDate: number; | ||
}) => { | ||
if (displayCaduqueWarning) { | ||
return <CaduqueWarning />; | ||
} | ||
|
||
if (displayActualisationWarning) { | ||
return <ActualisationWarning lastActivityDate={lastActivityDate} />; | ||
} | ||
|
||
return <WelcomeMessage />; | ||
}; |
9 changes: 9 additions & 0 deletions
9
packages/reva-candidate/src/app/_components/WelcomeMessage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const WelcomeMessage = () => ( | ||
<p | ||
className="max-w-xl my-4 pr-6 text-dsfrGray-500 text-base" | ||
data-test="welcome-message" | ||
> | ||
Bienvenue sur votre espace ! Toutes les étapes et informations relatives à | ||
votre parcours VAE se trouvent ici. | ||
</p> | ||
); |
Oops, something went wrong.