security->getUser(); if (!$user instanceof User) { throw new AccessDeniedHttpException('Authentication required.'); } $request = $this->requestStack->getCurrentRequest(); if (!$request) { return new Response('Missing request.', Response::HTTP_BAD_REQUEST); } $year = (int) $request->query->get('year', (string) (int) new DateTimeImmutable('now')->format('Y')); if ($year < 2000 || $year > 2100) { throw new UnprocessableEntityHttpException('year must be between 2000 and 2100.'); } $from = new DateTimeImmutable(sprintf('%d-01-01', $year)); $to = new DateTimeImmutable(sprintf('%d-12-31', $year)); // Filtre sites optionnel (vide = tout le perimetre). $rawSiteIds = (string) $request->query->get('siteIds', ''); $siteIds = array_values(array_filter(array_map('intval', array_filter(explode(',', $rawSiteIds), 'strlen')))); // Perimetre selon le profil : admin -> tous, chef de site -> ses sites. $employees = $this->employeeRepository->findScoped($user); $today = new DateTimeImmutable('today'); $bySite = []; $siteMeta = []; foreach ($employees as $employee) { if (!$this->hasContractInRange($employee, $from, $to)) { continue; } // Exclure les forfait (contrat courant). $currentContract = $this->contractResolver->resolveForEmployeeAndDate($employee, $today); if (null !== $currentContract && ContractType::FORFAIT === $currentContract->getType()) { continue; } $site = $employee->getSite(); if (null === $site) { continue; } $siteId = $site->getId(); if ([] !== $siteIds && !in_array($siteId, $siteIds, true)) { continue; } $bySite[$siteId][] = $employee; $siteMeta[$siteId] ??= [ 'name' => $site->getName(), 'order' => $site->getDisplayOrder(), 'color' => $site->getColor(), ]; } uasort($siteMeta, static function (array $a, array $b): int { return [$a['order'], $a['name']] <=> [$b['order'], $b['name']]; }); $groups = []; foreach ($siteMeta as $siteId => $meta) { $siteEmployees = $bySite[$siteId]; // Meme tri que le calendrier : displayOrder, puis nom, puis prenom. usort($siteEmployees, static function (Employee $a, Employee $b): int { return [$a->getDisplayOrder(), $a->getLastName(), $a->getFirstName()] <=> [$b->getDisplayOrder(), $b->getLastName(), $b->getFirstName()]; }); $rows = $this->exportBuilder->buildRows($siteEmployees, $year); $renderRows = []; foreach ($rows as $row) { $cells = []; for ($m = 1; $m <= 12; ++$m) { $cells[] = $row->months[$m] > 0 ? $this->formatMinutes($row->months[$m]) : '—'; } $renderRows[] = [ 'employeeName' => $row->employeeName, 'cells' => $cells, 'totalHours' => $this->formatMinutes($row->totalMinutes), 'capHours' => $row->capHours, 'exceeded' => $row->totalMinutes > $row->capHours * 60, ]; } $groups[] = ['siteName' => $meta['name'], 'siteColor' => $meta['color'], 'rows' => $renderRows]; } $options = new Options(); $options->set('isRemoteEnabled', true); $dompdf = new Dompdf($options); $html = $this->twig->render('overtime-contingent/print.html.twig', [ 'groups' => $groups, 'year' => $year, 'exportedAt' => new DateTimeImmutable('now')->format('d/m/Y H:i'), ]); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'landscape'); $dompdf->render(); $filename = sprintf('contingent_heures_supp_%d.pdf', $year); return new Response($dompdf->output(), Response::HTTP_OK, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => sprintf('attachment; filename="%s"', $filename), ]); } private function hasContractInRange(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool { $fromDay = $from->format('Y-m-d'); $toDay = $to->format('Y-m-d'); foreach ($employee->getContractPeriods() as $period) { $start = $period->getStartDate()->format('Y-m-d'); $end = $period->getEndDate()?->format('Y-m-d'); if ($start <= $toDay && (null === $end || $end >= $fromDay)) { return true; } } return false; } private function formatMinutes(int $minutes): string { $h = intdiv($minutes, 60); $m = $minutes % 60; return sprintf('%dh%02d', $h, $m); } }