feat(ReserverController): Gère les options de produits au panier et en session.

Ajoute la gestion des options de produits lors de l'ajout au panier et dans la session de réservation. Inclut des corrections pour les options orphelines.
```
This commit is contained in:
Serreau Jovann
2026-02-04 11:58:07 +01:00
parent d23e75034c
commit 900b55c07b
8 changed files with 361 additions and 55 deletions

View File

@@ -33,6 +33,7 @@ use App\Service\Pdf\DevisPdfService;
use App\Entity\Devis;
use App\Entity\DevisLine;
use App\Entity\CustomerAddress;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ReserverController extends AbstractController
{
@@ -318,10 +319,11 @@ class ReserverController extends AbstractController
}
#[Route('/basket/json', name: 'reservation_basket_json', methods: ['POST'])]
public function basketJson(Request $request, ProductRepository $productRepository, UploaderHelper $uploaderHelper): Response
public function basketJson(Request $request, ProductRepository $productRepository, \App\Repository\OptionsRepository $optionsRepository, UploaderHelper $uploaderHelper): Response
{
$data = json_decode($request->getContent(), true);
$ids = $data['ids'] ?? [];
$selectedOptionsMap = $data['options'] ?? [];
$startStr = $data['start'] ?? null;
$endStr = $data['end'] ?? null;
@@ -347,49 +349,108 @@ class ReserverController extends AbstractController
$foundIds = array_map(fn($p) => $p->getId(), $products);
$removedIds = array_values(array_diff($ids, $foundIds));
$items = [];
$totalHT = 0;
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$tvaRate = $tvaEnabled ? 0.20 : 0;
$items = [];
$totalHT = 0;
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$tvaRate = $tvaEnabled ? 0.20 : 0;
foreach ($products as $product) {
$price1Day = $product->getPriceDay();
$priceSup = $product->getPriceSup() ?? 0.0;
$rootOptions = [];
$processedProductIds = [];
// Calcul du coût total pour ce produit selon la durée
$productTotalHT = $price1Day + ($priceSup * max(0, $duration - 1));
$productTotalTTC = $productTotalHT * (1 + $tvaRate);
foreach ($products as $product) {
$processedProductIds[] = $product->getId();
$price1Day = $product->getPriceDay();
$priceSup = $product->getPriceSup() ?? 0.0;
$items[] = [
'id' => $product->getId(),
'name' => $product->getName(),
'image' => $uploaderHelper->asset($product, 'imageFile'),
'priceHt1Day' => $price1Day,
'priceHTSupDay' => $priceSup,
'priceTTC1Day' => $price1Day * (1 + $tvaRate),
'totalPriceHT' => $productTotalHT,
'totalPriceTTC' => $productTotalTTC,
];
// Calcul du coût total pour ce produit selon la durée
$productTotalHT = $price1Day + ($priceSup * max(0, $duration - 1));
$totalHT += $productTotalHT;
}
// Traitement des options
$productOptions = [];
$optionsTotalHT = 0;
$totalTva = $totalHT * $tvaRate;
$totalTTC = $totalHT + $totalTva;
if (isset($selectedOptionsMap[$product->getId()])) {
$optionIds = $selectedOptionsMap[$product->getId()];
if (!empty($optionIds)) {
$optionsEntities = $optionsRepository->findBy(['id' => $optionIds]);
foreach ($optionsEntities as $option) {
$optPrice = $option->getPriceHt();
$optData = [
'id' => $option->getId(),
'name' => $option->getName(),
'price' => $optPrice
];
return new JsonResponse([
'start_date' => $startStr,
'end_date' => $endStr,
'products' => $items,
'unavailable_products_ids' => $removedIds,
'total' => [
'totalHT' => $totalHT,
'totalTva' => $totalTva,
'totalTTC' => $totalTTC
]
]);
}
// Vérifier si l'option est réellement liée au produit
if ($product->getOptions()->contains($option)) {
$productOptions[] = $optData;
$optionsTotalHT += $optPrice;
} else {
// Option demandée mais pas liée au produit -> Root options
$rootOptions[] = $optData;
// On ajoute quand même le prix ? Le user a dit "options non lier".
// Généralement si c'est "non lié", c'est une erreur ou une option globale.
// On l'ajoute au root mais pas au total du produit.
// Faut-il l'ajouter au total général ?
// Supposons que oui, c'est un "extra".
$totalHT += $optPrice;
}
}
}
}
$productTotalHT += $optionsTotalHT;
$productTotalTTC = $productTotalHT * (1 + $tvaRate);
$items[] = [
'id' => $product->getId(),
'name' => $product->getName(),
'image' => $uploaderHelper->asset($product, 'imageFile'),
'priceHt1Day' => $price1Day,
'priceHTSupDay' => $priceSup,
'priceTTC1Day' => $price1Day * (1 + $tvaRate),
'totalPriceHT' => $productTotalHT,
'totalPriceTTC' => $productTotalTTC,
'options' => $productOptions
];
$totalHT += $productTotalHT;
}
// Traiter les options pour les produits qui ne sont PAS dans le panier (orphelins de produit)
foreach ($selectedOptionsMap as $prodId => $optIds) {
// Si le produit n'a pas été traité (absent de $products car pas dans $ids ou pas trouvé)
if (!in_array($prodId, $processedProductIds) && !empty($optIds)) {
$optionsEntities = $optionsRepository->findBy(['id' => $optIds]);
foreach ($optionsEntities as $option) {
$optPrice = $option->getPriceHt();
$rootOptions[] = [
'id' => $option->getId(),
'name' => $option->getName(),
'price' => $optPrice,
'orphan_product_id' => $prodId // Info debug
];
$totalHT += $optPrice;
}
}
}
$totalTva = $totalHT * $tvaRate;
$totalTTC = $totalHT + $totalTva;
return new JsonResponse([
'start_date' => $startStr,
'end_date' => $endStr,
'products' => $items,
'options' => $rootOptions,
'unavailable_products_ids' => $removedIds,
'total' => [
'totalHT' => $totalHT,
'totalTva' => $totalTva,
'totalTTC' => $totalTTC
]
]);
}
#[Route('/session', name: 'reservation_session_create', methods: ['POST'])]
public function createSession(Request $request, EntityManagerInterface $em, OrderSessionRepository $sessionRepository): Response
{
@@ -432,7 +493,8 @@ class ReserverController extends AbstractController
ProductRepository $productRepository,
UploaderHelper $uploaderHelper,
ProductReserveRepository $productReserveRepository,
EntityManagerInterface $em
EntityManagerInterface $em,
\App\Repository\OptionsRepository $optionsRepository
): Response {
$session = $repository->findOneBy(['uuid' => $sessionId]);
if (!$session) {
@@ -441,6 +503,7 @@ class ReserverController extends AbstractController
$sessionData = $session->getProducts();
$ids = $sessionData['ids'] ?? [];
$selectedOptionsMap = $sessionData['options'] ?? [];
$startStr = $sessionData['start'] ?? null;
$endStr = $sessionData['end'] ?? null;
@@ -483,13 +546,46 @@ class ReserverController extends AbstractController
$totalHT = 0;
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$tvaRate = $tvaEnabled ? 0.20 : 0;
$rootOptions = [];
$processedProductIds = [];
foreach ($products as $product) {
$processedProductIds[] = $product->getId();
$price1Day = $product->getPriceDay();
$priceSup = $product->getPriceSup() ?? 0.0;
// Calcul du coût total pour ce produit selon la durée
$productTotalHT = $price1Day + ($priceSup * max(0, $duration - 1));
// Traitement des options
$productOptions = [];
$optionsTotalHT = 0;
if (isset($selectedOptionsMap[$product->getId()])) {
$optionIds = $selectedOptionsMap[$product->getId()];
if (!empty($optionIds)) {
$optionsEntities = $optionsRepository->findBy(['id' => $optionIds]);
foreach ($optionsEntities as $option) {
$optPrice = $option->getPriceHt();
$optData = [
'id' => $option->getId(),
'name' => $option->getName(),
'price' => $optPrice
];
if ($product->getOptions()->contains($option)) {
$productOptions[] = $optData;
$optionsTotalHT += $optPrice;
} else {
$rootOptions[] = $optData;
$totalHT += $optPrice;
}
}
}
}
$productTotalHT += $optionsTotalHT;
$productTotalTTC = $productTotalHT * (1 + $tvaRate);
$items[] = [
@@ -499,10 +595,27 @@ class ReserverController extends AbstractController
'priceSup' => $priceSup,
'totalPriceHT' => $productTotalHT,
'totalPriceTTC' => $productTotalTTC,
'options' => $productOptions
];
$totalHT += $productTotalHT;
}
// Traiter les options orphelines
foreach ($selectedOptionsMap as $prodId => $optIds) {
if (!in_array($prodId, $processedProductIds) && !empty($optIds)) {
$optionsEntities = $optionsRepository->findBy(['id' => $optIds]);
foreach ($optionsEntities as $option) {
$optPrice = $option->getPriceHt();
$rootOptions[] = [
'id' => $option->getId(),
'name' => $option->getName(),
'price' => $optPrice
];
$totalHT += $optPrice;
}
}
}
$totalTva = $totalHT * $tvaRate;
$totalTTC = $totalHT + $totalTva;
@@ -511,6 +624,7 @@ class ReserverController extends AbstractController
'session' => $session,
'cart' => [
'items' => $items,
'options' => $rootOptions,
'startDate' => $startStr ? new \DateTimeImmutable($startStr) : null,
'endDate' => $endStr ? new \DateTimeImmutable($endStr) : null,
'duration' => $duration,
@@ -1018,4 +1132,84 @@ class ReserverController extends AbstractController
{
return $this->render('revervation/hosting.twig');
}
#[Route('/estimer-la-livraison', name: 'reservation_estimate_delivery')]
public function estimateDelivery(Request $request, HttpClientInterface $client): Response
{
$form = $this->createFormBuilder()
->add('address', TextType::class, ['required' => true])
->add('zipCode', TextType::class, ['required' => true])
->add('city', TextType::class, ['required' => true])
->getForm();
$form->handleRequest($request);
$estimation = null;
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$query = sprintf('%s %s %s', $data['address'], $data['zipCode'], $data['city']);
try {
$response = $client->request('GET', 'https://api-adresse.data.gouv.fr/search/', [
'query' => [
'q' => $query,
'limit' => 1
]
]);
$content = $response->toArray();
if (!empty($content['features'])) {
$coords = $content['features'][0]['geometry']['coordinates'];
$lon = $coords[0];
$lat = $coords[1];
// Point de départ
$startLat = 49.849;
$startLon = 3.286;
// Formule de Haversine
$earthRadius = 6371; // km
$dLat = deg2rad($lat - $startLat);
$dLon = deg2rad($lon - $startLon);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($startLat)) * cos(deg2rad($lat)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
$distance = $earthRadius * $c;
$rate = 0.50;
$trips = 4;
if ($distance <= 10) {
$estimation = 0.0;
$chargedDistance = 0.0;
} else {
$chargedDistance = $distance - 10;
$estimation = ($chargedDistance * $trips) * $rate;
}
$details = [
'distance' => $distance,
'chargedDistance' => $chargedDistance,
'trips' => $trips,
'rate' => $rate,
'isFree' => ($distance <= 10)
];
} else {
$this->addFlash('warning', 'Adresse introuvable.');
}
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur lors du calcul de l\'itinéraire.');
}
}
return $this->render('revervation/estimate_delivery.twig', [
'form' => $form->createView(),
'estimation' => $estimation,
'details' => $details ?? null
]);
}
}