diff --git a/packages/app/src/api/core-domain/useCases/SaveDeclaration.ts b/packages/app/src/api/core-domain/useCases/SaveDeclaration.ts index f8fb36e0e..7250a8474 100644 --- a/packages/app/src/api/core-domain/useCases/SaveDeclaration.ts +++ b/packages/app/src/api/core-domain/useCases/SaveDeclaration.ts @@ -13,7 +13,7 @@ import { DeclarationSource } from "@common/core-domain/domain/valueObjects/decla import { Siren } from "@common/core-domain/domain/valueObjects/Siren"; import { type CreateDeclarationDTO } from "@common/core-domain/dtos/DeclarationDTO"; import { companyMap } from "@common/core-domain/mappers/companyMap"; -import { AppError, type EntityPropsToJson, type UseCase } from "@common/shared-domain"; +import { AppError, type EntityPropsToJson, type UseCase, ValidationError } from "@common/shared-domain"; import { PositiveInteger, PositiveNumber } from "@common/shared-domain/domain/valueObjects"; import { keepEntryBy } from "@common/utils/object"; import { add, isAfter } from "date-fns"; @@ -102,7 +102,10 @@ export class SaveDeclaration implements UseCase { remunerations: { cseConsultationDate: dto.remunerations?.estCalculable === "oui" ? dto.remunerations.dateConsultationCSE : undefined, - favorablePopulation: dto["remunerations-resultat"]?.populationFavorable, + favorablePopulation: + dto["remunerations"]?.estCalculable === "oui" + ? dto["remunerations-resultat"]?.populationFavorable ?? "egalite" + : undefined, mode: remunerationsMode, notComputableReason: dto.remunerations?.estCalculable === "non" ? dto.remunerations.motifNonCalculabilité : undefined, @@ -113,21 +116,21 @@ export class SaveDeclaration implements UseCase { ? dto["remunerations-csp"]?.catégories.length ? dto["remunerations-csp"].catégories.map(({ nom, tranches }) => ({ name: nom, - ranges: keepEntryBy(tranches, val => val !== null), + ranges: keepEntryBy(tranches, val => val !== ""), })) : [] : remunerationsMode === "niveau_autre" ? dto["remunerations-coefficient-autre"]?.catégories.length ? dto["remunerations-coefficient-autre"].catégories.map(({ nom, tranches }) => ({ name: nom, - ranges: keepEntryBy(tranches, val => val !== null), + ranges: keepEntryBy(tranches, val => val !== ""), })) : [] : remunerationsMode === "niveau_branche" ? dto["remunerations-coefficient-branche"]?.catégories.length ? dto["remunerations-coefficient-branche"].catégories.map(({ nom, tranches }) => ({ name: nom, - ranges: keepEntryBy(tranches, val => val !== null), + ranges: keepEntryBy(tranches, val => val !== ""), })) : [] : ([] satisfies Categorie[]), @@ -136,10 +139,15 @@ export class SaveDeclaration implements UseCase { salaryRaises: { categories: dto.augmentations?.estCalculable === "oui" - ? dto.augmentations.catégories.map(category => category.écarts) + ? [ + dto.augmentations.catégories.ouv === "" ? null : dto.augmentations.catégories.ouv, + dto.augmentations.catégories.emp === "" ? null : dto.augmentations.catégories.emp, + dto.augmentations.catégories.tam === "" ? null : dto.augmentations.catégories.tam, + dto.augmentations.catégories.ic === "" ? null : dto.augmentations.catégories.ic, + ] : [null, null, null, null], favorablePopulation: - dto.augmentations?.estCalculable === "oui" ? dto.augmentations.populationFavorable : undefined, + dto.augmentations?.estCalculable === "oui" ? dto.augmentations.populationFavorable ?? "egalite" : undefined, notComputableReason: dto.augmentations?.estCalculable == "non" ? dto.augmentations.motifNonCalculabilité : undefined, result: dto.augmentations?.estCalculable === "oui" ? dto.augmentations.résultat : undefined, @@ -148,12 +156,18 @@ export class SaveDeclaration implements UseCase { promotions: { notComputableReason: dto.promotions?.estCalculable === "non" ? dto.promotions.motifNonCalculabilité : undefined, - favorablePopulation: dto.promotions?.estCalculable === "oui" ? dto.promotions.populationFavorable : undefined, + favorablePopulation: + dto.promotions?.estCalculable === "oui" ? dto.promotions.populationFavorable ?? "egalite" : undefined, result: dto.promotions?.estCalculable === "oui" ? dto.promotions.résultat : undefined, score: dto.promotions?.estCalculable === "oui" ? dto.promotions.note : undefined, categories: dto.promotions?.estCalculable === "oui" - ? dto.promotions.catégories.map(category => category.écarts) + ? [ + dto.promotions.catégories.ouv === "" ? null : dto.promotions.catégories.ouv, + dto.promotions.catégories.emp === "" ? null : dto.promotions.catégories.emp, + dto.promotions.catégories.tam === "" ? null : dto.promotions.catégories.tam, + dto.promotions.catégories.ic === "" ? null : dto.promotions.catégories.ic, + ] : [null, null, null, null], }, }), @@ -165,7 +179,7 @@ export class SaveDeclaration implements UseCase { : undefined, favorablePopulation: dto["augmentations-et-promotions"]?.estCalculable === "oui" - ? dto["augmentations-et-promotions"].populationFavorable + ? dto["augmentations-et-promotions"].populationFavorable ?? "egalite" : undefined, employeesCountResult: dto["augmentations-et-promotions"]?.estCalculable === "oui" @@ -199,11 +213,13 @@ export class SaveDeclaration implements UseCase { ? undefined : { result: dto["hautes-remunerations"].résultat, - favorablePopulation: dto["hautes-remunerations"].populationFavorable, + favorablePopulation: dto["hautes-remunerations"].populationFavorable ?? "egalite", score: dto["hautes-remunerations"].note, }, } satisfies EntityPropsToJson; + console.log("partialDeclaration", JSON.stringify(partialDeclaration, null, 2)); + let declaration: Declaration; try { @@ -272,8 +288,8 @@ export class SaveDeclaration implements UseCase { throw specification.lastError; } } catch (error: unknown) { - if (error instanceof DeclarationSpecificationError) { - console.error(error.message); + if (error instanceof DeclarationSpecificationError || error instanceof ValidationError) { + console.error(error); throw error; } diff --git a/packages/app/src/app/(default)/index-egapro/declaration/(remunerations)/RemunerationGenericForm.tsx b/packages/app/src/app/(default)/index-egapro/declaration/(remunerations)/RemunerationGenericForm.tsx index 10e309ac7..206976a2e 100644 --- a/packages/app/src/app/(default)/index-egapro/declaration/(remunerations)/RemunerationGenericForm.tsx +++ b/packages/app/src/app/(default)/index-egapro/declaration/(remunerations)/RemunerationGenericForm.tsx @@ -1,18 +1,18 @@ "use client"; import { fr } from "@codegouvfr/react-dsfr"; -import Alert from "@codegouvfr/react-dsfr/Alert"; import { Button } from "@codegouvfr/react-dsfr/Button"; import { cx } from "@codegouvfr/react-dsfr/tools/cx"; import { CSP } from "@common/core-domain/domain/valueObjects/CSP"; import { AgeRange } from "@common/core-domain/domain/valueObjects/declaration/AgeRange"; -import { type Catégorie, type DeclarationDTO } from "@common/core-domain/dtos/DeclarationDTO"; +import { type DeclarationDTO } from "@common/core-domain/dtos/DeclarationDTO"; import { type Remunerations } from "@common/models/generated"; -import { zodNumberOrNaNOrNull } from "@common/utils/form"; +import { zodNumberOrEmptyString } from "@common/utils/form"; import { zodFr } from "@common/utils/zod"; import { PercentageInput } from "@components/RHF/PercentageInput"; import { ClientOnly } from "@components/utils/ClientOnly"; import { SkeletonForm } from "@components/utils/skeleton/SkeletonForm"; +import { AlertMessage } from "@design-system/client"; import { ClientAnimate } from "@design-system/utils/client/ClientAnimate"; import { zodResolver } from "@hookform/resolvers/zod"; import { useDeclarationFormManager } from "@services/apiClient/useDeclarationFormManager"; @@ -20,8 +20,9 @@ import { useRouter } from "next/navigation"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { z } from "zod"; +import { NOT_ALL_EMPTY_CATEGORIES } from "../../../messages"; import { BackNextButtons } from "../BackNextButtons"; -import { assertOrRedirectCommencerStep, funnelConfig, type FunnelKey } from "../declarationFunnelConfiguration"; +import { funnelConfig, type FunnelKey } from "../declarationFunnelConfiguration"; import style from "./RemunerationGenericForm.module.scss"; const formSchema = zodFr.object({ @@ -30,36 +31,43 @@ const formSchema = zodFr.object({ z.object({ nom: z.string(), tranches: z.object({ - [AgeRange.Enum.LESS_THAN_30]: zodNumberOrNaNOrNull, - [AgeRange.Enum.FROM_30_TO_39]: zodNumberOrNaNOrNull, - [AgeRange.Enum.FROM_40_TO_49]: zodNumberOrNaNOrNull, - [AgeRange.Enum.FROM_50_TO_MORE]: zodNumberOrNaNOrNull, + [AgeRange.Enum.LESS_THAN_30]: zodNumberOrEmptyString, + [AgeRange.Enum.FROM_30_TO_39]: zodNumberOrEmptyString, + [AgeRange.Enum.FROM_40_TO_49]: zodNumberOrEmptyString, + [AgeRange.Enum.FROM_50_TO_MORE]: zodNumberOrEmptyString, }), }), ) - .superRefine((val, ctx) => { - if (notFilled(val)) { + .superRefine((catégories, ctx) => { + if (notFilled(catégories)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Vous devez renseigner au moins un écart si votre indicateur est calculable", + message: NOT_ALL_EMPTY_CATEGORIES, }); } }), }); -// Infer the TS type according to the zod schema. -type FormType = z.infer; - const notFilled = (catégories: FormType["catégories"]) => catégories.every( catégorie => - catégorie.tranches[AgeRange.Enum.LESS_THAN_30] === null && - catégorie.tranches[AgeRange.Enum.FROM_30_TO_39] === null && - catégorie.tranches[AgeRange.Enum.FROM_40_TO_49] === null && - catégorie.tranches[AgeRange.Enum.FROM_50_TO_MORE] === null, + catégorie.tranches[AgeRange.Enum.LESS_THAN_30] === "" && + catégorie.tranches[AgeRange.Enum.FROM_30_TO_39] === "" && + catégorie.tranches[AgeRange.Enum.FROM_40_TO_49] === "" && + catégorie.tranches[AgeRange.Enum.FROM_50_TO_MORE] === "", ); -const defaultTranch = { ":29": null, "30:39": null, "40:49": null, "50:": null }; +// Infer the TS type according to the zod schema. +type FormType = z.infer; + +type NumberOrEmptyString = number | ""; + +const defaultTranch = { + ":29": "" as NumberOrEmptyString, + "30:39": "" as NumberOrEmptyString, + "40:49": "" as NumberOrEmptyString, + "50:": "" as NumberOrEmptyString, +}; const buildDefaultCategories = (mode: Remunerations["mode"]) => mode === "csp" @@ -84,18 +92,20 @@ export const RemunerationGenericForm = ({ mode }: { mode: Remunerations["mode"] const router = useRouter(); const { formData, savePageData } = useDeclarationFormManager(); - assertOrRedirectCommencerStep(formData); + // assertOrRedirectCommencerStep(formData); const methods = useForm({ mode: "onChange", + shouldUnregister: true, resolver: zodResolver(formSchema), defaultValues: formData[stepName] || buildDefaultCategories(mode), }); const { control, + register, handleSubmit, + getValues, formState: { errors, isValid }, - setError, } = methods; const { @@ -111,26 +121,16 @@ export const RemunerationGenericForm = ({ mode }: { mode: Remunerations["mode"] }); const onSubmit = async (data: FormType) => { - if (notFilled(data.catégories)) { - return setError("root.catégories", { - message: - mode === "csp" - ? "Vous devez renseigner les écarts de rémunération pour les CSP et tranches d'âge concernés." - : "Vous devez renseigner les écarts de rémunération pour les tranches d'âge concernées.", - }); - } - savePageData(stepName, data as DeclarationDTO[typeof stepName]); router.push(funnelConfig(formData)[stepName].next().url); }; - const getCSPTitle = (catégorie: Catégorie) => - mode === "csp" ? new CSP(catégorie.nom as CSP.Enum).getLabel() : undefined; - return (
+ + }>
@@ -140,7 +140,9 @@ export const RemunerationGenericForm = ({ mode }: { mode: Remunerations["mode"]
{/* Name of catégorie doesn't matter when mode is coef, so don't bother with inconsistent name between storage & UI */} - {getCSPTitle(catégorie) || `Niveau ou coefficient ${index + 1}`} + {mode === "csp" + ? new CSP(catégorie.nom as CSP.Enum).getLabel() + : `Niveau ou coefficient ${index + 1}`} {mode !== "csp" && (