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); } $workDateRaw = (string) $request->query->get('workDate'); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDateRaw)) { throw new UnprocessableEntityHttpException('workDate must use YYYY-MM-DD format.'); } $date = new DateTimeImmutable($workDateRaw); $siteIdsRaw = (string) $request->query->get('siteIds', ''); $siteIds = array_values(array_filter(array_map( static fn (string $value): int => (int) trim($value), explode(',', $siteIdsRaw), ), static fn (int $id): bool => $id > 0)); if ([] === $siteIds) { throw new UnprocessableEntityHttpException('siteIds is required.'); } // Périmètre selon le profil : admin → tous, chef de site → ses sites uniquement. // Les siteIds demandés ne peuvent donc pas déborder du scope de l'utilisateur. $employees = $this->employeeRepository->findScoped($user); // Regroupement par site (ordre displayOrder), non-conducteurs uniquement. $bySite = []; $siteMeta = []; foreach ($employees as $employee) { if (true === $employee->getIsDriver()) { continue; } $site = $employee->getSite(); if (null === $site || !in_array($site->getId(), $siteIds, true)) { continue; } $siteId = $site->getId(); $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 = []; $legend = []; foreach ($siteMeta as $siteId => $meta) { $siteEmployees = $bySite[$siteId]; // Même tri que le calendrier : ordre manuel (displayOrder) puis nom, puis prénom. usort($siteEmployees, static function ($a, $b): int { return [$a->getDisplayOrder(), $a->getLastName(), $a->getFirstName()] <=> [$b->getDisplayOrder(), $b->getLastName(), $b->getFirstName()]; }); $rows = $this->exportBuilder->buildDayRowsForEmployees($siteEmployees, $date); if ([] === $rows) { continue; } $groups[] = ['siteName' => $meta['name'], 'siteColor' => $meta['color'], 'rows' => $rows]; // Légende : codes d'absence présents (hors férié), dédupliqués par code. foreach ($rows as $row) { if ($row['isHoliday'] || null === $row['statut'] || null === $row['statutLabel']) { continue; } $legend[$row['statut']] ??= [ 'code' => $row['statut'], 'label' => $row['statutLabel'], 'color' => $row['statutColor'] ?? '#e8e8e8', ]; } } ksort($legend); $legend = array_values($legend); $options = new Options(); $options->set('isRemoteEnabled', true); $dompdf = new Dompdf($options); $html = $this->twig->render('work-hour-day-export/print.html.twig', [ 'groups' => $groups, 'legend' => $legend, 'dateLabel' => $date->format('d/m/Y'), 'exportedAt' => new DateTimeImmutable('now')->format('d/m/Y H:i'), ]); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $dompdf->render(); $filename = sprintf('heures_jour_%s.pdf', $date->format('Y-m-d')); return new Response($dompdf->output(), Response::HTTP_OK, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => sprintf('attachment; filename="%s"', $filename), ]); } }