From fc2b184c505df8a4651223aecf613ead97c21fbe Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 3 Mar 2026 11:59:41 +0100 Subject: [PATCH] =?UTF-8?q?feat=20:=20ajout=20de=20la=20cl=C3=B4ture=20de?= =?UTF-8?q?=20contrat=20et=20de=20la=20cr=C3=A9ation=20de=20contrat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/functional-rules.md | 11 +- frontend/components/SiteFilterSelector.vue | 1 - frontend/components/employees/ContractTab.vue | 222 +++++++++++++ frontend/components/employees/LeaveTab.vue | 7 + frontend/components/employees/RttTab.vue | 7 + frontend/composables/useEmployeeDetailPage.ts | 296 ++++++++++++++++++ frontend/pages/employees/[id].vue | 155 +++++---- frontend/pages/employees/index.vue | 21 +- frontend/services/employees.ts | 25 +- frontend/utils/contract.ts | 17 + frontend/utils/date.ts | 11 + src/State/EmployeeWriteProcessor.php | 60 +++- 12 files changed, 712 insertions(+), 121 deletions(-) create mode 100644 frontend/components/employees/ContractTab.vue create mode 100644 frontend/components/employees/LeaveTab.vue create mode 100644 frontend/components/employees/RttTab.vue create mode 100644 frontend/composables/useEmployeeDetailPage.ts create mode 100644 frontend/utils/contract.ts diff --git a/doc/functional-rules.md b/doc/functional-rules.md index f407e7b..e3f5509 100644 --- a/doc/functional-rules.md +++ b/doc/functional-rules.md @@ -28,7 +28,8 @@ Ce document centralise les règles métier actuellement implémentées dans l'ap ### Règles de période - `CDI`: - - `endDate` doit être vide + - à la création d'une période: `endDate` doit être vide + - en clôture d'un contrat en cours: `endDate` peut être renseignée - `CDD` / `INTERIM`: - `endDate` obligatoire - `endDate` ne peut pas être antérieure à `startDate` @@ -134,6 +135,14 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer. - Détail employé: - onglet `Suivi contrat` avec affichage de l'historique des périodes de contrat - chaque ligne expose: nature (`CDI`/`CDD`/`INTERIM`), contrat/temps de travail, date de début, date de fin (ou "En cours") + - action `Clôturer`: + - bouton actif uniquement s'il existe un contrat en cours non déjà clôturé à la date du jour + - ouvre un drawer en lecture seule (type/temps de travail/date de début) + - seule la date de fin est saisissable (préremplie à aujourd'hui) + - backend: en mode clôture, seule `contractEndDate` est acceptée + - action `Ajouter`: + - conserve le flux d'ajout d'un nouveau contrat via drawer dédié + - disponible uniquement s'il n'y a pas de contrat en cours, ou si le contrat en cours a déjà une date de fin ## 10) Notifications diff --git a/frontend/components/SiteFilterSelector.vue b/frontend/components/SiteFilterSelector.vue index 9e50590..1fccddb 100644 --- a/frontend/components/SiteFilterSelector.vue +++ b/frontend/components/SiteFilterSelector.vue @@ -30,7 +30,6 @@ type="checkbox" class="h-4 w-4" /> - {{ site.name }} diff --git a/frontend/components/employees/ContractTab.vue b/frontend/components/employees/ContractTab.vue new file mode 100644 index 0000000..9caf238 --- /dev/null +++ b/frontend/components/employees/ContractTab.vue @@ -0,0 +1,222 @@ + + + diff --git a/frontend/components/employees/LeaveTab.vue b/frontend/components/employees/LeaveTab.vue new file mode 100644 index 0000000..248a6f9 --- /dev/null +++ b/frontend/components/employees/LeaveTab.vue @@ -0,0 +1,7 @@ + diff --git a/frontend/components/employees/RttTab.vue b/frontend/components/employees/RttTab.vue new file mode 100644 index 0000000..e117f17 --- /dev/null +++ b/frontend/components/employees/RttTab.vue @@ -0,0 +1,7 @@ + diff --git a/frontend/composables/useEmployeeDetailPage.ts b/frontend/composables/useEmployeeDetailPage.ts new file mode 100644 index 0000000..4d0f4cb --- /dev/null +++ b/frontend/composables/useEmployeeDetailPage.ts @@ -0,0 +1,296 @@ +import type { Contract } from '~/services/dto/contract' +import type { ContractHistoryItem, Employee } from '~/services/dto/employee' +import { CONTRACT_TYPES } from '~/services/dto/contract' +import { listContracts } from '~/services/contracts' +import { getEmployee, updateEmployee } from '~/services/employees' +import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date' +import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract' + +export const useEmployeeDetailPage = () => { + const route = useRoute() + const toast = useToast() + const employee = ref(null) + const isLoading = ref(false) + const activeTab = ref<'contract' | 'leave' | 'rtt'>('contract') + const contracts = ref([]) + const isContractDrawerOpen = ref(false) + const isContractSubmitting = ref(false) + const isCreateContractDrawerOpen = ref(false) + const isCreateContractSubmitting = ref(false) + + const contractForm = reactive({ + contractId: '' as number | '', + contractName: '', + weeklyHours: null as number | null, + contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM', + startDate: '', + endDate: '' + }) + + const validationTouched = reactive({ + endDate: false + }) + + const createContractForm = reactive({ + contractId: '' as number | '', + contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM', + startDate: '', + endDate: '' + }) + + const createValidationTouched = reactive({ + contractId: false, + contractNature: false, + startDate: false, + endDate: false + }) + + const contractHistory = computed(() => employee.value?.contractHistory ?? []) + const employeeContractWorkLabel = computed(() => { + const contract = employee.value?.contract + if (!contract) return '-' + if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait' + if (contract.weeklyHours !== null && contract.weeklyHours !== undefined) return `${contract.weeklyHours} heures` + return contract.name || '-' + }) + + const formatDate = (value?: string | null) => formatNullableYmdToFr(value) + + const contractHistoryLabel = (item: ContractHistoryItem) => { + if (item.weeklyHours !== null && item.weeklyHours !== undefined) { + return `${item.weeklyHours} heures` + } + return item.contractName ?? '-' + } + + const currentActiveContractPeriod = computed(() => { + const today = getTodayYmd() + const history = employee.value?.contractHistory ?? [] + return history.find((item) => item.startDate <= today && (!item.endDate || item.endDate >= today)) ?? null + }) + + const canCloseCurrentContract = computed(() => { + const active = currentActiveContractPeriod.value + if (!active) return false + if (!active.endDate) return true + return active.endDate > getTodayYmd() + }) + + const canCreateContract = computed(() => { + const active = currentActiveContractPeriod.value + if (!active) return true + return !!active.endDate + }) + + const isContractEndDateValid = computed(() => contractForm.endDate !== '') + const showContractEndDateError = computed(() => validationTouched.endDate && !isContractEndDateValid.value) + + const requiresCreateContractEndDate = computed(() => requiresContractEndDate(createContractForm.contractNature)) + const isCreateContractValid = computed(() => createContractForm.contractId !== '') + const isCreateContractNatureValid = computed(() => isContractNature(createContractForm.contractNature)) + const isCreateContractStartDateValid = computed(() => createContractForm.startDate !== '') + const isCreateContractEndDateValid = computed(() => !requiresCreateContractEndDate.value || createContractForm.endDate !== '') + const isCreateContractFormValid = computed(() => + isCreateContractValid.value && + isCreateContractNatureValid.value && + isCreateContractStartDateValid.value && + isCreateContractEndDateValid.value + ) + + const baseInputClass = + 'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20' + const readonlyFieldClass = `${baseInputClass} border-neutral-300 bg-neutral-100 text-neutral-700` + const contractEndDateFieldClass = computed(() => showContractEndDateError.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`) + const baseSelectClass = 'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900' + const createContractFieldClass = computed(() => createValidationTouched.contractId && !isCreateContractValid.value ? `${baseSelectClass} border-red-500` : `${baseSelectClass} border-neutral-300`) + const createContractNatureFieldClass = computed(() => createValidationTouched.contractNature && !isCreateContractNatureValid.value ? `${baseSelectClass} border-red-500` : `${baseSelectClass} border-neutral-300`) + const createContractStartDateFieldClass = computed(() => createValidationTouched.startDate && !isCreateContractStartDateValid.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`) + const createContractEndDateFieldClass = computed(() => createValidationTouched.endDate && !isCreateContractEndDateValid.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`) + const closeContractWorkedHoursLabel = computed(() => { + if (contractForm.weeklyHours !== null && contractForm.weeklyHours !== undefined) return `${contractForm.weeklyHours} heures` + return contractForm.contractName || '-' + }) + + const resetContractValidation = () => { + validationTouched.endDate = false + } + + const hydrateContractFormFromCurrent = () => { + const current = employee.value + const active = currentActiveContractPeriod.value + if (!current || !active) return + + contractForm.contractId = active.contractId ?? current.contract?.id ?? '' + contractForm.contractName = active.contractName ?? current.contract?.name ?? '' + contractForm.weeklyHours = active.weeklyHours ?? current.contract?.weeklyHours ?? null + contractForm.contractNature = active.contractNature + contractForm.startDate = active.startDate + contractForm.endDate = getTodayYmd() + } + + const openCloseContractDrawer = () => { + if (!employee.value || !canCloseCurrentContract.value) return + hydrateContractFormFromCurrent() + resetContractValidation() + isContractDrawerOpen.value = true + } + + const setContractDrawerOpen = (open: boolean) => { + isContractDrawerOpen.value = open + } + + const resetCreateValidation = () => { + createValidationTouched.contractId = false + createValidationTouched.contractNature = false + createValidationTouched.startDate = false + createValidationTouched.endDate = false + } + + const openCreateContractDrawer = () => { + if (!employee.value || !canCreateContract.value) return + createContractForm.contractId = '' + createContractForm.contractNature = 'CDI' + createContractForm.endDate = '' + createContractForm.startDate = currentActiveContractPeriod.value?.endDate + ? (shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate) + : getTodayYmd() + resetCreateValidation() + isCreateContractDrawerOpen.value = true + } + + const setCreateContractDrawerOpen = (open: boolean) => { + isCreateContractDrawerOpen.value = open + } + + const loadEmployee = async () => { + const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id + const employeeId = Number(idParam) + if (!Number.isInteger(employeeId) || employeeId <= 0) { + return + } + + isLoading.value = true + try { + employee.value = await getEmployee(employeeId) + } finally { + isLoading.value = false + } + } + + const submitContractUpdate = async () => { + if (!employee.value || isContractSubmitting.value || !currentActiveContractPeriod.value) return + + validationTouched.endDate = true + if (!isContractEndDateValid.value) return + + if (contractForm.endDate < currentActiveContractPeriod.value.startDate) { + toast.error({ + title: 'Erreur', + message: `La date de fin doit être postérieure au ${formatDate(currentActiveContractPeriod.value.startDate)}.` + }) + return + } + + isContractSubmitting.value = true + try { + await updateEmployee(employee.value.id, { + firstName: employee.value.firstName, + lastName: employee.value.lastName, + siteId: employee.value.site?.id ?? null, + contractId: Number(contractForm.contractId), + contractEndDate: contractForm.endDate || null + }) + + isContractDrawerOpen.value = false + await loadEmployee() + } finally { + isContractSubmitting.value = false + } + } + + const submitCreateContract = async () => { + if (!employee.value || isCreateContractSubmitting.value) return + + createValidationTouched.contractId = true + createValidationTouched.contractNature = true + createValidationTouched.startDate = true + createValidationTouched.endDate = true + if (!isCreateContractFormValid.value) return + + if (currentActiveContractPeriod.value?.endDate) { + const minStartDate = shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate + if (createContractForm.startDate < minStartDate) { + toast.error({ + title: 'Erreur', + message: `La date de début doit être au moins le ${formatDate(minStartDate)}.` + }) + return + } + } + + isCreateContractSubmitting.value = true + try { + await updateEmployee(employee.value.id, { + firstName: employee.value.firstName, + lastName: employee.value.lastName, + siteId: employee.value.site?.id ?? null, + contractId: Number(createContractForm.contractId), + contractNature: createContractForm.contractNature, + contractStartDate: createContractForm.startDate, + contractEndDate: createContractForm.endDate || null + }) + isCreateContractDrawerOpen.value = false + await loadEmployee() + } finally { + isCreateContractSubmitting.value = false + } + } + + watch(requiresCreateContractEndDate, (required) => { + if (!required) { + createContractForm.endDate = '' + } + }) + + onMounted(async () => { + contracts.value = await listContracts() + await loadEmployee() + }) + + return { + employee, + isLoading, + activeTab, + contracts, + contractHistory, + employeeContractWorkLabel, + contractForm, + createContractForm, + isContractDrawerOpen, + isContractSubmitting, + isCreateContractDrawerOpen, + isCreateContractSubmitting, + canCloseCurrentContract, + canCreateContract, + readonlyFieldClass, + closeContractWorkedHoursLabel, + contractEndDateFieldClass, + showContractEndDateError, + isContractEndDateValid, + createContractNatureFieldClass, + createContractFieldClass, + createContractStartDateFieldClass, + requiresCreateContractEndDate, + createContractEndDateFieldClass, + isCreateContractFormValid, + contractNatureLabel, + contractHistoryLabel, + formatDate, + openCloseContractDrawer, + openCreateContractDrawer, + setContractDrawerOpen, + setCreateContractDrawerOpen, + submitContractUpdate, + submitCreateContract + } +} diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue index fd8530c..49355df 100644 --- a/frontend/pages/employees/[id].vue +++ b/frontend/pages/employees/[id].vue @@ -14,7 +14,7 @@

