src/Controller/ProduitController.php line 168

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use DateTime;
  4. use App\Entity\User;
  5. use App\Entity\Image;
  6. use Twig\Environment;
  7. use App\Entity\Country;
  8. use App\Entity\Magasin;
  9. use App\Entity\Produit;
  10. use App\Entity\Category;
  11. use App\Data\SearchData;
  12. use App\Form\SearchForm;
  13. use App\Data\ShopContext;
  14. use App\Form\ProductType;
  15. use App\Service\PictureService;
  16. use Doctrine\ORM\EntityRepository;
  17. use App\Entity\MaintenanceSchedule;
  18. use App\Repository\ProduitRepository;
  19. use Doctrine\ORM\EntityManagerInterface;
  20. use Doctrine\Persistence\ManagerRegistry;
  21. use App\Controller\ProduitVariantController;
  22. use App\Repository\ProduitVariantRepository;
  23. use Symfony\Component\HttpFoundation\Request;
  24. use Symfony\Component\Security\Core\Security;
  25. use Symfony\Component\Translation\Translator;
  26. use Symfony\Component\HttpFoundation\Response;
  27. use Symfony\Component\Form\FormError;
  28. use Symfony\Component\Security\Csrf\CsrfToken;
  29. use Symfony\Component\Routing\Annotation\Route;
  30. use App\Repository\MaintenanceScheduleRepository;
  31. use Symfony\Component\Translation\LocaleSwitcher;
  32. use Symfony\Component\HttpFoundation\JsonResponse;
  33. use Symfony\Contracts\Translation\TranslatorInterface;
  34. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  35. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  36. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  37. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  38. class ProduitController extends AbstractController
  39. {
  40.     private CsrfTokenManagerInterface $csrfTokenManager;
  41.     private $params;
  42.     private $shopContext;
  43.     public function __construct(CsrfTokenManagerInterface $csrfTokenManager,
  44.         ShopContext $shopContext)
  45.     {
  46.         $this->csrfTokenManager $csrfTokenManager;
  47.         $this->shopContext $shopContext;
  48.     }
  49.     public function validateCsrfToken($token): bool
  50.     {
  51.         return $this->csrfTokenManager->isTokenValid(new CsrfToken('my-form-promo'$token));
  52.     }
  53.     /**
  54.      * Load shop_id => country_id map via raw SQL.
  55.      * DBAL 2.x / 3.x compatible.
  56.      */
  57.     private function loadShopCountryMap(EntityManagerInterface $entityManager): array
  58.     {
  59.         $map = [];
  60.         try {
  61.             $conn $entityManager->getConnection();
  62.             $rows = [];
  63.             if (method_exists($conn'fetchAllAssociative')) {
  64.                 $rows $conn->fetchAllAssociative('SELECT id, country_id FROM magasin');
  65.             } else {
  66.                 $stmt $conn->executeQuery('SELECT id, country_id FROM magasin');
  67.                 if (method_exists($stmt'fetchAllAssociative')) {
  68.                     $rows $stmt->fetchAllAssociative();
  69.                 } elseif (method_exists($stmt'fetchAll')) {
  70.                     $rows $stmt->fetchAll();
  71.                 }
  72.             }
  73.             foreach ($rows as $row) {
  74.                 $id  = isset($row['id']) ? (int) $row['id'] : null;
  75.                 $cid = isset($row['country_id']) && $row['country_id'] !== null ? (int) $row['country_id'] : null;
  76.                 if ($id !== null$map[$id] = $cid;
  77.             }
  78.         } catch (\Throwable $e) {
  79.             error_log('[Julico shopCountryMap ERROR] ' $e->getMessage());
  80.         }
  81.         return $map;
  82.     }
  83.     /**
  84.      * Convert anything iterable (Paginator, array, IteratorAggregate) to a plain array.
  85.      * Critical because findSearch() returns a Doctrine Paginator, not a real array.
  86.      */
  87.     private function toArray($iterable): array
  88.     {
  89.         if (is_array($iterable)) return $iterable;
  90.         $out = [];
  91.         try {
  92.             foreach ($iterable as $item) { $out[] = $item; }
  93.         } catch (\Throwable $e) {
  94.             error_log('[Julico toArray ERROR] ' $e->getMessage());
  95.         }
  96.         return $out;
  97.     }
  98.     /**
  99.      * Shop IDs the given user is linked to (their vendor shops).
  100.      */
  101.     private function vendorShopIds($user): array
  102.     {
  103.         $ids = [];
  104.         try {
  105.             if ($user && method_exists($user'getLinkedMagasins')) {
  106.                 foreach ($user->getLinkedMagasins() as $m) {
  107.                     if (is_object($m) && method_exists($m'getId')) {
  108.                         $ids[] = (int) $m->getId();
  109.                     }
  110.                 }
  111.             }
  112.         } catch (\Throwable $e) {}
  113.         return $ids;
  114.     }
  115.     // A shop is "live" on the storefront when it is active AND its subscription has
  116.     // not expired (a null paid-until date means grandfathered = live). This mirrors
  117.     // the rule in ProduitRepository so listings, the shop subnav and the product
  118.     // page all agree on which shops are visible to the public.
  119.     private function isShopLive($shop): bool
  120.     {
  121.         try {
  122.             if (!is_object($shop) || !method_exists($shop'isActive')) { return false; }
  123.             if ($shop->isActive() !== true) { return false; }
  124.             if (method_exists($shop'getSubscriptionPaidUntil')) {
  125.                 $until $shop->getSubscriptionPaidUntil();
  126.                 if ($until !== null && $until < new \DateTime('today')) { return false; }
  127.             }
  128.             return true;
  129.         } catch (\Throwable $e) {
  130.             return false;
  131.         }
  132.     }
  133.     #[Route('/locale/{_locale}'name'app_locale')]
  134.     public function setLocale(Request $request,SessionInterface $session 
  135.     ,TranslatorInterface $translator,LocaleSwitcher $localeSwitcher ):Response
  136.     {
  137.             $locale $request->get('_locale');
  138.             $currentLocale $localeSwitcher->getLocale();
  139.             if($locale !=null){
  140.                 $localeSwitcher->setLocale($locale);
  141.                 $translator->setLocale($locale);
  142.                 $request->getSession()->set('_locale',$locale);
  143.             }
  144.         $sessionlocal$request->getSession()->get('_locale');
  145.         return $this->redirectToRoute("app_home",['_locale'=>$sessionlocal]);
  146.     }
  147.     
  148.     #[Route(path: ['/home/{shopName}''/home'], name'app_home',
  149.     defaults: ['shopName' => null]
  150.     )]
  151.     public function index(ManagerRegistry $doctrineRequest $request
  152.     ,?string $shopName
  153.     ,Environment $twig,Security $security,SessionInterface $session 
  154.     ,TranslatorInterface $translator,LocaleSwitcher $localeSwitcher 
  155.     ,EntityManagerInterface $entityManager):Response
  156.     {   
  157.         
  158.         if ($request->getSession()->has('_locale')) {
  159.             $locale =  $request->getSession()->get('_locale');
  160.             if($locale !=null){
  161.                 $localeSwitcher->setLocale($locale);
  162.                 $translator->setLocale($locale);    
  163.                 $request->getSession()->set('lang',$locale);
  164.             }
  165.         }
  166.         else {
  167.             $request->getSession()->set('_locale','en');
  168.             $request->getSession()->set('lang','en');
  169.             $locale =  'en';
  170.             if($locale !=null){
  171.                 $localeSwitcher->setLocale($locale);
  172.                 $translator->setLocale($locale);    
  173.             }
  174.         }
  175.         
  176.         $session->set('srv_msg',null);
  177.         $shops = [];
  178.         if ($shopName) {
  179.             $slugs explode(','$shopName);
  180.             $shops $entityManager->getRepository(Magasin::class)->findBy(['nom' => $slugs]);
  181.             if (!$shops) {
  182.                 throw $this->createNotFoundException('Aucune boutique trouvée');
  183.             }
  184.             $this->shopContext->setShops($shops);
  185.         } elseif ($request->query->get('shopName')) {
  186.             $shops $entityManager->getRepository(Magasin::class)->findBy(['nom' => [$request->query->get('shopName')]]);
  187.             $this->shopContext->setShops($shops);
  188.         } else {
  189.             $this->shopContext->clear();
  190.         }
  191.         $lastActive$doctrine->getRepository(MaintenanceSchedule::class)
  192.                         ->findLastActiveSchedule();
  193.         if ($lastActive) {
  194.             return $this->redirectToRoute("maintenance_index",['_locale'=>$locale,'lastActiveId'=>$lastActive->getId()]);
  195.         }
  196.         $data = new SearchData();
  197.         $form $this->createForm(SearchForm::class, $data);
  198.         $form->handleRequest($request);
  199.         $data->categories $request->query->all('categories');
  200.         $data->shops $shops;
  201.         $data->promo $request->query->get('promo');
  202.         if (!$form->isSubmitted()) {
  203.             $data->q   $request->query->get('q');
  204.             $data->min $request->query->get('min');
  205.             $data->max $request->query->get('max');
  206.         }
  207.         $offset max(0$request->query->getInt('offset'0));
  208.         $findProduitsNouveaute = [];
  209.         $user $security->getUser();
  210.         $isAdmin 0;
  211.         if ($user) {
  212.             $roles $user->getRoles();
  213.             if (in_array('ROLE_ADMIN'$rolestrue)) {
  214.                 $isAdmin 1;
  215.             }
  216.         }
  217.         if($form->isSubmitted() && $form->isValid() ){
  218.             $produits $doctrine->getRepository(Produit::class)->findSearch($data,$offset,$isAdmin);         
  219.             $magasins $doctrine->getRepository(Magasin::class)->findAll();
  220.         }else{
  221.             $category $request->request->get('category'null);
  222.             if($category !=null){
  223.                 $data = new SearchData();
  224.                 $categories = [$category];
  225.                 $data->categories $categories;
  226.             }
  227.             else{
  228.                 $findProduitsNouveaute $doctrine->getRepository(Produit::class)
  229.                 ->findProduitsNouveaute($data,$offset,$isAdmin);
  230.             }
  231.             $produits $doctrine->getRepository(Produit::class)->findSearch($data,$offset,$isAdmin);
  232.             $magasins $doctrine->getRepository(Magasin::class)->findAll();
  233.         }
  234.         // Keep the shop list in step with the product filter: a non-admin must never
  235.         // see shops that are inactive or whose subscription has lapsed (findAll above
  236.         // returns every shop). Admins keep seeing all shops.
  237.         if (!$isAdmin) {
  238.             $magasins array_values(array_filter(
  239.                 $this->toArray($magasins),
  240.                 fn($m) => $this->isShopLive($m)
  241.             ));
  242.         }
  243.         $allCategories $entityManager->getRepository(Category::class)
  244.             ->findBy([], ['nom' => 'ASC']);
  245.         $activeFilters 0;
  246.         if (!empty($data->q))          $activeFilters++;
  247.         if (!empty($data->categories)) $activeFilters++;
  248.         if (!empty($data->promo))      $activeFilters++;
  249.         if (!empty($data->min))        $activeFilters++;
  250.         if (!empty($data->max))        $activeFilters++;
  251.         // ══════════════════════════════════════════════════════════════
  252.         // Phase 3 Step 3 — Country lock
  253.         // ══════════════════════════════════════════════════════════════
  254.         $selectedCountryId  null;
  255.         $selectedCountry    null;
  256.         $needsCountryPicker false;
  257.         $allCountries       = [];
  258.         try {
  259.             $allCountries $entityManager->getRepository(Country::class)
  260.                 ->findBy([], ['name' => 'ASC']);
  261.         } catch (\Throwable $e) {
  262.             error_log('[Julico countries-fetch index] ' $e->getMessage());
  263.             $allCountries = [];
  264.         }
  265.         try {
  266.             $selectedCountryId $request->getSession()->get('selected_country_id');
  267.             if ($selectedCountryId) {
  268.                 $selectedCountry $entityManager->getRepository(Country::class)
  269.                     ->find($selectedCountryId);
  270.                 if (!$selectedCountry) {
  271.                     $selectedCountryId null;
  272.                     $request->getSession()->remove('selected_country_id');
  273.                 }
  274.             }
  275.         } catch (\Throwable $e) {
  276.             error_log('[Julico selected-country index] ' $e->getMessage());
  277.             $selectedCountryId null;
  278.             $selectedCountry   null;
  279.         }
  280.         if (!$isAdmin) {
  281.             try {
  282.                 if (!$selectedCountryId && $user instanceof User) {
  283.                     if (method_exists($user'getAddresses')) {
  284.                         foreach ($user->getAddresses() as $addr) {
  285.                             $isDef method_exists($addr'isDefaultAddresse') ? $addr->isDefaultAddresse() : false;
  286.                             $addrCountry method_exists($addr'getCountry') ? $addr->getCountry() : null;
  287.                             if ($isDef && $addrCountry) {
  288.                                 $selectedCountryId $addrCountry->getId();
  289.                                 $selectedCountry   $addrCountry;
  290.                                 $request->getSession()->set('selected_country_id'$selectedCountryId);
  291.                                 break;
  292.                             }
  293.                         }
  294.                     }
  295.                 }
  296.             } catch (\Throwable $e) {
  297.                 error_log('[Julico auto-derive index] ' $e->getMessage());
  298.             }
  299.             if (!$selectedCountryId) {
  300.                 $needsCountryPicker true;
  301.             }
  302.         }
  303.         if (!$isAdmin && $selectedCountryId) {
  304.             $selCountryIdInt = (int) $selectedCountryId;
  305.             $shopCountryMap $this->loadShopCountryMap($entityManager);
  306.             $produitsArr   $this->toArray($produits);
  307.             $nouveauteArr  $this->toArray($findProduitsNouveaute);
  308.             $magasinsArr   $this->toArray($magasins);
  309.             $matchByShopId = function($shopId) use ($selCountryIdInt$shopCountryMap) {
  310.                 if ($shopId === null) return false;
  311.                 $cid $shopCountryMap[(int) $shopId] ?? null;
  312.                 return $cid !== null && $cid === $selCountryIdInt;
  313.             };
  314.             $shopIdOfProduct = function($p) {
  315.                 try {
  316.                     if (!is_object($p) || !method_exists($p'getMagasin')) return null;
  317.                     $shop $p->getMagasin();
  318.                     if (!$shop || !method_exists($shop'getId')) return null;
  319.                     return (int) $shop->getId();
  320.                 } catch (\Throwable $e) {
  321.                     return null;
  322.                 }
  323.             };
  324.             try {
  325.                 $produits array_values(array_filter($produitsArr, function($p) use ($shopIdOfProduct$matchByShopId) {
  326.                     return $matchByShopId($shopIdOfProduct($p));
  327.                 }));
  328.                 $findProduitsNouveaute array_values(array_filter($nouveauteArr, function($p) use ($shopIdOfProduct$matchByShopId) {
  329.                     return $matchByShopId($shopIdOfProduct($p));
  330.                 }));
  331.                 $magasins array_values(array_filter($magasinsArr, function($m) use ($matchByShopId) {
  332.                     try {
  333.                         if (!is_object($m) || !method_exists($m'getId')) return false;
  334.                         return $matchByShopId((int) $m->getId());
  335.                     } catch (\Throwable $e) {
  336.                         return false;
  337.                     }
  338.                 }));
  339.             } catch (\Throwable $e) {
  340.                 error_log('[Julico FILTER ERROR index] ' $e->getMessage());
  341.                 $produits = [];
  342.                 $findProduitsNouveaute = [];
  343.                 $magasins = [];
  344.             }
  345.         }
  346.         return new Response($twig->render('produit/index.html.twig',[
  347.             'produits'          => $produits,
  348.             'magasins'          => $magasins,
  349.             '_locale'           => $locale,
  350.             'produitsNouveaute' => $findProduitsNouveaute,
  351.             'form'              => $form->createView(),
  352.             'previous'          => $offset ProduitRepository::PAGINATOR_PER_PAGE,
  353.             'next'              => min(count($produits), $offset ProduitRepository::PAGINATOR_PER_PAGE),
  354.             'allCategories'     => $allCategories,
  355.             'activeFilters'     => $activeFilters,
  356.             'currentQ'          => $data->?? '',
  357.             'currentMin'        => $data->min ?? '',
  358.             'currentMax'        => $data->max ?? '',
  359.             'currentPromo'      => $data->promo ?? '',
  360.             'currentCategories' => $data->categories ?? [],
  361.             'currentSort'       => $request->query->get('sort'''),
  362.             'needsCountryPicker' => $needsCountryPicker,
  363.             'selectedCountryId'  => $selectedCountryId,
  364.             'selectedCountry'    => $selectedCountry,
  365.             'allCountries'       => $allCountries,
  366.         ])); 
  367.     }
  368.     #[Route('/produits/ajax-filter'name'ajax_product_filter'methods: ['POST'])]
  369.     public function ajaxFilterProducts(Request $request
  370.     CsrfTokenManagerInterface $csrfTokenManager,
  371.     ProduitRepository $produitRepository,ManagerRegistry $doctrine,
  372.     Environment $twig,Security $security,SessionInterface $session,
  373.     TranslatorInterface $translator,LocaleSwitcher $localeSwitcher): JsonResponse
  374.     {
  375.         $csrfToken $request->headers->get('X-CSRF-TOKEN');
  376.         if (!$this->validateCsrfToken($csrfToken)) {
  377.             return new JsonResponse(['error' => 'Invalid CSRF token'], 400);
  378.         }
  379.         $data = new SearchData();
  380.         $data->$request->request->get('q');
  381.         $data->categories $request->request->get('categories');
  382.         $data->promo $request->request->get('promo');
  383.         $data->min $request->request->get('min');
  384.         $data->max $request->request->get('max');
  385.         $data->dateproduction $request->request->get('dateproduction');
  386.         $data->dateproduction $request->request->get('dateexpiration');
  387.         $findProduitsPromo= [];
  388.         $user $security->getUser();
  389.         $isAdmin 0;
  390.         if ($user) {
  391.             $roles $user->getRoles();
  392.             if (in_array('ROLE_ADMIN'$rolestrue)) {
  393.                 $isAdmin 1;
  394.             }
  395.         }
  396.         $products $produitRepository->findSearchPromo($data,$isAdmin);
  397.         $productData = [];
  398.         foreach ($products as $product) {
  399.             $productData[] = [
  400.                 'id' => $product->getId(),
  401.                 'name' => $product->getProductName(),
  402.             ];
  403.         }
  404.         return new JsonResponse($productData);
  405.     }
  406.     #[Route('/produit/add'name'add_produit')]
  407.     #[Route('/produit/edit/{id}'name'edit_produit')]
  408.     public function new_edit(int $id =null  ,Request $request,EntityManagerInterface $entityManager,Produit $produit null,
  409.                                 PictureService $pictureService,ParameterBagInterface $params
  410.                                 ,TranslatorInterface $translator,LocaleSwitcher $localeSwitcher ): Response
  411.     {
  412.         // ── Vendor access: logged in, and either an admin or linked to a shop ──
  413.         $user $this->getUser();
  414.         if (!$user) {
  415.             return $this->redirectToRoute('app_login');
  416.         }
  417.         $isAdmin   $this->isGranted('ROLE_ADMIN');
  418.         $myShopIds $this->vendorShopIds($user);
  419.         if (!$isAdmin && empty($myShopIds)) {
  420.             return $this->redirectToRoute('vendor_dashboard');
  421.         }
  422.         if ($id!=null){
  423.             $produit $entityManager->getRepository(Produit::class)->find($id);
  424.         }
  425.         if (!$produit) {
  426.             $produit = new Produit();
  427.             $produitVariants $produit->getProduitVariants();
  428.             $option  "add";
  429.         }
  430.         else{
  431.             $option  "edit";
  432.             if (!$isAdmin) {
  433.                 $prodShopId = (method_exists($produit'getMagasin') && $produit->getMagasin())
  434.                     ? (int) $produit->getMagasin()->getId() : null;
  435.                 if ($prodShopId === null || !in_array($prodShopId$myShopIdstrue)) {
  436.                     throw $this->createAccessDeniedException('You can only edit products in your own shop.');
  437.                 }
  438.             }
  439.         }
  440.         $form $this->createForm(ProductType::class, $produit);
  441.         $form->handleRequest($request);
  442.         if ($form->isSubmitted() && $form->isValid()) {
  443.             $produit $form->getData();
  444.             // ── Defense in depth: a vendor can only save to one of their own shops ──
  445.             if (!$isAdmin) {
  446.                 $chosenShop   method_exists($produit'getMagasin') ? $produit->getMagasin() : null;
  447.                 $chosenShopId = ($chosenShop && method_exists($chosenShop'getId')) ? (int) $chosenShop->getId() : null;
  448.                 if ($chosenShopId === null || !in_array($chosenShopId$myShopIdstrue)) {
  449.                     throw $this->createAccessDeniedException('You can only assign products to your own shop.');
  450.                 }
  451.             }
  452.             $images $form->get('images')->getData();
  453.             foreach($images as $image){
  454.                 $folder 'products';
  455.                 $fichier $pictureService->add($image$folder300300);
  456.                 $img = new Image();
  457.                 $img->setFileName($fichier);
  458.                 $pictureService =  new PictureService($params);
  459.                 $img->setImageUrl($params->get('images_directory') . $folder);
  460.                 $produit->addImage($img);
  461.             }
  462.             $produit->setUser($this->getUser());
  463.             // ── Variants + their own images ──
  464.             // Iterate the SUB-FORMS so we can read each variant's unmapped "images"
  465.             // file field. Skip completely empty rows; coalesce blank price/stock to 0.
  466.             foreach ($form->get('produitVariants') as $variantForm) {
  467.                 $produitVariant $variantForm->getData();
  468.                 if (!$produitVariant) { continue; }
  469.                 $hasLabel trim((string) $produitVariant->getOptionLabel())  !== ''
  470.                          || trim((string) $produitVariant->getOptionLabel2()) !== ''
  471.                          || trim((string) $produitVariant->getOptionLabel3()) !== '';
  472.                 $rawPrix  $produitVariant->getPrix();
  473.                 $rawQtt   $produitVariant->getQtt();
  474.                 $hasPrix  $rawPrix !== null && $rawPrix !== '';
  475.                 $hasQtt   $rawQtt  !== null && $rawQtt  !== '';
  476.                 // Ignore rows the vendor left totally empty
  477.                 if (!$hasLabel && !$hasPrix && !$hasQtt) {
  478.                     continue;
  479.                 }
  480.                 // Keep NOT NULL columns happy if a box was left blank
  481.                 if (!$hasPrix) { $produitVariant->setPrix('0'); }
  482.                 if (!$hasQtt)  { $produitVariant->setQtt('0'); }
  483.                 $produitVariant->setProduit($produit);
  484.                 $produit->addProduitVariant($produitVariant);
  485.                 $variantImages $variantForm->has('images') ? $variantForm->get('images')->getData() : [];
  486.                 if (!empty($variantImages)) {
  487.                     foreach ($variantImages as $vfile) {
  488.                         if (!$vfile) { continue; }
  489.                         $folder  'products';
  490.                         $fichier $pictureService->add($vfile$folder300300);
  491.                         $vimg = new Image();
  492.                         $vimg->setFileName($fichier);
  493.                         $vimg->setImageUrl($params->get('images_directory') . $folder);
  494.                         $vimg->setProduit($produit);                 // Image.produit is required
  495.                         $vimg->setProduitVariant($produitVariant);   // link photo to this variant
  496.                         $produit->addImage($vimg);
  497.                     }
  498.                 }
  499.             }
  500.             // ── Auto-fill product Price / Quantity from the variants when left blank ──
  501.             $variants    $produit->getProduitVariants();
  502.             $hasVariants count($variants) > 0;
  503.             $priceEmpty  = ($produit->getPrix() === null || $produit->getPrix() === '');
  504.             $qtyEmpty    = ($produit->getQtt() === null);
  505.             if ($hasVariants) {
  506.                 if ($priceEmpty) {
  507.                     $minPrice null;
  508.                     foreach ($variants as $v) {
  509.                         $vp $v->getPrix();
  510.                         if ($vp !== null && $vp !== '' && (float) $vp 0) {
  511.                             $vpf = (float) $vp;
  512.                             if ($minPrice === null || $vpf $minPrice) { $minPrice $vpf; }
  513.                         }
  514.                     }
  515.                     $produit->setPrix($minPrice !== null ? (string) $minPrice '0');
  516.                 }
  517.                 if ($qtyEmpty) {
  518.                     $sumQty 0;
  519.                     foreach ($variants as $v) {
  520.                         $vq $v->getQtt();
  521.                         if ($vq !== null && $vq !== '') { $sumQty += (int) $vq; }
  522.                     }
  523.                     $produit->setQtt($sumQty);
  524.                 }
  525.             } else {
  526.                 // Simple product (no variants): Price & Quantity are required.
  527.                 if ($priceEmpty || $qtyEmpty) {
  528.                     if ($priceEmpty) {
  529.                         $form->get('prix')->addError(new FormError('Please enter a Price (or add variants and leave this blank to auto-fill it).'));
  530.                     }
  531.                     if ($qtyEmpty) {
  532.                         $form->get('qtt')->addError(new FormError('Please enter a Quantity (or add variants and leave this blank to auto-fill it).'));
  533.                     }
  534.                     return $this->render('produit/add.html.twig', [
  535.                         'formAddProduit' => $form->createView(),
  536.                         'option' => $option,
  537.                     ]);
  538.                 }
  539.             }
  540.             $date = new DateTime();
  541.             $produit->setPublishDate$date);
  542.             try{
  543.                 $entityManager->persist($produit);
  544.             }
  545.             catch (\Exception $e) {
  546.                 $this->addFlash('error''An error occurred while saving the entity.');
  547.             }
  548.             $entityManager->flush();
  549.             if ($option === 'edit') {
  550.                 return $this->redirectToRoute($option '_produit', [
  551.                     'id' => $produit->getId(),
  552.                 ]);
  553.             } else {
  554.                 return $this->redirectToRoute($option.'_produit');
  555.             }
  556.         }
  557.         if ($request->query->has('_locale')) {
  558.             $locale =  $request->getSession()->get('_locale');
  559.             if($locale !=null){
  560.                 $localeSwitcher->setLocale($locale);
  561.                 $translator->setLocale($locale);    
  562.             }
  563.         }
  564.             
  565.         return $this->render('produit/add.html.twig', [
  566.             'formAddProduit' => $form->createView(),
  567.             'option' => $option
  568.         ]);
  569.     }
  570.     #[Route('/produit/{id}'name'show_produit')]
  571.     public function showstring $idEntityManagerInterface $entityManager,
  572.         TranslatorInterface $translatorLocaleSwitcher $localeSwitcher,
  573.         Request $requestSecurity $security ): Response
  574.     {                                    
  575.         if ($id != null) {
  576.             $produit $entityManager->getRepository(Produit::class)->find($id);
  577.         }
  578.         if (!$produit) {
  579.             throw $this->createNotFoundException('Product not found');
  580.         }
  581.         // ── Block the public product page for a shop that isn't live (inactive OR
  582.         //    subscription expired). Clients are sent home; an admin or the shop's own
  583.         //    manager may still preview it. Same rule as the listings (isShopLive). ──
  584.         $viewer        $security->getUser();
  585.         $viewerIsAdmin $viewer && in_array('ROLE_ADMIN'$viewer->getRoles(), true);
  586.         $pShop         method_exists($produit'getMagasin') ? $produit->getMagasin() : null;
  587.         $shopLive      $this->isShopLive($pShop);
  588.         if (!$shopLive) {
  589.             $allowed $viewerIsAdmin;
  590.             if (!$allowed && $viewer instanceof User && method_exists($viewer'getLinkedMagasins')) {
  591.                 foreach ($viewer->getLinkedMagasins() as $om) {
  592.                     if ($om && $pShop && method_exists($om'getId') && (int) $om->getId() === (int) $pShop->getId()) {
  593.                         $allowed true;
  594.                         break;
  595.                     }
  596.                 }
  597.             }
  598.             if (!$allowed) {
  599.                 return $this->redirectToRoute('app_home');
  600.             }
  601.         }
  602.         $locale $request->getSession()->get('_locale');
  603.         if ($locale != null) {
  604.             $localeSwitcher->setLocale($locale);
  605.             $translator->setLocale($locale);    
  606.         }
  607.         $groupedVariantssize = [];
  608.         foreach ($produit->getProduitVariants() as $variant) {
  609.             $paramSize $variant->getParamSize();
  610.             if ($paramSize !== null) {
  611.                 $size $paramSize->getSizeAbreviation();
  612.                 if ($size !== null) {
  613.                     $groupedVariantssize[$size][] = $variant;
  614.                 }
  615.             }
  616.         }
  617.         $groupedVariantscolor = [];
  618.         foreach ($produit->getProduitVariants() as $variant) {
  619.             $paramColor $variant->getParamColor();
  620.             if ($paramColor !== null) {
  621.                 $color $paramColor->getColorName();
  622.                 if ($color !== null) {
  623.                     $groupedVariantscolor[$color][] = $variant;
  624.                 }
  625.             }
  626.         }
  627.         $variantsArray = [];
  628.         foreach ($produit->getProduitVariants() as $variant) {
  629.             if (($variant !== null && $variant->getParamColor() !== null) ) {
  630.                 if ($variant->getParamColor() !== null) {
  631.                     $variantsArray[$variant->getParamColor()->getColorName()] = [];
  632.                 }
  633.             }
  634.             if ($variant !== null && $variant->getParamSize() !== null) {
  635.                 $variantsArray[$variant->getParamColor()->getColorName()][] = $variant->getParamSize()->getSizeAbreviation();
  636.             }
  637.         }
  638.         $user $security->getUser();
  639.         $isAdmin false;
  640.         if ($user) {
  641.             $roles $user->getRoles();
  642.             $isAdmin in_array('ROLE_ADMIN'$rolestrue);
  643.         }
  644.         $selectedCountryId  null;
  645.         $selectedCountry    null;
  646.         $needsCountryPicker false;
  647.         $allCountries       = [];
  648.         try {
  649.             $allCountries $entityManager->getRepository(Country::class)
  650.                 ->findBy([], ['name' => 'ASC']);
  651.         } catch (\Throwable $e) {
  652.             error_log('[Julico countries-fetch show] ' $e->getMessage());
  653.             $allCountries = [];
  654.         }
  655.         try {
  656.             $selectedCountryId $request->getSession()->get('selected_country_id');
  657.             if ($selectedCountryId) {
  658.                 $selectedCountry $entityManager->getRepository(Country::class)
  659.                     ->find($selectedCountryId);
  660.                 if (!$selectedCountry) {
  661.                     $selectedCountryId null;
  662.                     $request->getSession()->remove('selected_country_id');
  663.                 }
  664.             }
  665.         } catch (\Throwable $e) {
  666.             error_log('[Julico selected-country show] ' $e->getMessage());
  667.             $selectedCountryId null;
  668.             $selectedCountry   null;
  669.         }
  670.         if (!$isAdmin) {
  671.             try {
  672.                 if (!$selectedCountryId && $user instanceof User) {
  673.                     if (method_exists($user'getAddresses')) {
  674.                         foreach ($user->getAddresses() as $addr) {
  675.                             $isDef method_exists($addr'isDefaultAddresse') ? $addr->isDefaultAddresse() : false;
  676.                             $addrCountry method_exists($addr'getCountry') ? $addr->getCountry() : null;
  677.                             if ($isDef && $addrCountry) {
  678.                                 $selectedCountryId $addrCountry->getId();
  679.                                 $selectedCountry   $addrCountry;
  680.                                 $request->getSession()->set('selected_country_id'$selectedCountryId);
  681.                                 break;
  682.                             }
  683.                         }
  684.                     }
  685.                 }
  686.                 if (!$selectedCountryId) {
  687.                     $needsCountryPicker true;
  688.                 }
  689.             } catch (\Throwable $e) {
  690.                 error_log('[Julico auto-derive show] ' $e->getMessage());
  691.                 if (!$selectedCountryId) {
  692.                     $needsCountryPicker true;
  693.                 }
  694.             }
  695.             if ($selectedCountryId) {
  696.                 try {
  697.                     $shop method_exists($produit'getMagasin') ? $produit->getMagasin() : null;
  698.                     if ($shop && method_exists($shop'getId')) {
  699.                         $shopId = (int) $shop->getId();
  700.                         $shopCountryMap $this->loadShopCountryMap($entityManager);
  701.                         $shopCountryId $shopCountryMap[$shopId] ?? null;
  702.                         if ($shopCountryId !== null && $shopCountryId !== ((int) $selectedCountryId)) {
  703.                             return $this->redirectToRoute('app_home');
  704.                         }
  705.                     }
  706.                 } catch (\Throwable $e) {
  707.                     error_log('[Julico country-block show] ' $e->getMessage());
  708.                 }
  709.             }
  710.         }
  711.         $shop method_exists($produit'getMagasin') ? $produit->getMagasin() : null;
  712.         $deliveryInfo = [
  713.             'address_set'  => false,
  714.             'has_region'   => false,
  715.             'region_name'  => null,
  716.             'can_deliver'  => null,
  717.             'fee'          => null,
  718.             'shop_name'    => $shop $shop->getNom() : null,
  719.             'shop_id'      => $shop $shop->getId() : null,
  720.         ];
  721.         try {
  722.             if ($user instanceof User && $shop) {
  723.                 $defaultAddress null;
  724.                 foreach ($user->getAddresses() as $addr) {
  725.                     if (method_exists($addr'isDefaultAddresse') && $addr->isDefaultAddresse()) {
  726.                         $defaultAddress $addr;
  727.                         break;
  728.                     }
  729.                 }
  730.                 if (!$defaultAddress && $user->getAddresses()->count() > 0) {
  731.                     $defaultAddress $user->getAddresses()->first();
  732.                 }
  733.                 if ($defaultAddress) {
  734.                     $deliveryInfo['address_set'] = true;
  735.                     $region method_exists($defaultAddress'getRegion') ? $defaultAddress->getRegion() : null;
  736.                     if ($region) {
  737.                         $deliveryInfo['has_region']  = true;
  738.                         $deliveryInfo['region_name'] = $region->getName();
  739.                         if (method_exists($shop'getDeliveryFeeForRegion')) {
  740.                             $fee $shop->getDeliveryFeeForRegion($region);
  741.                             if ($fee !== null) {
  742.                                 $deliveryInfo['can_deliver'] = (bool) $fee->canDeliver();
  743.                                 $deliveryInfo['fee']         = $fee->getFeeAsFloat();
  744.                             }
  745.                         }
  746.                     }
  747.                 }
  748.             }
  749.         } catch (\Throwable $e) {
  750.             error_log('[Julico Phase 3 delivery-info] ' $e->getMessage());
  751.         }
  752.         return $this->render('produit/show.html.twig', [
  753.             'produit'              => $produit,
  754.             'groupedVariantscolor' => $groupedVariantscolor,
  755.             'groupedVariantssize'  => $groupedVariantssize,
  756.             'variants'             => $variantsArray,
  757.             'deliveryInfo'         => $deliveryInfo,
  758.             'needsCountryPicker'   => $needsCountryPicker,
  759.             'selectedCountryId'    => $selectedCountryId,
  760.             'selectedCountry'      => $selectedCountry,
  761.             'allCountries'         => $allCountries,
  762.         ]); 
  763.     }   
  764. }