query->getString('search') ?: null; $qb = $this->repository->createListQueryBuilder($search); $this->applyDisplayDateOrder($qb, $request->query->all()); $this->applySiteScope($qb); // Export complet : pas de pagination (§ 4.5). On materialise toute la // selection filtree (cloisonnee par site) AVANT le mapping des colonnes. /** @var list $tickets */ $tickets = $qb->getQuery()->getResult(); $binary = $this->exporter->export( 'Tickets de pesée', $this->buildHeaders(), $this->buildRows($tickets), ); return $this->buildResponse($binary); } /** * Tri par date du ticket (§ 4.1), miroir de WeighingTicketProvider : * displayDate = COALESCE(full_date, empty_date) (getter calcule, pas une * colonne). Absent du payload -> tri par defaut du repository (number DESC). * * @param array $query */ private function applyDisplayDateOrder(QueryBuilder $qb, array $query): void { $order = $query['order'] ?? null; if (!is_array($order) || !isset($order['displayDate'])) { return; } $direction = 'asc' === strtolower((string) $order['displayDate']) ? 'ASC' : 'DESC'; $rootAlias = $qb->getRootAliases()[0]; $qb->orderBy(sprintf('COALESCE(%1$s.fullDate, %1$s.emptyDate)', $rootAlias), $direction); } /** * Cloisonnement par site courant (§ 2.3 / RG-5.09), miroir de * WeighingTicketProvider::applySiteScope() : restreint la selection au site * courant si l'user n'a pas le bypass et qu'un site est resolu. No-op sinon. */ private function applySiteScope(QueryBuilder $qb): void { $scopeSite = $this->siteScopeOrNull(); if (null === $scopeSite) { return; } $rootAlias = $qb->getRootAliases()[0]; $qb->andWhere(sprintf('%s.site = :scopeSite', $rootAlias)) ->setParameter('scopeSite', $scopeSite) ; } /** * Site servant a cloisonner, ou null si aucun cloisonnement ne s'applique * (user `sites.bypass_scope`, ou pas de site courant — module Sites off / * user sans currentSite). Miroir de WeighingTicketProvider::currentScopeSite(). */ private function siteScopeOrNull(): ?SiteInterface { if ($this->security->isGranted('sites.bypass_scope')) { return null; } return $this->currentSiteProvider->get(); } /** * Colonnes de l'export (spec § 4.5). * * @return list */ private function buildHeaders(): array { return [ 'Numéro', 'Type contrepartie', 'Contrepartie', 'Date', 'Immatriculation', 'Poids vide (kg)', 'Poids plein (kg)', 'Poids net (kg)', 'DSD vide', 'DSD plein', ]; } /** * @param list $tickets * * @return iterable> */ private function buildRows(array $tickets): iterable { foreach ($tickets as $ticket) { yield [ $ticket->getNumber(), $this->counterpartyTypeLabel($ticket->getCounterpartyType()), $this->counterpartyName($ticket), $ticket->getDisplayDate()?->format('d/m/Y H:i') ?? '', $ticket->getImmatriculation() ?? '', $ticket->getEmptyWeight() ?? '', $ticket->getFullWeight() ?? '', $ticket->getNetWeight() ?? '', $ticket->getEmptyDsd() ?? '', $ticket->getFullDsd() ?? '', ]; } } /** * Libelle FR du type de contrepartie (RG-5.03). Renvoie la valeur brute pour * une valeur inattendue (garde-fou : ne masque pas une donnee corrompue). */ private function counterpartyTypeLabel(?string $type): string { return match ($type) { 'CLIENT' => 'Client', 'FOURNISSEUR' => 'Fournisseur', 'AUTRE' => 'Autre', default => $type ?? '', }; } /** * Nom de la contrepartie selon le type (RG-5.03) : raison sociale du client, * du fournisseur, ou libelle libre « Autre ». Client / Supplier sont * fetch-joines par le repository (anti N+1, § 4.0). */ private function counterpartyName(WeighingTicket $ticket): string { return match ($ticket->getCounterpartyType()) { 'CLIENT' => $ticket->getClient()?->getCompanyName() ?? '', 'FOURNISSEUR' => $ticket->getSupplier()?->getCompanyName() ?? '', 'AUTRE' => $ticket->getOtherLabel() ?? '', default => '', }; } private function buildResponse(string $binary): Response { $filename = sprintf('tickets-pesee-%s.xlsx', new DateTimeImmutable()->format('Ymd')); $response = new Response($binary); $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $filename)); return $response; } }