diff --git a/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-page/fixtures/certification-bp-boucher.json b/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-page/fixtures/certification-bp-boucher.json index 7f13610a9..235fb8fb3 100644 --- a/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-page/fixtures/certification-bp-boucher.json +++ b/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-page/fixtures/certification-bp-boucher.json @@ -4,11 +4,15 @@ "id": "bf78b4d6-f6ac-4c8f-9e6b-d6c6ae9e891b", "label": "BP Boucher", "codeRncp": "37310", - "status": "BROUILLON", + "status": "A_VALIDER_PAR_CERTIFICATEUR", "rncpExpiresAt": 1788127200000, "rncpDeliveryDeadline": null, "availableAt": 1688162400000, - "typeDiplome": "Brevet Professionnel (BP)", + "languages": null, + "juryModalities": [], + "juryFrequency": null, + "juryFrequencyOther": null, + "juryPlace": null, "degree": { "id": "0ee2104b-9bdc-4e44-b232-123cf2e3b9a1", "label": "Niveau 4 : Baccalauréat" diff --git a/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-prerequisites-page/fixtures/certification-bp-boucher.json b/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-prerequisites-page/fixtures/certification-bp-boucher.json index 6e2afecc2..0d69d5d82 100644 --- a/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-prerequisites-page/fixtures/certification-bp-boucher.json +++ b/packages/reva-admin-react/cypress/e2e/responsable-certifications/certifications/update-certification-prerequisites-page/fixtures/certification-bp-boucher.json @@ -4,7 +4,7 @@ "id": "bf78b4d6-f6ac-4c8f-9e6b-d6c6ae9e891b", "label": "BP Boucher", "codeRncp": "37310", - "status": "BROUILLON", + "status": "A_VALIDER_PAR_CERTIFICATEUR", "rncpExpiresAt": 1788127200000, "rncpDeliveryDeadline": null, "availableAt": 1688162400000, diff --git a/packages/reva-admin-react/src/app/(admin)/certifications/add-certification/description/addCertification.hook.ts b/packages/reva-admin-react/src/app/(admin)/certifications/add-certification/description/addCertification.hook.ts index 087af2938..179c041d2 100644 --- a/packages/reva-admin-react/src/app/(admin)/certifications/add-certification/description/addCertification.hook.ts +++ b/packages/reva-admin-react/src/app/(admin)/certifications/add-certification/description/addCertification.hook.ts @@ -36,7 +36,7 @@ const getFCCertificationQuery = graphql(` } `); -const addCertificatioMutation = graphql(` +const addCertificationMutation = graphql(` mutation addFCCertificationForAddCertificationPage( $input: AddCertificationInput! ) { @@ -68,7 +68,7 @@ export const useAddCertificationPage = ({ rncp }: { rncp: string }) => { const addCertification = useMutation({ mutationFn: (input: { codeRncp: string }) => - graphqlClient.request(addCertificatioMutation, { + graphqlClient.request(addCertificationMutation, { input, }), }); diff --git a/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/description/page.tsx b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/description/page.tsx new file mode 100644 index 000000000..9c22f1813 --- /dev/null +++ b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/description/page.tsx @@ -0,0 +1,491 @@ +"use client"; +import { ReactNode } from "react"; +import { format } from "date-fns"; +import { useFieldArray, useForm } from "react-hook-form"; +import * as z from "zod"; +import { useRouter } from "next/navigation"; +import { useParams } from "next/navigation"; + +import { EnhancedSectionCard } from "@/components/card/enhanced-section-card/EnhancedSectionCard"; +import Input from "@codegouvfr/react-dsfr/Input"; + +import { zodResolver } from "@hookform/resolvers/zod"; + +import { useUpdateCertificationDescriptionPage } from "./updateCertificationDescription.hook"; +import { graphqlErrorToast, successToast } from "@/components/toast/toast"; +import Tag from "@codegouvfr/react-dsfr/Tag"; +import Notice from "@codegouvfr/react-dsfr/Notice"; +import RadioButtons from "@codegouvfr/react-dsfr/RadioButtons"; +import { FormButtons } from "@/components/form/form-footer/FormButtons"; +import Checkbox from "@codegouvfr/react-dsfr/Checkbox"; +import Select from "@codegouvfr/react-dsfr/Select"; + +import { + CertificationJuryModality, + CertificationJuryFrequency, +} from "@/graphql/generated/graphql"; + +const EvaluationModalities: { id: CertificationJuryModality; label: string }[] = + [ + { + id: "PRESENTIEL", + label: "Présentiel", + }, + { + id: "A_DISTANCE", + label: "À distance", + }, + { + id: "MISE_EN_SITUATION_PROFESSIONNELLE", + label: "Mise en situation professionnelle", + }, + { + id: "ORAL", + label: "Oral", + }, + ]; + +const JuryFrequencies: { id: CertificationJuryFrequency; label: string }[] = [ + { + id: "MONTHLY", + label: "Tous les mois", + }, + { + id: "TRIMESTERLY", + label: "Trimestrielle", + }, + { + id: "YEARLY", + label: "1 fois / an", + }, +]; + +const zodSchema = z + .object({ + languages: z.enum(["Aucune", "1", "2"], { + invalid_type_error: "Veuillez séléctionner une option", + }), + juryModalities: z + .object({ id: z.string(), label: z.string(), checked: z.boolean() }) + .array(), + juryFrequency: z.enum([ + "", + ...JuryFrequencies.map(({ id }) => id), + "Autre", + ]), + juryFrequencyOther: z.string().optional(), + juryPlace: z.string().optional(), + startOfVisibility: z.string({ + invalid_type_error: "Champs requis", + }), + endOfVisibility: z.string({ + invalid_type_error: "Champs requis", + }), + }) + .superRefine( + ( + { + juryModalities, + juryFrequency, + juryFrequencyOther, + startOfVisibility, + endOfVisibility, + }, + { addIssue }, + ) => { + if (juryModalities.findIndex((v) => v.checked) == -1) { + addIssue({ + path: ["juryModalities"], + message: "Veuillez séléctionner au moins une option", + code: z.ZodIssueCode.custom, + }); + } + + if (!juryFrequency) { + addIssue({ + path: ["juryFrequency"], + message: "Veuillez renseigner ce champ", + code: z.ZodIssueCode.custom, + }); + return; + } else if (juryFrequency == "Autre" && !juryFrequencyOther) { + addIssue({ + path: ["juryFrequencyOther"], + message: "Veuillez renseigner ce champ", + code: z.ZodIssueCode.custom, + }); + + return; + } + + if (isNaN(Date.parse(startOfVisibility))) { + addIssue({ + path: ["startOfVisibility"], + message: "Veuillez renseigner ce champ", + code: z.ZodIssueCode.custom, + }); + } + + if (isNaN(Date.parse(endOfVisibility))) { + addIssue({ + path: ["endOfVisibility"], + message: "Veuillez renseigner ce champ", + code: z.ZodIssueCode.custom, + }); + } + }, + ); + +type CompanySiretStepFormSchema = z.infer; + +type CertificationForPage = Exclude< + ReturnType["certification"], + undefined +>; + +export default function UpdateCertificationDescriptionForCertificationRegistryManagerPage() { + const { certificationId } = useParams<{ + certificationId: string; + }>(); + + const { + certification, + getCertificationQueryStatus, + updateCertificationDescription, + } = useUpdateCertificationDescriptionPage({ certificationId }); + + return getCertificationQueryStatus === "success" && certification ? ( + + ) : null; +} + +const PageContent = ({ + certification, + updateCertificationDescription, +}: { + certification: CertificationForPage; + updateCertificationDescription: ReturnType< + typeof useUpdateCertificationDescriptionPage + >["updateCertificationDescription"]; +}) => { + const router = useRouter(); + + const { + register, + handleSubmit, + control, + formState: { isSubmitting, isDirty, errors }, + watch, + } = useForm({ + resolver: zodResolver(zodSchema), + defaultValues: { + languages: + typeof certification.languages === "number" + ? certification.languages > 0 + ? (`${certification.languages}` as "1" | "2") + : "Aucune" + : undefined, + + juryModalities: EvaluationModalities.map((modality) => ({ + id: modality.id, + label: modality.label, + checked: certification.juryModalities.includes(modality.id), + })), + + juryFrequency: certification.juryFrequencyOther + ? "Autre" + : certification.juryFrequency || "", + juryFrequencyOther: certification.juryFrequencyOther || undefined, + + juryPlace: certification.juryPlace || undefined, + + startOfVisibility: certification.availableAt + ? format(certification.availableAt, "yyyy-MM-dd") + : undefined, + endOfVisibility: certification.rncpExpiresAt + ? format(certification.rncpExpiresAt, "yyyy-MM-dd") + : undefined, + }, + }); + + const juryFrequency = watch("juryFrequency"); + const { fields: juryModalities } = useFieldArray({ + control, + name: "juryModalities", + }); + + const handleFormSubmit = handleSubmit( + async (data) => { + try { + const frequency = + JuryFrequencies.find(({ id }) => id == data.juryFrequency)?.id || + null; + + await updateCertificationDescription.mutateAsync({ + certificationId: certification.id, + languages: + data.languages == "Aucune" ? 0 : parseInt(data.languages, 10), + juryModalities: data.juryModalities + .filter((modality) => modality.checked) + .map((modality) => modality.id) as CertificationJuryModality[], + juryFrequency: frequency, + juryFrequencyOther: frequency ? null : data.juryFrequencyOther, + juryPlace: data.juryPlace, + availableAt: new Date(data.startOfVisibility).getTime(), + expiresAt: new Date(data.endOfVisibility).getTime(), + }); + + successToast("La certification a bien été ajoutée"); + + router.push( + `/responsable-certifications/certifications/${certification.id}`, + ); + } catch (error) { + graphqlErrorToast(error); + } + }, + (errors) => { + console.log("errors", errors); + }, + ); + + return ( +
+

Descriptif de la certification

+

+ Complétez ou modifiez les informations concernant cette certification. + Une fois la certification validée et visible, ces informations seront + disponibles pour les AAP et les candidats. +

+ +
+ +
+ {certification.codeRncp} +

Descriptif de la certification

+ {certification.label} +
+ {certification.degree.label} + {certification.typeDiplome || "Inconnu"} + + {certification.rncpExpiresAt + ? format(certification.rncpExpiresAt, "dd/MM/yyyy") + : "Inconnue"} + + + {certification.rncpDeliveryDeadline + ? format(certification.rncpDeliveryDeadline, "dd/MM/yyyy") + : "Inconnue"} + +
+ +

Domaines et sous-domaines du Formacode

+ +
+ {certification.domains.length == 0 && ( +
Aucun formacode associé
+ )} + {certification.domains.map((domain) => ( +
+
{domain.label}
+
+ {domain.children.map((subDomain) => ( + + {`${subDomain.code} ${subDomain.label}`} + + ))} +
+
+ ))} +
+ + + Vous trouverez dans cette section des informations à compléter + et d’autres non modifiables. En cas d’erreur, contactez + directement France compétences. + + } + /> +
+
+ +
+
+

Informations complémentaires

+ + +

Jury

+ +
+ ({ + label: modality.label, + nativeInputProps: { + ...register(`juryModalities.${index}.checked`), + }, + }))} + state={errors.juryModalities ? "error" : "default"} + stateRelatedMessage={ + errors.juryModalities + ? "Veuillez séléctionner au moins une option" + : undefined + } + /> + +
+
+ + + +
+ + +
+
+ +

+ Visibilité de la certification sur la plateforme +

+

+ Si nous avons pu récupérer l’information, les champs ci-dessous + sont automatiquement remplis avec les dates de publication et + d’échéance fournies par France compétences. Si vous souhaitez + modifier ces dates et changer la visibilité de la certification + sur la plateforme, vous le pouvez. +

+ +
+ + + +
+
+ + +
+
+ ); +}; + +const Info = ({ + title, + children, + className, +}: { + title: string; + children: ReactNode; + className?: string; +}) => ( +
+
{title}
+
{children}
+
+); diff --git a/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/description/updateCertificationDescription.hook.ts b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/description/updateCertificationDescription.hook.ts new file mode 100644 index 000000000..a629b6324 --- /dev/null +++ b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/description/updateCertificationDescription.hook.ts @@ -0,0 +1,94 @@ +import { useGraphQlClient } from "@/components/graphql/graphql-client/GraphqlClient"; +import { graphql } from "@/graphql/generated"; +import { UpdateCertificationDescriptionInput } from "@/graphql/generated/graphql"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +const getCertificationQuery = graphql(` + query getCertificationForCertificationRegistryManagerUpdateCertificationDescriptionPage( + $certificationId: ID! + ) { + getCertification(certificationId: $certificationId) { + id + label + codeRncp + status + rncpExpiresAt + rncpDeliveryDeadline + availableAt + expiresAt + typeDiplome + languages + juryModalities + juryFrequency + juryFrequencyOther + juryPlace + degree { + id + label + } + domains { + id + code + label + children { + id + code + label + } + } + } + } +`); + +const updateCertificationDescriptionMutation = graphql(` + mutation updateCertificationDescriptionForCertificationRegistryManagerUpdateCertificationDescriptionPage( + $input: UpdateCertificationDescriptionInput! + ) { + referential_updateCertificationDescription(input: $input) { + id + } + } +`); + +export const useUpdateCertificationDescriptionPage = ({ + certificationId, +}: { + certificationId: string; +}) => { + const { graphqlClient } = useGraphQlClient(); + const queryClient = useQueryClient(); + + const { + data: getCertificationQueryResponse, + status: getCertificationQueryStatus, + } = useQuery({ + queryKey: [ + certificationId, + "certifications", + "getCertificationForCertificationRegistryManagerUpdateCertificationDescriptionPage", + ], + queryFn: () => + graphqlClient.request(getCertificationQuery, { + certificationId, + }), + }); + + const certification = getCertificationQueryResponse?.getCertification; + + const updateCertificationDescription = useMutation({ + mutationFn: (input: UpdateCertificationDescriptionInput) => + graphqlClient.request(updateCertificationDescriptionMutation, { + input, + }), + onSuccess: () => + queryClient.invalidateQueries({ + queryKey: [certificationId], + }), + }); + + return { + certification, + getCertificationQueryStatus, + updateCertificationDescription, + }; +}; diff --git a/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/page.tsx b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/page.tsx index 8c17cd2fe..0e71f5570 100644 --- a/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/page.tsx +++ b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/page.tsx @@ -1,11 +1,24 @@ "use client"; +import { ReactNode } from "react"; +import { format } from "date-fns"; import { useParams } from "next/navigation"; -import { useUpdateCertificationPage } from "./updateCertification.hook"; -import { CertificationCompetenceBlocsSummaryCard } from "@/components/certifications/certification-competence-blocs-summary-card/CertificationCompetenceBlocsSummaryCard"; import { useRouter } from "next/navigation"; + +import { Tag } from "@codegouvfr/react-dsfr/Tag"; + +import { EnhancedSectionCard } from "@/components/card/enhanced-section-card/EnhancedSectionCard"; + +import { CertificationCompetenceBlocsSummaryCard } from "@/components/certifications/certification-competence-blocs-summary-card/CertificationCompetenceBlocsSummaryCard"; import { SectionCard } from "../../../../components/card/section-card/SectionCard"; import { SmallNotice } from "../../../../components/small-notice/SmallNotice"; +import { useUpdateCertificationPage } from "./updateCertification.hook"; +import Notice from "@codegouvfr/react-dsfr/Notice"; +import { + CertificationJuryFrequency, + CertificationJuryModality, +} from "@/graphql/generated/graphql"; + type CertificationForPage = Exclude< ReturnType["certification"], undefined @@ -23,12 +36,58 @@ export default function UpdateCertificationForCertificationRegistryManagerPage() ) : null; } +const EvaluationModalities: { id: CertificationJuryModality; label: string }[] = + [ + { + id: "PRESENTIEL", + label: "Présentiel", + }, + { + id: "A_DISTANCE", + label: "À distance", + }, + { + id: "MISE_EN_SITUATION_PROFESSIONNELLE", + label: "Mise en situation professionnelle", + }, + { + id: "ORAL", + label: "Oral", + }, + ]; + +const JuryFrequencies: { id: CertificationJuryFrequency; label: string }[] = [ + { + id: "MONTHLY", + label: "Tous les mois", + }, + { + id: "TRIMESTERLY", + label: "Trimestrielle", + }, + { + id: "YEARLY", + label: "1 fois / an", + }, +] as const; + const PageContent = ({ certification, }: { certification: CertificationForPage; }) => { const router = useRouter(); + + const isEditable = certification.status == "A_VALIDER_PAR_CERTIFICATEUR"; + + const isDescriptionComplete = + typeof certification.languages === "number" && + certification.juryModalities.length > 0 && + ((certification.juryFrequency && certification.juryFrequency?.length > 0) || + certification.juryFrequencyOther) && + certification.availableAt && + certification.expiresAt; + return (

@@ -41,6 +100,97 @@ const PageContent = ({ aux AAP et aux candidats.

+ +
+ + {certification.availableAt && certification.expiresAt ? ( +
{`du ${format(certification.availableAt, "dd/MM/yyyy")} au ${format(certification.expiresAt, "dd/MM/yyyy")}`}
+ ) : ( + "Non renseigné" + )} +
+ +
+ +

{certification.label}

+
+
+ {certification.degree.label} + {certification.typeDiplome || "Inconnu"} + + {certification.rncpDeliveryDeadline + ? format(certification.rncpDeliveryDeadline, "dd/MM/yyyy") + : "Inconnue"} + +
+ +

Jury

+
+ + {certification.juryFrequencyOther || + JuryFrequencies.find( + ({ id }) => id == certification.juryFrequency, + )?.label || + "Non renseigné"} + + + {certification.juryModalities.length > 0 + ? certification.juryModalities.reduce( + (acc, modality) => + `${acc}${acc && ","} ${EvaluationModalities.find(({ id }) => id == modality)?.label}`, + "", + ) + : "Non renseigné"} + + {certification.juryPlace && ( + + {certification.juryPlace} + + )} +
+ +

Domaines et sous-domaines du Formacode

+
+ {certification.domains.length == 0 && ( +
Aucun formacode associé
+ )} + {certification.domains.map((domain) => ( +
+
{domain.label}
+
+ {domain.children.map((subDomain) => ( + + {`${subDomain.code} ${subDomain.label}`} + + ))} +
+
+ ))} +
+ + + Vous trouverez dans cette section des informations à compléter + et d’autres non modifiables. En cas d’erreur, contactez + directement France compétences. + + } + /> +
+
+ ); }; + +const Info = ({ + title, + children, + className, + "data-test": dataTest, +}: { + title: string; + children: ReactNode; + className?: string; + "data-test"?: string; +}) => ( +
+
{title}
+
{children}
+
+); diff --git a/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/updateCertification.hook.ts b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/updateCertification.hook.ts index 9e30e45fd..fac0e7577 100644 --- a/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/updateCertification.hook.ts +++ b/packages/reva-admin-react/src/app/responsable-certifications/certifications/[certificationId]/updateCertification.hook.ts @@ -10,6 +10,31 @@ const getCertificationQuery = graphql(` id label codeRncp + status + rncpExpiresAt + rncpDeliveryDeadline + availableAt + expiresAt + typeDiplome + languages + juryModalities + juryFrequency + juryFrequencyOther + juryPlace + degree { + id + label + } + domains { + id + code + label + children { + id + code + label + } + } competenceBlocs { id code diff --git a/packages/reva-api/modules/referential/features/updateCertificationDescription.ts b/packages/reva-api/modules/referential/features/updateCertificationDescription.ts new file mode 100644 index 000000000..d5fb9a011 --- /dev/null +++ b/packages/reva-api/modules/referential/features/updateCertificationDescription.ts @@ -0,0 +1,56 @@ +import { CertificationStatus } from "@prisma/client"; +import { UpdateCertificationDescriptionInput } from "../referential.types"; +import { getCertificationById } from "./getCertificationById"; +import { prismaClient } from "../../../prisma/client"; + +export const updateCertificationDescription = async ({ + certificationId, + languages, + juryModalities, + juryFrequency, + juryFrequencyOther, + juryPlace, + availableAt, + expiresAt, +}: UpdateCertificationDescriptionInput) => { + const certification = await getCertificationById({ certificationId }); + if (!certification) { + throw new Error("La certification n'a pas été trouvée"); + } + + const allowedStatus: CertificationStatus[] = [ + "BROUILLON", + "A_VALIDER_PAR_CERTIFICATEUR", + ]; + + if (!allowedStatus.includes(certification?.status)) { + throw new Error( + "Le statut de la certification ne permet pas de modifier la description", + ); + } + + if (juryModalities.length == 0) { + throw new Error("Renseigner au moins une modalité de jury"); + } + + if (juryFrequency && juryFrequencyOther) { + throw new Error("Renseigner une seule fréquence de jury"); + } + + if (!juryFrequency && !juryFrequencyOther) { + throw new Error("Renseigner au moins une fréquence de jury"); + } + + return await prismaClient.certification.update({ + where: { id: certificationId }, + data: { + languages, + juryModalities, + juryFrequency, + juryFrequencyOther, + juryPlace, + availableAt, + expiresAt, + }, + }); +}; diff --git a/packages/reva-api/modules/referential/referential.graphql b/packages/reva-api/modules/referential/referential.graphql index 7296fa607..7678a8462 100644 --- a/packages/reva-api/modules/referential/referential.graphql +++ b/packages/reva-api/modules/referential/referential.graphql @@ -11,6 +11,19 @@ type Domain { children: [SubDomain!]! } +enum CertificationJuryModality { + PRESENTIEL + A_DISTANCE + MISE_EN_SITUATION_PROFESSIONNELLE + ORAL +} + +enum CertificationJuryFrequency { + MONTHLY + TRIMESTERLY + YEARLY +} + type Certification { id: ID! label: String! @@ -34,6 +47,11 @@ type Certification { domains: [Domain!]! fcPrerequisites: String prerequisites: [CertificationPrerequisite!]! + languages: Int + juryModalities: [CertificationJuryModality!]! + juryFrequency: CertificationJuryFrequency + juryFrequencyOther: String + juryPlace: String } type CertificationPrerequisite { @@ -348,15 +366,28 @@ input SendCertificationToRegistryManagerInput { input ResetCompetenceBlocsByCertificationIdInput { certificationId: ID! } + input UpdateCertificationPrerequisitesInput { certificationId: ID! prerequisites: [CertificationPrerequisiteInput!]! } + input CertificationPrerequisiteInput { label: String! index: Int! } +input UpdateCertificationDescriptionInput { + certificationId: ID! + languages: Int! + juryModalities: [CertificationJuryModality!]! + juryFrequency: CertificationJuryFrequency + juryFrequencyOther: String + juryPlace: String + availableAt: Timestamp + expiresAt: Timestamp +} + type Mutation { referential_updateCompetenceBlocsByCertificationId( input: UpdateCompetenceBlocsInput! @@ -384,4 +415,7 @@ type Mutation { referential_updateCertificationPrerequisites( input: UpdateCertificationPrerequisitesInput ): Certification! + referential_updateCertificationDescription( + input: UpdateCertificationDescriptionInput + ): Certification! } diff --git a/packages/reva-api/modules/referential/referential.resolvers.ts b/packages/reva-api/modules/referential/referential.resolvers.ts index b2863c9d9..12e03476f 100644 --- a/packages/reva-api/modules/referential/referential.resolvers.ts +++ b/packages/reva-api/modules/referential/referential.resolvers.ts @@ -24,6 +24,7 @@ import { SendCertificationToRegistryManagerInput, ResetCompetenceBlocsByCertificationIdInput, UpdateCertificationPrerequisitesInput, + UpdateCertificationDescriptionInput, } from "./referential.types"; import { RNCPCertification, RNCPReferential } from "./rncp"; import { @@ -50,6 +51,7 @@ import { resetCompetenceBlocsByCertificationId } from "./features/resetCompetenc import { searchCertificationsV2ForRegistryManager } from "./features/searchCertificationsV2ForRegistryManager"; import { getCertificationPrerequisitesByCertificationId } from "./features/getCertificationPrerequisitesByCertificationId"; import { updateCertificationPrerequisites } from "./features/updateCertificationPrerequisites"; +import { updateCertificationDescription } from "./features/updateCertificationDescription"; const unsafeReferentialResolvers = { Certification: { @@ -226,6 +228,10 @@ const unsafeReferentialResolvers = { _parent: unknown, { input }: { input: UpdateCertificationPrerequisitesInput }, ) => updateCertificationPrerequisites(input), + referential_updateCertificationDescription: ( + _parent: unknown, + { input }: { input: UpdateCertificationDescriptionInput }, + ) => updateCertificationDescription(input), }, }; diff --git a/packages/reva-api/modules/referential/referential.security.ts b/packages/reva-api/modules/referential/referential.security.ts index 9b8f4e297..0e26408a5 100644 --- a/packages/reva-api/modules/referential/referential.security.ts +++ b/packages/reva-api/modules/referential/referential.security.ts @@ -34,6 +34,9 @@ export const referentialResolversSecurityMap = { "Mutation.referential_updateCertificationPrerequisites": isAdminOrCertificationRegistryManagerOfCertification, + "Mutation.referential_updateCertificationDescription": + isAdminOrCertificationRegistryManagerOfCertification, + "Query.getEtablissementAsAdmin": [hasRole(["admin"])], "Query.getCertificationCompetenceBloc": isAnyone, }; diff --git a/packages/reva-api/modules/referential/referential.types.ts b/packages/reva-api/modules/referential/referential.types.ts index f71d84b7c..26f15a714 100644 --- a/packages/reva-api/modules/referential/referential.types.ts +++ b/packages/reva-api/modules/referential/referential.types.ts @@ -1,3 +1,8 @@ +import { + CertificationJuryFrequency, + CertificationJuryModality, +} from "@prisma/client"; + export const CANDIDACY_FINANCING_METHOD_OTHER_SOURCE_ID = "a0d5b35b-06bb-46dd-8cf5-fbba5b01c711"; @@ -104,3 +109,14 @@ export interface UpdateCertificationPrerequisitesInput { certificationId: string; prerequisites: { label: string; index: number }[]; } + +export interface UpdateCertificationDescriptionInput { + certificationId: string; + languages: number; + juryModalities: CertificationJuryModality[]; + juryFrequency?: CertificationJuryFrequency; + juryFrequencyOther?: string; + juryPlace?: string; + availableAt: Date; + expiresAt: Date; +} diff --git a/packages/reva-api/prisma/migrations/20241212154403_add_languages_and_jury_properties_to_certification/migration.sql b/packages/reva-api/prisma/migrations/20241212154403_add_languages_and_jury_properties_to_certification/migration.sql new file mode 100644 index 000000000..d3986abfc --- /dev/null +++ b/packages/reva-api/prisma/migrations/20241212154403_add_languages_and_jury_properties_to_certification/migration.sql @@ -0,0 +1,12 @@ +-- CreateEnum +CREATE TYPE "CertificationJuryModality" AS ENUM ('PRESENTIEL', 'A_DISTANCE', 'MISE_EN_SITUATION_PROFESSIONNELLE', 'ORAL'); + +-- CreateEnum +CREATE TYPE "CertificationJuryFrequency" AS ENUM ('MONTHLY', 'TRIMESTERLY', 'YEARLY'); + +-- AlterTable +ALTER TABLE "certification" ADD COLUMN "jury_frequency" "CertificationJuryFrequency", +ADD COLUMN "jury_frequency_other" TEXT, +ADD COLUMN "jury_modalities" "CertificationJuryModality"[] DEFAULT ARRAY[]::"CertificationJuryModality"[], +ADD COLUMN "jury_place" TEXT, +ADD COLUMN "languages" INTEGER; diff --git a/packages/reva-api/prisma/schema.prisma b/packages/reva-api/prisma/schema.prisma index 9af2549c6..a82a0ad5e 100644 --- a/packages/reva-api/prisma/schema.prisma +++ b/packages/reva-api/prisma/schema.prisma @@ -29,6 +29,19 @@ enum FeasibilityFormat { DEMATERIALIZED } +enum CertificationJuryModality { + PRESENTIEL + A_DISTANCE + MISE_EN_SITUATION_PROFESSIONNELLE + ORAL +} + +enum CertificationJuryFrequency { + MONTHLY + TRIMESTERLY + YEARLY +} + model Certification { id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid label String @db.VarChar(255) @@ -50,6 +63,11 @@ model Certification { certificationAuthorityLocalAccountOnCertification CertificationAuthorityLocalAccountOnCertification[] availableAt DateTime @map("available_at") @db.Timestamptz(6) expiresAt DateTime @map("expires_at") @db.Timestamptz(6) + languages Int? + juryModalities CertificationJuryModality[] @default([]) @map("jury_modalities") + juryFrequency CertificationJuryFrequency? @map("jury_frequency") + juryFrequencyOther String? @map("jury_frequency_other") + juryPlace String? @map("jury_place") previousVersionCertificationId String? @unique @map("previous_version_certification_id") @db.Uuid previousVersion Certification? @relation("replacement", fields: [previousVersionCertificationId], references: [id]) nextVersion Certification? @relation("replacement")