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.
*