Skip to content

Commit a253d67

Browse files
committed
feat(candidate): implement actualisation form with last activity date update and validation logic
1 parent 3b1c6cf commit a253d67

File tree

3 files changed

+232
-1
lines changed

3 files changed

+232
-1
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useGraphQlClient } from "@/components/graphql/graphql-client/GraphqlClient";
2+
import { graphql } from "@/graphql/generated";
3+
import { useMutation, useQueryClient } from "@tanstack/react-query";
4+
5+
const UPDATE_LAST_ACTIVITY_DATE = graphql(`
6+
mutation updateLastActivityDate(
7+
$candidacyId: UUID!
8+
$readyForJuryEstimatedAt: Timestamp!
9+
) {
10+
candidacy_updateLastActivityDate(
11+
candidacyId: $candidacyId
12+
readyForJuryEstimatedAt: $readyForJuryEstimatedAt
13+
) {
14+
id
15+
}
16+
}
17+
`);
18+
19+
export const useActualisation = () => {
20+
const { graphqlClient } = useGraphQlClient();
21+
const queryClient = useQueryClient();
22+
23+
const { mutateAsync: updateLastActivityDate } = useMutation({
24+
mutationKey: ["updateLastActivityDate"],
25+
mutationFn: ({
26+
candidacyId,
27+
readyForJuryEstimatedAt,
28+
}: {
29+
candidacyId: string;
30+
readyForJuryEstimatedAt: number;
31+
}) =>
32+
graphqlClient.request(UPDATE_LAST_ACTIVITY_DATE, {
33+
candidacyId,
34+
readyForJuryEstimatedAt,
35+
}),
36+
onSuccess: () => {
37+
queryClient.invalidateQueries({
38+
queryKey: ["candidate"],
39+
});
40+
},
41+
});
42+
43+
return { updateLastActivityDate };
44+
};
Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,202 @@
11
"use client";
22

3+
import { useCandidacy } from "@/components/candidacy/candidacy.context";
34
import { useFeatureFlipping } from "@/components/feature-flipping/featureFlipping";
5+
import { FormButtons } from "@/components/form/form-footer/FormButtons";
6+
import { FormOptionalFieldsDisclaimer } from "@/components/legacy/atoms/FormOptionalFieldsDisclaimer/FormOptionalFieldsDisclaimer";
7+
import { graphqlErrorToast, successToast } from "@/components/toast/toast";
8+
import Button from "@codegouvfr/react-dsfr/Button";
9+
import Checkbox from "@codegouvfr/react-dsfr/Checkbox";
10+
import Input from "@codegouvfr/react-dsfr/Input";
11+
import { zodResolver } from "@hookform/resolvers/zod";
12+
import { format, isBefore } from "date-fns";
13+
import Link from "next/link";
14+
import { useCallback, useEffect, useMemo, useState } from "react";
15+
import { useForm } from "react-hook-form";
16+
import { z } from "zod";
17+
import { useActualisation } from "./actualisation.hooks";
18+
19+
const schema = z
20+
.object({
21+
candidateConfirmation: z.boolean(),
22+
readyForJuryEstimatedAt: z.string().nullable(),
23+
})
24+
.superRefine(({ candidateConfirmation, readyForJuryEstimatedAt }, ctx) => {
25+
if (!candidateConfirmation) {
26+
ctx.addIssue({
27+
code: z.ZodIssueCode.custom,
28+
message: "Vous devez confirmer être toujours en cours de parcours VAE.",
29+
path: ["candidateConfirmation"],
30+
});
31+
}
32+
33+
if (!readyForJuryEstimatedAt) {
34+
ctx.addIssue({
35+
code: z.ZodIssueCode.custom,
36+
message:
37+
"Veuillez sélectionner une date prévisionnelle de dépôt du dossier de validation",
38+
path: ["readyForJuryEstimatedAt"],
39+
});
40+
} else if (isBefore(new Date(readyForJuryEstimatedAt), new Date())) {
41+
ctx.addIssue({
42+
code: z.ZodIssueCode.custom,
43+
message: "Merci d'indiquer une date postérieure à la date du jour",
44+
path: ["readyForJuryEstimatedAt"],
45+
});
46+
}
47+
});
48+
49+
type ActualisationForm = z.infer<typeof schema>;
50+
51+
const HasBeenUpdatedComponent = ({
52+
readyForJuryEstimatedAt,
53+
}: {
54+
readyForJuryEstimatedAt: string;
55+
}) => {
56+
return (
57+
<div className="flex flex-col">
58+
<h1>Votre actualisation est enregistrée</h1>
59+
<p className="text-xl">
60+
Vous pouvez désormais continuer votre parcours ! Rendez-vous dans votre
61+
espace pour connaître les prochaines étapes.
62+
</p>
63+
<p className="text-xl">
64+
Pour information, votre date prévisionnelle de dépot du dossier de
65+
validation est le{" "}
66+
{format(new Date(readyForJuryEstimatedAt), "dd/MM/yyyy")}.
67+
</p>
68+
<div>
69+
<Link href="/">
70+
<Button data-test="actualisation-continue-button">
71+
Continuer mon parcours
72+
</Button>
73+
</Link>
74+
</div>
75+
</div>
76+
);
77+
};
478

