src/Controller/Front/HomeController.php line 61

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Front;
  3. use App\Entity\Category;
  4. use App\Entity\Declination;
  5. use App\Entity\Produit;
  6. use App\Entity\Slider;
  7. use App\Entity\ProduitDeclinationValue;
  8. use App\Entity\Promotion;
  9. use Doctrine\ORM\EntityManagerInterface;
  10. use Doctrine\ORM\QueryBuilder;
  11. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpFoundation\Response;
  14. use Symfony\Component\Routing\Annotation\Route;
  15. use Symfony\Component\HttpFoundation\JsonResponse;
  16. use App\Service\WebsiteSettingService;
  17. use App\Entity\Pack;
  18. use App\Entity\PackImageDeclination;
  19. class HomeController extends AbstractController
  20. {
  21.     use ImageTrait;
  22.     /** @var EntityManagerInterface */
  23.     private $em;
  24.     private WebsiteSettingService $websiteSettingService;
  25.     public function __construct(
  26.         EntityManagerInterface $manager,
  27.         WebsiteSettingService $websiteSettingService
  28.     )
  29.     {
  30.         $this->em $manager;
  31.         $this->websiteSettingService $websiteSettingService;
  32.     }
  33.     private function getNewProductDaysThreshold(): int
  34.     {
  35.         $days = (int) $this->websiteSettingService->get('newProductDays'30);
  36.         return $days $days 30;
  37.     }
  38.     private function getTopSalesPeriodThreshold(): int
  39.     {
  40.         $days = (int) $this->websiteSettingService->get('topSalesPeriod'30);
  41.         return $days $days 30;
  42.     }
  43.     private function getCatalogFilterMaxPrice(): float
  44.     {
  45.         $max = (float) $this->websiteSettingService->get('catalogFilterMaxPrice'999999);
  46.         return $max $max 999999;
  47.     }
  48.     /**
  49.      * @Route("/", name="home", options={"expose"=true})
  50.      */
  51.     public function home(WebsiteSettingService $websiteSettingService): Response
  52.     {
  53.         $categories $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1]);
  54.         $newProductDays $this->getNewProductDaysThreshold();
  55.         $date = new \DateTime('now');
  56.         $date->modify(sprintf('-%d day'$newProductDays));
  57.         // get the product repository for newest product
  58.         $produits $this->em->getRepository(Produit::class);
  59.         $sliders=$this->em->getRepository(Slider::class)->findBy(['isActive'=>true],['order'=>'asc'],3);
  60.         // Requete pour recuperer la nouvelle collection
  61.         $query $produits->createQueryBuilder('p')
  62.             ->where('p.createdAt >= :date')
  63.             ->andWhere('p.deletedAt is null')
  64.             ->andWhere('p.showInWebSite = 1')
  65.             ->setParameter('date'$date)
  66.             ->orderBy('p.createdAt''DESC')
  67.             ->setMaxResults(10);
  68.         $products $query->getQuery()->getResult();
  69.         $products array_slice($products010);
  70.         $productsWithImage = [];
  71.         foreach ($products as $product) {
  72.             $image $this->getDefaultImage($product);
  73.             if (!$image) {
  74.                 continue;
  75.             }
  76.             $product->image $image;
  77.             $product->aspectRatio $this->getCategoryAspectRatio($product);
  78.             $productsWithImage[] = $product;
  79.         }
  80.         $products $productsWithImage;
  81.         $topSalesPeriod $this->getTopSalesPeriodThreshold();
  82.         // Requete pour recuperer les top ventes
  83.         // On va recuperer les produits crees jusqu'a 1 mois
  84.         $date = new \DateTime('now');
  85.         $date->modify(sprintf('-%d day'$topSalesPeriod));
  86.         $query $this->em->createQueryBuilder();
  87.         $query->add('select''p')
  88.             ->from('App\Entity\Produit''p')
  89.             ->join('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  90.             ->join('App\Entity\DocumentDeclinationProduit''c''with''c.produitDeclinationValue=d')
  91.             ->where('p.deletedAt is null')
  92.             ->andWhere('c.createdAt > = :date')
  93.             ->andWhere('p.showInWebSite = 1')
  94.             ->setParameter('date'$date)
  95.             ->groupBy('p.id')
  96.             ->orderBy('count(p.id)''DESC')
  97.             ->setMaxResults(50);
  98.         // derniers 90 jours
  99.         $topProducts $query->getQuery()->getResult();
  100.         // limit to the first 10 top products
  101.         $topProducts array_slice($topProducts010);
  102.         $topProductsWithImage = [];
  103.         foreach ($topProducts as $product) {
  104.             $image $this->getDefaultImage($product);
  105.             if (!$image) {
  106.                 continue;
  107.             }
  108.             $product->image $image;
  109.             $product->aspectRatio $this->getCategoryAspectRatio($product);
  110.             $topProductsWithImage[] = $product;
  111.         }
  112.         $topProducts $topProductsWithImage;
  113.          // Maximum pour avoir une livraison gratuite
  114.         $max = (float) $websiteSettingService->get('freeDeliveryAmount'0);
  115.         $publicDir=$this->getParameter('kernel.project_dir')."/public";
  116.         if($this->getParameter('app.under_construction')=="true")
  117.             return $this->render('front/soon.html.twig');
  118.         else
  119.             return $this->render('front/home.html.twig', array(
  120.                 'sliders'=>$sliders,
  121.                 'products' => $products,
  122.                 'topProducts' => $topProducts,
  123.                 'max' => $max,
  124.                 'exist_banner1'=>file_exists($publicDir ."/images/banner/banner-9.jpg"),
  125.                 'exist_banner2'=>file_exists($publicDir."/images/banner/banner-10.jpg")
  126.             ));
  127.     }
  128.     /**
  129.      * @Route("/api/category_home", name="api_category_home", options={"expose"=true}, methods={"GET"})
  130.      */
  131.     public function newCategoryHomeAPI(Request $request): Response
  132.     {
  133.         $categories $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1], ['homepageOrder' => 'ASC'], 100);
  134.         $requestedMode strtolower(trim((string) $request->query->get('mode''')));
  135.         $catalogListMode in_array($requestedMode, ['product''declination'], true)
  136.             ? $requestedMode
  137.             strtolower(trim((string) $this->websiteSettingService->get('catalogListMode''product')));
  138.         if (!in_array($catalogListMode, ['product''declination'], true)) {
  139.             $catalogListMode 'product';
  140.         }
  141.         $metaOnly = (bool) $request->query->get('metaOnly'false);
  142.         $categoryId = (int) $request->query->get('categoryId'0);
  143.         if ($metaOnly) {
  144.             $data = [];
  145.             foreach ($categories as $category) {
  146.                 $data[] = (object) [
  147.                     'category' => $category,
  148.                     'products' => [],
  149.                     'loaded' => false,
  150.                 ];
  151.             }
  152.             return $this->jsonCachedResponse([
  153.                 'res' => 'OK',
  154.                 'data' => $data,
  155.                 'message' => 'Catgories rcuprs avec succs.',
  156.             ], 120);
  157.         }
  158.         if ($categoryId 0) {
  159.             $category $this->em->getRepository(Category::class)->find($categoryId);
  160.             if (!$category) {
  161.                 return new JsonResponse([
  162.                     'res' => 'ERROR',
  163.                     'message' => 'Catégorie introuvable.',
  164.                 ], 404);
  165.             }
  166.             $products $this->loadHomeCategoryProducts((int) $category->getId(), $catalogListMode$request);
  167.             return $this->jsonCachedResponse([
  168.                 'res' => 'OK',
  169.                 'data' => [
  170.                     (object) [
  171.                         'category' => $category,
  172.                         'products' => $products,
  173.                         'loaded' => true,
  174.                     ],
  175.                 ],
  176.                 'message' => 'Catgorie rcupre avec succs.',
  177.             ], 120);
  178.         }
  179.         $data = [];
  180.         foreach ($categories as $category) {
  181.             $products $this->loadHomeCategoryProducts((int) $category->getId(), $catalogListMode$request);
  182.             $data[] = (object) [
  183.                 'category' => $category,
  184.                 'products' => $products,
  185.                 'loaded' => true,
  186.             ];
  187.         }
  188.         $response = [
  189.             'res' => 'OK',
  190.             'data' => $data,
  191.             'message' => 'Catgories rcuprs avec succs.',
  192.         ];
  193.         return $this->jsonCachedResponse($response120);
  194.     }
  195.     /**
  196.      * @Route("/api/category_home_dec", name="api_category_home_dec", options={"expose"=true}, methods={"GET"})
  197.      */
  198.     public function newCategoryHomeDecAPI(Request $request): Response
  199.     {
  200.         $categories $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1], ['homepageOrder' => 'ASC'], 100);
  201.         $data = [];
  202.         foreach ($categories as $category) {
  203.             $categoryScope = [$category];
  204.             foreach ($category->getSubCategories() as $child) {
  205.                 $categoryScope[] = $child;
  206.                 foreach ($child->getSubCategories() as $sub) {
  207.                     $categoryScope[] = $sub;
  208.                 }
  209.             }
  210.             $query $this->em->getRepository(ProduitDeclinationValue::class)->createQueryBuilder('d');
  211.             $query->innerJoin('d.produit''p')
  212.                 ->where('p.categories in (:cats)')
  213.                 ->setParameter('cats'$categoryScope)
  214.                 ->andWhere('p.deletedAt is null')
  215.                 ->andWhere('p.showInWebSite = 1')
  216.                 ->orderBy('p.id''DESC')
  217.                 ->addOrderBy('d.id''DESC');
  218.             $products array_slice($this->buildHomePositionOneCards($query->getQuery()->getResult()), 08);
  219.             $data[] = (object) [
  220.                 'category' => $category,
  221.                 'products' => $products,
  222.             ];
  223.         }
  224.         return new JsonResponse([
  225.             'res' => 'OK',
  226.             'data' => $data,
  227.             'message' => 'Categories recuperees avec succes.',
  228.         ]);
  229.     }
  230.     /**
  231.      * @Route("/home", name="home_index")
  232.      */
  233.     public function index(): Response
  234.     {
  235.         $categories $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1]);
  236.         $newProductDays $this->getNewProductDaysThreshold();
  237.         $date = new \DateTime('now');
  238.         $date->modify(sprintf('-%d day'$newProductDays));
  239.         // get the product repository for newest product
  240.         $produits $this->em->getRepository(Produit::class);
  241.         // build the query for the doctrine paginator
  242.         $query $produits->createQueryBuilder('p')
  243.             ->where('p.createdAt >= :date')
  244.             ->andWhere('p.deletedAt is null')
  245.             ->setParameter('date'$date)
  246.             ->orderBy('p.createdAt''DESC')
  247.             ->setMaxResults(10);
  248.         $products $query->getQuery()->getResult();
  249.         // Recuperer les images
  250.         foreach ($products as $product) {
  251.             $product->image $this->getDefaultImage$product);
  252.         };
  253.         $query $this->em->createQueryBuilder();
  254.         $query->add('select''p')
  255.             ->from('App\Entity\Produit''p')
  256.             ->join('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  257.             ->join('App\Entity\DocumentDeclinationProduit''c''with''c.produitDeclinationValue=d')
  258.             ->where('p.deletedAt is null')
  259.             ->groupBy('p.id')
  260.             ->orderBy('count(p.id)''DESC')
  261.             ->setMaxResults(10);
  262.         $topProducts $query->getQuery()->getResult();
  263.         // Recuperer les images
  264.         foreach ($topProducts as $product) {
  265.             $product->image $this->getDefaultImage$product);
  266.         }
  267.         // Maximum pour avoir une livraison gratuite
  268.         $max = (float) $websiteSettingService->get('freeDeliveryAmount'0);
  269.         return $this->render('front/home.html.twig', array(
  270.             'products' => $products,
  271.             'topProducts' => $topProducts,
  272.             'max' => $max
  273.         ));
  274.     }
  275.     /**
  276.      * @Route("/new-products", name="new_products")
  277.      */
  278.     public function newProducts(): Response
  279.     {
  280.         return $this->render('front/product/listProducts.html.twig');
  281.     }
  282.     /**
  283.      * @Route("/new-products-dec", name="new_products_dec")
  284.      */
  285.     public function newProductsDec(): Response
  286.     {
  287.         return $this->render('front/dec/newProductsDec.html.twig');
  288.     }
  289.     /**
  290.      * @Route("/api/new-products", name="api_new_products", options={"expose"=true}, methods={"GET"})
  291.      */
  292.     public function newProductsAPI(Request $request): Response
  293.     {
  294.         $requestedMode strtolower(trim((string) $request->query->get('mode''')));
  295.         if ($requestedMode === 'declination') {
  296.             return $this->newProductsDecAPI($request);
  297.         }
  298.         $page max(1, (int) $request->query->get('page'1));
  299.         $pageSize max(1, (int) $request->query->get('pageSize'12));
  300.         $orderBy = (int) $request->query->get('orderBy'6);
  301.         //Les Filtres de recherche
  302.         $tailles preg_split('@,@'$request->query->get('tailles'), NULLPREG_SPLIT_NO_EMPTY);
  303.         $couleurs preg_split('@,@'$request->query->get('couleurs'), NULLPREG_SPLIT_NO_EMPTY);
  304.         $maxPrice = (float) $request->query->get('maxPrice'$this->getCatalogFilterMaxPrice());
  305.         if ($maxPrice <= 0) {
  306.             $maxPrice $this->getCatalogFilterMaxPrice();
  307.         }
  308.         $minPrice floatval($request->query->get('minPrice')) ?: 0;
  309.         $logContent sprintf(
  310.             "[%s] newProductsAPI params -> page:%s order:%s minPrice:%s maxPrice:%s tailles:%s couleurs:%s\n",
  311.             (new \DateTime())->format('Y-m-d H:i:s'),
  312.             $page$orderBy$minPrice$maxPrice,
  313.             json_encode(array_values(array_filter($tailles))),
  314.             json_encode(array_values(array_filter($couleurs)))
  315.         );
  316.         $newProductDays $this->getNewProductDaysThreshold();
  317.         $date = new \DateTime('now');
  318.         $date->modify(sprintf('-%d day'$newProductDays));
  319.         // build the query for the doctrine paginator
  320.         $query $this->em->getRepository(Produit::class)->createQueryBuilder('p')
  321.             ->where('p.createdAt >= :date')
  322.             ->andWhere('p.deletedAt is null')
  323.             ->andWhere('p.showInWebSite = 1')
  324.             ->setParameter('date'$date)
  325.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  326.             ->setParameter('maxPrice'$maxPrice)
  327.             ->setParameter('minPrice'$minPrice);
  328.         // Appliquer les filtres de tailles si ils sont appliqus
  329.         if( $tailles && !$couleurs) {
  330.             $declination $this->em->getRepository(Declination::class)->find(1);
  331.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  332.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  333.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  334.                 ->andWhere("v.name in (:tailles)")
  335.                 ->andWhere('v.declination = :declination')
  336.                 ->setParameter('declination'$declination)
  337.                 ->setParameter('tailles'$tailles);
  338.         }
  339.         // Appliquer les filtres de couleurs si ils sont appliqus seulement
  340.         if( $couleurs && !$tailles) {
  341.             $declination $this->em->getRepository(Declination::class)->find(2);
  342.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  343.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  344.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  345.                 ->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
  346.                 ->andWhere('v.declination = :declination')
  347.                 ->setParameter('declination'$declination)
  348.                 ->setParameter('couleurs'$couleurs);
  349.         }
  350.         // Appliquer les filtres de tailles et couleurs appliqus les deux
  351.         if( $tailles && $couleurs) {
  352.             $declinationTaille $this->em->getRepository(Declination::class)->find(1);
  353.             $declinationCouleur $this->em->getRepository(Declination::class)->find(2);
  354.             // Fusionner les deux filtres en un seul
  355.             $filtres array_merge($tailles$couleurs);
  356.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  357.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  358.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  359.                 ->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
  360.                 /* ->andWhere('v.declination = :declinationTaille OR v.declination = :declinationCouleur')
  361.                  ->setParameter('declinationTaille', $declinationTaille)*/
  362.                 ->setParameter('couleurs'$couleurs)
  363.                 ->setParameter('tailles'$tailles);
  364.         }
  365.         // Order by
  366.         switch ($orderBy) {
  367.             case 1:
  368.                 $query->orderBy('p.name''ASC');
  369.                 $query->addOrderBy('p.id''ASC');
  370.                 break;
  371.             case 2:
  372.                 $query->orderBy('p.name''DESC');
  373.                 $query->addOrderBy('p.id''DESC');
  374.                 break;
  375.             case 3:
  376.                 $query->orderBy('p.price_ttc''ASC');
  377.                 $query->addOrderBy('p.id''ASC');
  378.                 break;
  379.             case 4:
  380.                 $query->orderBy('p.price_ttc''DESC');
  381.                 $query->addOrderBy('p.id''DESC');
  382.                 break;
  383.             case 5:
  384.                 $query->orderBy('p.createdAt''ASC');
  385.                 $query->addOrderBy('p.id''ASC');
  386.                 break;
  387.             case 6:
  388.                 $query->orderBy('p.createdAt''DESC');
  389.                 $query->addOrderBy('p.id''DESC');
  390.                 break;
  391.             default:
  392.                 $query->orderBy('p.createdAt''DESC');
  393.                 $query->addOrderBy('p.id''DESC');
  394.                 break;
  395.         }
  396.         $query->getQuery();
  397.         // load doctrine Paginator
  398.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  399.         // you can get total items
  400.         $totalItems count($paginator);
  401.         // get total pages
  402.         $pagesCount ceil($totalItems $pageSize);
  403.         // now get one page's items:
  404.         $paginator
  405.             ->getQuery()
  406.             ->setFirstResult($pageSize * ($page-1)) // set the offset
  407.             ->setMaxResults($pageSize); // set the limit
  408.         $pageProducts iterator_to_array($paginatorfalse);
  409.         $hydratedProducts $this->preloadProductsForCards($pageProducts);
  410.         $data = [];
  411.         foreach ($pageProducts as $pageItem) {
  412.             $productId = (int) $pageItem->getId();
  413.             if (!isset($hydratedProducts[$productId])) {
  414.                 continue;
  415.             }
  416.             $data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
  417.         }
  418.         /*
  419.          * Ancien code conserve:
  420.          * $data= array();
  421.          * foreach ($paginator as $pageItem) {
  422.          *     $pageItem->aspectRatio = $this->getCategoryAspectRatio($pageItem);
  423.          *     array_push($data,$pageItem);
  424.          * }
  425.          * foreach ($data as $product) {
  426.          *     $product->image = $this->getDefaultImage($product);
  427.          *     $product->aspectRatio = $this->getCategoryAspectRatio($product);
  428.          * }
  429.          */
  430.         // Les nombres de pages
  431.         $pages = array();
  432.         for($i=max($page-31);$i<=min($page+3$pagesCount) ;$i++){
  433.             array_push($pages,$i);
  434.         }
  435.         $response = [
  436.             'res' => 'OK',
  437.             'data' => $data,
  438.             'pagesCount' => $pagesCount,
  439.             'total' => $totalItems,
  440.             'pages' => $pages,
  441.             'message' => 'Produits rcuprs avec succs.',
  442.         ];
  443.         return $this->jsonCachedResponse($response120);
  444.     }
  445.     /**
  446.      * @Route("/api/new-products-dec", name="api_new_products_dec", options={"expose"=true}, methods={"GET"})
  447.      */
  448.     public function newProductsDecAPI(Request $request): Response
  449.     {
  450.         $page max(1, (int) $request->query->get('page'1));
  451.         $pageSize max(1, (int) $request->query->get('pageSize'12));
  452.         $orderBy = (int) $request->query->get('orderBy'6);
  453.         //Les Filtres de recherche
  454.         $tailles preg_split('@,@'$request->query->get('tailles'), NULLPREG_SPLIT_NO_EMPTY);
  455.         $couleurs preg_split('@,@'$request->query->get('couleurs'), NULLPREG_SPLIT_NO_EMPTY);
  456.         $maxPrice = (float) $request->query->get('maxPrice'$this->getCatalogFilterMaxPrice());
  457.         if ($maxPrice <= 0) {
  458.             $maxPrice $this->getCatalogFilterMaxPrice();
  459.         }
  460.         $minPrice floatval($request->query->get('minPrice')) ?: 0;
  461.         $newProductDays $this->getNewProductDaysThreshold();
  462.         $date = new \DateTime('now');
  463.         $date->modify(sprintf('-%d day'$newProductDays));
  464.         // get the product dec repository
  465.         $produits $this->em->getRepository(ProduitDeclinationValue::class);
  466.         // build the query for the doctrine paginator
  467.         $query $produits->createQueryBuilder('d');
  468.         // build the query for the doctrine paginator
  469.         $query->innerJoin('App\Entity\Produit''p''with''d.produit=p')
  470.             ->where('p.createdAt >= :date')
  471.             ->andWhere('p.deletedAt is null')
  472.             ->andWhere('p.showInWebSite = 1')
  473.             ->setParameter('date'$date)
  474.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  475.             ->setParameter('maxPrice'$maxPrice)
  476.             ->setParameter('minPrice'$minPrice)
  477.             ->groupBy('p.id,v.id');
  478.         // Appliquer les filtres de tailles si ils sont ne pas appliqus
  479.         if( !$tailles && !$couleurs) {
  480.             $declination $this->em->getRepository(Declination::class)->find(1);
  481.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  482.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  483.                 ->andWhere('v.declination=2');
  484.         }
  485.         // Appliquer les filtres de tailles si ils sont appliqus
  486.         if( $tailles && !$couleurs) {
  487.             $declination $this->em->getRepository(Declination::class)->find(1);
  488.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  489.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  490.                 ->andWhere("v.name in (:tailles)")
  491.                 ->setParameter('tailles'$tailles);
  492.         }
  493.         // Appliquer les filtres de couleurs si ils sont appliqus seulement
  494.         if( $couleurs && !$tailles) {
  495.             $declination $this->em->getRepository(Declination::class)->find(2);
  496.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  497.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  498.                 ->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
  499.                 ->setParameter('couleurs'$couleurs);
  500.         }
  501.         // Appliquer les filtres de tailles et couleurs appliqus les deux
  502.         if( $tailles && $couleurs) {
  503.             $declinationTaille $this->em->getRepository(Declination::class)->find(1);
  504.             $declinationCouleur $this->em->getRepository(Declination::class)->find(2);
  505.             // Fusionner les deux filtres en un seul
  506.             $filtres array_merge($tailles$couleurs);
  507.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  508.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  509.                 ->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
  510.                 ->setParameter('couleurs'$couleurs)
  511.                 ->setParameter('tailles'$tailles)
  512.                 ->groupBy('v.declination')
  513.                 ->having('v.declination=2');
  514.         }
  515.         // Order by
  516.         switch ($orderBy) {
  517.             case 1:
  518.                 $query->orderBy('p.name''ASC');
  519.                 $query->addOrderBy('d.id''ASC');
  520.                 break;
  521.             case 2:
  522.                 $query->orderBy('p.name''DESC');
  523.                 $query->addOrderBy('d.id''DESC');
  524.                 break;
  525.             case 3:
  526.                 $query->orderBy('p.price_ttc''ASC');
  527.                 $query->addOrderBy('d.id''ASC');
  528.                 break;
  529.             case 4:
  530.                 $query->orderBy('p.price_ttc''DESC');
  531.                 $query->addOrderBy('d.id''DESC');
  532.                 break;
  533.             case 5:
  534.                 $query->orderBy('p.createdAt''ASC');
  535.                 $query->addOrderBy('d.id''ASC');
  536.                 break;
  537.             case 6:
  538.                 $query->orderBy('p.createdAt''DESC');
  539.                 $query->addOrderBy('d.id''DESC');
  540.                 break;
  541.             default:
  542.                 $query->orderBy('p.createdAt''DESC');
  543.                 $query->addOrderBy('d.id''DESC');
  544.                 break;
  545.         }
  546.         $this->applyDeclinationCardPreloads($query'd''p');
  547.         $query->getQuery();
  548.         // load doctrine Paginator
  549.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  550.         // you can get total items
  551.         $totalItems count($paginator);
  552.         // get total pages
  553.         $pagesCount ceil($totalItems $pageSize);
  554.         // now get one page's items:
  555.         $paginator
  556.             ->getQuery()
  557.             ->setFirstResult($pageSize * ($page-1)) // set the offset
  558.             ->setMaxResults($pageSize); // set the limit
  559.         $data= array();
  560.         foreach ($paginator as $pageItem) {
  561.             // do stuff with results...
  562.             $pageItem->setImage($this->getDefaultImage(null$pageItem));
  563.             $pageItem->aspectRatio $this->getCategoryAspectRatio($pageItem);
  564.             array_push($data,$pageItem);
  565.         }
  566.         // Les nombres de pages
  567.         $pages = array();
  568.         for($i=max($page-31);$i<=min($page+3$pagesCount) ;$i++){
  569.             array_push($pages,$i);
  570.         }
  571.         $response = [
  572.             'res' => 'OK',
  573.             'data' => $data,
  574.             'pagesCount' => $pagesCount,
  575.             'total' => $totalItems,
  576.             'pages' => $pages,
  577.             'message' => 'Produits rcuprs avec succs.',
  578.         ];
  579.         return new JsonResponse($response);
  580.     }
  581.     /**
  582.      * @Route("/api/top-products", name="api_top_products", options={"expose"=true}, methods={"GET"})
  583.      */
  584.     public function topProductsAPI(Request $request): Response
  585.     {
  586.         $requestedMode strtolower(trim((string) $request->query->get('mode''')));
  587.         if ($requestedMode === 'declination') {
  588.             return $this->topProductsDecAPI($request);
  589.         }
  590.         $page max(1, (int) $request->query->get('page'1));
  591.         $pageSize max(1, (int) $request->query->get('pageSize'12));
  592.         $orderBy = (int) $request->query->get('orderBy'6);
  593.         $minPrice floatval($request->query->get('minPrice'0));
  594.         $maxPrice = (float) $request->query->get('maxPrice'$this->getCatalogFilterMaxPrice());
  595.         if ($maxPrice <= 0) {
  596.             $maxPrice $this->getCatalogFilterMaxPrice();
  597.         }
  598.         $days $request->query->get('days');
  599.         $defaultDays = (int) $this->websiteSettingService->get('topSalesPeriod'30);
  600.         if ($defaultDays <= 0) {
  601.             $defaultDays 30;
  602.         }
  603.         $days is_numeric($days) ? (int) $days $defaultDays;
  604.         if ($days <= 0) {
  605.             $days $defaultDays;
  606.         }
  607.         $date = new \DateTime('now');
  608.         $date->modify(sprintf('-%d day'$days));
  609.         $query $this->em->createQueryBuilder();
  610.         $query->select('p')
  611.             ->addSelect('COALESCE(SUM(c.quantity), 0) AS HIDDEN salesQty')
  612.             ->from(Produit::class, 'p')
  613.             ->join(ProduitDeclinationValue::class, 'd''WITH''d.produit = p')
  614.             ->join('App\Entity\DocumentDeclinationProduit''c''WITH''c.produitDeclinationValue = d')
  615.             ->join('c.document''doc')
  616.             ->leftJoin('p.promotion''promo')
  617.             ->addSelect('promo')
  618.             ->where('p.deletedAt is null')
  619.             ->andWhere('p.showInWebSite = 1')
  620.             ->andWhere('c.createdAt >= :date')
  621.             ->andWhere('doc.category = :documentCategory')
  622.             ->andWhere('doc.type = :documentType')
  623.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  624.             ->setParameter('date'$date)
  625.             ->setParameter('documentCategory''client')
  626.             ->setParameter('documentType''commande')
  627.             ->setParameter('minPrice'$minPrice)
  628.             ->setParameter('maxPrice'$maxPrice)
  629.             ->groupBy('p.id');
  630.         $query->orderBy('salesQty''DESC');
  631.         $query->addOrderBy('p.createdAt''DESC');
  632.         $query->addOrderBy('p.id''DESC');
  633.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  634.         $totalItems count($paginator);
  635.         $pagesCount = (int) ceil($totalItems $pageSize);
  636.         $paginator->getQuery()
  637.             ->setFirstResult($pageSize * ($page 1))
  638.             ->setMaxResults($pageSize);
  639.         $pageProducts iterator_to_array($paginatorfalse);
  640.         $hydratedProducts $this->preloadProductsForCards($pageProducts);
  641.         $data = [];
  642.         foreach ($pageProducts as $product) {
  643.             $productId = (int) $product->getId();
  644.             if (!isset($hydratedProducts[$productId])) {
  645.                 continue;
  646.             }
  647.             $data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
  648.         }
  649.         /*
  650.          * Ancien code conserve:
  651.          * $data = [];
  652.          * foreach ($paginator as $product) {
  653.          *     $product->image = $this->getDefaultImage($product);
  654.          *     $data[] = $product;
  655.          * }
  656.          */
  657.         $response = [
  658.             'res' => 'OK',
  659.             'data' => $data,
  660.             'pagesCount' => $pagesCount,
  661.             'total' => $totalItems,
  662.             'message' => 'Top ventes rcupres avec succs.'
  663.         ];
  664.         return $this->jsonCachedResponse($response120);
  665.     }
  666.     private function topProductsDecAPI(Request $request): Response
  667.     {
  668.         $page max(1, (int) $request->query->get('page'1));
  669.         $pageSize max(1, (int) $request->query->get('pageSize'12));
  670.         $minPrice = (float) $request->query->get('minPrice'0);
  671.         $maxPrice = (float) $request->query->get('maxPrice'$this->getCatalogFilterMaxPrice());
  672.         if ($maxPrice <= 0) {
  673.             $maxPrice $this->getCatalogFilterMaxPrice();
  674.         }
  675.         $days $request->query->get('days');
  676.         $defaultDays = (int) $this->websiteSettingService->get('topSalesPeriod'30);
  677.         if ($defaultDays <= 0) {
  678.             $defaultDays 30;
  679.         }
  680.         $days is_numeric($days) ? (int) $days $defaultDays;
  681.         if ($days <= 0) {
  682.             $days $defaultDays;
  683.         }
  684.         $date = new \DateTime('now');
  685.         $date->modify(sprintf('-%d day'$days));
  686.         $qb $this->em->createQueryBuilder();
  687.         $qb->select('d')
  688.             ->addSelect('COALESCE(SUM(c.quantity), 0) AS HIDDEN salesQty')
  689.             ->from(ProduitDeclinationValue::class, 'd')
  690.             ->innerJoin('d.produit''p')
  691.             ->innerJoin('App\Entity\DocumentDeclinationProduit''c''WITH''c.produitDeclinationValue = d')
  692.             ->innerJoin('c.document''doc')
  693.             ->where('p.deletedAt IS NULL')
  694.             ->andWhere('p.showInWebSite = 1')
  695.             ->andWhere('c.createdAt >= :date')
  696.             ->andWhere('doc.category = :documentCategory')
  697.             ->andWhere('doc.type = :documentType')
  698.             ->andWhere('p.price_ttc BETWEEN :minPrice AND :maxPrice')
  699.             ->setParameter('date'$date)
  700.             ->setParameter('documentCategory''client')
  701.             ->setParameter('documentType''commande')
  702.             ->setParameter('minPrice'$minPrice)
  703.             ->setParameter('maxPrice'$maxPrice)
  704.             ->groupBy('d.id')
  705.             ->orderBy('salesQty''DESC')
  706.             ->addOrderBy('p.createdAt''DESC')
  707.             ->addOrderBy('d.id''DESC');
  708.         $this->applyDeclinationCardPreloads($qb'd''p');
  709.         $rows $qb->getQuery()->getResult();
  710.         $grouped = [];
  711.         $order = [];
  712.         $mixedAspectRatio = (float) $this->websiteSettingService->get('mixedAspectRatio'0.8);
  713.         foreach ($rows as $dec) {
  714.             if (!$dec instanceof ProduitDeclinationValue) {
  715.                 continue;
  716.             }
  717.             $product $dec->getProduit();
  718.             if (!$product) {
  719.                 continue;
  720.             }
  721.             $rawMainValue null;
  722.             $sizeValue null;
  723.             foreach ($dec->getGroupDeclinationValues() as $gdv) {
  724.                 $decl $gdv->getDeclination();
  725.                 if (!$decl) {
  726.                     continue;
  727.                 }
  728.                 if ((int) $decl->getPosition() === 1) {
  729.                     $rawMainValue $gdv->getValue();
  730.                 }
  731.                 if ((int) $decl->getPosition() === 2) {
  732.                     $sizeValue $gdv->getValue();
  733.                 }
  734.             }
  735.             if (!$rawMainValue) {
  736.                 continue;
  737.             }
  738.             $mainValue = (method_exists($rawMainValue'getParent') && $rawMainValue->getParent())
  739.                 ? $rawMainValue->getParent()
  740.                 : $rawMainValue;
  741.             $key = (int) $product->getId() . '_' . (int) $mainValue->getId();
  742.             $pictures = [];
  743.             $selectedImage null;
  744.             foreach ($dec->getPicture() as $pic) {
  745.                 $imgName $pic->getImageName();
  746.                 if (!$imgName) {
  747.                     continue;
  748.                 }
  749.                 $pictures[] = $imgName;
  750.                 if ($selectedImage === null && method_exists($pic'getIsSelected') && $pic->getIsSelected()) {
  751.                     $selectedImage $imgName;
  752.                 }
  753.             }
  754.             $img $selectedImage ?: (!empty($pictures) ? $pictures[0] : null);
  755.             if (!$img) {
  756.                 $img $product->getImage() ?: 'no-image.jpg';
  757.             }
  758.             $hoverImage null;
  759.             foreach ($pictures as $p) {
  760.                 if ($p !== $img) {
  761.                     $hoverImage $p;
  762.                     break;
  763.                 }
  764.             }
  765.             if (!isset($grouped[$key])) {
  766.                 $grouped[$key] = [
  767.                     'id' => (int) $dec->getId(),
  768.                     'idProduit' => (int) $product->getId(),
  769.                     'productId' => (int) $product->getId(),
  770.                     'name' => trim($product->getName() . ' ' $mainValue->getName()),
  771.                     'reference' => $product->getReference(),
  772.                     'image' => $img,
  773.                     'hoverImage' => $hoverImage,
  774.                     'priceTTC' => (float) $product->getPriceTtc(),
  775.                     'ratingScore' => (float) ($product->getRatingScore() ?? 0),
  776.                     'ratingCount' => (int) ($product->getRatingCount() ?? 0),
  777.                     'stock' => false,
  778.                     'aspectRatio' => $mixedAspectRatio,
  779.                     'colorSwatches' => [],
  780.                     'activeSizes' => [],
  781.                     'promo' => $product->getPromotion(),
  782.                 ];
  783.                 $order[] = $key;
  784.             }
  785.             $qty method_exists($dec'getQtyAvailableForWebsite') ? (int) $dec->getQtyAvailableForWebsite() : 0;
  786.             $sizeRow = [
  787.                 'id' => (int) $dec->getId(),
  788.                 'idDec' => (int) $dec->getId(),
  789.                 'label' => $sizeValue ? (string) $sizeValue->getName() : (string) $dec->getName(),
  790.                 'qty' => $qty,
  791.             ];
  792.             $swatch = [
  793.                 'id' => (int) $mainValue->getId(),
  794.                 'name' => (string) $mainValue->getName(),
  795.                 'code' => method_exists($mainValue'getCode') ? ((string) $mainValue->getCode() ?: null) : null,
  796.                 'image' => $img,
  797.                 'hoverImage' => $hoverImage,
  798.                 'idDec' => (int) $dec->getId(),
  799.                 'sizes' => [$sizeRow],
  800.             ];
  801.             $existingSwatchIndex null;
  802.             foreach ($grouped[$key]['colorSwatches'] as $index => $existingSwatch) {
  803.                 if ((int) ($existingSwatch['id'] ?? 0) === (int) $swatch['id']) {
  804.                     $existingSwatchIndex $index;
  805.                     break;
  806.                 }
  807.             }
  808.             if ($existingSwatchIndex === null) {
  809.                 $grouped[$key]['colorSwatches'][] = $swatch;
  810.             } else {
  811.                 $grouped[$key]['colorSwatches'][$existingSwatchIndex]['sizes'][] = $sizeRow;
  812.             }
  813.             $grouped[$key]['activeSizes'][] = $sizeRow;
  814.             if ($qty 0) {
  815.                 $grouped[$key]['stock'] = true;
  816.             }
  817.         }
  818.         $cards = [];
  819.         foreach ($order as $key) {
  820.             $cards[] = $grouped[$key];
  821.         }
  822.         $totalItems count($cards);
  823.         $pagesCount = (int) ceil($totalItems $pageSize);
  824.         $data array_slice($cards$pageSize * ($page 1), $pageSize);
  825.         return $this->jsonCachedResponse([
  826.             'res' => 'OK',
  827.             'data' => $data,
  828.             'pagesCount' => $pagesCount,
  829.             'total' => $totalItems,
  830.             'message' => 'Top ventes recuperees avec succes.',
  831.         ], 120);
  832.     }
  833.     /**
  834.      * @Route("/api/top-rated-products", name="api_top_rated_products", options={"expose"=true}, methods={"GET"})
  835.      */
  836.     public function topRatedProductsAPI(Request $request): Response
  837.     {
  838.         $page max(1, (int) $request->query->get('page'1));
  839.         $pageSize max(1, (int) $request->query->get('pageSize'12));
  840.         $minPrice floatval($request->query->get('minPrice'0));
  841.         $maxPrice = (float) $request->query->get('maxPrice'$this->getCatalogFilterMaxPrice());
  842.         if ($maxPrice <= 0) {
  843.             $maxPrice $this->getCatalogFilterMaxPrice();
  844.         }
  845.         $query $this->em->createQueryBuilder();
  846.         $query->select('p')
  847.             ->from(Produit::class, 'p')
  848.             ->leftJoin('p.promotion''promo')
  849.             ->addSelect('promo')
  850.             ->where('p.deletedAt is null')
  851.             ->andWhere('p.showInWebSite = 1')
  852.             ->andWhere('p.ratingCount > 0')
  853.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  854.             ->setParameter('minPrice'$minPrice)
  855.             ->setParameter('maxPrice'$maxPrice)
  856.             ->orderBy('p.ratingScore''DESC')
  857.             ->addOrderBy('p.ratingCount''DESC')
  858.             ->addOrderBy('p.createdAt''DESC')
  859.             ->addOrderBy('p.id''DESC');
  860.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  861.         $totalItems count($paginator);
  862.         $pagesCount = (int) ceil($totalItems $pageSize);
  863.         $paginator->getQuery()
  864.             ->setFirstResult($pageSize * ($page 1))
  865.             ->setMaxResults($pageSize);
  866.         $pageProducts iterator_to_array($paginatorfalse);
  867.         $hydratedProducts $this->preloadProductsForCards($pageProducts);
  868.         $data = [];
  869.         foreach ($pageProducts as $product) {
  870.             $productId = (int) $product->getId();
  871.             if (!isset($hydratedProducts[$productId])) {
  872.                 continue;
  873.             }
  874.             $data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
  875.         }
  876.         /*
  877.          * Ancien code conserve:
  878.          * $data = [];
  879.          * foreach ($paginator as $product) {
  880.          *     $product->image = $this->getDefaultImage($product);
  881.          *     $product->aspectRatio = $this->getCategoryAspectRatio($product);
  882.          *     $data[] = $product;
  883.          * }
  884.          */
  885.         $response = [
  886.             'res' => 'OK',
  887.             'data' => $data,
  888.             'pagesCount' => $pagesCount,
  889.             'total' => $totalItems,
  890.             'message' => 'Produits top rated rcuprs avec succs.'
  891.         ];
  892.         return $this->jsonCachedResponse($response120);
  893.     }
  894.     /**
  895.      * @Route("/promotion", name="promo_products")
  896.      */
  897.     public function promoProducts(): Response
  898.     {
  899.         return $this->render('front/product/listProducts.html.twig');
  900.     }
  901.     /**
  902.      * @Route("/promotion-dec", name="promo_products_dec")
  903.      */
  904.     public function promoProductsDec(): Response
  905.     {
  906.         return $this->render('front/dec/promoProductsDec.html.twig');
  907.     }
  908.     /**
  909.      * @Route("/api/promotion", name="api_promo_products", options={"expose"=true}, methods={"GET"})
  910.      */
  911.     public function promoProductsAPI(Request $request): Response
  912.     {
  913.         $requestedMode strtolower(trim((string) $request->query->get('mode''')));
  914.         if ($requestedMode === 'declination') {
  915.             return $this->promoProductsAPIDec($request);
  916.         }
  917.         $page max(1, (int) $request->query->get('page'1));
  918.         $pageSize max(1, (int) $request->query->get('pageSize'12));
  919.         $orderBy = (int) $request->query->get('orderBy'6);
  920.         //Les Filtres de recherche
  921.         $tailles preg_split('@,@'$request->query->get('tailles'), NULLPREG_SPLIT_NO_EMPTY);
  922.         $couleurs preg_split('@,@'$request->query->get('couleurs'), NULLPREG_SPLIT_NO_EMPTY);
  923.         $maxPrice = (float) $request->query->get('maxPrice'$this->getCatalogFilterMaxPrice());
  924.         if ($maxPrice <= 0) {
  925.             $maxPrice $this->getCatalogFilterMaxPrice();
  926.         }
  927.         $minPrice floatval($request->query->get('minPrice')) ?: 0;
  928.         // Rcuprer la date encours
  929.         $date = new \DateTime('now');
  930.         $query $this->em->createQueryBuilder();
  931.         $query->add('select''p')
  932.             ->from('App\Entity\Produit''p')
  933.             ->join('App\Entity\Promotion''c''with''p.promotion=c')
  934.             ->where('p.promotion is not null')
  935.             ->andWhere('c.startAt <= :date')
  936.             ->andWhere('c.endAt >= :date')
  937.             ->setParameter('date'$date)
  938.             ->andWhere('p.deletedAt is null')
  939.             ->andWhere('p.showInWebSite = 1')
  940.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  941.             ->setParameter('maxPrice'$maxPrice)
  942.             ->setParameter('minPrice'$minPrice);
  943.         // Appliquer les filtres de tailles si ils sont appliqus
  944.         if( $tailles && !$couleurs) {
  945.             $declination $this->em->getRepository(Declination::class)->find(1);
  946.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  947.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  948.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  949.                 ->andWhere("v.name in (:tailles)")
  950.                 ->andWhere('v.declination = :declination')
  951.                 ->setParameter('declination'$declination)
  952.                 ->setParameter('tailles'$tailles);
  953.         }
  954.         // Appliquer les filtres de couleurs si ils sont appliqus seulement
  955.         if( $couleurs && !$tailles) {
  956.             $declination $this->em->getRepository(Declination::class)->find(2);
  957.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  958.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  959.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  960.                 ->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
  961.                 ->andWhere('v.declination = :declination')
  962.                 ->setParameter('declination'$declination)
  963.                 ->setParameter('couleurs'$couleurs);
  964.         }
  965.         // Appliquer les filtres de tailles et couleurs appliqus les deux
  966.         if( $tailles && $couleurs) {
  967.             $declinationTaille $this->em->getRepository(Declination::class)->find(1);
  968.             $declinationCouleur $this->em->getRepository(Declination::class)->find(2);
  969.             // Fusionner les deux filtres en un seul
  970.             $filtres array_merge($tailles$couleurs);
  971.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  972.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  973.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  974.                 ->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
  975.                 /* ->andWhere('v.declination = :declinationTaille OR v.declination = :declinationCouleur')
  976.                  ->setParameter('declinationTaille', $declinationTaille)*/
  977.                 ->setParameter('couleurs'$couleurs)
  978.                 ->setParameter('tailles'$tailles);
  979.         }
  980.         // Order by
  981.         switch ($orderBy) {
  982.             case 1:
  983.                 $query->orderBy('p.name''ASC');
  984.                 $query->addOrderBy('p.id''ASC');
  985.                 break;
  986.             case 2:
  987.                 $query->orderBy('p.name''DESC');
  988.                 $query->addOrderBy('p.id''DESC');
  989.                 break;
  990.             case 3:
  991.                 $query->orderBy('p.price_ttc''ASC');
  992.                 $query->addOrderBy('p.id''ASC');
  993.                 break;
  994.             case 4:
  995.                 $query->orderBy('p.price_ttc''DESC');
  996.                 $query->addOrderBy('p.id''DESC');
  997.                 break;
  998.             case 5:
  999.                 $query->orderBy('p.createdAt''ASC');
  1000.                 $query->addOrderBy('p.id''ASC');
  1001.                 break;
  1002.             case 6:
  1003.                 $query->orderBy('p.createdAt''DESC');
  1004.                 $query->addOrderBy('p.id''DESC');
  1005.                 break;
  1006.             default:
  1007.                 $query->orderBy('p.createdAt''DESC');
  1008.                 $query->addOrderBy('p.id''DESC');
  1009.                 break;
  1010.         }
  1011.         // load doctrine Paginator
  1012.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  1013.         // you can get total items
  1014.         $totalItems count($paginator);
  1015.         // get total pages
  1016.         $pagesCount ceil($totalItems $pageSize);
  1017.         // now get one page's items:
  1018.         $paginator
  1019.             ->getQuery()
  1020.             ->setFirstResult($pageSize * ($page 1)) // set the offset
  1021.             ->setMaxResults($pageSize); // set the limit
  1022.         $pageProducts iterator_to_array($paginatorfalse);
  1023.         $hydratedProducts $this->preloadProductsForCards($pageProducts);
  1024.         $data = [];
  1025.         foreach ($pageProducts as $pageItem) {
  1026.             $productId = (int) $pageItem->getId();
  1027.             if (!isset($hydratedProducts[$productId])) {
  1028.                 continue;
  1029.             }
  1030.             $data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
  1031.         }
  1032.         /*
  1033.          * Ancien code conserve:
  1034.          * $data = array();
  1035.          * foreach ($paginator as $pageItem) {
  1036.          *     array_push($data, $pageItem);
  1037.          * }
  1038.          * foreach ($data as $product) {
  1039.          *     $product->image = $this->getDefaultImage($product);
  1040.          * }
  1041.          */
  1042.         // Les nombres de pages
  1043.         $pages = array();
  1044.         for ($i max($page 31); $i <= min($page 3$pagesCount); $i++) {
  1045.             array_push($pages$i);
  1046.         }
  1047.         $response = [
  1048.             'res' => 'OK',
  1049.             'data' => $data,
  1050.             'pagesCount' => $pagesCount,
  1051.             'total' => $totalItems,
  1052.             'pages' => $pages,
  1053.             'message' => 'Produits rcuprs avec succs.',
  1054.         ];
  1055.         return $this->jsonCachedResponse($response120);
  1056.     }
  1057.     /**
  1058.      * @Route("/api/promotion-dec", name="api_promo_products_dec", options={"expose"=true}, methods={"GET"})
  1059.      */
  1060.     public function promoProductsAPIDec(Request $request): Response
  1061.     {
  1062.         $page max(1, (int) $request->query->get('page'1));
  1063.         $pageSize max(1, (int) $request->query->get('pageSize'12));
  1064.         $orderBy = (int) $request->query->get('orderBy'6);
  1065.         //Les Filtres de recherche
  1066.         $tailles preg_split('@,@'$request->query->get('tailles'), NULLPREG_SPLIT_NO_EMPTY);
  1067.         $couleurs preg_split('@,@'$request->query->get('couleurs'), NULLPREG_SPLIT_NO_EMPTY);
  1068.         $maxPrice = (float) $request->query->get('maxPrice'$this->getCatalogFilterMaxPrice());
  1069.         if ($maxPrice <= 0) {
  1070.             $maxPrice $this->getCatalogFilterMaxPrice();
  1071.         }
  1072.         $minPrice floatval($request->query->get('minPrice')) ?: 0;
  1073.         // Rcuprer la date encours
  1074.         $date = new \DateTime('now');
  1075.         //$query = $this->em->createQueryBuilder();
  1076.         $produits $this->em->getRepository(ProduitDeclinationValue::class);
  1077.         // build the query for the doctrine paginator
  1078.         $query $produits->createQueryBuilder('d');
  1079.         $query->innerJoin('App\Entity\Produit''p''with''d.produit=p')
  1080.             ->innerJoin('App\Entity\Promotion''c''with''p.promotion=c')
  1081.             ->where('p.promotion is not null')
  1082.             ->andWhere('c.startAt <= :date')
  1083.             ->andWhere('c.endAt >= :date')
  1084.             ->setParameter('date'$date)
  1085.             ->andWhere('p.deletedAt is null')
  1086.             ->andWhere('p.showInWebSite = 1')
  1087.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  1088.             ->setParameter('maxPrice'$maxPrice)
  1089.             ->setParameter('minPrice'$minPrice)
  1090.             ->groupBy('p.id,v.id');;
  1091.         // Appliquer les filtres de tailles si ils sont ne pas appliqus
  1092.         if( !$tailles && !$couleurs) {
  1093.             $declination $this->em->getRepository(Declination::class)->find(1);
  1094.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1095.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1096.                 ->andWhere('v.declination=2');
  1097.         }
  1098.         // Appliquer les filtres de tailles si ils sont appliqus
  1099.         if( $tailles && !$couleurs) {
  1100.             $declination $this->em->getRepository(Declination::class)->find(1);
  1101.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1102.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1103.                 ->andWhere("v.name in (:tailles)")
  1104.                 ->setParameter('tailles'$tailles);
  1105.         }
  1106.         // Appliquer les filtres de couleurs si ils sont appliqus seulement
  1107.         if( $couleurs && !$tailles) {
  1108.             $declination $this->em->getRepository(Declination::class)->find(2);
  1109.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1110.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1111.                 ->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
  1112.                 ->setParameter('couleurs'$couleurs);
  1113.         }
  1114.         // Appliquer les filtres de tailles et couleurs appliqus les deux
  1115.         if( $tailles && $couleurs) {
  1116.             $declinationTaille $this->em->getRepository(Declination::class)->find(1);
  1117.             $declinationCouleur $this->em->getRepository(Declination::class)->find(2);
  1118.             // Fusionner les deux filtres en un seul
  1119.             $filtres array_merge($tailles$couleurs);
  1120.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1121.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1122.                 ->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
  1123.                 ->setParameter('couleurs'$couleurs)
  1124.                 ->setParameter('tailles'$tailles)
  1125.                 ->groupBy('v.declination')
  1126.                 ->having('v.declination=2');
  1127.         }
  1128.         // Order by
  1129.         switch ($orderBy) {
  1130.             case 1:
  1131.                 $query->orderBy('p.name''ASC');
  1132.                 $query->addOrderBy('d.id''ASC');
  1133.                 break;
  1134.             case 2:
  1135.                 $query->orderBy('p.name''DESC');
  1136.                 $query->addOrderBy('d.id''DESC');
  1137.                 break;
  1138.             case 3:
  1139.                 $query->orderBy('p.price_ttc''ASC');
  1140.                 $query->addOrderBy('d.id''ASC');
  1141.                 break;
  1142.             case 4:
  1143.                 $query->orderBy('p.price_ttc''DESC');
  1144.                 $query->addOrderBy('d.id''DESC');
  1145.                 break;
  1146.             case 5:
  1147.                 $query->orderBy('p.createdAt''ASC');
  1148.                 $query->addOrderBy('d.id''ASC');
  1149.                 break;
  1150.             case 6:
  1151.                 $query->orderBy('p.createdAt''DESC');
  1152.                 $query->addOrderBy('d.id''DESC');
  1153.                 break;
  1154.             default:
  1155.                 $query->orderBy('p.createdAt''DESC');
  1156.                 $query->addOrderBy('d.id''DESC');
  1157.                 break;
  1158.         }
  1159.         $this->applyDeclinationCardPreloads($query'd''p');
  1160.         // load doctrine Paginator
  1161.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  1162.         // you can get total items
  1163.         $totalItems count($paginator);
  1164.         // get total pages
  1165.         $pagesCount ceil($totalItems $pageSize);
  1166.         // now get one page's items:
  1167.         $paginator
  1168.             ->getQuery()
  1169.             ->setFirstResult($pageSize * ($page 1)) // set the offset
  1170.             ->setMaxResults($pageSize); // set the limit
  1171.         $data = array();
  1172.         foreach ($paginator as $pageItem) {
  1173.             // do stuff with results...
  1174.             $pageItem->setImage($this->getDefaultImage(null$pageItem));
  1175.             array_push($data,$pageItem);
  1176.         }
  1177.         // Les nombres de pages
  1178.         $pages = array();
  1179.         for ($i max($page 31); $i <= min($page 3$pagesCount); $i++) {
  1180.             array_push($pages$i);
  1181.         }
  1182.         $response = [
  1183.             'res' => 'OK',
  1184.             'data' => $data,
  1185.             'pagesCount' => $pagesCount,
  1186.             'total' => $totalItems,
  1187.             'pages' => $pages,
  1188.             'message' => 'Produits rcuprs avec succs.',
  1189.         ];
  1190.         return new JsonResponse($response);
  1191.     }
  1192.     /**
  1193.      * Suggestions de recherche (header)
  1194.      * - Produits: ref commence par / nom contient
  1195.      * - Catgories: nom contient (pour la colonne "suggestions")
  1196.      *
  1197.      * @Route("/api/header-search-suggest", name="api_header_search_suggest", options={"expose"=true}, methods={"GET"})
  1198.      */
  1199.     public function headerSearchSuggest(Request $request): JsonResponse
  1200.     {
  1201.         $q trim((string) $request->query->get('q'''));
  1202.         $idCategory = (int) $request->query->get('idCategory'0);
  1203.         $normalizedQ mb_strtolower($q);
  1204.         if (mb_strlen($q) < 2) {
  1205.             return new JsonResponse(['products' => [], 'categories' => [], 'references' => []]);
  1206.         }
  1207.         // --------- Catgories slectionnes (si filtre) + enfants ---------
  1208.         $categoriesFilter = [];
  1209.         if ($idCategory 0) {
  1210.             $cat $this->em->getRepository(Category::class)->find($idCategory);
  1211.             if ($cat) {
  1212.                 $categoriesFilter[] = $cat;
  1213.                 foreach ($cat->getSubCategories() as $child) {
  1214.                     $categoriesFilter[] = $child;
  1215.                     foreach ($child->getSubCategories() as $sub) {
  1216.                         $categoriesFilter[] = $sub;
  1217.                     }
  1218.                 }
  1219.             }
  1220.         }
  1221.         // --------- Produits ---------
  1222.         $qb $this->em->getRepository(Produit::class)->createQueryBuilder('p');
  1223.         $qb->andWhere('p.deletedAt IS NULL')
  1224.             ->andWhere('p.showInWebSite = 1')
  1225.             ->andWhere('(p.reference LIKE :refStart OR p.reference LIKE :refContains OR p.name LIKE :nameContains)')
  1226.             ->setParameter('refStart'$q '%')
  1227.             ->setParameter('refContains''%' $q '%')
  1228.             ->setParameter('nameContains''%' $q '%')
  1229.             ->orderBy('p.createdAt''DESC')
  1230.             ->setMaxResults(12);
  1231.         if (!empty($categoriesFilter)) {
  1232.             $qb->andWhere('p.categories IN (:cats)')
  1233.             ->setParameter('cats'$categoriesFilter);
  1234.         }
  1235.         $products $qb->getQuery()->getResult();
  1236.         usort($products, function (Produit $aProduit $b) use ($normalizedQ) {
  1237.             $scoreA $this->getSearchSuggestionScore($a$normalizedQ);
  1238.             $scoreB $this->getSearchSuggestionScore($b$normalizedQ);
  1239.             if ($scoreA !== $scoreB) {
  1240.                 return $scoreA $scoreB : -1;
  1241.             }
  1242.             $dateA $a->getCreatedAt() ? $a->getCreatedAt()->getTimestamp() : 0;
  1243.             $dateB $b->getCreatedAt() ? $b->getCreatedAt()->getTimestamp() : 0;
  1244.             if ($dateA !== $dateB) {
  1245.                 return $dateB <=> $dateA;
  1246.             }
  1247.             return $b->getId() <=> $a->getId();
  1248.         });
  1249.         $now = new \DateTime(); // mme style que ton admin
  1250.         $productsPayload = [];
  1251.         $referencesPayload = [];
  1252.         $seenReferences = [];
  1253.         foreach ($products as $p) {
  1254.             // image via ton trait
  1255.             $img $this->getDefaultImage($p);
  1256.             // ===== Promo (sans changer l'existant, on enrichit) =====
  1257.             $priceWithoutPromo = (float) $p->getPriceTtc();
  1258.             $finalPrice $priceWithoutPromo;
  1259.             $inPromo false;
  1260.             $promoLabel null;
  1261.             $promotionType null;
  1262.             $value null;
  1263.             $promotion $p->getPromotion();
  1264.             if ($promotion) {
  1265.                 // on garde la logique "promo active entre start/end"
  1266.                 if ($promotion->getStartAt() <= $now && $promotion->getEndAt() >= $now) {
  1267.                     $value = (float) $promotion->getDiscountValue();
  1268.                     $promotionType $promotion->getDiscountType(); // 'percent' ou autre (montant)
  1269.                     if ($promotionType === 'percent') {
  1270.                         $finalPrice round($priceWithoutPromo * ($value 100), 3);
  1271.                         $promoLabel '-' rtrim(rtrim(number_format($value2'.'''), '0'), '.') . '%';
  1272.                     } else {
  1273.                         // remise fixe
  1274.                         $finalPrice round($priceWithoutPromo $value3);
  1275.                         if ($finalPrice 0) { $finalPrice 0.0; }
  1276.                         $promoLabel '-' rtrim(rtrim(number_format($value3'.'''), '0'), '.') . ' TND';
  1277.                     }
  1278.                     // scurit : promo relle
  1279.                     if ($finalPrice $priceWithoutPromo && $priceWithoutPromo 0) {
  1280.                         $inPromo true;
  1281.                     } else {
  1282.                         // sinon on annule pour ne pas envoyer un "faux promo"
  1283.                         $finalPrice $priceWithoutPromo;
  1284.                         $inPromo false;
  1285.                         $promoLabel null;
  1286.                         $promotionType null;
  1287.                         $value null;
  1288.                     }
  1289.                 }
  1290.             }
  1291.             // ===== Payload (on garde les cls existantes) =====
  1292.             $productsPayload[] = [
  1293.                 'id'        => $p->getId(),
  1294.                 'reference' => $p->getReference(),
  1295.                 'name'      => $p->getName(),
  1296.                 'price'     => $finalPrice,
  1297.                 'image'     => $img ?: null,
  1298.                 'category'  => $p->getCategories() ? $p->getCategories()->getName() : null,
  1299.                 'in_promo'           => $inPromo,
  1300.                 'price_without_promo'=> $priceWithoutPromo,
  1301.                 'promo_label'        => $promoLabel,
  1302.                 'promotionType'      => $promotionType,
  1303.                 'value'              => $value,
  1304.                 'aspectRatio' => $this->getMixedAspectRatioFallback(),
  1305.                 'is_new' => $p->isNew(),
  1306.                 'match_type' => $this->resolveSuggestionMatchType($p$normalizedQ),
  1307.             ];
  1308.             $reference trim((string) $p->getReference());
  1309.             if ($reference !== '' && !isset($seenReferences[$reference]) && str_contains(mb_strtolower($reference), $normalizedQ)) {
  1310.                 $seenReferences[$reference] = true;
  1311.                 $referencesPayload[] = [
  1312.                     'id' => $p->getId(),
  1313.                     'reference' => $reference,
  1314.                     'name' => $p->getName(),
  1315.                 ];
  1316.             }
  1317.         }
  1318.         // --------- Suggestions catgories (colonne droite) ---------
  1319.         $catQb $this->em->getRepository(Category::class)->createQueryBuilder('c');
  1320.         $catQb->andWhere('c.name LIKE :cq')
  1321.             ->setParameter('cq''%' $q '%')
  1322.             ->orderBy('c.name''ASC')
  1323.             ->setMaxResults(10);
  1324.         $cats $catQb->getQuery()->getResult();
  1325.         $catsPayload = [];
  1326.         foreach ($cats as $c) {
  1327.             $catsPayload[] = [
  1328.                 'id'   => $c->getId(),
  1329.                 'name' => $c->getName(),
  1330.             ];
  1331.         }
  1332.         return new JsonResponse([
  1333.             'products' => $productsPayload,
  1334.             'categories' => $catsPayload,
  1335.             'references' => array_slice($referencesPayload08),
  1336.         ]);
  1337.     }
  1338.     private function getSearchSuggestionScore(Produit $productstring $normalizedQ): int
  1339.     {
  1340.         $reference mb_strtolower((string) ($product->getReference() ?? ''));
  1341.         $name mb_strtolower((string) ($product->getName() ?? ''));
  1342.         if ($reference !== '' && str_starts_with($reference$normalizedQ)) {
  1343.             return 300;
  1344.         }
  1345.         if ($name !== '' && str_starts_with($name$normalizedQ)) {
  1346.             return 220;
  1347.         }
  1348.         if ($reference !== '' && str_contains($reference$normalizedQ)) {
  1349.             return 180;
  1350.         }
  1351.         if ($name !== '' && str_contains($name$normalizedQ)) {
  1352.             return 140;
  1353.         }
  1354.         return 100;
  1355.     }
  1356.     private function resolveSuggestionMatchType(Produit $productstring $normalizedQ): string
  1357.     {
  1358.         $reference mb_strtolower((string) ($product->getReference() ?? ''));
  1359.         if ($reference !== '' && str_contains($reference$normalizedQ)) {
  1360.             return 'reference';
  1361.         }
  1362.         return 'product';
  1363.     }
  1364.     /**
  1365.      * @Route("/search/{idCategory}/{search}", name="product_search", options={"expose"=true}, methods={"GET"})
  1366.      */
  1367.     public function searchProducts(Request $requestint $idCategorystring $searchEntityManagerInterface $em): Response
  1368.     {
  1369.         $category null;
  1370.         if ($idCategory 0) {
  1371.             $category $em->getRepository(Category::class)->find($idCategory);
  1372.         }
  1373.         return $this->render('front/product/listProducts.html.twig', [
  1374.             'pageMode'    => 'search',
  1375.             'search'      => $search,
  1376.             'category_id' => $idCategory,
  1377.             'categorie'   => $category,   // optionnel
  1378.             'categories'  => [],          // si tu ne veux plus fournir la liste
  1379.         ]);
  1380.     }
  1381.     /**
  1382.      * @Route("/search-dec/{idCategory}/{search}", name="product_search_dec", options={"expose"=true}, methods={"GET"})
  1383.      */
  1384.     public function searchProductsDec(Request $request,$idCategory,$search): Response
  1385.     {
  1386.         $categories = [];
  1387.         if($idCategory) {
  1388.             $category $this->em->getRepository(Category::class)->find($idCategory);
  1389.             // Rcuprer les catgories fils
  1390.             array_push($categories$category);
  1391.             foreach ($category->getSubCategories() as $child) {
  1392.                 array_push($categories$child);
  1393.                 foreach ($child->getSubCategories() as $sub) {
  1394.                     array_push($categories$sub);
  1395.                 }
  1396.             }
  1397.         }
  1398.         return $this->render('front/dec/searchProductsDec.html.twig', [
  1399.             'idCategory' => $idCategory,
  1400.             'categories' => $categories,
  1401.             'search' => $search,
  1402.         ]);
  1403.     }
  1404.     /**
  1405.      * @Route("/api/product-search", name="api_product_search", options={"expose"=true}, methods={"GET"})
  1406.      */
  1407.     public function serachProductsAPI(Request $request): Response
  1408.     {
  1409.         $category_id $request->query->get('id');
  1410.         $search $request->query->get('search');
  1411.         $page $request->query->get('page');
  1412.         $orderBy $request->query->get('orderBy');
  1413.         $categories = [];
  1414.         $category $this->em->getRepository(Category::class)->find($category_id);
  1415.         if ($category){
  1416.             // Rcuprer kes catgories fils
  1417.             array_push($categories$category);
  1418.             foreach ($category->getSubCategories() as $child) {
  1419.                 array_push($categories$child);
  1420.                 foreach ($child->getSubCategories() as $sub) {
  1421.                     array_push($categories$sub);
  1422.                 }
  1423.             }
  1424.         }
  1425.         //Les Filtres de recherche
  1426.         $tailles preg_split('@,@'$request->query->get('tailles'), NULLPREG_SPLIT_NO_EMPTY);
  1427.         $couleurs preg_split('@,@'$request->query->get('couleurs'), NULLPREG_SPLIT_NO_EMPTY);
  1428.         $maxPrice floatval($request->query->get('maxPrice'));
  1429.         $minPrice floatval($request->query->get('minPrice'));
  1430.         // get the product repository
  1431.         $produits $this->em->getRepository(Produit::class);
  1432.         // build the query for the doctrine paginator
  1433.         $query $produits->createQueryBuilder('p')
  1434.             ->where('p.name like :search OR p.description like :search OR p.reference like :search')
  1435.             ->setParameter('search''%' $search '%')
  1436.             ->andWhere('p.deletedAt is null')
  1437.             ->andWhere('p.showInWebSite = 1')
  1438.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  1439.             ->setParameter('maxPrice'$maxPrice)
  1440.             ->setParameter('minPrice'$minPrice);
  1441.         // Si catgorie est slectionne
  1442.         if( $category) {
  1443.             $query->andWhere('p.categories in (:cat)')
  1444.                 ->setParameter('cat'$categories);
  1445.         }
  1446.         // Appliquer les filtres de tailles si ils sont appliqus
  1447.         if( $tailles && !$couleurs) {
  1448.             $declination $this->em->getRepository(Declination::class)->find(1);
  1449.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  1450.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1451.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1452.                 ->andWhere("v.name in (:tailles)")
  1453.                 ->andWhere('v.declination = :declination')
  1454.                 ->setParameter('declination'$declination)
  1455.                 ->setParameter('tailles'$tailles);
  1456.         }
  1457.         // Appliquer les filtres de couleurs si ils sont appliqus seulement
  1458.         if( $couleurs && !$tailles) {
  1459.             $declination $this->em->getRepository(Declination::class)->find(2);
  1460.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  1461.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1462.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1463.                 ->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
  1464.                 ->andWhere('v.declination = :declination')
  1465.                 ->setParameter('declination'$declination)
  1466.                 ->setParameter('couleurs'$couleurs);
  1467.         }
  1468.         // Appliquer les filtres de tailles et couleurs appliqus les deux
  1469.         if( $tailles && $couleurs) {
  1470.             $declinationTaille $this->em->getRepository(Declination::class)->find(1);
  1471.             $declinationCouleur $this->em->getRepository(Declination::class)->find(2);
  1472.             // Fusionner les deux filtres en un seul
  1473.             $filtres array_merge($tailles$couleurs);
  1474.             $query->innerJoin('App\Entity\ProduitDeclinationValue''d''with''d.produit=p')
  1475.                 ->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1476.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1477.                 ->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
  1478.                 /* ->andWhere('v.declination = :declinationTaille OR v.declination = :declinationCouleur')
  1479.                  ->setParameter('declinationTaille', $declinationTaille)*/
  1480.                 ->setParameter('couleurs'$couleurs)
  1481.                 ->setParameter('tailles'$tailles);
  1482.         }
  1483.         // Order by
  1484.         switch ($orderBy) {
  1485.             case 1:
  1486.                 $query->orderBy('p.name''ASC');
  1487.                 break;
  1488.             case 2:
  1489.                 $query->orderBy('p.name''DESC');
  1490.                 break;
  1491.             case 3:
  1492.                 $query->orderBy('p.price_ttc''ASC');
  1493.                 break;
  1494.             case 4:
  1495.                 $query->orderBy('p.price_ttc''DESC');
  1496.                 break;
  1497.             case 5:
  1498.                 $query->orderBy('p.createdAt''ASC');
  1499.                 break;
  1500.             case 6:
  1501.                 $query->orderBy('p.createdAt''DESC');
  1502.                 break;
  1503.         }
  1504.             $query->getQuery();
  1505.         // set page size
  1506.         $pageSize $request->query->get('pageSize');;
  1507.         // load doctrine Paginator
  1508.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  1509.         // you can get total items
  1510.         $totalItems count($paginator);
  1511.         // get total pages
  1512.         $pagesCount ceil($totalItems $pageSize);
  1513.         // now get one page's items:
  1514.         $paginator
  1515.             ->getQuery()
  1516.             ->setFirstResult($pageSize * ($page-1)) // set the offset
  1517.             ->setMaxResults($pageSize); // set the limit
  1518.         $data= array();
  1519.         foreach ($paginator as $pageItem) {
  1520.             // do stuff with results...
  1521.             array_push($data$pageItem);
  1522.         }
  1523.         foreach ($data as $product){
  1524.             $product->image $this->getDefaultImage$product);
  1525.         }
  1526.         // Les nombres de pages
  1527.         $pages = array();
  1528.         for($i=max($page-31);$i<=min($page+3$pagesCount) ;$i++){
  1529.             array_push($pages,$i);
  1530.         }
  1531.         $response = [
  1532.             'res' => 'OK',
  1533.             'data' => $data,
  1534.             'pagesCount' => $pagesCount,
  1535.             'total' => $totalItems,
  1536.             'pages' => $pages,
  1537.             'message' => 'Produits rcuprs avec succs.',
  1538.         ];
  1539.         return new JsonResponse($response);
  1540.     }
  1541.     /**
  1542.      * @Route("/api/product-search-dec", name="api_product_search_dec", options={"expose"=true}, methods={"GET"})
  1543.      */
  1544.     public function serachProductsDecAPI(Request $request): Response
  1545.     {
  1546.         $category_id $request->query->get('id');
  1547.         $search $request->query->get('search');
  1548.         $page $request->query->get('page');
  1549.         $orderBy $request->query->get('orderBy');
  1550.         $categories = [];
  1551.         $category $this->em->getRepository(Category::class)->find($category_id);
  1552.         if ($category){
  1553.             // Rcuprer kes catgories fils
  1554.             array_push($categories$category);
  1555.             foreach ($category->getSubCategories() as $child) {
  1556.                 array_push($categories$child);
  1557.                 foreach ($child->getSubCategories() as $sub) {
  1558.                     array_push($categories$sub);
  1559.                 }
  1560.             }
  1561.         }
  1562.         //Les Filtres de recherche
  1563.         $tailles preg_split('@,@'$request->query->get('tailles'), NULLPREG_SPLIT_NO_EMPTY);
  1564.         $couleurs preg_split('@,@'$request->query->get('couleurs'), NULLPREG_SPLIT_NO_EMPTY);
  1565.         $maxPrice floatval($request->query->get('maxPrice'));
  1566.         $minPrice floatval($request->query->get('minPrice'));
  1567.         // get the product dec repository
  1568.         $produits $this->em->getRepository(ProduitDeclinationValue::class);
  1569.         // build the query for the doctrine paginator
  1570.         $query $produits->createQueryBuilder('d');
  1571.         // build the query for the doctrine paginator
  1572.         $query->innerJoin('App\Entity\Produit''p''with''d.produit=p')
  1573.             ->where('p.name like :search OR p.description like :search OR p.reference like :search')
  1574.             ->setParameter('search''%' $search '%')
  1575.             ->andWhere('p.deletedAt is null')
  1576.             ->andWhere('p.showInWebSite = 1')
  1577.             ->andWhere('p.price_ttc between :minPrice and :maxPrice')
  1578.             ->setParameter('maxPrice'$maxPrice)
  1579.             ->setParameter('minPrice'$minPrice)
  1580.             ->groupBy('p.id,v.id');
  1581.         // Si catgorie est slectionne
  1582.         if( $category) {
  1583.             $query->andWhere('p.categories in (:cat)')
  1584.                 ->setParameter('cat'$categories);
  1585.         }
  1586.         // Appliquer les filtres de tailles si ils sont ne pas appliqus
  1587.         if( !$tailles && !$couleurs) {
  1588.             $declination $this->em->getRepository(Declination::class)->find(1);
  1589.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1590.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1591.                 ->andWhere('v.declination=2');
  1592.         }
  1593.         // Appliquer les filtres de tailles si ils sont appliqus
  1594.         if( $tailles && !$couleurs) {
  1595.             $declination $this->em->getRepository(Declination::class)->find(1);
  1596.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1597.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1598.                 ->andWhere("v.name in (:tailles)")
  1599.                 ->setParameter('tailles'$tailles);
  1600.         }
  1601.         // Appliquer les filtres de couleurs si ils sont appliqus seulement
  1602.         if( $couleurs && !$tailles) {
  1603.             $declination $this->em->getRepository(Declination::class)->find(2);
  1604.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1605.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1606.                 ->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
  1607.                 ->setParameter('couleurs'$couleurs);
  1608.         }
  1609.         // Appliquer les filtres de tailles et couleurs appliqus les deux
  1610.         if( $tailles && $couleurs) {
  1611.             $declinationTaille $this->em->getRepository(Declination::class)->find(1);
  1612.             $declinationCouleur $this->em->getRepository(Declination::class)->find(2);
  1613.             // Fusionner les deux filtres en un seul
  1614.             $filtres array_merge($tailles$couleurs);
  1615.             $query->innerJoin('App\Entity\GroupDeclinationValue''g''with''g.produitDeclination= d')
  1616.                 ->innerJoin('App\Entity\ValueDeclination''v''with''v= g.value')
  1617.                 ->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
  1618.                 ->setParameter('couleurs'$couleurs)
  1619.                 ->setParameter('tailles'$tailles)
  1620.                 ->groupBy('v.declination')
  1621.                 ->having('v.declination=2');
  1622.         }
  1623.         // Order by
  1624.         switch ($orderBy) {
  1625.             case 1:
  1626.                 $query->orderBy('p.name''ASC');
  1627.                 break;
  1628.             case 2:
  1629.                 $query->orderBy('p.name''DESC');
  1630.                 break;
  1631.             case 3:
  1632.                 $query->orderBy('p.price_ttc''ASC');
  1633.                 break;
  1634.             case 4:
  1635.                 $query->orderBy('p.price_ttc''DESC');
  1636.                 break;
  1637.             case 5:
  1638.                 $query->orderBy('p.createdAt''ASC');
  1639.                 break;
  1640.             case 6:
  1641.                 $query->orderBy('p.createdAt''DESC');
  1642.                 break;
  1643.         }
  1644.         $query->getQuery();
  1645.         // set page size
  1646.         $pageSize $request->query->get('pageSize');;
  1647.         // load doctrine Paginator
  1648.         $paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
  1649.         // you can get total items
  1650.         $totalItems count($paginator);
  1651.         // get total pages
  1652.         $pagesCount ceil($totalItems $pageSize);
  1653.         // now get one page's items:
  1654.         $paginator
  1655.             ->getQuery()
  1656.             ->setFirstResult($pageSize * ($page-1)) // set the offset
  1657.             ->setMaxResults($pageSize); // set the limit
  1658.         $data= array();
  1659.         foreach ($paginator as $pageItem) {
  1660.             // do stuff with results...
  1661.             $pageItem->setImage($this->getDefaultImage(null$pageItem));
  1662.             array_push($data,$pageItem);
  1663.         }
  1664.         // Les nombres de pages
  1665.         $pages = array();
  1666.         for($i=max($page-31);$i<=min($page+3$pagesCount) ;$i++){
  1667.             array_push($pages,$i);
  1668.         }
  1669.         $response = [
  1670.             'res' => 'OK',
  1671.             'data' => $data,
  1672.             'pagesCount' => $pagesCount,
  1673.             'total' => $totalItems,
  1674.             'pages' => $pages,
  1675.             'message' => 'Produits rcuprs avec succs.',
  1676.         ];
  1677.         return new JsonResponse($response);
  1678.     }
  1679.    /**
  1680.      * @Route("/good-plan", name="pack_products")
  1681.      */
  1682.     public function goodPlan(WebsiteSettingService $websiteSettingService): Response
  1683.     {
  1684.         $defaultMode = (string) $websiteSettingService->get('catalogListMode''product');
  1685.         $defaultMode in_array($defaultMode, ['product''declination'], true) ? $defaultMode 'product';
  1686.         return $this->render('front/pages/packProducts.html.twig', [
  1687.             'defaultMode' => $defaultMode,
  1688.         ]);
  1689.     }
  1690.     /**
  1691.      * @Route("/good-plan/config/{id}/{slug}", name="pack_products_config", requirements={"id"="\d+"}, defaults={"slug"=""}, options={"expose"=true})
  1692.      */
  1693.     public function goodPlanConfig(Pack $packWebsiteSettingService $websiteSettingService): Response
  1694.     {
  1695.         if ($pack->getIsArchived() === true || !$pack->getShowInWebSite()) {
  1696.             throw $this->createNotFoundException('Pack introuvable.');
  1697.         }
  1698.         $defaultMode = (string) $websiteSettingService->get('catalogListMode''product');
  1699.         $defaultMode in_array($defaultMode, ['product''declination'], true) ? $defaultMode 'product';
  1700.         return $this->render('front/pages/packConfigure.html.twig', [
  1701.             'defaultMode' => $defaultMode,
  1702.             'selectedPackId' => $pack->getId(),
  1703.         ]);
  1704.     }
  1705.     /**
  1706.      * @Route("/api/packs", name="front_pack_list_api", methods={"GET"}, options={"expose"=true})
  1707.      */
  1708.     public function packListApi(): JsonResponse
  1709.     {
  1710.         $packs $this->em->getRepository(Pack::class)->createQueryBuilder('p')
  1711.             ->andWhere('(p.isArchived = 0 OR p.isArchived IS NULL)')
  1712.             ->andWhere('p.showInWebSite = 1')
  1713.             ->orderBy('p.createdAt''DESC')
  1714.             ->addOrderBy('p.id''DESC')
  1715.             ->getQuery()
  1716.             ->getResult();
  1717.         $data = [];
  1718.         foreach ($packs as $pack) {
  1719.             // Swatches (position 1) de tous les produits du pack
  1720.             $swatchesMap = [];
  1721.             foreach ($pack->getItems() as $item) {
  1722.                 $prod $item->getProduit();
  1723.                 if (!$prod) continue;
  1724.                 foreach ($prod->getProduitDeclinationValues() as $decVal) {
  1725.                     foreach ($decVal->getGroupDeclinationValues() as $gdv) {
  1726.                         $decl $gdv->getDeclination();
  1727.                         if (!$decl || (int)$decl->getPosition() !== 1) continue;
  1728.                         $val $gdv->getValue();
  1729.                         if (!$val) continue;
  1730.                         $code method_exists($val'getCode') ? $val->getCode() : null;
  1731.                         if (!$code && method_exists($val'getParent') && $val->getParent() && method_exists($val->getParent(), 'getCode')) {
  1732.                             $code $val->getParent()->getCode();
  1733.                         }
  1734.                         $swatchesMap[(int)$val->getId()] = [
  1735.                             'id' => (int)$val->getId(),
  1736.                             'name' => (string)$val->getName(),
  1737.                             'code' => (string)($code ?? ''),
  1738.                         ];
  1739.                     }
  1740.                 }
  1741.             }
  1742.             // Images pack associes  une valeur position 1
  1743.             $packImageByPos1 = [];
  1744.             $links $this->em->getRepository(PackImageDeclination::class)->createQueryBuilder('pid')
  1745.                 ->join('pid.file''f')
  1746.                 ->join('pid.valueDeclination''vd')
  1747.                 ->where('pid.pack = :pack')
  1748.                 ->setParameter('pack'$pack)
  1749.                 ->getQuery()
  1750.                 ->getResult();
  1751.             foreach ($links as $link) {
  1752.                 $val $link->getValueDeclination();
  1753.                 $file $link->getFile();
  1754.                 if ($val && $file && $file->getImageName()) {
  1755.                     $packImageByPos1[(string)$val->getId()] = (string)$file->getImageName();
  1756.                 }
  1757.             }
  1758.             $data[] = [
  1759.                 'id' => $pack->getId(),
  1760.                 'reference' => $pack->getReference(),
  1761.                 'name' => $pack->getName(),
  1762.                 'description' => $pack->getDescription(),
  1763.                 'price_ttc' => (float)($pack->getPriceTtc() ?? 0),
  1764.                 'final_price_ttc' => (float)($pack->getFinalPriceTtc() ?? 0),
  1765.                 'remise' => (float)($pack->getRemise() ?? 0),
  1766.                 'picture' => $this->getPackMainImageName($pack),
  1767.                 'items_count' => $pack->getItems()->count(),
  1768.                 'createdAt' => $pack->getCreatedAt() ? $pack->getCreatedAt()->format(\DateTimeInterface::ATOM) : null,
  1769.                 'swatches' => array_values($swatchesMap),
  1770.                 'pack_image_by_pos1' => $packImageByPos1,
  1771.             ];
  1772.         }
  1773.         return new JsonResponse(['res' => 'OK''data' => $data]);
  1774.     }
  1775.     /**
  1776.      * @Route("/api/pack/{id}/configurator", name="front_pack_configurator_api", methods={"GET"}, options={"expose"=true})
  1777.      */
  1778.     public function packConfiguratorApi(Pack $pack): JsonResponse
  1779.     {
  1780.         if ($pack->getIsArchived() === true || !$pack->getShowInWebSite()) {
  1781.             return new JsonResponse(['res' => 'ERROR''message' => 'Pack introuvable'], 404);
  1782.         }
  1783.         $items = [];
  1784.         foreach ($pack->getItems() as $item) {
  1785.             $p $item->getProduit();
  1786.             if (!$p) continue;
  1787.             $items[] = [
  1788.                 'product_id' => $p->getId(),
  1789.                 'product_reference' => $p->getReference(),
  1790.                 'product_name' => $p->getName(),
  1791.                 'product_picture' => $this->getDefaultImage($p),
  1792.             ];
  1793.         }
  1794.         $links $this->em->getRepository(PackImageDeclination::class)->createQueryBuilder('pid')
  1795.             ->join('pid.file''f')
  1796.             ->join('pid.valueDeclination''vd')
  1797.             ->where('pid.pack = :pack')
  1798.             ->setParameter('pack'$pack)
  1799.             ->getQuery()
  1800.             ->getResult();
  1801.         $packImageByPos1 = [];
  1802.         foreach ($links as $link) {
  1803.             $val $link->getValueDeclination();
  1804.             $file $link->getFile();
  1805.             if (!$val || !$file || !$file->getImageName()) {
  1806.                 continue;
  1807.             }
  1808.             $packImageByPos1[(string)$val->getId()] = [
  1809.                 'id' => (string)$val->getId(),
  1810.                 'name' => (string)$val->getName(),
  1811.                 'picture' => (string)$file->getImageName(),
  1812.             ];
  1813.         }
  1814.         return new JsonResponse([
  1815.             'res' => 'OK',
  1816.             'data' => [
  1817.                 'id' => $pack->getId(),
  1818.                 'reference' => $pack->getReference(),
  1819.                 'name' => $pack->getName(),
  1820.                 'description' => $pack->getDescription(),
  1821.                 'price_ttc' => (float)($pack->getPriceTtc() ?? 0),
  1822.                 'final_price_ttc' => (float)($pack->getFinalPriceTtc() ?? 0),
  1823.                 'remise' => (float)($pack->getRemise() ?? 0),
  1824.                 'picture' => $this->getPackMainImageName($pack),
  1825.                 'items' => $items,
  1826.                 'pack_image_by_pos1' => $packImageByPos1
  1827.             ]
  1828.         ]);
  1829.     }
  1830.     private function getPackMainImageName(Pack $pack): string
  1831.     {
  1832.         foreach ($pack->getPicture() as $img) {
  1833.             if ($img->getIsSelected()) return (string)$img->getImageName();
  1834.         }
  1835.         foreach ($pack->getPicture() as $img) {
  1836.             return (string)$img->getImageName();
  1837.         }
  1838.         return 'no-image.jpg';
  1839.     }
  1840.     private function preloadProductsForCards(array $products): array
  1841.     {
  1842.         $ids = [];
  1843.         foreach ($products as $product) {
  1844.             if ($product instanceof Produit && $product->getId()) {
  1845.                 $ids[] = (int) $product->getId();
  1846.             }
  1847.         }
  1848.         $ids array_values(array_unique($ids));
  1849.         if (!$ids) {
  1850.             return [];
  1851.         }
  1852.         $rows $this->em->getRepository(Produit::class)->createQueryBuilder('p')
  1853.             ->leftJoin('p.promotion''promo')->addSelect('promo')
  1854.             ->leftJoin('p.picture''picture')->addSelect('picture')
  1855.             ->leftJoin('p.produitDeclinationValues''dec')->addSelect('dec')
  1856.             ->leftJoin('dec.picture''decPicture')->addSelect('decPicture')
  1857.             ->leftJoin('dec.groupDeclinationValues''gdv')->addSelect('gdv')
  1858.             ->leftJoin('gdv.declination''decl')->addSelect('decl')
  1859.             ->leftJoin('gdv.value''value')->addSelect('value')
  1860.             ->leftJoin('value.parent''parent')->addSelect('parent')
  1861.             ->leftJoin('dec.stocks''stock')->addSelect('stock')
  1862.             ->where('p.id IN (:ids)')
  1863.             ->setParameter('ids'$ids)
  1864.             ->getQuery()
  1865.             ->getResult();
  1866.         $map = [];
  1867.         foreach ($rows as $row) {
  1868.             $map[(int) $row->getId()] = $row;
  1869.         }
  1870.         return $map;
  1871.     }
  1872.     private function mapHomeProductCard(Produit $product): array
  1873.     {
  1874.         $swatches $this->buildProductColorSwatches($product);
  1875.         return [
  1876.             'id' => (int) $product->getId(),
  1877.             'idProduit' => (int) $product->getId(),
  1878.             'productId' => (int) $product->getId(),
  1879.             'name' => (string) $product->getName(),
  1880.             'reference' => (string) ($product->getReference() ?? ''),
  1881.             'priceTTC' => (float) ($product->getPriceTtc() ?? 0),
  1882.             'image' => $this->resolveProductPrimaryImage($product),
  1883.             'hoverImage' => $this->resolveProductHoverImage($product),
  1884.             'aspectRatio' => $this->getCategoryAspectRatio($product),
  1885.             'promo' => $this->mapPromotionData($product->getPromotion()),
  1886.             'ratingScore' => (float) ($product->getRatingScore() ?? 0),
  1887.             'ratingCount' => (int) ($product->getRatingCount() ?? 0),
  1888.             'colorSwatches' => $swatches,
  1889.             'activeSizes' => isset($swatches[0]['sizes']) && is_array($swatches[0]['sizes']) ? $swatches[0]['sizes'] : [],
  1890.             'stock' => $this->hasProductStock($product),
  1891.         ];
  1892.     }
  1893.     private function loadHomeCategoryProducts(int $categoryIdstring $modeRequest $request): array
  1894.     {
  1895.         $limit = (int) $this->websiteSettingService->get('homepageProductsPerTab'12);
  1896.         if ($limit <= 0) {
  1897.             $limit 12;
  1898.         }
  1899.         $category $this->em->getRepository(Category::class)->find($categoryId);
  1900.         if (!$category) {
  1901.             return [];
  1902.         }
  1903.         $categories = [$category];
  1904.         foreach ($category->getSubCategories() as $child) {
  1905.             $categories[] = $child;
  1906.             foreach ($child->getSubCategories() as $sub) {
  1907.                 $categories[] = $sub;
  1908.             }
  1909.         }
  1910.         if ($mode === 'declination') {
  1911.             $qb $this->em->getRepository(ProduitDeclinationValue::class)->createQueryBuilder('d')
  1912.                 ->innerJoin('d.produit''p')
  1913.                 ->where('p.categories IN (:cats)')
  1914.                 ->andWhere('p.deletedAt IS NULL')
  1915.                 ->andWhere('p.showInWebSite = 1')
  1916.                 ->setParameter('cats'$categories)
  1917.                 ->orderBy('p.createdAt''DESC')
  1918.                 ->addOrderBy('d.id''DESC');
  1919.             $this->applyDeclinationCardPreloads($qb'd''p');
  1920.             $rows $qb->getQuery()->getResult();
  1921.             return array_slice($this->buildHomePositionOneCards($rows), 0$limit);
  1922.         }
  1923.         $products $this->em->getRepository(Produit::class)->createQueryBuilder('p')
  1924.             ->where('p.categories IN (:cats)')
  1925.             ->andWhere('p.deletedAt IS NULL')
  1926.             ->andWhere('p.showInWebSite = 1')
  1927.             ->setParameter('cats'$categories)
  1928.             ->orderBy('p.createdAt''DESC')
  1929.             ->addOrderBy('p.id''DESC')
  1930.             ->setMaxResults($limit)
  1931.             ->getQuery()
  1932.             ->getResult();
  1933.         $hydratedProducts $this->preloadProductsForCards($products);
  1934.         $data = [];
  1935.         foreach ($products as $product) {
  1936.             $productId = (int) $product->getId();
  1937.             if (!isset($hydratedProducts[$productId])) {
  1938.                 continue;
  1939.             }
  1940.             $data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
  1941.         }
  1942.         return $data;
  1943.     }
  1944.     private function loadHomeCategoriesDataset(array $categoriesstring $mode): array
  1945.     {
  1946.         $limit = (int) $this->websiteSettingService->get('homepageProductsPerTab'12);
  1947.         if ($limit <= 0) {
  1948.             $limit 12;
  1949.         }
  1950.         $dataset = [];
  1951.         $categoryScopes = [];
  1952.         $topCategoryIdsByDescendantId = [];
  1953.         $allScopeCategories = [];
  1954.         foreach ($categories as $category) {
  1955.             if (!$category instanceof Category) {
  1956.                 continue;
  1957.             }
  1958.             $topCategoryId = (int) $category->getId();
  1959.             $scope = [$category];
  1960.             foreach ($category->getSubCategories() as $child) {
  1961.                 $scope[] = $child;
  1962.                 foreach ($child->getSubCategories() as $sub) {
  1963.                     $scope[] = $sub;
  1964.                 }
  1965.             }
  1966.             $dataset[$topCategoryId] = (object) [
  1967.                 'category' => $category,
  1968.                 'products' => [],
  1969.             ];
  1970.             $categoryScopes[$topCategoryId] = $scope;
  1971.             foreach ($scope as $scopeCategory) {
  1972.                 if (!$scopeCategory instanceof Category) {
  1973.                     continue;
  1974.                 }
  1975.                 $scopeCategoryId = (int) $scopeCategory->getId();
  1976.                 $allScopeCategories[$scopeCategoryId] = $scopeCategory;
  1977.                 if (!isset($topCategoryIdsByDescendantId[$scopeCategoryId])) {
  1978.                     $topCategoryIdsByDescendantId[$scopeCategoryId] = [];
  1979.                 }
  1980.                 $topCategoryIdsByDescendantId[$scopeCategoryId][$topCategoryId] = $topCategoryId;
  1981.             }
  1982.         }
  1983.         if (empty($dataset)) {
  1984.             return [];
  1985.         }
  1986.         if ($mode === 'declination') {
  1987.             $rows $this->em->getRepository(ProduitDeclinationValue::class)->createQueryBuilder('d')
  1988.                 ->innerJoin('d.produit''p')
  1989.                 ->innerJoin('p.categories''c')
  1990.                 ->where('c IN (:cats)')
  1991.                 ->andWhere('p.deletedAt IS NULL')
  1992.                 ->andWhere('p.showInWebSite = 1')
  1993.                 ->setParameter('cats'array_values($allScopeCategories))
  1994.                 ->orderBy('p.createdAt''DESC')
  1995.                 ->addOrderBy('d.id''DESC')
  1996.                 ->getQuery()
  1997.                 ->getResult();
  1998.             $cards $this->buildHomePositionOneCards($rows);
  1999.             $productTopCategoryMap = [];
  2000.             foreach ($rows as $row) {
  2001.                 if (!$row instanceof ProduitDeclinationValue || !$row->getProduit()) {
  2002.                     continue;
  2003.                 }
  2004.                 $product $row->getProduit();
  2005.                 $productId = (int) $product->getId();
  2006.                 if (isset($productTopCategoryMap[$productId])) {
  2007.                     continue;
  2008.                 }
  2009.                 $matchedTopIds = [];
  2010.                 foreach ($product->getCategories() as $productCategory) {
  2011.                     $productCategoryId = (int) $productCategory->getId();
  2012.                     foreach ($topCategoryIdsByDescendantId[$productCategoryId] ?? [] as $topCategoryId) {
  2013.                         $matchedTopIds[$topCategoryId] = $topCategoryId;
  2014.                     }
  2015.                 }
  2016.                 $productTopCategoryMap[$productId] = array_values($matchedTopIds);
  2017.             }
  2018.             $seenPerTopCategory = [];
  2019.             foreach ($cards as $card) {
  2020.                 $productId = (int) ($card['idProduit'] ?? $card['productId'] ?? 0);
  2021.                 if ($productId <= 0) {
  2022.                     continue;
  2023.                 }
  2024.                 foreach ($productTopCategoryMap[$productId] ?? [] as $topCategoryId) {
  2025.                     if (!isset($dataset[$topCategoryId])) {
  2026.                         continue;
  2027.                     }
  2028.                     if (!isset($seenPerTopCategory[$topCategoryId])) {
  2029.                         $seenPerTopCategory[$topCategoryId] = [];
  2030.                     }
  2031.                     $cardId = (int) ($card['id'] ?? 0);
  2032.                     if ($cardId && isset($seenPerTopCategory[$topCategoryId][$cardId])) {
  2033.                         continue;
  2034.                     }
  2035.                     if (count($dataset[$topCategoryId]->products) >= $limit) {
  2036.                         continue;
  2037.                     }
  2038.                     $dataset[$topCategoryId]->products[] = $card;
  2039.                     if ($cardId 0) {
  2040.                         $seenPerTopCategory[$topCategoryId][$cardId] = true;
  2041.                     }
  2042.                 }
  2043.             }
  2044.             return array_values($dataset);
  2045.         }
  2046.         $products $this->em->getRepository(Produit::class)->createQueryBuilder('p')
  2047.             ->innerJoin('p.categories''c')
  2048.             ->where('c IN (:cats)')
  2049.             ->andWhere('p.deletedAt IS NULL')
  2050.             ->andWhere('p.showInWebSite = 1')
  2051.             ->setParameter('cats'array_values($allScopeCategories))
  2052.             ->orderBy('p.createdAt''DESC')
  2053.             ->addOrderBy('p.id''DESC')
  2054.             ->getQuery()
  2055.             ->getResult();
  2056.         $dedupedProducts = [];
  2057.         foreach ($products as $product) {
  2058.             if (!$product instanceof Produit) {
  2059.                 continue;
  2060.             }
  2061.             $productId = (int) $product->getId();
  2062.             if (!isset($dedupedProducts[$productId])) {
  2063.                 $dedupedProducts[$productId] = $product;
  2064.             }
  2065.         }
  2066.         $hydratedProducts $this->preloadProductsForCards(array_values($dedupedProducts));
  2067.         $seenPerTopCategory = [];
  2068.         foreach ($dedupedProducts as $productId => $product) {
  2069.             $matchedTopIds = [];
  2070.             foreach ($product->getCategories() as $productCategory) {
  2071.                 $productCategoryId = (int) $productCategory->getId();
  2072.                 foreach ($topCategoryIdsByDescendantId[$productCategoryId] ?? [] as $topCategoryId) {
  2073.                     $matchedTopIds[$topCategoryId] = $topCategoryId;
  2074.                 }
  2075.             }
  2076.             if (empty($matchedTopIds) || !isset($hydratedProducts[$productId])) {
  2077.                 continue;
  2078.             }
  2079.             $card $this->mapHomeProductCard($hydratedProducts[$productId]);
  2080.             foreach (array_values($matchedTopIds) as $topCategoryId) {
  2081.                 if (!isset($dataset[$topCategoryId])) {
  2082.                     continue;
  2083.                 }
  2084.                 if (!isset($seenPerTopCategory[$topCategoryId])) {
  2085.                     $seenPerTopCategory[$topCategoryId] = [];
  2086.                 }
  2087.                 if (isset($seenPerTopCategory[$topCategoryId][$productId])) {
  2088.                     continue;
  2089.                 }
  2090.                 if (count($dataset[$topCategoryId]->products) >= $limit) {
  2091.                     continue;
  2092.                 }
  2093.                 $dataset[$topCategoryId]->products[] = $card;
  2094.                 $seenPerTopCategory[$topCategoryId][$productId] = true;
  2095.             }
  2096.         }
  2097.         return array_values($dataset);
  2098.     }
  2099.     private function buildProductColorSwatches(Produit $product): array
  2100.     {
  2101.         $swatches = [];
  2102.         $indexByValueId = [];
  2103.         foreach ($product->getProduitDeclinationValues() as $declinationValue) {
  2104.             $colorValue null;
  2105.             $sizeValue null;
  2106.             foreach ($declinationValue->getGroupDeclinationValues() as $group) {
  2107.                 $declination $group->getDeclination();
  2108.                 if (!$declination) {
  2109.                     continue;
  2110.                 }
  2111.                 if ((int) $declination->getPosition() === 1) {
  2112.                     $rawValue $group->getValue();
  2113.                     $colorValue $rawValue && method_exists($rawValue'getParent') && $rawValue->getParent()
  2114.                         ? $rawValue->getParent()
  2115.                         : $rawValue;
  2116.                 }
  2117.                 if ((int) $declination->getPosition() === 2) {
  2118.                     $sizeValue $group->getValue();
  2119.                 }
  2120.             }
  2121.             if (!$colorValue) {
  2122.                 continue;
  2123.             }
  2124.             $valueId = (int) $colorValue->getId();
  2125.             if (!isset($indexByValueId[$valueId])) {
  2126.                 $images $this->resolveDeclinationImages($declinationValue$product);
  2127.                 $swatches[] = [
  2128.                     'id' => $valueId,
  2129.                     'code' => (string) ($colorValue->getCode() ?: ($colorValue->getParent() ? $colorValue->getParent()->getCode() : '')),
  2130.                     'name' => (string) $colorValue->getName(),
  2131.                     'image' => $images[0],
  2132.                     'hoverImage' => $images[1],
  2133.                     'idDec' => (int) $declinationValue->getId(),
  2134.                     'sizes' => [],
  2135.                 ];
  2136.                 $indexByValueId[$valueId] = count($swatches) - 1;
  2137.             }
  2138.             $label $sizeValue ? (string) $sizeValue->getName() : (string) $declinationValue->getName();
  2139.             $swatches[$indexByValueId[$valueId]]['sizes'][] = [
  2140.                 'id' => (int) $declinationValue->getId(),
  2141.                 'idDec' => (int) $declinationValue->getId(),
  2142.                 'label' => $label,
  2143.                 'qty' => (int) $declinationValue->getQtyAvailableForWebsite(),
  2144.             ];
  2145.         }
  2146.         return $swatches;
  2147.     }
  2148.     private function hasProductStock(Produit $product): bool
  2149.     {
  2150.         foreach ($product->getProduitDeclinationValues() as $declinationValue) {
  2151.             if ((int) $declinationValue->getQtyAvailableForWebsite() > 0) {
  2152.                 return true;
  2153.             }
  2154.         }
  2155.         return ((int) $product->getAvailableQuantityForWebsite()) > 0;
  2156.     }
  2157.     private function resolveProductPrimaryImage(Produit $product): string
  2158.     {
  2159.         foreach ($product->getPicture() as $picture) {
  2160.             if ($picture->getIsSelected() && $picture->getImageName()) {
  2161.                 return (string) $picture->getImageName();
  2162.             }
  2163.         }
  2164.         foreach ($product->getPicture() as $picture) {
  2165.             if ($picture->getImageName()) {
  2166.                 return (string) $picture->getImageName();
  2167.             }
  2168.         }
  2169.         return $this->getDefaultImage($product);
  2170.     }
  2171.     private function resolveProductHoverImage(Produit $product): ?string
  2172.     {
  2173.         $primary $this->resolveProductPrimaryImage($product);
  2174.         foreach ($product->getPicture() as $picture) {
  2175.             $imageName $picture->getImageName();
  2176.             if ($imageName && $imageName !== $primary) {
  2177.                 return (string) $imageName;
  2178.             }
  2179.         }
  2180.         return null;
  2181.     }
  2182.     private function resolveDeclinationImages(ProduitDeclinationValue $declinationValueProduit $product): array
  2183.     {
  2184.         $primary null;
  2185.         $hover null;
  2186.         $pictures = [];
  2187.         foreach ($declinationValue->getPicture() as $picture) {
  2188.             $imageName $picture->getImageName();
  2189.             if (!$imageName) {
  2190.                 continue;
  2191.             }
  2192.             $pictures[] = (string) $imageName;
  2193.             if ($primary === null && $picture->getIsSelected()) {
  2194.                 $primary = (string) $imageName;
  2195.             }
  2196.         }
  2197.         if ($primary === null && isset($pictures[0])) {
  2198.             $primary $pictures[0];
  2199.         }
  2200.         if ($primary === null) {
  2201.             $primary $this->resolveProductPrimaryImage($product);
  2202.         }
  2203.         foreach ($pictures as $picture) {
  2204.             if ($picture !== $primary) {
  2205.                 $hover $picture;
  2206.                 break;
  2207.             }
  2208.         }
  2209.         return [$primary$hover];
  2210.     }
  2211.     private function mapPromotionData(?Promotion $promotion): ?array
  2212.     {
  2213.         if (!$promotion || !$promotion->isValid()) {
  2214.             return null;
  2215.         }
  2216.         return [
  2217.             'id' => $promotion->getId(),
  2218.             'discountType' => $promotion->getDiscountType(),
  2219.             'discountValue' => (float) ($promotion->getDiscountValue() ?? 0),
  2220.             'isValid' => true,
  2221.         ];
  2222.     }
  2223.     private function buildHomePositionOneCards(array $rows): array
  2224.     {
  2225.         $cards = [];
  2226.         $order = [];
  2227.         $productSwatches = [];
  2228.         foreach ($rows as $dec) {
  2229.             if (!$dec instanceof ProduitDeclinationValue) {
  2230.                 continue;
  2231.             }
  2232.             $product $dec->getProduit();
  2233.             if (!$product) {
  2234.                 continue;
  2235.             }
  2236.             $rawMainValue null;
  2237.             $sizeValue null;
  2238.             foreach ($dec->getGroupDeclinationValues() as $gdv) {
  2239.                 $decl $gdv->getDeclination();
  2240.                 if (!$decl) {
  2241.                     continue;
  2242.                 }
  2243.                 if ((int) $decl->getPosition() === 1) {
  2244.                     $rawMainValue $gdv->getValue();
  2245.                 }
  2246.                 if ((int) $decl->getPosition() === 2) {
  2247.                     $sizeValue $gdv->getValue();
  2248.                 }
  2249.             }
  2250.             if (!$rawMainValue) {
  2251.                 continue;
  2252.             }
  2253.             $mainValue = (method_exists($rawMainValue'getParent') && $rawMainValue->getParent())
  2254.                 ? $rawMainValue->getParent()
  2255.                 : $rawMainValue;
  2256.             $productId = (int) $product->getId();
  2257.             $mainValueId = (int) $mainValue->getId();
  2258.             $mainCode method_exists($mainValue'getCode') ? (string) $mainValue->getCode() : '';
  2259.             $mainName = (string) $mainValue->getName();
  2260.             $key $productId '_' $mainValueId;
  2261.             $pictures = [];
  2262.             $selectedImage null;
  2263.             foreach ($dec->getPicture() as $pic) {
  2264.                 $imgName $pic->getImageName();
  2265.                 if (!$imgName) {
  2266.                     continue;
  2267.                 }
  2268.                 $pictures[] = $imgName;
  2269.                 if ($selectedImage === null && method_exists($pic'getIsSelected') && $pic->getIsSelected()) {
  2270.                     $selectedImage $imgName;
  2271.                 }
  2272.             }
  2273.             $img $selectedImage ?: (!empty($pictures) ? $pictures[0] : null);
  2274.             if (!$img) {
  2275.                 $img $product->getImage() ?: 'no-image.jpg';
  2276.             }
  2277.             $hoverImage null;
  2278.             foreach ($pictures as $picture) {
  2279.                 if ($picture !== $img) {
  2280.                     $hoverImage $picture;
  2281.                     break;
  2282.                 }
  2283.             }
  2284.             if (!isset($productSwatches[$productId])) {
  2285.                 $productSwatches[$productId] = [];
  2286.             }
  2287.             if (!isset($productSwatches[$productId][$mainValueId])) {
  2288.                 $productSwatches[$productId][$mainValueId] = [
  2289.                     'id' => $mainValueId,
  2290.                     'name' => $mainName,
  2291.                     'code' => $mainCode !== '' $mainCode null,
  2292.                     'image' => $img,
  2293.                     'hoverImage' => $hoverImage,
  2294.                     'idDec' => (int) $dec->getId(),
  2295.                     'sizes' => [],
  2296.                 ];
  2297.             }
  2298.             if (!isset($cards[$key])) {
  2299.                 $cards[$key] = [
  2300.                     'id' => (int) $dec->getId(),
  2301.                     'idProduit' => $productId,
  2302.                     'productId' => $productId,
  2303.                     'name' => trim($product->getName() . ' ' $mainName),
  2304.                     'reference' => $product->getReference(),
  2305.                     'image' => $img,
  2306.                     'hoverImage' => $hoverImage,
  2307.                     'priceTTC' => (float) ($product->getPriceTtc() ?? 0),
  2308.                     'promo' => $product->getPromotion(),
  2309.                     'ratingScore' => (float) ($product->getRatingScore() ?? 0),
  2310.                     'ratingCount' => (int) ($product->getRatingCount() ?? 0),
  2311.                     'stock' => false,
  2312.                     'aspectRatio' => $this->getCategoryAspectRatio($dec),
  2313.                     'colorSwatches' => [],
  2314.                     'activeSizes' => [],
  2315.                 ];
  2316.                 $order[] = $key;
  2317.             }
  2318.             $qty method_exists($dec'getQtyAvailableForWebsite') ? (int) $dec->getQtyAvailableForWebsite() : 0;
  2319.             $label $sizeValue ? (string) $sizeValue->getName() : (string) $dec->getName();
  2320.             $sizeRow = [
  2321.                 'id' => (int) $dec->getId(),
  2322.                 'idDec' => (int) $dec->getId(),
  2323.                 'label' => $label,
  2324.                 'qty' => $qty,
  2325.             ];
  2326.             $cards[$key]['activeSizes'][] = $sizeRow;
  2327.             $productSwatches[$productId][$mainValueId]['sizes'][] = $sizeRow;
  2328.             if ($qty 0) {
  2329.                 $cards[$key]['stock'] = true;
  2330.             }
  2331.         }
  2332.         $grouped = [];
  2333.         foreach ($order as $cardKey) {
  2334.             $productId = (int) $cards[$cardKey]['productId'];
  2335.             $cards[$cardKey]['colorSwatches'] = isset($productSwatches[$productId]) ? array_values($productSwatches[$productId]) : [];
  2336.             $grouped[] = $cards[$cardKey];
  2337.         }
  2338.         return $grouped;
  2339.     }
  2340.     private function applyDeclinationCardPreloads(QueryBuilder $qbstring $declinationAliasstring $productAlias): void
  2341.     {
  2342.         $qb->addSelect($productAlias)
  2343.             ->leftJoin($productAlias '.promotion''promo_preload')->addSelect('promo_preload')
  2344.             ->leftJoin($declinationAlias '.picture''decl_picture_preload')->addSelect('decl_picture_preload')
  2345.             ->leftJoin($declinationAlias '.groupDeclinationValues''gdv_preload')->addSelect('gdv_preload')
  2346.             ->leftJoin('gdv_preload.declination''decl_preload')->addSelect('decl_preload')
  2347.             ->leftJoin('gdv_preload.value''value_preload')->addSelect('value_preload')
  2348.             ->leftJoin('value_preload.parent''value_parent_preload')->addSelect('value_parent_preload');
  2349.     }
  2350.     private function jsonCachedResponse(array $payloadint $ttl 120): JsonResponse
  2351.     {
  2352.         $response = new JsonResponse($payload);
  2353.         $response->setPublic();
  2354.         $response->setMaxAge($ttl);
  2355.         $response->setSharedMaxAge($ttl);
  2356.         $response->headers->addCacheControlDirective('stale-while-revalidate'min(30$ttl));
  2357.         return $response;
  2358.     }
  2359.     /**
  2360.      * @Route("/who-are-we", name="who_are_we")
  2361.      */
  2362.     public function whoAreWe(): Response
  2363.     {
  2364.         return $this->render('front/pages/whoAreWe.html.twig');
  2365.     }
  2366.     /**
  2367.      * @Route("/delivery-information", name="delivery_information")
  2368.      */
  2369.     public function deliveryInformation(): Response
  2370.     {
  2371.         return $this->render('front/pages/deliveryInformation.html.twig');
  2372.     }
  2373.     /**
  2374.      * @Route("/return-and-exchange", name="return_and_exchange")
  2375.      */
  2376.     public function returnAndExchange(): Response
  2377.     {
  2378.         return $this->render('front/pages/returnAndExchange.html.twig');
  2379.     }
  2380.     /**
  2381.      * @Route("/question", name="question")
  2382.      */
  2383.     public function question(): Response
  2384.     {
  2385.         return $this->render('front/pages/question.html.twig');
  2386.     }
  2387.     /**
  2388.      * @Route("/size-guide", name="size_guide")
  2389.      */
  2390.     public function sizeGuide(): Response
  2391.     {
  2392.         return $this->render('front/pages/sizeGuide.html.twig');
  2393.     }
  2394.     /**
  2395.      * @Route("/terms-of-sales", name="terms_of_sales")
  2396.      */
  2397.     public function termsOfSales(): Response
  2398.     {
  2399.         return $this->render('front/pages/termsOfSales.html.twig');
  2400.     }
  2401.     /**
  2402.      * @Route("/privacy-policy", name="privacy_policy")
  2403.      */
  2404.     public function privacyPolicy(): Response
  2405.     {
  2406.         return $this->render('front/pages/privacyPolicy.html.twig');
  2407.     }
  2408.     /**
  2409.      * @Route("/data-deletion", name="data_deletion")
  2410.      */
  2411.     public function dataDeletion(): Response
  2412.     {
  2413.         return $this->render('front/pages/dataDeletion.html.twig');
  2414.     }
  2415.     /**
  2416.      * @Route("/terms", name="terms")
  2417.      */
  2418.     public function terms(): Response
  2419.     {
  2420.         return $this->render('front/pages/terms.html.twig');
  2421.     }
  2422.     /**
  2423.      * @Route("/contact", name="contact")
  2424.      */
  2425.     public function contact(): Response
  2426.     {
  2427.         return $this->render('front/pages/contact.html.twig');
  2428.     }
  2429.     private function getCategoryAspectRatio(Produit|ProduitDeclinationValue|null $product): float
  2430.     {
  2431.         if (!$product) {
  2432.             return 0.8;
  2433.         }
  2434.         if ($product instanceof ProduitDeclinationValue) {
  2435.             $product $product->getProduit();
  2436.         }
  2437.         $category $product->getCategories();
  2438.         if ($category && $category->getAspectRatio()) {
  2439.             return $category->getAspectRatio();
  2440.         }
  2441.         return $this->getMixedAspectRatioFallback();
  2442.     }
  2443.     private function getMixedAspectRatioFallback(): float
  2444.     {
  2445.         $ratio = (float) $this->websiteSettingService->get('mixedAspectRatio'0.8);
  2446.         return $ratio $ratio 0.8;
  2447.     }
  2448. }