Ajout des notification + page employé (#6)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #6 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #6.
This commit is contained in:
36
src/Service/Contracts/EmployeeContractChangeRequest.php
Normal file
36
src/Service/Contracts/EmployeeContractChangeRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Contracts;
|
||||
|
||||
use App\Enum\ContractNature;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class EmployeeContractChangeRequest
|
||||
{
|
||||
public function __construct(
|
||||
public ?ContractNature $contractNature,
|
||||
public ?DateTimeImmutable $contractStartDate,
|
||||
public ?DateTimeImmutable $contractEndDate,
|
||||
public ?bool $contractPaidLeaveSettled,
|
||||
public ?string $contractComment,
|
||||
) {}
|
||||
|
||||
public function hasPeriodChangeRequest(): bool
|
||||
{
|
||||
return null !== $this->contractNature
|
||||
|| null !== $this->contractStartDate
|
||||
|| null !== $this->contractEndDate
|
||||
|| null !== $this->contractPaidLeaveSettled
|
||||
|| null !== $this->contractComment;
|
||||
}
|
||||
|
||||
public function isCloseOnlyRequest(bool $contractChanged): bool
|
||||
{
|
||||
return !$contractChanged
|
||||
&& null === $this->contractStartDate
|
||||
&& null === $this->contractNature
|
||||
&& null !== $this->contractEndDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Contracts;
|
||||
|
||||
use App\Entity\Employee;
|
||||
use App\Enum\ContractNature;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
final class EmployeeContractChangeRequestFactory
|
||||
{
|
||||
public function fromEmployee(Employee $employee): EmployeeContractChangeRequest
|
||||
{
|
||||
return new EmployeeContractChangeRequest(
|
||||
contractNature: $this->resolveContractNature($employee->getContractNature()),
|
||||
contractStartDate: $this->parseOptionalYmd($employee->getContractStartDate(), 'contractStartDate'),
|
||||
contractEndDate: $this->parseOptionalYmd($employee->getContractEndDate(), 'contractEndDate'),
|
||||
contractPaidLeaveSettled: $employee->getContractPaidLeaveSettled(),
|
||||
contractComment: $employee->getContractComment(),
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveContractNature(?string $raw): ?ContractNature
|
||||
{
|
||||
if (null === $raw || '' === trim($raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ContractNature::tryFrom(trim($raw))
|
||||
?? throw new UnprocessableEntityHttpException('contractNature must be one of CDI, CDD, INTERIM.');
|
||||
}
|
||||
|
||||
private function parseOptionalYmd(?string $raw, string $field): ?DateTimeImmutable
|
||||
{
|
||||
if (null === $raw || '' === trim($raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($raw);
|
||||
$date = DateTimeImmutable::createFromFormat('Y-m-d', $value);
|
||||
if (!$date || $date->format('Y-m-d') !== $value) {
|
||||
throw new UnprocessableEntityHttpException(sprintf('%s must use Y-m-d format.', $field));
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
30
src/Service/Contracts/EmployeeContractPeriodBuilder.php
Normal file
30
src/Service/Contracts/EmployeeContractPeriodBuilder.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Contracts;
|
||||
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Enum\ContractNature;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class EmployeeContractPeriodBuilder
|
||||
{
|
||||
public function build(
|
||||
Employee $employee,
|
||||
Contract $contract,
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
ContractNature $nature,
|
||||
): EmployeeContractPeriod {
|
||||
return new EmployeeContractPeriod()
|
||||
->setEmployee($employee)
|
||||
->setContract($contract)
|
||||
->setStartDate($startDate)
|
||||
->setEndDate($endDate)
|
||||
->setContractNature($nature)
|
||||
;
|
||||
}
|
||||
}
|
||||
98
src/Service/Contracts/EmployeeContractPeriodManager.php
Normal file
98
src/Service/Contracts/EmployeeContractPeriodManager.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Contracts;
|
||||
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Enum\ContractNature;
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
final readonly class EmployeeContractPeriodManager implements EmployeeContractPeriodManagerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private EmployeeContractPeriodRepository $periodRepository,
|
||||
private EmployeeContractPeriodBuilder $periodBuilder,
|
||||
private EmployeeContractPeriodValidator $periodValidator,
|
||||
) {}
|
||||
|
||||
public function ensureContractPeriodExists(
|
||||
Employee $employee,
|
||||
Contract $contract,
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
ContractNature $nature,
|
||||
): void {
|
||||
$this->periodValidator->assertPeriodDates($startDate, $endDate, $nature);
|
||||
|
||||
$covered = $this->periodRepository->findOneCoveringDate($employee, $startDate);
|
||||
if (null !== $covered) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function closeCurrentPeriod(
|
||||
?EmployeeContractPeriod $todayPeriod,
|
||||
DateTimeImmutable $requestedEndDate,
|
||||
bool $paidLeaveSettled,
|
||||
?string $comment = null
|
||||
): void {
|
||||
if (null === $todayPeriod) {
|
||||
throw new UnprocessableEntityHttpException('No active contract period to close.');
|
||||
}
|
||||
|
||||
$this->periodValidator->assertCloseEndDateCanBeApplied(
|
||||
$todayPeriod->getStartDate(),
|
||||
$todayPeriod->getEndDate(),
|
||||
$requestedEndDate,
|
||||
$todayPeriod->getContractNatureEnum()
|
||||
);
|
||||
|
||||
$todayPeriod->setEndDate($requestedEndDate);
|
||||
$todayPeriod->setPaidLeaveSettled($paidLeaveSettled);
|
||||
$todayPeriod->setComment($comment);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function createNextPeriod(
|
||||
Employee $employee,
|
||||
Contract $contract,
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
ContractNature $nature,
|
||||
?EmployeeContractPeriod $todayPeriod
|
||||
): void {
|
||||
$this->periodValidator->assertPeriodDates($startDate, $endDate, $nature);
|
||||
|
||||
if (null !== $todayPeriod) {
|
||||
$this->periodValidator->assertNextStartDateCompatible($startDate, $todayPeriod);
|
||||
|
||||
if (null === $todayPeriod->getEndDate()) {
|
||||
$todayPeriod->setEndDate($startDate->modify('-1 day'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
private function persistNewPeriod(
|
||||
Employee $employee,
|
||||
Contract $contract,
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
ContractNature $nature,
|
||||
): void {
|
||||
$period = $this->periodBuilder->build($employee, $contract, $startDate, $endDate, $nature);
|
||||
$this->entityManager->persist($period);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Contracts;
|
||||
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Enum\ContractNature;
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface EmployeeContractPeriodManagerInterface
|
||||
{
|
||||
public function ensureContractPeriodExists(
|
||||
Employee $employee,
|
||||
Contract $contract,
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
ContractNature $nature,
|
||||
): void;
|
||||
|
||||
public function closeCurrentPeriod(
|
||||
?EmployeeContractPeriod $todayPeriod,
|
||||
DateTimeImmutable $requestedEndDate,
|
||||
bool $paidLeaveSettled,
|
||||
?string $comment = null
|
||||
): void;
|
||||
|
||||
public function createNextPeriod(
|
||||
Employee $employee,
|
||||
Contract $contract,
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
ContractNature $nature,
|
||||
?EmployeeContractPeriod $todayPeriod
|
||||
): void;
|
||||
}
|
||||
63
src/Service/Contracts/EmployeeContractPeriodValidator.php
Normal file
63
src/Service/Contracts/EmployeeContractPeriodValidator.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Contracts;
|
||||
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Enum\ContractNature;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
final class EmployeeContractPeriodValidator
|
||||
{
|
||||
public function assertPeriodDates(
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $endDate,
|
||||
ContractNature $nature,
|
||||
bool $allowCdiEndDate = false
|
||||
): void {
|
||||
if (null !== $endDate && $endDate < $startDate) {
|
||||
throw new UnprocessableEntityHttpException('contractEndDate cannot be before contractStartDate.');
|
||||
}
|
||||
|
||||
if ($nature->requiresEndDate() && null === $endDate) {
|
||||
throw new UnprocessableEntityHttpException('contractEndDate is required for CDD and INTERIM.');
|
||||
}
|
||||
|
||||
if (!$allowCdiEndDate && ContractNature::CDI === $nature && null !== $endDate) {
|
||||
throw new UnprocessableEntityHttpException('contractEndDate must be empty for CDI.');
|
||||
}
|
||||
}
|
||||
|
||||
public function assertCloseEndDateCanBeApplied(
|
||||
DateTimeImmutable $startDate,
|
||||
?DateTimeImmutable $currentEndDate,
|
||||
DateTimeImmutable $requestedEndDate,
|
||||
ContractNature $nature
|
||||
): void {
|
||||
$this->assertPeriodDates($startDate, $requestedEndDate, $nature, true);
|
||||
|
||||
if (null !== $currentEndDate && $requestedEndDate > $currentEndDate) {
|
||||
throw new UnprocessableEntityHttpException('contractEndDate cannot be increased on current contract.');
|
||||
}
|
||||
}
|
||||
|
||||
public function assertNextStartDateCompatible(
|
||||
DateTimeImmutable $startDate,
|
||||
EmployeeContractPeriod $currentPeriod
|
||||
): void {
|
||||
$currentEndDate = $currentPeriod->getEndDate();
|
||||
if (null === $currentEndDate) {
|
||||
if ($startDate <= $currentPeriod->getStartDate()) {
|
||||
throw new UnprocessableEntityHttpException('contractStartDate must be after current contract start date.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($startDate <= $currentEndDate) {
|
||||
throw new UnprocessableEntityHttpException('contractStartDate must be after current contract end date.');
|
||||
}
|
||||
}
|
||||
}
|
||||
396
src/Service/Leave/LeaveBalanceComputationService.php
Normal file
396
src/Service/Leave/LeaveBalanceComputationService.php
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Leave;
|
||||
|
||||
use App\Entity\Absence;
|
||||
use App\Entity\Employee;
|
||||
use App\Enum\LeaveRuleCode;
|
||||
use App\Repository\AbsenceRepository;
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use App\Repository\EmployeeLeaveBalanceRepository;
|
||||
use App\Service\PublicHolidayServiceInterface;
|
||||
use DateTimeImmutable;
|
||||
use Throwable;
|
||||
|
||||
final readonly class LeaveBalanceComputationService
|
||||
{
|
||||
private const int FORFAIT_TARGET_WORKED_DAYS = 218;
|
||||
private const float STANDARD_ANNUAL_DAYS = 25.0;
|
||||
private const float STANDARD_ANNUAL_SATURDAYS = 5.0;
|
||||
private const float STANDARD_ACCRUAL_PER_MONTH = self::STANDARD_ANNUAL_DAYS / 12.0;
|
||||
private const float STANDARD_SATURDAY_ACCRUAL_PER_MONTH = self::STANDARD_ANNUAL_SATURDAYS / 12.0;
|
||||
private const float FOUR_HOUR_ANNUAL_DAYS = 10.0;
|
||||
private const float FOUR_HOUR_ACCRUAL_PER_MONTH = 0.83;
|
||||
|
||||
public function __construct(
|
||||
private AbsenceRepository $absenceRepository,
|
||||
private EmployeeContractPeriodRepository $periodRepository,
|
||||
private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
|
||||
private PublicHolidayServiceInterface $publicHolidayService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{float, float}
|
||||
*/
|
||||
public function computeDynamicClosingForYear(Employee $employee, LeaveRuleCode $ruleCode, int $targetYear): array
|
||||
{
|
||||
$firstYear = $this->resolveFirstComputationYear($employee, $ruleCode, $targetYear);
|
||||
if ($targetYear < $firstYear) {
|
||||
return [0.0, 0.0];
|
||||
}
|
||||
|
||||
$previousRemainingDays = 0.0;
|
||||
$previousRemainingSaturdays = 0.0;
|
||||
|
||||
for ($year = $firstYear; $year <= $targetYear; ++$year) {
|
||||
[$from, $to] = $this->resolvePeriodBounds($ruleCode, $year);
|
||||
|
||||
$carryDays = 0.0;
|
||||
$carrySaturdays = 0.0;
|
||||
if ($year > $firstYear) {
|
||||
[$previousFrom, $previousTo] = $this->resolvePeriodBounds($ruleCode, $year - 1);
|
||||
$hasSettlementOnPreviousYear = $this->periodRepository->hasPaidLeaveSettledClosureBetween($employee, $previousFrom, $previousTo);
|
||||
if (!$hasSettlementOnPreviousYear) {
|
||||
$carryDays = $previousRemainingDays;
|
||||
$carrySaturdays = LeaveRuleCode::CDI_CDD_NON_FORFAIT === $ruleCode ? $previousRemainingSaturdays : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
$effectiveFrom = $this->resolveEffectivePeriodStart($employee, $from, $to);
|
||||
if ($effectiveFrom > $from) {
|
||||
$carryDays = 0.0;
|
||||
$carrySaturdays = 0.0;
|
||||
}
|
||||
|
||||
$fractionedDays = $this->resolveFractionedDays($employee, $ruleCode, $year);
|
||||
|
||||
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
|
||||
$acquiredDays = $carryDays + (float) max(0, $this->countBusinessDays($from, $to) - self::FORFAIT_TARGET_WORKED_DAYS) + $fractionedDays;
|
||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
|
||||
[$takenDays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, false, false);
|
||||
$previousRemainingDays = max(0.0, $acquiredDays - $takenDays);
|
||||
$previousRemainingSaturdays = 0.0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$generatedDays = $this->computeAccruedDays(
|
||||
$this->resolveAnnualDays($employee),
|
||||
$this->resolveDaysAccrualPerMonth($employee),
|
||||
$effectiveFrom,
|
||||
$to
|
||||
);
|
||||
$generatedSaturdays = $this->computeAccruedDays(
|
||||
$this->resolveAnnualSaturdays($employee),
|
||||
$this->resolveSaturdayAccrualPerMonth($employee),
|
||||
$effectiveFrom,
|
||||
$to
|
||||
);
|
||||
|
||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
|
||||
[$takenDays, $takenSaturdays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, true, true);
|
||||
|
||||
$acquiredWithFractioned = $carryDays + $fractionedDays;
|
||||
$takenFromAcquired = min(max(0.0, $acquiredWithFractioned), $takenDays);
|
||||
$remainingAcquired = $acquiredWithFractioned - $takenFromAcquired;
|
||||
$remainingToImpute = max(0.0, $takenDays - $takenFromAcquired);
|
||||
$remainingGenerated = $generatedDays - $remainingToImpute;
|
||||
|
||||
$takenFromAcquiredSaturdays = min(max(0.0, $carrySaturdays), $takenSaturdays);
|
||||
$remainingAcquiredSaturdays = $carrySaturdays - $takenFromAcquiredSaturdays;
|
||||
$remainingSaturdaysToImpute = max(0.0, $takenSaturdays - $takenFromAcquiredSaturdays);
|
||||
$remainingGeneratedSaturdays = $generatedSaturdays - $remainingSaturdaysToImpute;
|
||||
|
||||
$previousRemainingDays = $remainingAcquired + $remainingGenerated;
|
||||
$previousRemainingSaturdays = $remainingAcquiredSaturdays + $remainingGeneratedSaturdays;
|
||||
}
|
||||
|
||||
return [$previousRemainingDays, $previousRemainingSaturdays];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{DateTimeImmutable, DateTimeImmutable}
|
||||
*/
|
||||
public function resolvePeriodBounds(LeaveRuleCode $ruleCode, int $year): array
|
||||
{
|
||||
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
|
||||
return [
|
||||
new DateTimeImmutable(sprintf('%d-01-01', $year)),
|
||||
new DateTimeImmutable(sprintf('%d-12-31', $year)),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
new DateTimeImmutable(sprintf('%d-06-01', $year - 1)),
|
||||
new DateTimeImmutable(sprintf('%d-05-31', $year)),
|
||||
];
|
||||
}
|
||||
|
||||
public function hasPaidLeaveSettledClosureBetween(
|
||||
Employee $employee,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to
|
||||
): bool {
|
||||
return $this->periodRepository->hasPaidLeaveSettledClosureBetween($employee, $from, $to);
|
||||
}
|
||||
|
||||
private function resolveFirstComputationYear(Employee $employee, LeaveRuleCode $ruleCode, int $fallbackYear): int
|
||||
{
|
||||
$history = $employee->getContractHistory();
|
||||
if ([] === $history) {
|
||||
return $fallbackYear;
|
||||
}
|
||||
|
||||
$oldestStartDate = null;
|
||||
foreach ($history as $item) {
|
||||
$start = DateTimeImmutable::createFromFormat('Y-m-d', $item->startDate);
|
||||
if (!$start) {
|
||||
continue;
|
||||
}
|
||||
if (null === $oldestStartDate || $start < $oldestStartDate) {
|
||||
$oldestStartDate = $start;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $oldestStartDate) {
|
||||
return $fallbackYear;
|
||||
}
|
||||
|
||||
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
|
||||
return (int) $oldestStartDate->format('Y');
|
||||
}
|
||||
|
||||
$startYear = (int) $oldestStartDate->format('Y');
|
||||
$startMonth = (int) $oldestStartDate->format('n');
|
||||
|
||||
return $startMonth >= 6 ? $startYear + 1 : $startYear;
|
||||
}
|
||||
|
||||
private function resolveEffectivePeriodStart(
|
||||
Employee $employee,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to
|
||||
): DateTimeImmutable {
|
||||
$latestSettledClosure = $this->periodRepository->findLatestPaidLeaveSettledClosureDateBetween($employee, $from, $to);
|
||||
$start = $from;
|
||||
if (null !== $latestSettledClosure) {
|
||||
$nextDay = $latestSettledClosure->modify('+1 day');
|
||||
if ($nextDay > $start) {
|
||||
$start = $nextDay;
|
||||
}
|
||||
}
|
||||
|
||||
$earliestContractStart = $this->resolveEarliestContractStartWithinRange($employee, $from, $to);
|
||||
if (null !== $earliestContractStart && $earliestContractStart > $start) {
|
||||
$start = $earliestContractStart;
|
||||
}
|
||||
|
||||
return $start;
|
||||
}
|
||||
|
||||
private function resolveEarliestContractStartWithinRange(
|
||||
Employee $employee,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to
|
||||
): ?DateTimeImmutable {
|
||||
$earliest = null;
|
||||
foreach ($employee->getContractHistory() as $period) {
|
||||
$start = DateTimeImmutable::createFromFormat('Y-m-d', $period->startDate);
|
||||
if (!$start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$end = null;
|
||||
if (null !== $period->endDate && '' !== trim($period->endDate)) {
|
||||
$end = DateTimeImmutable::createFromFormat('Y-m-d', $period->endDate);
|
||||
}
|
||||
|
||||
if ($start > $to) {
|
||||
continue;
|
||||
}
|
||||
if ($end instanceof DateTimeImmutable && $end < $from) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidate = $start < $from ? $from : $start;
|
||||
if (null === $earliest || $candidate < $earliest) {
|
||||
$earliest = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $earliest;
|
||||
}
|
||||
|
||||
private function resolveFractionedDays(Employee $employee, LeaveRuleCode $ruleCode, int $year): float
|
||||
{
|
||||
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
|
||||
|
||||
return null !== $balance ? $balance->getFractionedDays() : 0.0;
|
||||
}
|
||||
|
||||
private function resolveAnnualDays(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? self::FOUR_HOUR_ANNUAL_DAYS
|
||||
: self::STANDARD_ANNUAL_DAYS;
|
||||
}
|
||||
|
||||
private function resolveAnnualSaturdays(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? 0.0
|
||||
: self::STANDARD_ANNUAL_SATURDAYS;
|
||||
}
|
||||
|
||||
private function resolveDaysAccrualPerMonth(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? self::FOUR_HOUR_ACCRUAL_PER_MONTH
|
||||
: self::STANDARD_ACCRUAL_PER_MONTH;
|
||||
}
|
||||
|
||||
private function resolveSaturdayAccrualPerMonth(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
? 0.0
|
||||
: self::STANDARD_SATURDAY_ACCRUAL_PER_MONTH;
|
||||
}
|
||||
|
||||
private function computeAccruedDays(
|
||||
float $annualCap,
|
||||
float $accrualPerMonth,
|
||||
DateTimeImmutable $periodStart,
|
||||
DateTimeImmutable $periodEnd
|
||||
): float {
|
||||
if ($accrualPerMonth <= 0.0 || $periodEnd < $periodStart) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$monthsElapsed = ((int) $periodEnd->format('Y') - (int) $periodStart->format('Y')) * 12
|
||||
+ ((int) $periodEnd->format('n') - (int) $periodStart->format('n'))
|
||||
+ 1;
|
||||
|
||||
return min($annualCap, $monthsElapsed * $accrualPerMonth);
|
||||
}
|
||||
|
||||
private function countBusinessDays(DateTimeImmutable $from, DateTimeImmutable $to): int
|
||||
{
|
||||
$publicHolidays = $this->buildPublicHolidayMap($from, $to);
|
||||
$count = 0;
|
||||
for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
|
||||
$weekDay = (int) $cursor->format('N');
|
||||
$dayKey = $cursor->format('Y-m-d');
|
||||
if ($weekDay <= 5 && !isset($publicHolidays[$dayKey])) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Absence> $absences
|
||||
*
|
||||
* @return array{float, float}
|
||||
*/
|
||||
private function computeTakenAbsences(
|
||||
array $absences,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to,
|
||||
bool $countOnlyCp,
|
||||
bool $splitSaturdays
|
||||
): array {
|
||||
$takenDays = 0.0;
|
||||
$takenSaturdays = 0.0;
|
||||
|
||||
foreach ($absences as $absence) {
|
||||
if ($countOnlyCp) {
|
||||
$typeCode = strtoupper((string) $absence->getType()?->getCode());
|
||||
if ('C' !== $typeCode) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $absence->getType()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = DateTimeImmutable::createFromInterface($absence->getStartDate());
|
||||
$end = DateTimeImmutable::createFromInterface($absence->getEndDate());
|
||||
$rangeStart = $start < $from ? $from : $start;
|
||||
$rangeEnd = $end > $to ? $to : $end;
|
||||
if ($rangeEnd < $rangeStart) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($cursor = $rangeStart; $cursor <= $rangeEnd; $cursor = $cursor->modify('+1 day')) {
|
||||
[$am, $pm] = $this->resolveSegmentsForDate($absence, $cursor->format('Y-m-d'));
|
||||
$dayAmount = ($am ? 0.5 : 0.0) + ($pm ? 0.5 : 0.0);
|
||||
if ($dayAmount <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isSaturday = $splitSaturdays && '6' === $cursor->format('N');
|
||||
if ($isSaturday) {
|
||||
$takenSaturdays += $dayAmount;
|
||||
} else {
|
||||
$takenDays += $dayAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$takenDays, $takenSaturdays];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{bool, bool}
|
||||
*/
|
||||
private function resolveSegmentsForDate(Absence $absence, string $date): array
|
||||
{
|
||||
$startYmd = DateTimeImmutable::createFromInterface($absence->getStartDate())->format('Y-m-d');
|
||||
$endYmd = DateTimeImmutable::createFromInterface($absence->getEndDate())->format('Y-m-d');
|
||||
$startHalf = $absence->getStartHalf()->value;
|
||||
$endHalf = $absence->getEndHalf()->value;
|
||||
|
||||
$isSingleDay = $startYmd === $endYmd;
|
||||
$isStartDay = $date === $startYmd;
|
||||
$isEndDay = $date === $endYmd;
|
||||
|
||||
if ($isSingleDay) {
|
||||
return ['AM' === $startHalf, 'PM' === $endHalf];
|
||||
}
|
||||
if ($isStartDay) {
|
||||
return ['AM' === $startHalf, true];
|
||||
}
|
||||
if ($isEndDay) {
|
||||
return [true, 'PM' === $endHalf];
|
||||
}
|
||||
|
||||
return [true, true];
|
||||
}
|
||||
}
|
||||
377
src/Service/Rtt/RttRecoveryComputationService.php
Normal file
377
src/Service/Rtt/RttRecoveryComputationService.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Rtt;
|
||||
|
||||
use App\Dto\WorkHours\WorkMetrics;
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\WorkHour;
|
||||
use App\Enum\ContractNature;
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\TrackingMode;
|
||||
use App\Repository\AbsenceRepository;
|
||||
use App\Repository\WorkHourRepository;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class RttRecoveryComputationService
|
||||
{
|
||||
public function __construct(
|
||||
private WorkHourRepository $workHourRepository,
|
||||
private AbsenceRepository $absenceRepository,
|
||||
private AbsenceSegmentsResolver $absenceSegmentsResolver,
|
||||
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
|
||||
private EmployeeContractResolver $contractResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{DateTimeImmutable, DateTimeImmutable}
|
||||
*/
|
||||
public function resolveExerciseBounds(int $exerciseYear): array
|
||||
{
|
||||
return [
|
||||
new DateTimeImmutable(sprintf('%d-06-01', $exerciseYear - 1)),
|
||||
new DateTimeImmutable(sprintf('%d-05-31', $exerciseYear)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{month:int,weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}>
|
||||
*/
|
||||
public function buildWeeksForExercise(DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||
{
|
||||
$dayOfWeek = (int) $from->format('N');
|
||||
$weekStart = $from->modify(sprintf('-%d days', $dayOfWeek - 1));
|
||||
|
||||
$weeks = [];
|
||||
while ($weekStart <= $to) {
|
||||
$start = $weekStart;
|
||||
$end = $start->modify('+6 days');
|
||||
$effectiveStart = $start < $from ? $from : $start;
|
||||
$effectiveEnd = $end > $to ? $to : $end;
|
||||
|
||||
if ($effectiveEnd >= $effectiveStart) {
|
||||
$saturday = $start->modify('+5 days');
|
||||
$monthAnchor = $saturday < $from ? $from : ($saturday > $to ? $to : $saturday);
|
||||
$weeks[] = [
|
||||
'month' => (int) $monthAnchor->format('n'),
|
||||
'weekNumber' => (int) $effectiveStart->format('W'),
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
];
|
||||
}
|
||||
$weekStart = $weekStart->modify('+7 days');
|
||||
}
|
||||
|
||||
return $weeks;
|
||||
}
|
||||
|
||||
public function computeTotalRecoveryForExercise(Employee $employee, int $exerciseYear): int
|
||||
{
|
||||
[$from, $to] = $this->resolveExerciseBounds($exerciseYear);
|
||||
$weeks = $this->buildWeeksForExercise($from, $to);
|
||||
$weekRanges = array_map(
|
||||
static fn (array $week): array => [
|
||||
'month' => (int) $week['month'],
|
||||
'weekNumber' => (int) $week['weekNumber'],
|
||||
'start' => $week['start'],
|
||||
'end' => $week['end'],
|
||||
],
|
||||
$weeks
|
||||
);
|
||||
|
||||
$byWeek = $this->computeRecoveryByWeek($employee, $weekRanges, $from, $to, null);
|
||||
|
||||
return array_sum($byWeek);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{month:int,weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}> $weeks
|
||||
*
|
||||
* @return array<string, int>
|
||||
*/
|
||||
public function computeRecoveryByWeek(
|
||||
Employee $employee,
|
||||
array $weeks,
|
||||
DateTimeImmutable $periodFrom,
|
||||
DateTimeImmutable $periodTo,
|
||||
?DateTimeImmutable $limitDate
|
||||
): array {
|
||||
if ([] === $weeks) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$days = [];
|
||||
for ($cursor = $periodFrom; $cursor <= $periodTo; $cursor = $cursor->modify('+1 day')) {
|
||||
$days[] = $cursor->format('Y-m-d');
|
||||
}
|
||||
|
||||
$contractsByDate = $this->contractResolver->resolveForEmployeesAndDays([$employee], $days);
|
||||
$naturesByDate = $this->contractResolver->resolveNaturesForEmployeesAndDays([$employee], $days);
|
||||
$employeeId = (int) $employee->getId();
|
||||
|
||||
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($periodFrom, $periodTo, [$employee]);
|
||||
$absences = $this->absenceRepository->findForPrint($periodFrom, $periodTo, [$employee]);
|
||||
|
||||
$metricsByDate = [];
|
||||
foreach ($workHours as $workHour) {
|
||||
$dateKey = $workHour->getWorkDate()->format('Y-m-d');
|
||||
$metricsByDate[$dateKey] = $this->computeMetrics($workHour);
|
||||
}
|
||||
|
||||
$creditedByDate = [];
|
||||
foreach ($absences as $absence) {
|
||||
$start = $absence->getStartDate()->format('Y-m-d');
|
||||
$end = $absence->getEndDate()->format('Y-m-d');
|
||||
for ($cursor = $periodFrom; $cursor <= $periodTo; $cursor = $cursor->modify('+1 day')) {
|
||||
$date = $cursor->format('Y-m-d');
|
||||
if ($date < $start || $date > $end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$absentMorning, $absentAfternoon] = $this->absenceSegmentsResolver->resolveForDate($absence, $date);
|
||||
$creditedByDate[$date] = ($creditedByDate[$date] ?? 0)
|
||||
+ $this->workedHoursCreditPolicy->computeCreditedMinutes($absence, $date, $absentMorning, $absentAfternoon);
|
||||
}
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($weeks as $week) {
|
||||
$weekStart = $week['start'];
|
||||
$weekEnd = $week['end'];
|
||||
$weekKey = $weekStart->format('Y-m-d');
|
||||
$effectiveStart = $weekStart < $periodFrom ? $periodFrom : $weekStart;
|
||||
$effectiveEnd = $weekEnd > $periodTo ? $periodTo : $weekEnd;
|
||||
|
||||
if ($effectiveEnd < $effectiveStart) {
|
||||
$results[$weekKey] = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($limitDate instanceof DateTimeImmutable && $effectiveStart > $limitDate) {
|
||||
$results[$weekKey] = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$weekDays = [];
|
||||
for ($cursor = $effectiveStart; $cursor <= $effectiveEnd; $cursor = $cursor->modify('+1 day')) {
|
||||
$weekDays[] = $cursor->format('Y-m-d');
|
||||
}
|
||||
|
||||
$weeklyTotalMinutes = 0;
|
||||
$employeeContractsByDate = [];
|
||||
foreach ($weekDays as $date) {
|
||||
$employeeContractsByDate[$date] = $contractsByDate[$employeeId][$date] ?? null;
|
||||
if ($limitDate instanceof DateTimeImmutable && new DateTimeImmutable($date) > $limitDate) {
|
||||
continue;
|
||||
}
|
||||
$metrics = $metricsByDate[$date] ?? new WorkMetrics();
|
||||
$metrics->addCreditedMinutes($creditedByDate[$date] ?? 0);
|
||||
$weeklyTotalMinutes += $metrics->totalMinutes;
|
||||
}
|
||||
|
||||
if ([] === $weekDays) {
|
||||
$results[$weekKey] = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$weekAnchorNature = $naturesByDate[$employeeId][$weekDays[0]] ?? ContractNature::CDI;
|
||||
$weekAnchorContract = $employeeContractsByDate[$weekDays[0]] ?? null;
|
||||
$isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode();
|
||||
$disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorNature);
|
||||
$overtimeReferenceMinutes = $this->computeWeeklyOvertimeReferenceMinutes($weekDays, $employeeContractsByDate);
|
||||
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate);
|
||||
$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;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function computeMetrics(WorkHour $workHour): WorkMetrics
|
||||
{
|
||||
$ranges = [
|
||||
[$workHour->getMorningFrom(), $workHour->getMorningTo()],
|
||||
[$workHour->getAfternoonFrom(), $workHour->getAfternoonTo()],
|
||||
[$workHour->getEveningFrom(), $workHour->getEveningTo()],
|
||||
];
|
||||
|
||||
$totalMinutes = 0;
|
||||
$nightMinutes = 0;
|
||||
foreach ($ranges as [$from, $to]) {
|
||||
$totalMinutes += $this->intervalMinutes($from, $to);
|
||||
$nightMinutes += $this->nightIntervalMinutes($from, $to);
|
||||
}
|
||||
|
||||
$dayMinutes = max(0, $totalMinutes - $nightMinutes);
|
||||
|
||||
return new WorkMetrics(
|
||||
dayMinutes: $dayMinutes,
|
||||
nightMinutes: $nightMinutes,
|
||||
totalMinutes: $totalMinutes,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|array{int, int}
|
||||
*/
|
||||
private function resolveInterval(?string $from, ?string $to): ?array
|
||||
{
|
||||
$fromMinutes = $this->toMinutes($from);
|
||||
$toMinutes = $this->toMinutes($to);
|
||||
if (null === $fromMinutes || null === $toMinutes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$end = $toMinutes <= $fromMinutes ? $toMinutes + 1440 : $toMinutes;
|
||||
|
||||
return [$fromMinutes, $end];
|
||||
}
|
||||
|
||||
private function toMinutes(?string $time): ?int
|
||||
{
|
||||
if (null === $time || '' === $time) {
|
||||
return null;
|
||||
}
|
||||
[$hours, $minutes] = array_map('intval', explode(':', $time));
|
||||
|
||||
return ($hours * 60) + $minutes;
|
||||
}
|
||||
|
||||
private function intervalMinutes(?string $from, ?string $to): int
|
||||
{
|
||||
$interval = $this->resolveInterval($from, $to);
|
||||
if (null === $interval) {
|
||||
return 0;
|
||||
}
|
||||
[$start, $end] = $interval;
|
||||
|
||||
return max(0, $end - $start);
|
||||
}
|
||||
|
||||
private function nightIntervalMinutes(?string $from, ?string $to): int
|
||||
{
|
||||
$interval = $this->resolveInterval($from, $to);
|
||||
if (null === $interval) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
[$start, $end] = $interval;
|
||||
$windows = [[0, 360], [1260, 1440]];
|
||||
$total = 0;
|
||||
|
||||
for ($dayOffset = 0; $dayOffset <= 1; ++$dayOffset) {
|
||||
$shift = $dayOffset * 1440;
|
||||
foreach ($windows as [$windowStart, $windowEnd]) {
|
||||
$total += $this->overlap($start, $end, $windowStart + $shift, $windowEnd + $shift);
|
||||
}
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function overlap(int $startA, int $endA, int $startB, int $endB): int
|
||||
{
|
||||
$start = max($startA, $startB);
|
||||
$end = min($endA, $endB);
|
||||
|
||||
return max(0, $end - $start);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $days
|
||||
* @param array<string, ?Contract> $contractsByDate
|
||||
*/
|
||||
private function computeWeeklyOvertimeReferenceMinutes(array $days, array $contractsByDate): int
|
||||
{
|
||||
$total = 0;
|
||||
foreach ($days as $date) {
|
||||
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
||||
$contract = $contractsByDate[$date] ?? null;
|
||||
$hours = $contract?->getWeeklyHours();
|
||||
$referenceHours = (null !== $hours && $hours > 0) ? max(35, $hours) : null;
|
||||
$total += $this->resolveDailyReferenceMinutes($referenceHours, $isoDay);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $days
|
||||
* @param array<string, ?Contract> $contractsByDate
|
||||
*/
|
||||
private function computeWeeklyOvertime25StartMinutes(array $days, array $contractsByDate): int
|
||||
{
|
||||
$total = 0;
|
||||
foreach ($days as $date) {
|
||||
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
||||
$contract = $contractsByDate[$date] ?? null;
|
||||
$hours = $contract?->getWeeklyHours();
|
||||
$startHours = (null !== $hours && $hours >= 39) ? 39 : 35;
|
||||
$total += $this->resolveDailyReferenceMinutes($startHours, $isoDay);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function computeOvertime25BonusMinutes(int $weeklyTotalMinutes, int $startMinutes): int
|
||||
{
|
||||
$trancheMinutes = max(0, min($weeklyTotalMinutes, 43 * 60) - $startMinutes);
|
||||
|
||||
return (int) round($trancheMinutes * 0.25);
|
||||
}
|
||||
|
||||
private function computeOvertime50BonusMinutes(int $weeklyTotalMinutes): int
|
||||
{
|
||||
$trancheMinutes = max(0, $weeklyTotalMinutes - (43 * 60));
|
||||
|
||||
return (int) round($trancheMinutes * 0.5);
|
||||
}
|
||||
|
||||
private function hasDisabledOvertimeBonuses(?Contract $contract, ContractNature $contractNature): bool
|
||||
{
|
||||
if (ContractNature::INTERIM === $contractNature) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$type = ContractType::resolve(
|
||||
$contract?->getName(),
|
||||
$contract?->getTrackingMode(),
|
||||
$contract?->getWeeklyHours()
|
||||
);
|
||||
|
||||
return ContractType::INTERIM === $type;
|
||||
}
|
||||
|
||||
private function resolveDailyReferenceMinutes(?int $weeklyHours, int $isoWeekDay): int
|
||||
{
|
||||
if ($isoWeekDay >= 6 || null === $weeklyHours || $weeklyHours <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (39 === $weeklyHours) {
|
||||
return 5 === $isoWeekDay ? 7 * 60 : 8 * 60;
|
||||
}
|
||||
if (35 === $weeklyHours) {
|
||||
return 7 * 60;
|
||||
}
|
||||
|
||||
return (int) round(($weeklyHours * 60) / 5);
|
||||
}
|
||||
}
|
||||
@@ -69,13 +69,8 @@ final readonly class WorkedHoursCreditPolicy
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Règle forfait:
|
||||
// - demi-journée d'absence => 0.5 travaillé
|
||||
// - journée complète d'absence => 0 travaillé
|
||||
if ($absentMorning xor $absentAfternoon) {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
// Règle forfait: les absences ne créditent jamais de présence.
|
||||
// Seules les checkboxes cochées par l'employé comptent.
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user