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
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
This commit is contained in:
@@ -22,8 +22,10 @@ use App\State\EmployeeRttPaymentProvider;
|
||||
)]
|
||||
final class EmployeeRttPaymentInput
|
||||
{
|
||||
public int $month = 0;
|
||||
public int $minutes = 0;
|
||||
public string $rate = '25';
|
||||
public ?int $year = null;
|
||||
public int $month = 0;
|
||||
public int $base25Minutes = 0;
|
||||
public int $bonus25Minutes = 0;
|
||||
public int $base50Minutes = 0;
|
||||
public int $bonus50Minutes = 0;
|
||||
public ?int $year = null;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,12 @@ use App\State\EmployeeRttSummaryProvider;
|
||||
final class EmployeeRttSummary
|
||||
{
|
||||
public int $year = 0;
|
||||
public int $carryMonth = 5;
|
||||
public int $carryFromPreviousYearMinutes = 0;
|
||||
public int $carryBase25Minutes = 0;
|
||||
public int $carryBonus25Minutes = 0;
|
||||
public int $carryBase50Minutes = 0;
|
||||
public int $carryBonus50Minutes = 0;
|
||||
public int $currentYearRecoveryMinutes = 0;
|
||||
public int $availableMinutes = 0;
|
||||
public int $totalPaidMinutes = 0;
|
||||
|
||||
@@ -92,7 +92,7 @@ final class RttRolloverCommand extends Command
|
||||
|
||||
try {
|
||||
$previousYear = $targetYear - 1;
|
||||
$carryMinutes = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $previousYear);
|
||||
$carry = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $previousYear);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error('Error computing carry for employee.', ['employeeId' => $employee->getId(), 'error' => $e->getMessage()]);
|
||||
++$skipped;
|
||||
@@ -103,12 +103,15 @@ final class RttRolloverCommand extends Command
|
||||
$balance = new EmployeeRttBalance()
|
||||
->setEmployee($employee)
|
||||
->setYear($targetYear)
|
||||
->setOpeningMinutes($carryMinutes)
|
||||
->setOpeningBase25Minutes($carry->base25Minutes)
|
||||
->setOpeningBonus25Minutes($carry->bonus25Minutes)
|
||||
->setOpeningBase50Minutes($carry->base50Minutes)
|
||||
->setOpeningBonus50Minutes($carry->bonus50Minutes)
|
||||
->setIsLocked(false)
|
||||
;
|
||||
|
||||
$this->entityManager->persist($balance);
|
||||
$this->logger->info('Balance created.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'carryMinutes' => $carryMinutes]);
|
||||
$this->logger->info('Balance created.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'carryMinutes' => $carry->totalMinutes]);
|
||||
++$created;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ final class EmployeeRttWeekSummary
|
||||
public int $weekNumber,
|
||||
public string $weekStart,
|
||||
public string $weekEnd,
|
||||
public int $recoveryMinutes,
|
||||
public int $overtimeMinutes = 0,
|
||||
public int $base25Minutes = 0,
|
||||
public int $bonus25Minutes = 0,
|
||||
public int $base50Minutes = 0,
|
||||
public int $bonus50Minutes = 0,
|
||||
public int $totalMinutes = 0,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ final class RttMonthPayment
|
||||
{
|
||||
public function __construct(
|
||||
public int $month,
|
||||
public int $paidMinutes25 = 0,
|
||||
public int $paidMinutes50 = 0,
|
||||
public int $paidBase25Minutes = 0,
|
||||
public int $paidBonus25Minutes = 0,
|
||||
public int $paidBase50Minutes = 0,
|
||||
public int $paidBonus50Minutes = 0,
|
||||
) {}
|
||||
}
|
||||
|
||||
17
src/Dto/Rtt/WeekRecoveryDetail.php
Normal file
17
src/Dto/Rtt/WeekRecoveryDetail.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dto\Rtt;
|
||||
|
||||
final class WeekRecoveryDetail
|
||||
{
|
||||
public function __construct(
|
||||
public int $overtimeMinutes = 0,
|
||||
public int $base25Minutes = 0,
|
||||
public int $bonus25Minutes = 0,
|
||||
public int $base50Minutes = 0,
|
||||
public int $bonus50Minutes = 0,
|
||||
public int $totalMinutes = 0,
|
||||
) {}
|
||||
}
|
||||
@@ -26,8 +26,20 @@ class EmployeeRttBalance
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Annee d exercice (year = annee de fin, ex: 2026 = 01/06/2025 -> 31/05/2026).'])]
|
||||
private int $year = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 en minutes (solde d ouverture).'])]
|
||||
private int $openingMinutes = 0;
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Mois de fin du report (1-12). Le report s affiche dans le mois suivant.', 'default' => 5])]
|
||||
private int $month = 5;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 base 25% en minutes.', 'default' => 0])]
|
||||
private int $openingBase25Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 bonus 25% en minutes.', 'default' => 0])]
|
||||
private int $openingBonus25Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 base 50% en minutes.', 'default' => 0])]
|
||||
private int $openingBase50Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 bonus 50% en minutes.', 'default' => 0])]
|
||||
private int $openingBonus50Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'boolean', options: ['default' => false, 'comment' => 'Indique si le solde est fige (verrouille RH).'])]
|
||||
private bool $isLocked = false;
|
||||
@@ -74,18 +86,71 @@ class EmployeeRttBalance
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOpeningMinutes(): int
|
||||
public function getMonth(): int
|
||||
{
|
||||
return $this->openingMinutes;
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
public function setOpeningMinutes(int $openingMinutes): self
|
||||
public function setMonth(int $month): self
|
||||
{
|
||||
$this->openingMinutes = $openingMinutes;
|
||||
$this->month = $month;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOpeningBase25Minutes(): int
|
||||
{
|
||||
return $this->openingBase25Minutes;
|
||||
}
|
||||
|
||||
public function setOpeningBase25Minutes(int $openingBase25Minutes): self
|
||||
{
|
||||
$this->openingBase25Minutes = $openingBase25Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOpeningBonus25Minutes(): int
|
||||
{
|
||||
return $this->openingBonus25Minutes;
|
||||
}
|
||||
|
||||
public function setOpeningBonus25Minutes(int $openingBonus25Minutes): self
|
||||
{
|
||||
$this->openingBonus25Minutes = $openingBonus25Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOpeningBase50Minutes(): int
|
||||
{
|
||||
return $this->openingBase50Minutes;
|
||||
}
|
||||
|
||||
public function setOpeningBase50Minutes(int $openingBase50Minutes): self
|
||||
{
|
||||
$this->openingBase50Minutes = $openingBase50Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOpeningBonus50Minutes(): int
|
||||
{
|
||||
return $this->openingBonus50Minutes;
|
||||
}
|
||||
|
||||
public function setOpeningBonus50Minutes(int $openingBonus50Minutes): self
|
||||
{
|
||||
$this->openingBonus50Minutes = $openingBonus50Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalOpeningMinutes(): int
|
||||
{
|
||||
return $this->openingBase25Minutes + $this->openingBonus25Minutes + $this->openingBase50Minutes + $this->openingBonus50Minutes;
|
||||
}
|
||||
|
||||
public function isLocked(): bool
|
||||
{
|
||||
return $this->isLocked;
|
||||
|
||||
@@ -28,11 +28,17 @@ class EmployeeRttPayment
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Mois du paiement.'])]
|
||||
private int $month = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Duree en minutes.'])]
|
||||
private int $minutes = 0;
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Base heures palier 25% en minutes.', 'default' => 0])]
|
||||
private int $base25Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 10, options: ['comment' => 'Taux applique.'])]
|
||||
private string $rate = '';
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Bonus 25% en minutes.', 'default' => 0])]
|
||||
private int $bonus25Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Base heures palier 50% en minutes.', 'default' => 0])]
|
||||
private int $base50Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['comment' => 'Bonus 50% en minutes.', 'default' => 0])]
|
||||
private int $bonus50Minutes = 0;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
@@ -88,26 +94,50 @@ class EmployeeRttPayment
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinutes(): int
|
||||
public function getBase25Minutes(): int
|
||||
{
|
||||
return $this->minutes;
|
||||
return $this->base25Minutes;
|
||||
}
|
||||
|
||||
public function setMinutes(int $minutes): self
|
||||
public function setBase25Minutes(int $base25Minutes): self
|
||||
{
|
||||
$this->minutes = $minutes;
|
||||
$this->base25Minutes = $base25Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRate(): string
|
||||
public function getBonus25Minutes(): int
|
||||
{
|
||||
return $this->rate;
|
||||
return $this->bonus25Minutes;
|
||||
}
|
||||
|
||||
public function setRate(string $rate): self
|
||||
public function setBonus25Minutes(int $bonus25Minutes): self
|
||||
{
|
||||
$this->rate = $rate;
|
||||
$this->bonus25Minutes = $bonus25Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBase50Minutes(): int
|
||||
{
|
||||
return $this->base50Minutes;
|
||||
}
|
||||
|
||||
public function setBase50Minutes(int $base50Minutes): self
|
||||
{
|
||||
$this->base50Minutes = $base50Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBonus50Minutes(): int
|
||||
{
|
||||
return $this->bonus50Minutes;
|
||||
}
|
||||
|
||||
public function setBonus50Minutes(int $bonus50Minutes): self
|
||||
{
|
||||
$this->bonus50Minutes = $bonus50Minutes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -19,13 +19,12 @@ final class EmployeeRttPaymentRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, EmployeeRttPayment::class);
|
||||
}
|
||||
|
||||
public function findOneByEmployeeYearMonthRate(Employee $employee, int $year, int $month, string $rate): ?EmployeeRttPayment
|
||||
public function findOneByEmployeeYearMonth(Employee $employee, int $year, int $month): ?EmployeeRttPayment
|
||||
{
|
||||
return $this->findOneBy([
|
||||
'employee' => $employee,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'rate' => $rate,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -139,29 +139,40 @@ final class WorkHourRepository extends ServiceEntityRepository implements WorkHo
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, float> YYYY-MM => presence day count (0.5 for half-days)
|
||||
* Count weekend worked days by month.
|
||||
* >= 5h total = 1.0 day, < 5h = 0.5 day.
|
||||
*
|
||||
* @return array<string, float> YYYY-MM => weekend worked day count
|
||||
*/
|
||||
public function countPresenceDaysByMonth(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||
public function countWeekendWorkedDaysByMonth(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT TO_CHAR(work_date, 'YYYY-MM') AS month,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN (morning_from IS NOT NULL OR is_present_morning = true)
|
||||
AND (afternoon_from IS NOT NULL OR is_present_afternoon = true)
|
||||
THEN 1.0
|
||||
WHEN (morning_from IS NOT NULL OR is_present_morning = true)
|
||||
OR (afternoon_from IS NOT NULL OR is_present_afternoon = true)
|
||||
THEN 0.5
|
||||
WHEN total_minutes >= 300 THEN 1.0
|
||||
WHEN total_minutes > 0 THEN 0.5
|
||||
ELSE 0
|
||||
END
|
||||
) AS cnt
|
||||
FROM work_hours
|
||||
WHERE employee_id = :employee
|
||||
AND work_date >= :from
|
||||
AND work_date <= :to
|
||||
AND (morning_from IS NOT NULL OR is_present_morning = true
|
||||
OR afternoon_from IS NOT NULL OR is_present_afternoon = true)
|
||||
FROM (
|
||||
SELECT work_date,
|
||||
COALESCE(
|
||||
EXTRACT(EPOCH FROM (morning_to::time - morning_from::time)) / 60, 0
|
||||
)
|
||||
+ COALESCE(
|
||||
EXTRACT(EPOCH FROM (afternoon_to::time - afternoon_from::time)) / 60, 0
|
||||
)
|
||||
+ COALESCE(
|
||||
EXTRACT(EPOCH FROM (evening_to::time - evening_from::time)) / 60, 0
|
||||
) AS total_minutes
|
||||
FROM work_hours
|
||||
WHERE employee_id = :employee
|
||||
AND work_date >= :from
|
||||
AND work_date <= :to
|
||||
AND EXTRACT(ISODOW FROM work_date) IN (6, 7)
|
||||
AND (morning_from IS NOT NULL OR afternoon_from IS NOT NULL OR evening_from IS NOT NULL)
|
||||
) sub
|
||||
GROUP BY month
|
||||
SQL;
|
||||
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Enum\HalfDay;
|
||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||
use App\Service\PublicHolidayServiceInterface;
|
||||
use DateInterval;
|
||||
use DatePeriod;
|
||||
use DateTime;
|
||||
@@ -22,6 +23,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Throwable;
|
||||
|
||||
final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
{
|
||||
@@ -30,6 +32,7 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
private AbsenceReadRepositoryInterface $absenceRepository,
|
||||
private WorkHourReadRepositoryInterface $workHourRepository,
|
||||
private Security $security,
|
||||
private PublicHolidayServiceInterface $publicHolidayService,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
@@ -132,10 +135,15 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
throw new UnprocessableEntityHttpException('La demi-journée de fin ne peut pas être avant la demi-journée de début.');
|
||||
}
|
||||
|
||||
$days = new DatePeriod($start, new DateInterval('P1D'), $end->modify('+1 day'));
|
||||
$days = new DatePeriod($start, new DateInterval('P1D'), $end->modify('+1 day'));
|
||||
$publicHolidays = $this->buildPublicHolidayMap($start, $end);
|
||||
|
||||
$segments = [];
|
||||
foreach ($days as $day) {
|
||||
if (isset($publicHolidays[$day->format('Y-m-d')])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isFirst = $day->format('Y-m-d') === $start->format('Y-m-d');
|
||||
$isLast = $day->format('Y-m-d') === $end->format('Y-m-d');
|
||||
$isSame = $isFirst && $isLast;
|
||||
@@ -246,4 +254,27 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
->setIsValid(false)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildPublicHolidayMap(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->getHolidaysDayByYears('metropole', (string) $year);
|
||||
foreach ($holidays as $date => $label) {
|
||||
$map[(string) $date] = (string) $label;
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
|
||||
|
||||
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
|
||||
$summary->presenceDaysByMonth = $this->workHourRepository->countPresenceDaysByMonth($employee, $periodFrom, $periodTo);
|
||||
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
@@ -178,8 +178,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
|
||||
$accrualCalculationEnd = $this->resolveAccrualCalculationEndDate($leavePolicy['ruleCode'], $year, $to, $employee);
|
||||
$takenCalculationEnd = $this->resolveTakenCalculationEndDate($to, $employee);
|
||||
$suspensions = $this->resolveSuspensionsForPeriod($employee, $effectiveFrom, $to);
|
||||
$generatedDays = $leavePolicy['accrualPerMonth'] > 0.0
|
||||
$suspensions = $this->suspensionDaysCalculator->applyFirstMonthGrace(
|
||||
$this->resolveSuspensionsForPeriod($employee, $effectiveFrom, $to)
|
||||
);
|
||||
$generatedDays = $leavePolicy['accrualPerMonth'] > 0.0
|
||||
? $this->computeAccruedDaysFromStart(
|
||||
$leavePolicy['acquiredDays'],
|
||||
$leavePolicy['accrualPerMonth'],
|
||||
@@ -235,16 +237,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
$previousRemainingSaturdays = $remainingAcquiredSaturdays + $remainingGeneratedSaturdays;
|
||||
} else {
|
||||
// Forfait: no "en cours d'acquisition" counter, all rights are in acquired.
|
||||
$acquiredDays = $carryDays + $leavePolicy['acquiredDays'];
|
||||
$suspensions = $this->resolveSuspensionsForPeriod($employee, $from, $to);
|
||||
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 + $leavePolicy['acquiredDays'] * $ratio;
|
||||
}
|
||||
}
|
||||
// Suspensions do not impact forfait 218 leave calculation.
|
||||
$acquiredDays = $carryDays + $leavePolicy['acquiredDays'];
|
||||
$accruingDays = 0.0;
|
||||
$remainingDays = max(0.0, $acquiredDays - $takenDays);
|
||||
$acquiredSaturdays = 0.0;
|
||||
@@ -539,6 +533,66 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presence days = business days (Mon-Fri) - public holidays + weekend worked days - absence days.
|
||||
*
|
||||
* @return array<string, float> YYYY-MM => presence day count
|
||||
*/
|
||||
private function computePresenceDaysByMonth(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||
{
|
||||
$publicHolidays = $this->buildPublicHolidayMap($from, $to);
|
||||
$weekendWorkedDays = $this->workHourRepository->countWeekendWorkedDaysByMonth($employee, $from, $to);
|
||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to);
|
||||
|
||||
// Count absence days per month (0.5 for half-days).
|
||||
$absenceDaysByMonth = [];
|
||||
foreach ($absences as $absence) {
|
||||
$date = DateTimeImmutable::createFromInterface($absence->getStartDate());
|
||||
$monthKey = $date->format('Y-m');
|
||||
$days = 1.0;
|
||||
if ($absence->getStartHalf() === $absence->getEndHalf()) {
|
||||
$days = 0.5;
|
||||
}
|
||||
$absenceDaysByMonth[$monthKey] = ($absenceDaysByMonth[$monthKey] ?? 0.0) + $days;
|
||||
}
|
||||
|
||||
// Count business days and public holidays per month.
|
||||
$result = [];
|
||||
$cursor = $from->modify('first day of this month')->setTime(0, 0);
|
||||
while ($cursor <= $to) {
|
||||
$monthKey = $cursor->format('Y-m');
|
||||
$monthStart = $cursor < $from ? $from : $cursor;
|
||||
$monthEnd = $cursor->modify('last day of this month')->setTime(0, 0);
|
||||
if ($monthEnd > $to) {
|
||||
$monthEnd = $to;
|
||||
}
|
||||
|
||||
$businessDays = 0;
|
||||
for (
|
||||
$day = $monthStart;
|
||||
$day <= $monthEnd;
|
||||
$day = $day->modify('+1 day')
|
||||
) {
|
||||
$weekDay = (int) $day->format('N');
|
||||
if ($weekDay <= 5 && !isset($publicHolidays[$day->format('Y-m-d')])) {
|
||||
++$businessDays;
|
||||
}
|
||||
}
|
||||
|
||||
$weekend = $weekendWorkedDays[$monthKey] ?? 0.0;
|
||||
$absenced = $absenceDaysByMonth[$monthKey] ?? 0.0;
|
||||
|
||||
$presence = max(0.0, (float) $businessDays + $weekend - $absenced);
|
||||
if ($presence > 0.0) {
|
||||
$result[$monthKey] = $presence;
|
||||
}
|
||||
|
||||
$cursor = $cursor->modify('first day of next month');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{DateTimeImmutable, DateTimeImmutable}
|
||||
*/
|
||||
@@ -731,55 +785,6 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
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>
|
||||
*/
|
||||
|
||||
@@ -16,8 +16,6 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
use function in_array;
|
||||
|
||||
final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
@@ -42,32 +40,27 @@ final readonly class EmployeeRttPaymentProcessor implements ProcessorInterface
|
||||
throw new NotFoundHttpException('Employee not found.');
|
||||
}
|
||||
|
||||
if (!in_array($data->rate, ['25', '50'], true)) {
|
||||
throw new UnprocessableEntityHttpException('rate must be "25" or "50".');
|
||||
}
|
||||
|
||||
if ($data->month < 1 || $data->month > 12) {
|
||||
throw new UnprocessableEntityHttpException('month must be between 1 and 12.');
|
||||
}
|
||||
|
||||
if ($data->minutes < 0) {
|
||||
throw new UnprocessableEntityHttpException('minutes must be >= 0.');
|
||||
}
|
||||
|
||||
$year = $data->year ?? $this->resolveCurrentExerciseYear();
|
||||
|
||||
$payment = $this->rttPaymentRepository->findOneByEmployeeYearMonthRate($employee, $year, $data->month, $data->rate);
|
||||
$payment = $this->rttPaymentRepository->findOneByEmployeeYearMonth($employee, $year, $data->month);
|
||||
|
||||
if (null === $payment) {
|
||||
$payment = new EmployeeRttPayment();
|
||||
$payment->setEmployee($employee);
|
||||
$payment->setYear($year);
|
||||
$payment->setMonth($data->month);
|
||||
$payment->setRate($data->rate);
|
||||
$this->entityManager->persist($payment);
|
||||
}
|
||||
|
||||
$payment->setMinutes($data->minutes);
|
||||
$payment->setBase25Minutes($data->base25Minutes);
|
||||
$payment->setBonus25Minutes($data->bonus25Minutes);
|
||||
$payment->setBase50Minutes($data->base50Minutes);
|
||||
$payment->setBonus50Minutes($data->bonus50Minutes);
|
||||
$payment->touch();
|
||||
$this->entityManager->flush();
|
||||
|
||||
$data->year = $year;
|
||||
|
||||
@@ -9,6 +9,7 @@ use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\EmployeeRttSummary;
|
||||
use App\Dto\Rtt\EmployeeRttWeekSummary;
|
||||
use App\Dto\Rtt\RttMonthPayment;
|
||||
use App\Dto\Rtt\WeekRecoveryDetail;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\User;
|
||||
use App\Repository\EmployeeRepository;
|
||||
@@ -76,22 +77,36 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
$limitDate = $periodFrom->modify('-1 day');
|
||||
}
|
||||
|
||||
$currentByWeekStart = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate);
|
||||
$carryMinutes = $this->resolveCarryMinutes($employee, $year);
|
||||
$currentByWeekStart = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate);
|
||||
[$carry, $carryMonth] = $this->resolveCarry($employee, $year);
|
||||
|
||||
$summary = new EmployeeRttSummary();
|
||||
$summary->year = $year;
|
||||
$summary->carryFromPreviousYearMinutes = $carryMinutes;
|
||||
$summary->currentYearRecoveryMinutes = array_sum($currentByWeekStart);
|
||||
$summary->carryMonth = $carryMonth;
|
||||
$summary->carryFromPreviousYearMinutes = $carry->totalMinutes;
|
||||
$summary->carryBase25Minutes = $carry->base25Minutes;
|
||||
$summary->carryBonus25Minutes = $carry->bonus25Minutes;
|
||||
$summary->carryBase50Minutes = $carry->base50Minutes;
|
||||
$summary->carryBonus50Minutes = $carry->bonus50Minutes;
|
||||
$summary->currentYearRecoveryMinutes = array_sum(array_map(static fn ($d) => $d->totalMinutes, $currentByWeekStart));
|
||||
$summary->availableMinutes = $summary->carryFromPreviousYearMinutes + $summary->currentYearRecoveryMinutes;
|
||||
$summary->weeks = array_map(
|
||||
static fn (array $week) => new EmployeeRttWeekSummary(
|
||||
month: (int) $week['month'],
|
||||
weekNumber: (int) $week['weekNumber'],
|
||||
weekStart: $week['start']->format('Y-m-d'),
|
||||
weekEnd: $week['end']->format('Y-m-d'),
|
||||
recoveryMinutes: (int) ($currentByWeekStart[$week['start']->format('Y-m-d')] ?? 0),
|
||||
),
|
||||
static function (array $week) use ($currentByWeekStart) {
|
||||
$detail = $currentByWeekStart[$week['start']->format('Y-m-d')] ?? new WeekRecoveryDetail();
|
||||
|
||||
return new EmployeeRttWeekSummary(
|
||||
month: (int) $week['month'],
|
||||
weekNumber: (int) $week['weekNumber'],
|
||||
weekStart: $week['start']->format('Y-m-d'),
|
||||
weekEnd: $week['end']->format('Y-m-d'),
|
||||
overtimeMinutes: $detail->overtimeMinutes,
|
||||
base25Minutes: $detail->base25Minutes,
|
||||
bonus25Minutes: $detail->bonus25Minutes,
|
||||
base50Minutes: $detail->base50Minutes,
|
||||
bonus50Minutes: $detail->bonus50Minutes,
|
||||
totalMinutes: $detail->totalMinutes,
|
||||
);
|
||||
},
|
||||
$weekRanges
|
||||
);
|
||||
|
||||
@@ -101,21 +116,20 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
foreach ($payments as $payment) {
|
||||
$m = $payment->getMonth();
|
||||
if (!isset($monthBuckets[$m])) {
|
||||
$monthBuckets[$m] = ['paidMinutes25' => 0, 'paidMinutes50' => 0];
|
||||
}
|
||||
if ('25' === $payment->getRate()) {
|
||||
$monthBuckets[$m]['paidMinutes25'] += $payment->getMinutes();
|
||||
} else {
|
||||
$monthBuckets[$m]['paidMinutes50'] += $payment->getMinutes();
|
||||
$monthBuckets[$m] = ['base25' => 0, 'bonus25' => 0, 'base50' => 0, 'bonus50' => 0];
|
||||
}
|
||||
$monthBuckets[$m]['base25'] += $payment->getBase25Minutes();
|
||||
$monthBuckets[$m]['bonus25'] += $payment->getBonus25Minutes();
|
||||
$monthBuckets[$m]['base50'] += $payment->getBase50Minutes();
|
||||
$monthBuckets[$m]['bonus50'] += $payment->getBonus50Minutes();
|
||||
}
|
||||
|
||||
$monthPayments = [];
|
||||
$totalPaidMinutes = 0;
|
||||
|
||||
foreach ($monthBuckets as $m => $bucket) {
|
||||
$monthPayments[] = new RttMonthPayment($m, $bucket['paidMinutes25'], $bucket['paidMinutes50']);
|
||||
$totalPaidMinutes += $bucket['paidMinutes25'] + $bucket['paidMinutes50'];
|
||||
$monthPayments[] = new RttMonthPayment($m, $bucket['base25'], $bucket['bonus25'], $bucket['base50'], $bucket['bonus50']);
|
||||
$totalPaidMinutes += $bucket['base25'] + $bucket['bonus25'] + $bucket['base50'] + $bucket['bonus50'];
|
||||
}
|
||||
|
||||
$summary->totalPaidMinutes = $totalPaidMinutes;
|
||||
@@ -125,14 +139,29 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function resolveCarryMinutes(Employee $employee, int $year): int
|
||||
/**
|
||||
* @return array{WeekRecoveryDetail, int} [carry, month]
|
||||
*/
|
||||
private function resolveCarry(Employee $employee, int $year): array
|
||||
{
|
||||
$balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $year);
|
||||
if (null !== $balance) {
|
||||
return $balance->getOpeningMinutes();
|
||||
return [
|
||||
new WeekRecoveryDetail(
|
||||
base25Minutes: $balance->getOpeningBase25Minutes(),
|
||||
bonus25Minutes: $balance->getOpeningBonus25Minutes(),
|
||||
base50Minutes: $balance->getOpeningBase50Minutes(),
|
||||
bonus50Minutes: $balance->getOpeningBonus50Minutes(),
|
||||
totalMinutes: $balance->getTotalOpeningMinutes(),
|
||||
),
|
||||
$balance->getMonth(),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $year - 1);
|
||||
return [
|
||||
$this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $year - 1),
|
||||
5,
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveYear(): int
|
||||
|
||||
Reference in New Issue
Block a user