143278a368
Auto Tag Develop / tag (push) Successful in 10s
L'export des heures de la vue Jour était réservé aux admins. Il est désormais ouvert aux chefs de site, restreint à leurs sites : - sécurité endpoint ROLE_ADMIN -> ROLE_USER - périmètre résolu côté backend via EmployeeRepository::findScoped() (un siteIds hors périmètre est ignoré, aucune fuite inter-sites) - bouton Exporter visible pour admin + chef de site (masqué pour ROLE_SELF) - doc, doc in-app et CLAUDE.md mis à jour Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
5.3 KiB
PHP
141 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\State;
|
|
|
|
use ApiPlatform\Metadata\Operation;
|
|
use ApiPlatform\State\ProviderInterface;
|
|
use App\Entity\User;
|
|
use App\Repository\EmployeeRepository;
|
|
use App\Service\WorkHours\YearlyHoursExportBuilder;
|
|
use DateTimeImmutable;
|
|
use Dompdf\Dompdf;
|
|
use Dompdf\Options;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
use Twig\Environment;
|
|
|
|
class WorkHourDayExportProvider implements ProviderInterface
|
|
{
|
|
public function __construct(
|
|
private Environment $twig,
|
|
private readonly RequestStack $requestStack,
|
|
private EmployeeRepository $employeeRepository,
|
|
private YearlyHoursExportBuilder $exportBuilder,
|
|
private readonly Security $security,
|
|
) {}
|
|
|
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
|
{
|
|
$user = $this->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),
|
|
]);
|
|
}
|
|
}
|