e688fe7e0b
GET /api/carriers/export.xlsx (mêmes filtres que la liste : includeArchived,
search, certificationType) et GET /api/carriers/{id}/prices/export.xlsx (tableau
Prix regroupé Benne / Fond Mouvant). Controllers Symfony custom avec
#[Route(priority: 1)] pour éviter le conflit API Platform {id}, génération
déléguée au service Shared SpreadsheetExporterInterface.
171 lines
6.7 KiB
PHP
171 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Transport\Infrastructure\Controller;
|
|
|
|
use App\Module\Transport\Domain\Entity\Carrier;
|
|
use App\Module\Transport\Domain\Entity\CarrierPrice;
|
|
use App\Module\Transport\Domain\Repository\CarrierRepositoryInterface;
|
|
use App\Shared\Domain\Contract\SpreadsheetExporterInterface;
|
|
use DateTimeImmutable;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
/**
|
|
* Export XLSX du tableau Prix d'un transporteur (M4, spec-back § 4.6 / spec-front
|
|
* § « Onglet Prix »). Reproduit le tableau de consultation regroupe par type de
|
|
* contenant (Fond Mouvant / Benne — colonnes du docx p.10).
|
|
*
|
|
* Controller Symfony custom (binaire de fichier, pas une representation Hydra).
|
|
* `priority: 1` est OBLIGATOIRE : sans cela API Platform capterait
|
|
* `/api/carriers/{id}/prices/export.xlsx` via ses routes generiques.
|
|
*
|
|
* Separation des responsabilites : le COMMENT (generation) est delegue au service
|
|
* Shared {@see SpreadsheetExporterInterface} ; le QUOI (chargement du transporteur,
|
|
* regroupement par contenant, mapping metier des colonnes) vit ICI.
|
|
*
|
|
* Adresses cross-module : les contrats Shared (ClientInterface / SupplierInterface
|
|
* / SiteInterface) exposent volontairement le minimum (regle ABSOLUE n°1). Faute
|
|
* d'acceder au detail postal d'une adresse Client/Fournisseur sans coupler au
|
|
* module Commercial, les colonnes d'adresse identifient le point par le libelle
|
|
* disponible : nom du site pour un Site, raison sociale du client/fournisseur pour
|
|
* une adresse de livraison/approvisionnement.
|
|
*/
|
|
#[AsController]
|
|
final class CarrierPriceExportController
|
|
{
|
|
/** Libelles d'affichage des enums (spec-front « Onglet Prix »). */
|
|
private const array CONTAINER_LABELS = ['BENNE' => '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<string>
|
|
*/
|
|
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<list<null|scalar>>
|
|
*/
|
|
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;
|
|
}
|
|
}
|