fix : wip

This commit is contained in:
2026-02-19 17:44:37 +01:00
parent c2e118dc33
commit 13274ff297
31 changed files with 1539 additions and 126 deletions

View File

@@ -7,11 +7,16 @@ namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\WorkHourWeeklySummary;
use App\Dto\WorkHours\WorkMetrics;
use App\Entity\Absence;
use App\Entity\Employee;
use App\Entity\User;
use App\Entity\WorkHour;
use App\Repository\AbsenceRepository;
use App\Repository\EmployeeRepository;
use App\Repository\WorkHourRepository;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
use DateTimeImmutable;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -25,11 +30,15 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
private RequestStack $requestStack,
private EmployeeRepository $employeeRepository,
private WorkHourRepository $workHourRepository,
private AbsenceRepository $absenceRepository,
private AbsenceSegmentsResolver $absenceSegmentsResolver,
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
{
$user = $this->security->getUser();
// Endpoint protégé: résumé hebdo réservé aux utilisateurs authentifiés.
if (!$user instanceof User) {
throw new AccessDeniedHttpException('Authentication required.');
}
@@ -39,12 +48,13 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$employees = $this->employeeRepository->findScoped($user);
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($weekStart, $weekEnd, $employees);
$absences = $this->absenceRepository->findForPrint($weekStart, $weekEnd, $employees);
$summary = new WorkHourWeeklySummary();
$summary->weekStart = $weekStart->format('Y-m-d');
$summary->weekEnd = $weekEnd->format('Y-m-d');
$summary->days = $days;
$summary->rows = $this->buildRows($employees, $workHours, $days);
$summary->rows = $this->buildRows($employees, $workHours, $absences, $days);
return $summary;
}
@@ -54,11 +64,13 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$query = $this->requestStack->getCurrentRequest()?->query;
$raw = (string) ($query?->get('weekStart') ?? '');
// Sans paramètre, on ancre la semaine sur aujourd'hui.
if ('' === $raw) {
return new DateTimeImmutable('today');
}
$date = DateTimeImmutable::createFromFormat('Y-m-d', $raw);
// Validation stricte du format attendu.
if (!$date || $date->format('Y-m-d') !== $raw) {
throw new UnprocessableEntityHttpException('weekStart must use Y-m-d format.');
}
@@ -71,6 +83,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
*/
private function resolveWeek(DateTimeImmutable $anchorDate): array
{
// Convention ISO: semaine de lundi (1) à dimanche (7).
$dayOfWeek = (int) $anchorDate->format('N');
$weekStart = $anchorDate->modify(sprintf('-%d days', $dayOfWeek - 1));
$weekEnd = $weekStart->modify('+6 days');
@@ -86,6 +99,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
/**
* @param list<Employee> $employees
* @param list<WorkHour> $workHours
* @param list<Absence> $absences
* @param list<string> $days
*
* @return list<array{
@@ -100,11 +114,13 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
* weeklyNightMinutes:int,
* weeklyTotalMinutes:int,
* weeklyPresenceCount:float,
* weeklyOvertimeTotalMinutes:int,
* weeklyOvertime25Minutes:int,
* weeklyOvertime50Minutes:int
* weeklyOvertime50Minutes:int,
* weeklyRecoveryMinutes:int
* }>
*/
private function buildRows(array $employees, array $workHours, array $days): array
private function buildRows(array $employees, array $workHours, array $absences, array $days): array
{
$metricsByEmployeeDate = [];
foreach ($workHours as $workHour) {
@@ -113,6 +129,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
continue;
}
// Pré-calcul des métriques par salarié/date pour simplifier l'agrégation finale.
$dateKey = $workHour->getWorkDate()->format('Y-m-d');
$metricsByEmployeeDate[$employeeId][$dateKey] = [
'metrics' => $this->computeMetrics($workHour),
@@ -121,6 +138,30 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
];
}
$creditedByEmployeeDate = [];
$creditedPresenceByEmployeeDate = [];
foreach ($absences as $absence) {
$employeeId = $absence->getEmployee()?->getId();
if (!$employeeId) {
continue;
}
$start = $absence->getStartDate()->format('Y-m-d');
$end = $absence->getEndDate()->format('Y-m-d');
foreach ($days as $date) {
// On ne crédite que les dates couvertes par l'intervalle d'absence.
if ($date < $start || $date > $end) {
continue;
}
[$absentMorning, $absentAfternoon] = $this->absenceSegmentsResolver->resolveForDate($absence, $date);
$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);
}
}
$rows = [];
foreach ($employees as $employee) {
$employeeId = $employee->getId();
@@ -133,62 +174,76 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$weeklyTotalMinutes = 0;
$weeklyPresenceCount = 0.0;
$daily = [];
$isPresenceTracking = 'PRESENCE' === $employee->getContract()?->getTrackingMode();
// Les contrats au suivi "présence" ne manipulent pas les heures, mais des demi-journées.
$isPresenceTracking = 'PRESENCE' === $employee->getContract()?->getTrackingMode();
foreach ($days as $date) {
$entry = $metricsByEmployeeDate[$employeeId][$date] ?? null;
$metrics = $entry['metrics'] ?? [
'dayMinutes' => 0,
'nightMinutes' => 0,
'totalMinutes' => 0,
];
$entry = $metricsByEmployeeDate[$employeeId][$date] ?? null;
$metrics = $entry['metrics'] ?? new WorkMetrics();
$creditedMinutes = $creditedByEmployeeDate[$employeeId][$date] ?? 0;
// Les absences "comptées comme travaillées" alimentent le total du jour.
$metrics->addCreditedMinutes($creditedMinutes);
$present = null;
if ($isPresenceTracking) {
$morning = ($entry['isPresentMorning'] ?? false) ? 0.5 : 0.0;
$afternoon = ($entry['isPresentAfternoon'] ?? false) ? 0.5 : 0.0;
$present = $morning + $afternoon;
$morning = ($entry['isPresentMorning'] ?? false) ? 0.5 : 0.0;
$afternoon = ($entry['isPresentAfternoon'] ?? false) ? 0.5 : 0.0;
$creditedPresence = $creditedPresenceByEmployeeDate[$employeeId][$date] ?? 0.0;
$present = min(1.0, $morning + $afternoon + $creditedPresence);
}
$weeklyDayMinutes += $metrics['dayMinutes'];
$weeklyNightMinutes += $metrics['nightMinutes'];
$weeklyTotalMinutes += $metrics['totalMinutes'];
$weeklyDayMinutes += $metrics->dayMinutes;
$weeklyNightMinutes += $metrics->nightMinutes;
$weeklyTotalMinutes += $metrics->totalMinutes;
if (null !== $present) {
$weeklyPresenceCount += $present;
}
$daily[] = [
'date' => $date,
'dayMinutes' => $metrics['dayMinutes'],
'nightMinutes' => $metrics['nightMinutes'],
'totalMinutes' => $metrics['totalMinutes'],
'dayMinutes' => $metrics->dayMinutes,
'nightMinutes' => $metrics->nightMinutes,
'totalMinutes' => $metrics->totalMinutes,
'present' => $present,
];
}
$contractWeeklyHours = $employee->getContract()?->getWeeklyHours();
$weeklyOvertimeTotalMinutes = $isPresenceTracking
? 0
: $this->computeOvertimeTotalMinutes($weeklyTotalMinutes, $contractWeeklyHours);
$weeklyOvertime25Minutes = $isPresenceTracking
? 0
: $this->computeOvertime25BonusMinutes($weeklyTotalMinutes, $contractWeeklyHours);
$weeklyOvertime50Minutes = $isPresenceTracking
? 0
: $this->computeOvertime50BonusMinutes($weeklyTotalMinutes);
$weeklyRecoveryMinutes = $isPresenceTracking
? 0
: $weeklyOvertimeTotalMinutes + $weeklyOvertime25Minutes + $weeklyOvertime50Minutes;
$rows[] = [
'employeeId' => $employeeId,
'firstName' => $employee->getFirstName(),
'lastName' => $employee->getLastName(),
'siteName' => $employee->getSite()?->getName(),
'contractName' => $employee->getContract()?->getName(),
'trackingMode' => $employee->getContract()?->getTrackingMode(),
'daily' => $daily,
'weeklyDayMinutes' => $weeklyDayMinutes,
'weeklyNightMinutes' => $weeklyNightMinutes,
'weeklyTotalMinutes' => $weeklyTotalMinutes,
'weeklyPresenceCount' => $weeklyPresenceCount,
'weeklyOvertime25Minutes' => $isPresenceTracking ? 0 : $this->computeOvertime25Minutes($weeklyTotalMinutes),
'weeklyOvertime50Minutes' => $isPresenceTracking ? 0 : $this->computeOvertime50Minutes($weeklyTotalMinutes),
'employeeId' => $employeeId,
'firstName' => $employee->getFirstName(),
'lastName' => $employee->getLastName(),
'siteName' => $employee->getSite()?->getName(),
'contractName' => $employee->getContract()?->getName(),
'trackingMode' => $employee->getContract()?->getTrackingMode(),
'daily' => $daily,
'weeklyDayMinutes' => $weeklyDayMinutes,
'weeklyNightMinutes' => $weeklyNightMinutes,
'weeklyTotalMinutes' => $weeklyTotalMinutes,
'weeklyPresenceCount' => $weeklyPresenceCount,
'weeklyOvertimeTotalMinutes' => $weeklyOvertimeTotalMinutes,
'weeklyOvertime25Minutes' => $weeklyOvertime25Minutes,
'weeklyOvertime50Minutes' => $weeklyOvertime50Minutes,
'weeklyRecoveryMinutes' => $weeklyRecoveryMinutes,
];
}
return $rows;
}
/**
* @return array{dayMinutes:int, nightMinutes:int, totalMinutes:int}
*/
private function computeMetrics(WorkHour $workHour): array
private function computeMetrics(WorkHour $workHour): WorkMetrics
{
$ranges = [
[$workHour->getMorningFrom(), $workHour->getMorningTo()],
@@ -206,11 +261,11 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$dayMinutes = max(0, $totalMinutes - $nightMinutes);
return [
'dayMinutes' => $dayMinutes,
'nightMinutes' => $nightMinutes,
'totalMinutes' => $totalMinutes,
];
return new WorkMetrics(
dayMinutes: $dayMinutes,
nightMinutes: $nightMinutes,
totalMinutes: $totalMinutes,
);
}
/**
@@ -224,6 +279,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
return null;
}
// Si fin <= début, on considère un passage à minuit.
$end = $toMinutes <= $fromMinutes ? $toMinutes + 1440 : $toMinutes;
return [$fromMinutes, $end];
@@ -260,9 +316,11 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
}
[$start, $end] = $interval;
$windows = [[0, 360], [1260, 1440]];
$total = 0;
// Fenêtres de nuit: 00:00-06:00 et 21:00-24:00.
$windows = [[0, 360], [1260, 1440]];
$total = 0;
// On projette aussi sur J+1 pour couvrir les shifts qui traversent minuit.
for ($dayOffset = 0; $dayOffset <= 1; ++$dayOffset) {
$shift = $dayOffset * 1440;
foreach ($windows as [$windowStart, $windowEnd]) {
@@ -281,13 +339,34 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
return max(0, $end - $start);
}
private function computeOvertime25Minutes(int $weeklyTotalMinutes): int
private function computeOvertimeTotalMinutes(int $weeklyTotalMinutes, ?int $contractWeeklyHours): int
{
return max(0, min($weeklyTotalMinutes, 43 * 60) - (35 * 60));
if (null === $contractWeeklyHours || $contractWeeklyHours <= 0) {
return 0;
}
// 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));
}
private function computeOvertime50Minutes(int $weeklyTotalMinutes): int
private function computeOvertime25BonusMinutes(int $weeklyTotalMinutes, ?int $contractWeeklyHours): int
{
return max(0, $weeklyTotalMinutes - (43 * 60));
// 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));
return (int) round($trancheMinutes * 0.25);
}
private function computeOvertime50BonusMinutes(int $weeklyTotalMinutes): int
{
// Bonus 50% appliqué au-delà de 43h.
$trancheMinutes = max(0, $weeklyTotalMinutes - (43 * 60));
return (int) round($trancheMinutes * 0.5);
}
}