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', // Contrepartie eclatee en 3 colonnes mutuellement exclusives (miroir de // la liste / repertoire, ERP-193) plutot que « type + nom ». 'Fournisseur', 'Client', 'Autre', 'Date', 'Immatriculation', 'Poids vide (kg)', 'Poids plein (kg)', 'Poids net (kg)', 'DSD vide', 'DSD plein', 'Statut', ]; } /** * @param list $tickets * * @return iterable> */ private function buildRows(array $tickets): iterable { foreach ($tickets as $ticket) { $type = $ticket->getCounterpartyType(); yield [ $ticket->getNumber() ?? '', // Une seule des 3 colonnes est renseignee selon le type (RG-5.03). 'FOURNISSEUR' === $type ? ($ticket->getSupplier()?->getCompanyName() ?? '') : '', 'CLIENT' === $type ? ($ticket->getClient()?->getCompanyName() ?? '') : '', 'AUTRE' === $type ? ($ticket->getOtherLabel() ?? '') : '', $ticket->getDisplayDate()?->format('d/m/Y H:i') ?? '', $ticket->getImmatriculation() ?? '', $ticket->getEmptyWeight() ?? '', $ticket->getFullWeight() ?? '', $ticket->getNetWeight() ?? '', $ticket->getEmptyDsd() ?? '', $ticket->getFullDsd() ?? '', $this->statusLabel($ticket->getStatus()), ]; } } /** * Libelle FR du statut du cycle de vie (ERP-193) : « En attente » (DRAFT) ou * « Terminée » (VALIDATED). Renvoie la valeur brute pour une valeur inattendue * (garde-fou : ne masque pas une donnee corrompue). */ private function statusLabel(string $status): string { return match ($status) { WeighingTicket::STATUS_DRAFT => 'En attente', WeighingTicket::STATUS_VALIDATED => 'Terminée', default => $status, }; } 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; } }