getEmployee(); if (null === $employee) { return $data; } $user = $this->security->getUser(); $isAdmin = $user instanceof User && in_array('ROLE_ADMIN', $user->getRoles(), true); if ($operation instanceof DeleteOperationInterface) { if ($this->isLockedByValidation($employee, $data->getStartDate(), $data->getEndDate(), $isAdmin)) { throw new ConflictHttpException('Impossible de modifier une absence sur une période validée.'); } $typeName = $data->getType()?->getLabel() ?? 'inconnu'; $startDate = $data->getStartDate()->format('d/m/Y'); $endDate = $data->getEndDate()->format('d/m/Y'); $empName = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? '')); $this->auditLogger->log( $employee, 'delete', 'absence', $data->getId(), sprintf('Absence %s supprimée pour %s du %s au %s', $typeName, $empName, $startDate, $endDate), ['old' => ['type' => $typeName, 'start' => $startDate, 'end' => $endDate, 'startHalf' => $data->getStartHalf()->value, 'endHalf' => $data->getEndHalf()->value, 'comment' => $data->getComment()]], DateTimeImmutable::createFromInterface($data->getStartDate()), ); $this->entityManager->remove($data); $this->entityManager->flush(); return null; } $segments = $this->expandAbsenceRange($data); if ([] === $segments) { throw new UnprocessableEntityHttpException('La période de l\'absence est invalide.'); } $from = DateTimeImmutable::createFromInterface($segments[0]['date']); $to = DateTimeImmutable::createFromInterface($segments[count($segments) - 1]['date']); if ($this->isLockedByValidation($employee, $from, $to, $isAdmin)) { throw new ConflictHttpException('Impossible de modifier une absence sur une période validée.'); } $existing = $this->absenceRepository->findByEmployeeAndDateRange($employee, $from, $to); foreach ($existing as $existingAbsence) { if ($existingAbsence->getId() === $data->getId()) { continue; } throw new ConflictHttpException('Cette période chevauche déjà une absence existante.'); } $first = array_shift($segments); if (null === $first) { throw new UnprocessableEntityHttpException('La période de l\'absence est invalide.'); } $data ->setStartDate($this->toMutableDate($first['date'])) ->setEndDate($this->toMutableDate($first['date'])) ->setStartHalf($first['startHalf']) ->setEndHalf($first['endHalf']) ; $this->clearWorkHoursForSegment($employee, $first); $this->entityManager->persist($data); foreach ($segments as $segment) { $absence = new Absence() ->setEmployee($employee) ->setType($data->getType()) ->setComment($data->getComment()) ->setStartDate($this->toMutableDate($segment['date'])) ->setEndDate($this->toMutableDate($segment['date'])) ->setStartHalf($segment['startHalf']) ->setEndHalf($segment['endHalf']) ; $this->clearWorkHoursForSegment($employee, $segment); $this->entityManager->persist($absence); } $typeName = $data->getType()?->getLabel() ?? 'inconnu'; $startDate = $data->getStartDate()->format('d/m/Y'); $endDate = $data->getEndDate()->format('d/m/Y'); $empName = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? '')); $this->auditLogger->log( $employee, 'create', 'absence', null, sprintf('Absence %s créée pour %s du %s au %s', $typeName, $empName, $startDate, $endDate), ['new' => ['type' => $typeName, 'start' => $startDate, 'end' => $endDate, 'startHalf' => $data->getStartHalf()->value, 'endHalf' => $data->getEndHalf()->value, 'comment' => $data->getComment()]], DateTimeImmutable::createFromInterface($data->getStartDate()), ); $this->entityManager->flush(); return $data; } /** * @return list */ private function expandAbsenceRange(Absence $absence): array { $start = DateTimeImmutable::createFromInterface($absence->getStartDate()); $end = DateTimeImmutable::createFromInterface($absence->getEndDate()); if ($start > $end) { throw new UnprocessableEntityHttpException('La date de fin ne peut pas être avant la date de début.'); } if ( $start->format('Y-m-d') === $end->format('Y-m-d') && HalfDay::PM === $absence->getStartHalf() && HalfDay::AM === $absence->getEndHalf() ) { throw new UnprocessableEntityHttpException('La demi-journée de fin ne peut pas être avant la demi-journée de début.'); } $days = new DatePeriod($start, new DateInterval('P1D'), $end->modify('+1 day')); $publicHolidays = $this->buildPublicHolidayMap($start, $end); $segments = []; foreach ($days as $day) { if (isset($publicHolidays[$day->format('Y-m-d')])) { continue; } $isFirst = $day->format('Y-m-d') === $start->format('Y-m-d'); $isLast = $day->format('Y-m-d') === $end->format('Y-m-d'); $isSame = $isFirst && $isLast; if ($isSame) { $segments[] = [ 'date' => $day, 'startHalf' => $absence->getStartHalf(), 'endHalf' => $absence->getEndHalf(), ]; continue; } if ($isFirst && HalfDay::PM === $absence->getStartHalf()) { $segments[] = [ 'date' => $day, 'startHalf' => HalfDay::PM, 'endHalf' => HalfDay::PM, ]; continue; } if ($isLast && HalfDay::AM === $absence->getEndHalf()) { $segments[] = [ 'date' => $day, 'startHalf' => HalfDay::AM, 'endHalf' => HalfDay::AM, ]; continue; } $segments[] = [ 'date' => $day, 'startHalf' => HalfDay::AM, 'endHalf' => HalfDay::PM, ]; } return $segments; } private function toMutableDate(DateTimeImmutable $date): DateTime { return DateTime::createFromImmutable($date); } private function isLockedByValidation(Employee $employee, DateTimeInterface $from, DateTimeInterface $to, bool $isAdmin): bool { if ($this->workHourRepository->hasValidatedInRange($employee, $from, $to)) { return true; } if ($isAdmin) { return false; } return $this->workHourRepository->hasSiteValidatedInRange($employee, $from, $to); } /** * @param array{date: DateTimeImmutable, startHalf: HalfDay, endHalf: HalfDay} $segment */ private function clearWorkHoursForSegment(Employee $employee, array $segment): void { $workHour = $this->workHourRepository->findOneByEmployeeAndDate($employee, $segment['date']); if (null === $workHour) { return; } // Demi-journée matin: on efface uniquement la plage du matin. if (HalfDay::AM === $segment['startHalf'] && HalfDay::AM === $segment['endHalf']) { $workHour ->setMorningFrom(null) ->setMorningTo(null) ->setIsSiteValid(false) ->setIsValid(false) ; return; } // Demi-journée après-midi: on efface après-midi + soirée. if (HalfDay::PM === $segment['startHalf'] && HalfDay::PM === $segment['endHalf']) { $workHour ->setAfternoonFrom(null) ->setAfternoonTo(null) ->setEveningFrom(null) ->setEveningTo(null) ->setIsSiteValid(false) ->setIsValid(false) ; return; } // Journée complète: on efface toutes les plages horaires. $workHour ->setMorningFrom(null) ->setMorningTo(null) ->setAfternoonFrom(null) ->setAfternoonTo(null) ->setEveningFrom(null) ->setEveningTo(null) ->setIsSiteValid(false) ->setIsValid(false) ; } /** * @return array */ private function buildPublicHolidayMap(DateTimeImmutable $from, DateTimeImmutable $to): array { $map = []; $startYear = (int) $from->format('Y'); $endYear = (int) $to->format('Y'); try { for ($year = $startYear; $year <= $endYear; ++$year) { $holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year); foreach ($holidays as $date => $label) { $map[(string) $date] = (string) $label; } } } catch (Throwable) { return []; } return $map; } }