feat : Ajout du système de RTT sur la page employé avec le repport annuel des heures
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s

This commit is contained in:
2026-03-13 10:26:33 +01:00
parent 1858817649
commit 4a2c3a8eed
29 changed files with 1595 additions and 391 deletions

View File

@@ -69,18 +69,9 @@ final readonly class LeaveBalanceComputationService
$fractionedDays = $this->resolveFractionedDays($employee, $ruleCode, $year);
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
$totalBusinessDays = $this->countBusinessDays($from, $to);
$baseAcquiredDays = (float) max(0, $totalBusinessDays - self::FORFAIT_TARGET_WORKED_DAYS);
$suspensions = $this->resolveSuspensionsForEmployeePeriod($employee, $from, $to);
$acquiredDays = $carryDays + $baseAcquiredDays + $fractionedDays;
if ([] !== $suspensions) {
$totalMonths = $this->countFractionalMonths($from, $to);
$suspendedMonths = $this->countSuspendedFractionalMonths($from, $to, $suspensions);
if ($totalMonths > 0) {
$ratio = max(0.0, ($totalMonths - $suspendedMonths) / $totalMonths);
$acquiredDays = $carryDays + $baseAcquiredDays * $ratio + $fractionedDays;
}
}
$totalBusinessDays = $this->countBusinessDays($from, $to);
$baseAcquiredDays = (float) max(0, $totalBusinessDays - self::FORFAIT_TARGET_WORKED_DAYS);
$acquiredDays = $carryDays + $baseAcquiredDays + $fractionedDays;
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
[$takenDays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, false, false);
$previousRemainingDays = max(0.0, $acquiredDays - $takenDays);
@@ -89,7 +80,9 @@ final readonly class LeaveBalanceComputationService
continue;
}
$suspensions = $this->resolveSuspensionsForEmployeePeriod($employee, $from, $to);
$suspensions = $this->suspensionDaysCalculator->applyFirstMonthGrace(
$this->resolveSuspensionsForEmployeePeriod($employee, $from, $to)
);
$generatedDays = $this->computeAccruedDays(
$this->resolveAnnualDays($employee),
$this->resolveDaysAccrualPerMonth($employee),
@@ -425,55 +418,6 @@ final readonly class LeaveBalanceComputationService
return [$takenDays, $takenSaturdays];
}
private function countFractionalMonths(DateTimeImmutable $from, DateTimeImmutable $to): float
{
$from = $this->normalizeDate($from);
$to = $this->normalizeDate($to);
$months = 0.0;
$cursor = $from->modify('first day of this month')->setTime(0, 0);
while ($cursor <= $to) {
$monthStart = $cursor > $from ? $cursor : $from;
$monthEnd = $cursor->modify('last day of this month')->setTime(0, 0);
if ($monthEnd > $to) {
$monthEnd = $to;
}
$coveredDays = ((int) $monthEnd->diff($monthStart)->format('%a')) + 1;
$daysInMonth = (int) $cursor->format('t');
$months += $coveredDays / $daysInMonth;
$cursor = $cursor->modify('first day of next month');
}
return $months;
}
/**
* @param list<ContractSuspension> $suspensions
*/
private function countSuspendedFractionalMonths(DateTimeImmutable $from, DateTimeImmutable $to, array $suspensions): float
{
$from = $this->normalizeDate($from);
$to = $this->normalizeDate($to);
$months = 0.0;
$cursor = $from->modify('first day of this month')->setTime(0, 0);
while ($cursor <= $to) {
$monthStart = $cursor > $from ? $cursor : $from;
$monthEnd = $cursor->modify('last day of this month')->setTime(0, 0);
if ($monthEnd > $to) {
$monthEnd = $to;
}
$daysInMonth = (int) $cursor->format('t');
$suspendedDays = $this->suspensionDaysCalculator->countSuspendedDaysInMonth($monthStart, $monthEnd, $suspensions);
$months += $suspendedDays / $daysInMonth;
$cursor = $cursor->modify('first day of next month');
}
return $months;
}
/**
* @return list<ContractSuspension>
*/

View File

