security->getUser(); if (!$user instanceof User) { throw new AccessDeniedHttpException('Authentication required.'); } $employeeId = (int) ($uriVariables['id'] ?? 0); if ($employeeId <= 0) { throw new UnprocessableEntityHttpException('id must be a positive integer.'); } $employee = $this->employeeRepository->find($employeeId); if (!$employee instanceof Employee) { throw new NotFoundHttpException('Employee not found.'); } if (!$this->employeeScopeService->canAccessEmployee($user, $employee)) { throw new AccessDeniedHttpException('Employee outside your scope.'); } $year = $this->resolveYear(); $today = new DateTimeImmutable('today'); $currentExerciseYear = $this->resolveCurrentExerciseYear($today); [$periodFrom, $periodTo] = $this->rttRecoveryService->resolveExerciseBounds($year); $weeks = $this->rttRecoveryService->buildWeeksForExercise($periodFrom, $periodTo); $weekRanges = array_map( static fn (array $week): array => [ 'month' => (int) $week['month'], 'weekNumber' => (int) $week['weekNumber'], 'start' => $week['start'], 'end' => $week['end'], ], $weeks ); $limitDate = null; if ($year > $currentExerciseYear) { $limitDate = $periodFrom->modify('-1 day'); } $currentByWeekStart = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate); [$carry, $carryMonth] = $this->resolveCarry($employee, $year); $summary = new EmployeeRttSummary(); $summary->year = $year; $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 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 ); $payments = $this->rttPaymentRepository->findByEmployeeAndYear($employee, $year); $monthBuckets = []; foreach ($payments as $payment) { $m = $payment->getMonth(); if (!isset($monthBuckets[$m])) { $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['base25'], $bucket['bonus25'], $bucket['base50'], $bucket['bonus50']); $totalPaidMinutes += $bucket['base25'] + $bucket['bonus25'] + $bucket['base50'] + $bucket['bonus50']; } $summary->totalPaidMinutes = $totalPaidMinutes; $summary->monthPayments = $monthPayments; $summary->availableMinutes -= $totalPaidMinutes; return $summary; } /** * @return array{WeekRecoveryDetail, int} [carry, month] */ private function resolveCarry(Employee $employee, int $year): array { $balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $year); if (null !== $balance) { 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), 5, ]; } private function resolveYear(): int { $raw = (string) ($this->requestStack->getCurrentRequest()?->query->get('year') ?? ''); if ('' === $raw) { return $this->resolveCurrentExerciseYear(new DateTimeImmutable('today')); } if (!preg_match('/^\d{4}$/', $raw)) { throw new UnprocessableEntityHttpException('year must use YYYY format.'); } $year = (int) $raw; if ($year < 2000 || $year > 2100) { throw new UnprocessableEntityHttpException('year must be between 2000 and 2100.'); } return $year; } private function resolveCurrentExerciseYear(DateTimeImmutable $today): int { $year = (int) $today->format('Y'); $month = (int) $today->format('n'); return $month >= 6 ? $year + 1 : $year; } }