579
export default function ActualisationPage() {
80+
const [hasBeenUpdated, setHasBeenUpdated] = useState(false);
681
const { isFeatureActive } = useFeatureFlipping();
782
const candidacyActualisationFeatureIsActive = isFeatureActive(
883
"candidacy_actualisation",
984
);
85+
const { candidacy } = useCandidacy();
86+
const { updateLastActivityDate } = useActualisation();
87+
88+
const defaultValues = useMemo(
89+
() => ({
90+
candidateConfirmation: false,
91+
readyForJuryEstimatedAt: candidacy?.readyForJuryEstimatedAt
92+
? format(new Date(candidacy.readyForJuryEstimatedAt), "yyyy-MM-dd")
93+
: null,
94+
}),
95+
[candidacy?.readyForJuryEstimatedAt],
96+
);
97+
98+
const {
99+
register,
100+
handleSubmit,
101+
reset,
102+
formState: { isDirty, isSubmitting, errors },
103+
watch,
104+
} = useForm<ActualisationForm>({
105+
resolver: zodResolver(schema),
106+
defaultValues,
107+
});
108+
109+
const handleFormSubmit = async ({
110+
readyForJuryEstimatedAt,
111+
}: ActualisationForm) => {
112+
if (!candidacy?.id || !readyForJuryEstimatedAt) {
113+
return;
114+
}
115+
try {
116+
await updateLastActivityDate({
117+
candidacyId: candidacy?.id,
118+
readyForJuryEstimatedAt: new Date(readyForJuryEstimatedAt).getTime(),
119+
});
120+
successToast("Votre actualisation est enregistrée");
121+
setHasBeenUpdated(true);
122+
} catch (error) {
123+
graphqlErrorToast(error);
124+
}
125+
};
126+
127+
const resetForm = useCallback(
128+
() => reset(defaultValues),
129+
[reset, defaultValues],
130+
);
131+
132+
useEffect(resetForm, [resetForm]);
10133

11134
if (!candidacyActualisationFeatureIsActive) {
12135
return null;
13136
}
14137

15-
return <div>Actualisation</div>;
138+
return hasBeenUpdated ? (
139+
<HasBeenUpdatedComponent
140+
readyForJuryEstimatedAt={watch("readyForJuryEstimatedAt") as string}
141+
/>
142+
) : (
143+
<div className="flex flex-col">
144+
<h1 className="mb-0">Recevabilité du candidat</h1>
145+
<FormOptionalFieldsDisclaimer />
146+
<p className="text-xl mb-12">
147+
Afin de continuer votre parcours VAE, nous vous demandons de vous
148+
actualiser tous les 6 mois. Cette action est nécessaire pour que votre
149+
recevabilité soit toujours valable.
150+
</p>
151+
152+
<form
153+
onSubmit={handleSubmit(handleFormSubmit)}
154+
onReset={(e) => {
155+
e.preventDefault();
156+
resetForm();
157+
}}
158+
>
159+
<Checkbox
160+
state={errors.candidateConfirmation ? "error" : "default"}
161+
stateRelatedMessage={errors.candidateConfirmation?.message}
162+
options={[
163+
{
164+
label: "Je confirme être toujours en cours de parcours VAE.",
165+
nativeInputProps: register("candidateConfirmation"),
166+
},
167+
]}
168+
className="mb-10"
169+
data-test="actualisation-candidate-confirmation-checkbox"
170+
/>
171+
<h2 className="mb-2">
172+
Date prévisionnelle de dépôt du dossier de validation
173+
</h2>
174+
<p className="text-lg">
175+
La date prévisionnelle de dépôt du dossier de validation est une
176+
simple estimation, elle ne vous engage pas. Elle permet au
177+
certificateur d'avoir une idée du moment où vous aurez fini votre
178+
dossier et d'anticiper l'organisation de votre jury.
179+
</p>
180+
<Input
181+
className="max-w-xs mt-4"
182+
label="Date prévisionnelle"
183+
hintText="Si un accompagnateur a renseigné une date, vous la retrouverez ci-dessous."
184+
nativeInputProps={{
185+
type: "date",
186+
...register("readyForJuryEstimatedAt"),
187+
}}
188+
state={errors.readyForJuryEstimatedAt ? "error" : "default"}
189+
stateRelatedMessage={errors.readyForJuryEstimatedAt?.message}
190+
data-test="actualisation-date-input"
191+
/>
192+
<FormButtons
193+
backUrl="/"
194+
formState={{
195+
isDirty,
196+
isSubmitting,
197+
}}
198+
/>
199+
</form>
200+
</div>
201+
);
16202
}

packages/reva-candidate/src/components/candidacy/candidacy.context.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const GET_CANDIDATE_WITH_CANDIDACY = graphql(`
5656
status
5757
firstAppointmentOccuredAt
5858
lastActivityDate
59+
readyForJuryEstimatedAt
5960
candidacyDropOut {
6061
createdAt
6162
}

0 commit comments

Comments
 (0)