fix(calendar) : date-only contract filter + eager-load periods for print
Review follow-ups: (1) createFromFormat('Y-m-d') keeps the current time, so a raw
DateTime comparison wrongly excluded an employee ending on the from-day (and dropped
first-day absences); normalize from/to to day bounds and compare contract periods on
date only (Y-m-d), mirroring the calendar view. (2) eager-load contractPeriods in
findForPrintBySiteIds to avoid an N+1 during filtering. Added a boundary test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,10 @@ final class EmployeeRepository extends ServiceEntityRepository implements Employ
|
|||||||
->addSelect('s')
|
->addSelect('s')
|
||||||
->leftJoin('e.contract', 'c')
|
->leftJoin('e.contract', 'c')
|
||||||
->addSelect('c')
|
->addSelect('c')
|
||||||
|
// Eager-load des périodes pour le filtre d'intersection contrat/période (impression),
|
||||||
|
// évite un N+1 sur getContractPeriods() lors du filtrage des employés.
|
||||||
|
->leftJoin('e.contractPeriods', 'cp')
|
||||||
|
->addSelect('cp')
|
||||||
->orderBy('s.name', 'ASC')
|
->orderBy('s.name', 'ASC')
|
||||||
->addOrderBy('e.displayOrder', 'ASC')
|
->addOrderBy('e.displayOrder', 'ASC')
|
||||||
->addOrderBy('e.lastName', 'ASC')
|
->addOrderBy('e.lastName', 'ASC')
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ class AbsencePrintProvider implements ProviderInterface
|
|||||||
if (!$fromDate instanceof DateTimeImmutable || !$toDate instanceof DateTimeImmutable) {
|
if (!$fromDate instanceof DateTimeImmutable || !$toDate instanceof DateTimeImmutable) {
|
||||||
return new Response('Invalid from/to date format.', Response::HTTP_BAD_REQUEST);
|
return new Response('Invalid from/to date format.', Response::HTTP_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
// createFromFormat('Y-m-d', ...) garde l'heure courante : on borne explicitement aux
|
||||||
|
// extrémités de journée, sinon les comparaisons de dates (présence d'un contrat/absence
|
||||||
|
// le jour de `from`) échouent contre les dates BDD à minuit.
|
||||||
|
$fromDate = $fromDate->setTime(0, 0, 0);
|
||||||
|
$toDate = $toDate->setTime(23, 59, 59);
|
||||||
|
|
||||||
$siteIds = $this->parseIds($request->query->get('sites'));
|
$siteIds = $this->parseIds($request->query->get('sites'));
|
||||||
$workContractIds = $this->parseIds($request->query->get('workContracts'));
|
$workContractIds = $this->parseIds($request->query->get('workContracts'));
|
||||||
@@ -141,14 +146,18 @@ class AbsencePrintProvider implements ProviderInterface
|
|||||||
/**
|
/**
|
||||||
* Vrai si au moins une période de contrat de l'employé intersecte [$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.
|
* 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.
|
* Comparaison sur la date seule (`Y-m-d`), insensible à l'heure des bornes — aligné avec
|
||||||
|
* le filtre `hasContractInSelectedMonth` de la vue Calendrier (comparaison de chaînes).
|
||||||
*/
|
*/
|
||||||
private function hasContractInRange(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool
|
private function hasContractInRange(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool
|
||||||
{
|
{
|
||||||
|
$fromDay = $from->format('Y-m-d');
|
||||||
|
$toDay = $to->format('Y-m-d');
|
||||||
|
|
||||||
foreach ($employee->getContractPeriods() as $period) {
|
foreach ($employee->getContractPeriods() as $period) {
|
||||||
$start = $period->getStartDate();
|
$start = $period->getStartDate()->format('Y-m-d');
|
||||||
$end = $period->getEndDate();
|
$end = $period->getEndDate()?->format('Y-m-d');
|
||||||
if ($start <= $to && (null === $end || $end >= $from)) {
|
if ($start <= $toDay && (null === $end || $end >= $fromDay)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,21 @@ final class AbsencePrintProviderTest extends TestCase
|
|||||||
self::assertFalse($this->hasInRange($provider, $employee, '2026-05-01', '2026-05-31'));
|
self::assertFalse($this->hasInRange($provider, $employee, '2026-05-01', '2026-05-31'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHasContractInRangeIncludesEmployeeEndingOnFromDayDespiteTimeComponent(): void
|
||||||
|
{
|
||||||
|
// Garde-fou : `from` portant une heure (cf. createFromFormat) ne doit pas exclure
|
||||||
|
// un employé dont le contrat finit pile le jour de `from` (comparaison date seule).
|
||||||
|
$provider = new ReflectionClass(AbsencePrintProvider::class)->newInstanceWithoutConstructor();
|
||||||
|
$employee = $this->buildEmployeeWithPeriod('2025-01-01', '2026-05-01');
|
||||||
|
|
||||||
|
$result = new ReflectionClass($provider::class)
|
||||||
|
->getMethod('hasContractInRange')
|
||||||
|
->invoke($provider, $employee, new DateTimeImmutable('2026-05-01 08:33:53'), new DateTimeImmutable('2026-05-31 23:59:59'))
|
||||||
|
;
|
||||||
|
|
||||||
|
self::assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
private function hasInRange(object $provider, Employee $employee, string $from, string $to): bool
|
private function hasInRange(object $provider, Employee $employee, string $from, string $to): bool
|
||||||
{
|
{
|
||||||
return new ReflectionClass($provider::class)
|
return new ReflectionClass($provider::class)
|
||||||
|
|||||||
Reference in New Issue
Block a user