diff --git a/frontend/pages/leave-recap.vue b/frontend/pages/leave-recap.vue index bbaae3a..83a3f3d 100644 --- a/frontend/pages/leave-recap.vue +++ b/frontend/pages/leave-recap.vue @@ -34,8 +34,8 @@ Prénom Contrat CP N-1 restant - CP N Samedis + CP N RTT
@@ -58,8 +58,8 @@ {{ row.firstName }} {{ row.contractName ?? '-' }} {{ formatNumber(row.cpN1Remaining) }} - {{ row.cpN }} {{ row.acquiredSaturdays }} + {{ row.cpN }} {{ row.rtt }}
diff --git a/src/ApiResource/EmployeeLeaveRecap.php b/src/ApiResource/EmployeeLeaveRecap.php index c930687..5ede558 100644 --- a/src/ApiResource/EmployeeLeaveRecap.php +++ b/src/ApiResource/EmployeeLeaveRecap.php @@ -27,6 +27,7 @@ final class EmployeeLeaveRecap public ?string $siteName = null; public ?string $siteColor = null; public ?string $contractName = null; + public int $contractSortKey = 99; public float $cpN1Remaining = 0.0; public string $cpN = '-'; public string $acquiredSaturdays = '-'; diff --git a/src/Service/Leave/LeaveBalanceComputationService.php b/src/Service/Leave/LeaveBalanceComputationService.php index 58a1f3e..f716b7d 100644 --- a/src/Service/Leave/LeaveBalanceComputationService.php +++ b/src/Service/Leave/LeaveBalanceComputationService.php @@ -71,7 +71,10 @@ final readonly class LeaveBalanceComputationService $fractionedDays = $this->resolveFractionedDays($employee, $ruleCode, $year); if (LeaveRuleCode::FORFAIT_218 === $ruleCode) { - $totalBusinessDays = $this->countBusinessDays($from, $to); + // Business days for forfait must use the RAW holiday list (excluded holidays + // like "Lundi de Pentecôte" / journée de solidarité still count as non-working + // days for the 218-day legal target). + $totalBusinessDays = $this->countBusinessDaysInRange($from, $to, $this->buildRawPublicHolidayMap($from, $to)); $baseAcquiredDays = (float) max(0, $totalBusinessDays - self::FORFAIT_TARGET_WORKED_DAYS); $acquiredDays = $carryDays + $baseAcquiredDays + $fractionedDays; $absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to); @@ -406,6 +409,29 @@ final readonly class LeaveBalanceComputationService return $map; } + /** + * @return array + */ + private function buildRawPublicHolidayMap(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->getRawHolidaysDayByYears('metropole', (string) $year); + foreach ($holidays as $date => $label) { + $map[(string) $date] = (string) $label; + } + } + } catch (Throwable) { + return []; + } + + return $map; + } + /** * @param list $absences * diff --git a/src/Service/PublicHolidayService.php b/src/Service/PublicHolidayService.php index e942ae0..6335398 100644 --- a/src/Service/PublicHolidayService.php +++ b/src/Service/PublicHolidayService.php @@ -78,12 +78,25 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa * @throws ClientExceptionInterface */ public function getHolidaysDayByYears(string $zone, string $years): array + { + return $this->applyExclusions($this->fetchHolidaysByYears($zone, $years)); + } + + public function getRawHolidaysDayByYears(string $zone, string $years): array + { + return $this->fetchHolidaysByYears($zone, $years); + } + + /** + * @return array + */ + private function fetchHolidaysByYears(string $zone, string $years): array { $zone = strtolower(trim($zone)); $years = trim($years); $key = "public_holidays_{$zone}_{$years}"; - $holidays = $this->cache->get($key, function (ItemInterface $item) use ($zone, $years): array { + return $this->cache->get($key, function (ItemInterface $item) use ($zone, $years): array { $item->expiresAfter(30 * 86400); $url = $this->holidayUrl."{$zone}/{$years}.json"; @@ -101,8 +114,6 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa return json_decode($response->getContent(), true); }); - - return $this->applyExclusions($holidays); } /** diff --git a/src/Service/PublicHolidayServiceInterface.php b/src/Service/PublicHolidayServiceInterface.php index 2045b64..f7a8c3f 100644 --- a/src/Service/PublicHolidayServiceInterface.php +++ b/src/Service/PublicHolidayServiceInterface.php @@ -9,4 +9,11 @@ interface PublicHolidayServiceInterface public function getHolidaysDay(string $zone): array; public function getHolidaysDayByYears(string $zone, string $years): array; + + /** + * Same as getHolidaysDayByYears but WITHOUT the configured exclusions applied. + * Used for legal/contractual computations (e.g. forfait 218 days) where excluded + * holidays (journée de solidarité) must still count as non-working days. + */ + public function getRawHolidaysDayByYears(string $zone, string $years): array; } diff --git a/src/State/EmployeeLeaveRecapProvider.php b/src/State/EmployeeLeaveRecapProvider.php index b89d6de..897ea9d 100644 --- a/src/State/EmployeeLeaveRecapProvider.php +++ b/src/State/EmployeeLeaveRecapProvider.php @@ -7,8 +7,10 @@ namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\EmployeeLeaveRecap; +use App\Entity\Contract; use App\Entity\Employee; use App\Entity\User; +use App\Enum\ContractType; use App\Repository\EmployeeRepository; use App\Security\EmployeeScopeService; use App\Service\Leave\LeaveRecapRowBuilder; @@ -63,6 +65,7 @@ final readonly class EmployeeLeaveRecapProvider implements ProviderInterface $resource->siteName = $site?->getName(); $resource->siteColor = $site?->getColor(); $resource->contractName = $row['contractName'] ?? null; + $resource->contractSortKey = $this->resolveContractSortKey($employee->getContract()); $resource->cpN1Remaining = is_numeric($row['cpN1Remaining']) ? (float) $row['cpN1Remaining'] : 0.0; $resource->cpN = (string) $row['cpN']; $resource->acquiredSaturdays = (string) $row['acquiredSaturdays']; @@ -78,6 +81,10 @@ final readonly class EmployeeLeaveRecapProvider implements ProviderInterface if (0 !== $siteCmp) { return $siteCmp; } + $contractCmp = $a->contractSortKey <=> $b->contractSortKey; + if (0 !== $contractCmp) { + return $contractCmp; + } $lastCmp = strcmp($a->lastName, $b->lastName); if (0 !== $lastCmp) { return $lastCmp; @@ -89,6 +96,30 @@ final readonly class EmployeeLeaveRecapProvider implements ProviderInterface return $rows; } + /** + * Sort order: FORFAIT → 39h → 35h → 25h → 4h → autres. + */ + private function resolveContractSortKey(?Contract $contract): int + { + if (null === $contract) { + return 99; + } + + if (ContractType::FORFAIT === $contract->getType()) { + return 0; + } + + $weeklyHours = $contract->getWeeklyHours(); + + return match ($weeklyHours) { + 39 => 1, + 35 => 2, + 25 => 3, + 4 => 4, + default => 99, + }; + } + /** * @return list */ diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php index bd844b3..e1b7c28 100644 --- a/src/State/EmployeeLeaveSummaryProvider.php +++ b/src/State/EmployeeLeaveSummaryProvider.php @@ -561,7 +561,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface { $type = $employee->getContract()?->getType(); if (ContractType::FORFAIT === $type) { - $businessDaysInPeriod = $this->countBusinessDays($from, $to); + // Business days for forfait must use the RAW holiday list (excluded holidays like + // "Lundi de Pentecôte" / journée de solidarité still count as non-working days for + // the 218-day legal target). + $businessDaysInPeriod = $this->countBusinessDays($from, $to, $this->buildRawPublicHolidayMap($from, $to)); $publicHolidays = $this->buildPublicHolidayMap($from, $to); $weekdayHolidays = array_filter( array_keys($publicHolidays), @@ -655,6 +658,29 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface return $map; } + /** + * @return array + */ + private function buildRawPublicHolidayMap(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->getRawHolidaysDayByYears('metropole', (string) $year); + foreach ($holidays as $date => $label) { + $map[(string) $date] = (string) $label; + } + } + } catch (Throwable) { + return []; + } + + return $map; + } + /** * Presence days = business days (Mon-Fri) - public holidays + weekend worked days - absence days. *