[#339] Ajout d'une page listant les règles de calcules (#5)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled

| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|        #339          |        Ajout d'une page listant les règles de calcules         |

## Description de la PR
[#339] Ajout d'une page listant les règles de calcules

## Modification du .env

## Check list

- [ ] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #5
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #5.
This commit is contained in:
2026-02-20 16:17:22 +00:00
committed by Autin
parent 49fecfc27a
commit f8ca5e50a0
16 changed files with 664 additions and 59 deletions

View File

@@ -11,6 +11,7 @@ use App\Dto\WorkHours\WeeklyDaySummary;
use App\Dto\WorkHours\WeeklySummaryRow;
use App\Dto\WorkHours\WorkMetrics;
use App\Entity\Absence;
use App\Entity\Contract;
use App\Entity\Employee;
use App\Entity\User;
use App\Entity\WorkHour;
@@ -19,6 +20,7 @@ use App\Enum\TrackingMode;
use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
use DateTimeImmutable;
@@ -37,6 +39,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
private AbsenceReadRepositoryInterface $absenceRepository,
private AbsenceSegmentsResolver $absenceSegmentsResolver,
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
private EmployeeContractResolver $contractResolver,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
@@ -58,7 +61,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$summary->weekStart = $weekStart->format('Y-m-d');
$summary->weekEnd = $weekEnd->format('Y-m-d');
$summary->days = $days;
$summary->rows = $this->buildRows($employees, $workHours, $absences, $days);
$summary->rows = $this->buildRows($employees, $workHours, $absences, $days, $anchorDate->format('Y-m-d'));
return $summary;
}
@@ -108,9 +111,10 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
*
* @return list<WeeklySummaryRow>
*/
private function buildRows(array $employees, array $workHours, array $absences, array $days): array
private function buildRows(array $employees, array $workHours, array $absences, array $days, string $anchorDateYmd): array
{
$metricsByEmployeeDate = [];
$contractsByEmployeeDate = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
$metricsByEmployeeDate = [];
foreach ($workHours as $workHour) {
$employeeId = $workHour->getEmployee()?->getId();
if (!$employeeId) {
@@ -158,7 +162,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$creditedByEmployeeDate[$employeeId][$date] = ($creditedByEmployeeDate[$employeeId][$date] ?? 0)
+ $this->workedHoursCreditPolicy->computeCreditedMinutes($absence, $date, $absentMorning, $absentAfternoon);
$creditedPresenceByEmployeeDate[$employeeId][$date] = ($creditedPresenceByEmployeeDate[$employeeId][$date] ?? 0.0)
+ $this->workedHoursCreditPolicy->computeCreditedPresenceUnits($absence, $absentMorning, $absentAfternoon);
+ $this->workedHoursCreditPolicy->computeCreditedPresenceUnits($absence, $date, $absentMorning, $absentAfternoon);
}
}
@@ -175,12 +179,20 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$weeklyPresenceCount = 0.0;
$daily = [];
// Les contrats au suivi "présence" ne manipulent pas les heures, mais des demi-journées.
$isPresenceTracking = TrackingMode::PRESENCE->value === $employee->getContract()?->getTrackingMode();
$weekAnchorContract = $contractsByEmployeeDate[$employeeId][$anchorDateYmd]
?? $contractsByEmployeeDate[$employeeId][$days[0]]
?? null;
$employeeContractsByDate = [];
foreach ($days as $date) {
$employeeContractsByDate[$date] = $contractsByEmployeeDate[$employeeId][$date] ?? null;
}
foreach ($days as $date) {
$entry = $metricsByEmployeeDate[$employeeId][$date] ?? null;
$metrics = $entry['metrics'] ?? new WorkMetrics();
$creditedMinutes = $creditedByEmployeeDate[$employeeId][$date] ?? 0;
$entry = $metricsByEmployeeDate[$employeeId][$date] ?? null;
$metrics = $entry['metrics'] ?? new WorkMetrics();
$creditedMinutes = $creditedByEmployeeDate[$employeeId][$date] ?? 0;
$contractAtDate = $employeeContractsByDate[$date] ?? null;
$isPresenceTracking = TrackingMode::PRESENCE->value === $contractAtDate?->getTrackingMode();
// Les absences "comptées comme travaillées" alimentent le total du jour.
$metrics->addCreditedMinutes($creditedMinutes);
$present = null;
@@ -210,18 +222,20 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
);
}
$contractWeeklyHours = $employee->getContract()?->getWeeklyHours();
$disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($employee);
$weeklyOvertimeTotalMinutes = $isPresenceTracking
$isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode();
$disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract);
$overtimeReferenceMinutes = $this->computeWeeklyOvertimeReferenceMinutes($days, $employeeContractsByDate);
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($days, $employeeContractsByDate);
$weeklyOvertimeTotalMinutes = $isWeekPresenceTracking
? 0
: $this->computeOvertimeTotalMinutes($weeklyTotalMinutes, $contractWeeklyHours);
$weeklyOvertime25Minutes = ($isPresenceTracking || $disableOvertimeBonuses)
: max(0, $weeklyTotalMinutes - $overtimeReferenceMinutes);
$weeklyOvertime25Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $this->computeOvertime25BonusMinutes($weeklyTotalMinutes, $contractWeeklyHours);
$weeklyOvertime50Minutes = ($isPresenceTracking || $disableOvertimeBonuses)
: $this->computeOvertime25BonusMinutes($weeklyTotalMinutes, $overtime25StartMinutes);
$weeklyOvertime50Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $this->computeOvertime50BonusMinutes($weeklyTotalMinutes);
$weeklyRecoveryMinutes = ($isPresenceTracking || $disableOvertimeBonuses)
$weeklyRecoveryMinutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $weeklyOvertimeTotalMinutes + $weeklyOvertime25Minutes + $weeklyOvertime50Minutes;
@@ -230,9 +244,9 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
firstName: $employee->getFirstName(),
lastName: $employee->getLastName(),
siteName: $employee->getSite()?->getName(),
contractName: $employee->getContract()?->getName(),
contractType: $employee->getContract()?->getType()->value,
trackingMode: $employee->getContract()?->getTrackingMode(),
contractName: $weekAnchorContract?->getName(),
contractType: $weekAnchorContract?->getType()->value,
trackingMode: $weekAnchorContract?->getTrackingMode(),
daily: $daily,
weeklyDayMinutes: $weeklyDayMinutes,
weeklyNightMinutes: $weeklyNightMinutes,
@@ -344,25 +358,43 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
return max(0, $end - $start);
}
private function computeOvertimeTotalMinutes(int $weeklyTotalMinutes, ?int $contractWeeklyHours): int
/**
* @param array<string, ?Contract> $contractsByDate
*/
private function computeWeeklyOvertimeReferenceMinutes(array $days, array $contractsByDate): int
{
if (null === $contractWeeklyHours || $contractWeeklyHours <= 0) {
return 0;
$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);
}
// Règle métier: tout contrat < 35h est traité comme un 35h pour la base supp.
$referenceHours = max(35, $contractWeeklyHours);
return max(0, $weeklyTotalMinutes - ($referenceHours * 60));
return $total;
}
private function computeOvertime25BonusMinutes(int $weeklyTotalMinutes, ?int $contractWeeklyHours): int
/**
* @param array<string, ?Contract> $contractsByDate
*/
private function computeWeeklyOvertime25StartMinutes(array $days, array $contractsByDate): int
{
// Règle métier:
// - contrats <= 35h: 25% entre 35h et 43h
// - contrats >= 39h: 25% entre 39h et 43h
$startHours = (null !== $contractWeeklyHours && $contractWeeklyHours >= 39) ? 39 : 35;
$trancheMinutes = max(0, min($weeklyTotalMinutes, 43 * 60) - ($startHours * 60));
$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);
}
@@ -375,10 +407,9 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
return (int) round($trancheMinutes * 0.5);
}
private function hasDisabledOvertimeBonuses(Employee $employee): bool
private function hasDisabledOvertimeBonuses(?Contract $contract): bool
{
$contract = $employee->getContract();
$type = ContractType::resolve(
$type = ContractType::resolve(
$contract?->getName(),
$contract?->getTrackingMode(),
$contract?->getWeeklyHours()
@@ -386,4 +417,26 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
return ContractType::INTERIM === $type;
}
private function resolveDailyReferenceMinutes(?int $weeklyHours, int $isoWeekDay): int
{
// Week-end hors base de référence.
if ($isoWeekDay >= 6) {
return 0;
}
if (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);
}
}