@@ -38,6 +38,35 @@ final class SuspensionDaysCalculator
return $total;
}
/**
* Return adjusted suspensions where the first month of each suspension is excluded (grace period).
*
* @param list<ContractSuspension> $suspensions
*
* @return list<ContractSuspension>
*/
public function applyFirstMonthGrace(array $suspensions): array
{
$adjusted = [];
foreach ($suspensions as $suspension) {
$gracedStart = $suspension->getStartDate()->modify('+1 month');
$end = $suspension->getEndDate();
if ($end instanceof DateTimeImmutable && $gracedStart > $end) {
continue;
}
$copy = new ContractSuspension();
$copy->setStartDate($gracedStart);
$copy->setEndDate($end);
$adjusted[] = $copy;
}
return $adjusted;
}
/**
* Count business days (Mon-Fri, excl. public holidays) suspended within a period.
*

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Service\Rtt;
use App\Dto\Rtt\WeekRecoveryDetail;
use App\Dto\WorkHours\WorkMetrics;
use App\Entity\Contract;
use App\Entity\Employee;
@@ -70,7 +71,7 @@ final readonly class RttRecoveryComputationService
return $weeks;
}
public function computeTotalRecoveryForExercise(Employee $employee, int $exerciseYear): int
public function computeTotalRecoveryForExercise(Employee $employee, int $exerciseYear): WeekRecoveryDetail
{
[$from, $to] = $this->resolveExerciseBounds($exerciseYear);
$weeks = $this->buildWeeksForExercise($from, $to);
@@ -86,13 +87,25 @@ final readonly class RttRecoveryComputationService
$byWeek = $this->computeRecoveryByWeek($employee, $weekRanges, $from, $to, null);
return array_sum($byWeek);
$total = new WeekRecoveryDetail();
foreach ($byWeek as $detail) {
$total = new WeekRecoveryDetail(
overtimeMinutes: $total->overtimeMinutes + $detail->overtimeMinutes,
base25Minutes: $total->base25Minutes + $detail->base25Minutes,
bonus25Minutes: $total->bonus25Minutes + $detail->bonus25Minutes,
base50Minutes: $total->base50Minutes + $detail->base50Minutes,
bonus50Minutes: $total->bonus50Minutes + $detail->bonus50Minutes,
totalMinutes: $total->totalMinutes + $detail->totalMinutes,
);
}
return $total;
}
/**
* @param list<array{month:int,weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}> $weeks
*
* @return array<string, int>
* @return array<string, WeekRecoveryDetail>
*/
public function computeRecoveryByWeek(
Employee $employee,
@@ -148,13 +161,13 @@ final readonly class RttRecoveryComputationService
$effectiveEnd = $weekEnd > $periodTo ? $periodTo : $weekEnd;
if ($effectiveEnd < $effectiveStart) {
$results[$weekKey] = 0;
$results[$weekKey] = new WeekRecoveryDetail();
continue;
}
if ($limitDate instanceof DateTimeImmutable && $effectiveStart > $limitDate) {
$results[$weekKey] = 0;
$results[$weekKey] = new WeekRecoveryDetail();
continue;
}
@@ -177,7 +190,7 @@ final readonly class RttRecoveryComputationService
}
if ([] === $weekDays) {
$results[$weekKey] = 0;
$results[$weekKey] = new WeekRecoveryDetail();
continue;
}
@@ -191,15 +204,22 @@ final readonly class RttRecoveryComputationService
$weeklyOvertimeTotalMinutes = $isWeekPresenceTracking
? 0
: max(0, $weeklyTotalMinutes - $overtimeReferenceMinutes);
$weeklyOvertime25Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $this->computeOvertime25BonusMinutes($weeklyTotalMinutes, $overtime25StartMinutes);
$weeklyOvertime50Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $this->computeOvertime50BonusMinutes($weeklyTotalMinutes);
$results[$weekKey] = ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $weeklyOvertimeTotalMinutes + $weeklyOvertime25Minutes + $weeklyOvertime50Minutes;
$base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes);
$bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base25 * 0.25);
$base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, $weeklyTotalMinutes - 43 * 60);
$bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base50 * 0.5);
$results[$weekKey] = new WeekRecoveryDetail(
overtimeMinutes: $weeklyOvertimeTotalMinutes,
base25Minutes: $base25,
bonus25Minutes: $bonus25,
base50Minutes: $base50,
bonus50Minutes: $bonus50,
totalMinutes: ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $weeklyOvertimeTotalMinutes + $bonus25 + $bonus50,
);
}
return $results;