{{ employee.firstName }} {{ employee.lastName }}

-

{{ contractNatureLabel(employee.currentContractNature) }} {{ employee.contract?.weeklyHours ?? '-' }} heures

+

{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}

{{ employee.site?.name ?? '-' }}

@@ -53,97 +53,86 @@ -
-
-
-

Contrat

-

Heures

-

Date de début

-

Date de fin

-
-
- Aucun historique de contrat. -
-
-
-

{{ contractNatureLabel(item.contractNature) }}

-

{{ contractHistoryLabel(item) }}

-

{{ formatDate(item.startDate) }}

-

{{ formatDate(item.endDate) }}

-
-
-
-
- - -
-
-
- -
-
- -
+ + + diff --git a/frontend/pages/employees/index.vue b/frontend/pages/employees/index.vue index 951cf65..887f790 100644 --- a/frontend/pages/employees/index.vue +++ b/frontend/pages/employees/index.vue @@ -154,10 +154,10 @@ La date de début est obligatoire.

-
+
(() => { }) }) -const contractNatureLabel = (value?: 'CDI' | 'CDD' | 'INTERIM') => { - if (value === 'CDD') return 'CDD' - if (value === 'INTERIM') return 'Intérim' - return 'CDI' -} - const form = reactive({ firstName: '', lastName: '', @@ -266,11 +261,11 @@ const isFirstNameValid = computed(() => form.firstName.trim() !== '') const isLastNameValid = computed(() => form.lastName.trim() !== '') const isSiteValid = computed(() => form.siteId !== '') const isContractValid = computed(() => form.contractId !== '') -const isContractNatureValid = computed(() => ['CDI', 'CDD', 'INTERIM'].includes(form.contractNature)) +const isContractNatureValid = computed(() => isContractNature(form.contractNature)) const isContractStartDateValid = computed(() => form.contractStartDate !== '') -const requiresContractEndDate = computed(() => form.contractNature === 'CDD' || form.contractNature === 'INTERIM') +const requiresContractEndDateComputed = computed(() => requiresContractEndDate(form.contractNature)) const isContractEndDateValid = computed(() => { - if (!requiresContractEndDate.value) return true + if (!requiresContractEndDateComputed.value) return true return form.contractEndDate !== '' }) const isFormValid = computed( @@ -433,7 +428,7 @@ const handleSubmit = async () => { contractId: Number(form.contractId), contractNature: form.contractNature, contractStartDate: form.contractStartDate, - contractEndDate: requiresContractEndDate.value ? form.contractEndDate : null + contractEndDate: requiresContractEndDateComputed.value ? form.contractEndDate : null }) } @@ -464,7 +459,7 @@ watch(isDrawerOpen, (isOpen) => { } }) -watch(requiresContractEndDate, (required) => { +watch(requiresContractEndDateComputed, (required) => { if (!required) { form.contractEndDate = '' } diff --git a/frontend/services/employees.ts b/frontend/services/employees.ts index 3b8c92f..c0eaad4 100644 --- a/frontend/services/employees.ts +++ b/frontend/services/employees.ts @@ -56,7 +56,7 @@ export const updateEmployee = async ( firstName: string lastName: string siteId?: number | null - contractId: number + contractId?: number contractNature?: 'CDI' | 'CDD' | 'INTERIM' contractStartDate?: string contractEndDate?: string | null @@ -64,16 +64,27 @@ export const updateEmployee = async ( } ) => { const api = useApi() - return api.patch(`/employees/${id}`, { + const body: Record = { firstName: payload.firstName, lastName: payload.lastName, site: payload.siteId ? `/api/sites/${payload.siteId}` : null, - contract: `/api/contracts/${payload.contractId}`, - contractNature: payload.contractNature, - contractStartDate: payload.contractStartDate, - contractEndDate: payload.contractEndDate ?? null, displayOrder: payload.displayOrder - }, { + } + + if (payload.contractId !== undefined) { + body.contract = `/api/contracts/${payload.contractId}` + } + if (payload.contractNature !== undefined) { + body.contractNature = payload.contractNature + } + if (payload.contractStartDate !== undefined) { + body.contractStartDate = payload.contractStartDate + } + if (payload.contractEndDate !== undefined) { + body.contractEndDate = payload.contractEndDate ?? null + } + + return api.patch(`/employees/${id}`, body, { toastSuccessKey: 'success.employee.update', toastErrorKey: 'errors.employee.update' }) diff --git a/frontend/utils/contract.ts b/frontend/utils/contract.ts new file mode 100644 index 0000000..6b8a3a2 --- /dev/null +++ b/frontend/utils/contract.ts @@ -0,0 +1,17 @@ +export const CONTRACT_NATURES = ['CDI', 'CDD', 'INTERIM'] as const + +export type ContractNature = (typeof CONTRACT_NATURES)[number] + +export const contractNatureLabel = (value?: ContractNature) => { + if (value === 'CDD') return 'CDD' + if (value === 'INTERIM') return 'Intérim' + return 'CDI' +} + +export const requiresContractEndDate = (nature: ContractNature) => { + return nature === 'CDD' || nature === 'INTERIM' +} + +export const isContractNature = (value: string): value is ContractNature => { + return (CONTRACT_NATURES as readonly string[]).includes(value) +} diff --git a/frontend/utils/date.ts b/frontend/utils/date.ts index 2acd56f..d165856 100644 --- a/frontend/utils/date.ts +++ b/frontend/utils/date.ts @@ -6,6 +6,17 @@ export const toYmd = (year: number, month: number, day: number) => { export const normalizeDate = (value: string) => value.slice(0, 10) +export const formatYmdToFr = (value: string) => { + const [year, month, day] = value.split('-') + if (!year || !month || !day) return value + return `${day}/${month}/${year}` +} + +export const formatNullableYmdToFr = (value?: string | null, fallback = 'En cours') => { + if (!value) return fallback + return formatYmdToFr(value) +} + export const parseYmd = (value: string) => { const [year, month, day] = value.split('-').map(Number) if (!year || !month || !day) return null diff --git a/src/State/EmployeeWriteProcessor.php b/src/State/EmployeeWriteProcessor.php index 1d27a18..998560f 100644 --- a/src/State/EmployeeWriteProcessor.php +++ b/src/State/EmployeeWriteProcessor.php @@ -70,26 +70,53 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface return $result; } - $startDate = $requestedStartDate ?? $today; - $todayPeriod = $this->periodRepository->findOneCoveringDate($data, $today); - $nature = $requestedContractNature ?? $todayPeriod?->getContractNatureEnum() ?? ContractNature::CDI; - $endDate = $requestedEndDate; - $this->assertPeriodDates($startDate, $endDate, $nature); + $todayPeriod = $this->periodRepository->findOneCoveringDate($data, $today); + $currentPeriodContract = $todayPeriod?->getContract(); + $contractChanged = $currentPeriodContract instanceof Contract + ? $currentPeriodContract->getId() !== $currentContract->getId() + : true; + $isCloseOnlyRequest = !$contractChanged + && null === $requestedStartDate + && null === $requestedContractNature + && null !== $requestedEndDate; - if ( - null !== $todayPeriod - && null === $todayPeriod->getEndDate() - && $todayPeriod->getStartDate()->format('Y-m-d') === $startDate->format('Y-m-d') - ) { - $todayPeriod->setContract($currentContract); - $todayPeriod->setContractNature($nature); - $todayPeriod->setEndDate($endDate); + if ($isCloseOnlyRequest) { + if (null === $todayPeriod) { + throw new UnprocessableEntityHttpException('No active contract period to close.'); + } + + $currentNature = $todayPeriod->getContractNatureEnum(); + $this->assertPeriodDates($todayPeriod->getStartDate(), $requestedEndDate, $currentNature, true); + + $currentEndDate = $todayPeriod->getEndDate(); + if (null !== $currentEndDate && $requestedEndDate > $currentEndDate) { + throw new UnprocessableEntityHttpException('contractEndDate cannot be increased on current contract.'); + } + + $todayPeriod->setEndDate($requestedEndDate); $this->entityManager->flush(); return $result; } - $this->periodRepository->closeOpenPeriods($data, $startDate->modify('-1 day')); + $startDate = $requestedStartDate ?? $today; + $nature = $requestedContractNature ?? $todayPeriod?->getContractNatureEnum() ?? ContractNature::CDI; + $endDate = $requestedEndDate; + $this->assertPeriodDates($startDate, $endDate, $nature); + + if (null !== $todayPeriod) { + $currentEndDate = $todayPeriod->getEndDate(); + if (null === $currentEndDate) { + if ($startDate <= $todayPeriod->getStartDate()) { + throw new UnprocessableEntityHttpException('contractStartDate must be after current contract start date.'); + } + + $todayPeriod->setEndDate($startDate->modify('-1 day')); + } elseif ($startDate <= $currentEndDate) { + throw new UnprocessableEntityHttpException('contractStartDate must be after current contract end date.'); + } + } + $this->createPeriod($data, $currentContract, $startDate, $endDate, $nature); $this->entityManager->flush(); @@ -179,7 +206,8 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface private function assertPeriodDates( DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, - ContractNature $nature + ContractNature $nature, + bool $allowCdiEndDate = false ): void { if (null !== $endDate && $endDate < $startDate) { throw new UnprocessableEntityHttpException('contractEndDate cannot be before contractStartDate.'); @@ -189,7 +217,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface throw new UnprocessableEntityHttpException('contractEndDate is required for CDD and INTERIM.'); } - if (ContractNature::CDI === $nature && null !== $endDate) { + if (!$allowCdiEndDate && ContractNature::CDI === $nature && null !== $endDate) { throw new UnprocessableEntityHttpException('contractEndDate must be empty for CDI.'); } }