'Benne', 'FOND_MOUVANT' => 'Fond Mouvant']; private const array PRICE_STATE_LABELS = [ 'EN_COURS' => 'En cours', 'VALIDE' => 'Validé', 'NON_VALIDE' => 'Non validé', ]; public function __construct( #[Autowire(service: 'App\Module\Transport\Infrastructure\Doctrine\DoctrineCarrierRepository')] private readonly CarrierRepositoryInterface $repository, private readonly SpreadsheetExporterInterface $exporter, ) {} #[Route('/api/carriers/{id}/prices/export.xlsx', name: 'transport_carrier_prices_export_xlsx', requirements: ['id' => '\d+'], methods: ['GET'], priority: 1)] #[IsGranted('transport.carriers.view')] public function __invoke(int $id): Response { $carrier = $this->repository->findById($id); // Soft-delete jamais expose (comme CarrierProvider::provideItem) : 404. if (null === $carrier || null !== $carrier->getDeletedAt()) { throw new NotFoundHttpException('Transporteur introuvable.'); } $binary = $this->exporter->export( 'Prix transporteur', $this->buildHeaders(), $this->buildRows($carrier), ); return $this->buildResponse($carrier, $binary); } /** * Colonnes du tableau Prix regroupe (spec-front « Onglet Prix » / docx p.10). * * @return list */ private function buildHeaders(): array { return [ 'Type de contenant', 'Transporteurs', 'Adresse APRO ou Adresse Sites', 'Adresse livraisons', 'Forfait €', 'Tonne €', 'Indexation', 'État du prix', ]; } /** * Lignes regroupees par type de contenant (Fond Mouvant / Benne). On trie les * prix par contenant puis position pour materialiser le regroupement. * * @return iterable> */ private function buildRows(Carrier $carrier): iterable { $prices = $carrier->getPrices()->toArray(); usort( $prices, static fn (CarrierPrice $a, CarrierPrice $b): int => [$a->getContainerType(), $a->getPosition()] <=> [$b->getContainerType(), $b->getPosition()], ); // Indexation : portee par le transporteur (RG-4.03), identique pour toutes // ses lignes de prix. Vide si non renseigne (spec-front). $indexation = $carrier->getIndexationRate() ?? ''; foreach ($prices as $price) { $isForfait = 'FORFAIT' === $price->getPricingUnit(); yield [ self::CONTAINER_LABELS[$price->getContainerType()] ?? $price->getContainerType(), $carrier->getName(), $this->formatDeparture($price), $this->formatDelivery($price), $isForfait ? $price->getPrice() : '', $isForfait ? '' : $price->getPrice(), $indexation, self::PRICE_STATE_LABELS[$price->getPriceState()] ?? $price->getPriceState(), ]; } } /** * Point de depart du prix (colonne « Adresse APRO ou Adresse Sites ») : * - branche CLIENT : le site de depart (un des 3 sites 86/17/82) ; * - branche FOURNISSEUR : l'adresse d'approvisionnement, identifiee par la * raison sociale du fournisseur (cf. note de classe sur les contrats Shared). */ private function formatDeparture(CarrierPrice $price): string { if ('CLIENT' === $price->getDirection()) { return $price->getDepartureSite()?->getName() ?? ''; } return $price->getSupplierSupplyAddress()?->getSupplier()?->getCompanyName() ?? ''; } /** * Point de livraison du prix (colonne « Adresse livraisons ») : * - branche CLIENT : l'adresse de livraison, identifiee par la raison sociale * du client ; * - branche FOURNISSEUR : le site de livraison (un des 3 sites 86/17/82). */ private function formatDelivery(CarrierPrice $price): string { if ('CLIENT' === $price->getDirection()) { return $price->getClientDeliveryAddress()?->getClient()?->getCompanyName() ?? ''; } return $price->getDeliverySite()?->getName() ?? ''; } private function buildResponse(Carrier $carrier, string $binary): Response { $filename = sprintf('prix-transporteur-%d-%s.xlsx', (int) $carrier->getId(), 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; } }