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 @@
+
+
+
+
+
Contrat
+
Heures
+
Date de début
+
Date de fin
+
+
+ Aucun historique de contrat.
+
+
+
+
{{ contractNatureLabel(item.contractNature) }}
+
{{ contractHistoryLabel(item) }}
+
{{ formatDate(item.startDate) }}
+
{{ formatDate(item.endDate) }}
+
+
+
+
+
+
+ Clôturer
+
+
+
+ Ajouter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ Bloc Congé (à implémenter)
+
+
+
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 @@
+
+
+
+ Bloc RTT (à implémenter)
+
+
+
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) }}
-
-
-
-
- Modifier
-
-
- Ajouter
-
-
-
-
-
+
+
+
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.
-
+
Fin contrat
- *
+ *
(() => {
})
})
-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.');
}
}