fix : écran du récap. congés ordre d'affichage + Calcule des jours ouvrés pour les FORFAIT

This commit is contained in:
2026-04-14 15:54:57 +02:00
parent 0897154460
commit c2eaa06aff
7 changed files with 109 additions and 7 deletions

View File

@@ -34,8 +34,8 @@
<span class="text-left">Prénom</span> <span class="text-left">Prénom</span>
<span class="text-left">Contrat</span> <span class="text-left">Contrat</span>
<span class="text-right">CP N-1 restant</span> <span class="text-right">CP N-1 restant</span>
<span class="text-right">CP N</span>
<span class="text-right">Samedis</span> <span class="text-right">Samedis</span>
<span class="text-right">CP N</span>
<span class="text-right">RTT</span> <span class="text-right">RTT</span>
</div> </div>
<div class="border-x border-b border-primary-500 rounded-b-md"> <div class="border-x border-b border-primary-500 rounded-b-md">
@@ -58,8 +58,8 @@
<span class="truncate">{{ row.firstName }}</span> <span class="truncate">{{ row.firstName }}</span>
<span class="truncate">{{ row.contractName ?? '-' }}</span> <span class="truncate">{{ row.contractName ?? '-' }}</span>
<span class="text-right tabular-nums">{{ formatNumber(row.cpN1Remaining) }}</span> <span class="text-right tabular-nums">{{ formatNumber(row.cpN1Remaining) }}</span>
<span class="text-right tabular-nums">{{ row.cpN }}</span>
<span class="text-right tabular-nums">{{ row.acquiredSaturdays }}</span> <span class="text-right tabular-nums">{{ row.acquiredSaturdays }}</span>
<span class="text-right tabular-nums">{{ row.cpN }}</span>
<span class="text-right tabular-nums">{{ row.rtt }}</span> <span class="text-right tabular-nums">{{ row.rtt }}</span>
</div> </div>
</div> </div>

View File

@@ -27,6 +27,7 @@ final class EmployeeLeaveRecap
public ?string $siteName = null; public ?string $siteName = null;
public ?string $siteColor = null; public ?string $siteColor = null;
public ?string $contractName = null; public ?string $contractName = null;
public int $contractSortKey = 99;
public float $cpN1Remaining = 0.0; public float $cpN1Remaining = 0.0;
public string $cpN = '-'; public string $cpN = '-';
public string $acquiredSaturdays = '-'; public string $acquiredSaturdays = '-';

View File

@@ -71,7 +71,10 @@ final readonly class LeaveBalanceComputationService
$fractionedDays = $this->resolveFractionedDays($employee, $ruleCode, $year); $fractionedDays = $this->resolveFractionedDays($employee, $ruleCode, $year);
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) { 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); $baseAcquiredDays = (float) max(0, $totalBusinessDays - self::FORFAIT_TARGET_WORKED_DAYS);
$acquiredDays = $carryDays + $baseAcquiredDays + $fractionedDays; $acquiredDays = $carryDays + $baseAcquiredDays + $fractionedDays;
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to); $absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
@@ -406,6 +409,29 @@ final readonly class LeaveBalanceComputationService
return $map; return $map;
} }
/**
* @return array<string, string>
*/
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<Absence> $absences * @param list<Absence> $absences
* *

View File

@@ -78,12 +78,25 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
*/ */
public function getHolidaysDayByYears(string $zone, string $years): array 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<string, string>
*/
private function fetchHolidaysByYears(string $zone, string $years): array
{ {
$zone = strtolower(trim($zone)); $zone = strtolower(trim($zone));
$years = trim($years); $years = trim($years);
$key = "public_holidays_{$zone}_{$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); $item->expiresAfter(30 * 86400);
$url = $this->holidayUrl."{$zone}/{$years}.json"; $url = $this->holidayUrl."{$zone}/{$years}.json";
@@ -101,8 +114,6 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
return json_decode($response->getContent(), true); return json_decode($response->getContent(), true);
}); });
return $this->applyExclusions($holidays);
} }
/** /**

View File

@@ -9,4 +9,11 @@ interface PublicHolidayServiceInterface
public function getHolidaysDay(string $zone): array; public function getHolidaysDay(string $zone): array;
public function getHolidaysDayByYears(string $zone, string $years): 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;
} }

View File

@@ -7,8 +7,10 @@ namespace App\State;
use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\ProviderInterface;
use App\ApiResource\EmployeeLeaveRecap; use App\ApiResource\EmployeeLeaveRecap;
use App\Entity\Contract;
use App\Entity\Employee; use App\Entity\Employee;
use App\Entity\User; use App\Entity\User;
use App\Enum\ContractType;
use App\Repository\EmployeeRepository; use App\Repository\EmployeeRepository;
use App\Security\EmployeeScopeService; use App\Security\EmployeeScopeService;
use App\Service\Leave\LeaveRecapRowBuilder; use App\Service\Leave\LeaveRecapRowBuilder;
@@ -63,6 +65,7 @@ final readonly class EmployeeLeaveRecapProvider implements ProviderInterface
$resource->siteName = $site?->getName(); $resource->siteName = $site?->getName();
$resource->siteColor = $site?->getColor(); $resource->siteColor = $site?->getColor();
$resource->contractName = $row['contractName'] ?? null; $resource->contractName = $row['contractName'] ?? null;
$resource->contractSortKey = $this->resolveContractSortKey($employee->getContract());
$resource->cpN1Remaining = is_numeric($row['cpN1Remaining']) ? (float) $row['cpN1Remaining'] : 0.0; $resource->cpN1Remaining = is_numeric($row['cpN1Remaining']) ? (float) $row['cpN1Remaining'] : 0.0;
$resource->cpN = (string) $row['cpN']; $resource->cpN = (string) $row['cpN'];
$resource->acquiredSaturdays = (string) $row['acquiredSaturdays']; $resource->acquiredSaturdays = (string) $row['acquiredSaturdays'];
@@ -78,6 +81,10 @@ final readonly class EmployeeLeaveRecapProvider implements ProviderInterface
if (0 !== $siteCmp) { if (0 !== $siteCmp) {
return $siteCmp; return $siteCmp;
} }
$contractCmp = $a->contractSortKey <=> $b->contractSortKey;
if (0 !== $contractCmp) {
return $contractCmp;
}
$lastCmp = strcmp($a->lastName, $b->lastName); $lastCmp = strcmp($a->lastName, $b->lastName);
if (0 !== $lastCmp) { if (0 !== $lastCmp) {
return $lastCmp; return $lastCmp;
@@ -89,6 +96,30 @@ final readonly class EmployeeLeaveRecapProvider implements ProviderInterface
return $rows; 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<Employee> * @return list<Employee>
*/ */

View File

@@ -561,7 +561,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
{ {
$type = $employee->getContract()?->getType(); $type = $employee->getContract()?->getType();
if (ContractType::FORFAIT === $type) { 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); $publicHolidays = $this->buildPublicHolidayMap($from, $to);
$weekdayHolidays = array_filter( $weekdayHolidays = array_filter(
array_keys($publicHolidays), array_keys($publicHolidays),
@@ -655,6 +658,29 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
return $map; return $map;
} }
/**
* @return array<string, string>
*/
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. * Presence days = business days (Mon-Fri) - public holidays + weekend worked days - absence days.
* *