From ddd1b8116e4a9898f9d411dec64874713c391741 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 21 May 2026 08:26:20 +0200 Subject: [PATCH] fix(calendar) : exclude departed employees from absence PDF print The calendar view hides employees whose contract doesn't intersect the displayed month, but the absence PDF print still listed them. Apply the same intersection filter (hasContractInRange over [from, to]) in AbsencePrintProvider, and reject invalid from/to dates. A employee who left in April no longer appears on a May print. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/State/AbsencePrintProvider.php | 32 +++++++-- tests/State/AbsencePrintProviderTest.php | 84 ++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 tests/State/AbsencePrintProviderTest.php diff --git a/src/State/AbsencePrintProvider.php b/src/State/AbsencePrintProvider.php index 3c81c68..a969910 100644 --- a/src/State/AbsencePrintProvider.php +++ b/src/State/AbsencePrintProvider.php @@ -6,6 +6,7 @@ namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; +use App\Entity\Employee; use App\Entity\Formation; use App\Enum\ContractNature; use App\Enum\HalfDay; @@ -56,12 +57,15 @@ class AbsencePrintProvider implements ProviderInterface $fromDate = DateTimeImmutable::createFromFormat('Y-m-d', $from); $toDate = DateTimeImmutable::createFromFormat('Y-m-d', $to); + if (!$fromDate instanceof DateTimeImmutable || !$toDate instanceof DateTimeImmutable) { + return new Response('Invalid from/to date format.', Response::HTTP_BAD_REQUEST); + } $siteIds = $this->parseIds($request->query->get('sites')); $workContractIds = $this->parseIds($request->query->get('workContracts')); $contractNatures = $this->parseContractNatures($request->query->get('contractNatures')); - $employees = $this->loadEmployees($siteIds, $contractNatures, $workContractIds); + $employees = $this->loadEmployees($siteIds, $contractNatures, $workContractIds, $fromDate, $toDate); $absences = $this->loadAbsences($fromDate, $toDate, $employees); $formations = $this->formationRepository->findByDateRangeAndEmployees($fromDate, $toDate, $employees); @@ -117,21 +121,41 @@ class AbsencePrintProvider implements ProviderInterface return array_values(array_unique($ids)); } - private function loadEmployees(array $siteIds, array $contractNatures, array $workContractIds): array + private function loadEmployees(array $siteIds, array $contractNatures, array $workContractIds, DateTimeImmutable $from, DateTimeImmutable $to): array { $employees = $this->employeeRepository->findForPrintBySiteIds($siteIds); - return array_values(array_filter($employees, static function ($employee) use ($contractNatures, $workContractIds): bool { + return array_values(array_filter($employees, function ($employee) use ($contractNatures, $workContractIds, $from, $to): bool { $employeeNature = (string) $employee->getCurrentContractNature(); $employeeContractId = $employee->getContract()?->getId(); $natureMatches = [] === $contractNatures || in_array($employeeNature, $contractNatures, true); $contractMatches = [] === $workContractIds || (null !== $employeeContractId && in_array($employeeContractId, $workContractIds, true)); - return $natureMatches && $contractMatches; + // Exclut les employés dont aucune période de contrat n'intersecte la période imprimée + // (ex. un salarié parti en avril ne doit pas apparaître sur une impression de mai). + return $natureMatches && $contractMatches && $this->hasContractInRange($employee, $from, $to); })); } + /** + * Vrai si au moins une période de contrat de l'employé intersecte [$from, $to]. + * Une période sans date de fin (contrat en cours) est considérée ouverte jusqu'à l'infini. + * Aligné avec le filtre `hasContractInSelectedMonth` de la vue Calendrier. + */ + private function hasContractInRange(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool + { + foreach ($employee->getContractPeriods() as $period) { + $start = $period->getStartDate(); + $end = $period->getEndDate(); + if ($start <= $to && (null === $end || $end >= $from)) { + return true; + } + } + + return false; + } + private function loadAbsences(DateTimeImmutable $from, DateTimeImmutable $to, array $employees): array { return $this->absenceRepository->findForPrint($from, $to, $employees); diff --git a/tests/State/AbsencePrintProviderTest.php b/tests/State/AbsencePrintProviderTest.php new file mode 100644 index 0000000..2665497 --- /dev/null +++ b/tests/State/AbsencePrintProviderTest.php @@ -0,0 +1,84 @@ +newInstanceWithoutConstructor(); + $employee = $this->buildEmployeeWithPeriod('2025-01-01', '2026-04-30'); + + // Imprime mai : l'employé parti le 30/04 ne doit pas être inclus. + self::assertFalse($this->hasInRange($provider, $employee, '2026-05-01', '2026-05-31')); + } + + public function testHasContractInRangeTrueWhenContractOverlapsRange(): void + { + $provider = new ReflectionClass(AbsencePrintProvider::class)->newInstanceWithoutConstructor(); + $employee = $this->buildEmployeeWithPeriod('2025-01-01', '2026-05-15'); + + // Contrat finissant le 15/05 → chevauche le mois de mai → inclus. + self::assertTrue($this->hasInRange($provider, $employee, '2026-05-01', '2026-05-31')); + } + + public function testHasContractInRangeTrueForOpenEndedContract(): void + { + $provider = new ReflectionClass(AbsencePrintProvider::class)->newInstanceWithoutConstructor(); + $employee = $this->buildEmployeeWithPeriod('2020-01-01', null); + + self::assertTrue($this->hasInRange($provider, $employee, '2026-05-01', '2026-05-31')); + } + + public function testHasContractInRangeFalseWhenContractStartsAfterRange(): void + { + $provider = new ReflectionClass(AbsencePrintProvider::class)->newInstanceWithoutConstructor(); + $employee = $this->buildEmployeeWithPeriod('2026-06-01', null); + + self::assertFalse($this->hasInRange($provider, $employee, '2026-05-01', '2026-05-31')); + } + + public function testHasContractInRangeFalseWhenNoPeriods(): void + { + $provider = new ReflectionClass(AbsencePrintProvider::class)->newInstanceWithoutConstructor(); + $employee = new Employee(); + + self::assertFalse($this->hasInRange($provider, $employee, '2026-05-01', '2026-05-31')); + } + + private function hasInRange(object $provider, Employee $employee, string $from, string $to): bool + { + return new ReflectionClass($provider::class) + ->getMethod('hasContractInRange') + ->invoke($provider, $employee, new DateTimeImmutable($from), new DateTimeImmutable($to)) + ; + } + + private function buildEmployeeWithPeriod(string $start, ?string $end): Employee + { + $employee = new Employee(); + $period = new EmployeeContractPeriod(); + $period->setEmployee($employee); + $period->setStartDate(new DateTimeImmutable($start)); + $period->setEndDate(null !== $end ? new DateTimeImmutable($end) : null); + $employee->getContractPeriods()->add($period); + + return $employee; + } +}